Skip to content

feat: explain exports/imports conditions that wrap subpaths - #676

Merged
alexander-akait merged 2 commits into
mainfrom
claude/enhanced-resolve-660-qo14h7
Sep 21, 2026
Merged

alexander-akait merged 2 commits into
mainfrom
claude/enhanced-resolve-660-qo14h7

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Summary

An exports field shaped like { "import": { ".": "./esm/index.js", "./*": "./esm/*.js" }, "require": "./build/bundle.js" } is not supported by Node.js: a condition may only map to a target or to further conditions, so the subpaths under "import" are read as condition names and match nothing. The failure surfaced as "./foo" is not exported under the conditions [...], which points at the request rather than the mistake and reads as though the package forgot to export ./foo — #325 took its reporter a day to diagnose. That error now carries the reason: the offending keys, and the arrangement that works with the subpaths at the top level and the conditions nested inside them. Found at any depth and inside array targets, with the same diagnosis for # keys under a condition in imports. Closes #325.

What kind of change does this PR introduce?

feat — a better error message; resolution behaviour is unchanged.

Did you add tests for your changes?

Yes — test/entrypoints.test.js covers the shorthand form, a nested offender, an offender inside an array target, the imports counterpart, and the cases that must stay silent. The 1616 existing tests are unchanged and pass.

Does this PR introduce a breaking change?

No, and deliberately so. The diagnosis runs only where a request has already failed, never while building the field, so nothing that resolves today starts failing — the field above still resolves . under require. Validating the field instead would have broken that, leaving a consumer who cannot patch the upstream package with no way forward, and it would have cost a full tree walk of every package.json to catch a mistake almost none of them make: on a 1000-subpath field that took construction from 0.0674 ms to 0.2888 ms. As written, construction stays at main's cost (0.0648 vs 0.0674 ms, retained heap identical at 2.83 KB) and resolve throughput is unmoved — interleaved runs of the exports benchmarks gave 9089 and 9217 ops/s against main's 8811 and 9141, a gap smaller than the spread within either arm.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a — the changeset covers the changelog.

Use of AI

Written with Claude Code. It measured rather than assumed at each step: it reproduced the current behaviour first (under import the field resolves nothing at all, not just subpaths), instrumented a benchmark run to confirm the field processor is built once across 3823 samples, and benchmarked an eager-validation version against this one, which is what moved the design from rejecting the field to diagnosing the failure. Reviewed by me before opening.

🤖 Generated with Claude Code

https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved resolution errors for unsupported exports and imports configurations by identifying problematic subpaths and showing the supported structure.
    • Errors while processing these fields now identify the relevant package.json file for easier troubleshooting.
    • Resolution behavior remains unchanged; successful resolutions are unaffected.
  • Tests

    • Added coverage for nested, conditional subpath diagnostics across exports and imports configurations, including valid and invalid field formats.

An exports field shaped like

    "exports": {
      "import": { ".": "./esm/index.js", "./*": "./esm/*.js" },
      "require": "./build/bundle.js"
    }

is not supported by Node.js: a condition may only map to a target or to
further conditions, so the subpaths nested under "import" are read as
condition names and match nothing. What a user saw was

    "./foo" is not exported under the conditions ["import","node","default"]
    from package /path (see exports field in /path/package.json)

which points at the request, not at the mistake, and reads as though the
package forgot to export "./foo". The reporter of #325 spent a day finding
the real cause.

That failure now carries the reason:

    ... (see exports field in /path/package.json). In that exports field,
    the value at "import" is an object with subpath keys (".", "./*"),
    which is not supported - a condition can only map to a target or to
    further conditions, so those subpaths never match. Put the subpaths at
    the top level and nest the conditions inside them instead, e.g.
    { ".": { "import": ... }, "./*": { "import": ... } }.

It is found at any depth and inside array targets, and the imports field
gets the same diagnosis for "#" keys nested under a condition.

The check is diagnostic, not validation, and that is deliberate on two
counts. Rejecting the field while building it would break packages that
resolve today - the field above still resolves "." under "require", and a
consumer who cannot fix the upstream package would have no way forward.
And it would cost a full tree walk of every package.json to catch a mistake
almost none of them make: measured over a 1000-subpath field, validating
eagerly took field construction from 0.067 to 0.289 ms, roughly 4x. Running
it only where a request has already failed puts construction back at
main's cost (0.0648 vs 0.0674 ms, retained heap identical at 2.83 KB) and
leaves resolve throughput unmoved - interleaved benchmark runs gave 9089
and 9217 ops/s against main's 8811 and 9141, a gap smaller than the spread
within either arm.

Errors from processing either field now name the package.json as well.
They carried no path at all before - it appeared only in the resolver log,
which is off unless a caller passes one.

Closes #325

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF
@changeset-bot

changeset-bot Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 22bbd7b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
enhanced-resolve Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.46%. Comparing base (e1bdb75) to head (22bbd7b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #676      +/-   ##
==========================================
+ Coverage   98.44%   98.46%   +0.02%     
==========================================
  Files          52       52              
  Lines       10753    10903     +150     
==========================================
+ Hits        10586    10736     +150     
  Misses        167      167              
Flag Coverage Δ
integration 98.46% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d3374bd6-5783-4681-9d0a-39c1363fc0d7

📥 Commits

Reviewing files that changed from the base of the PR and between 8863114 and 22bbd7b.

📒 Files selected for processing (1)
  • lib/util/entrypoints.js

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


Walkthrough

The change adds explainSubpathsInConditions to detect unsupported nested subpath mappings in exports and imports fields. Failed resolutions now include these explanations when applicable. Field-processing errors now include the originating package.json path. Resolution behavior is unchanged because the diagnostic runs only after resolution fails. Tests and TypeScript declarations cover the new helper.

Priority: ➖ Normal

Severity of issue fixed: Medium

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding explanations for exports/imports conditions that wrap subpaths.
Linked Issues check ✅ Passed Issue #325 requires a clear diagnostic for unsupported exports structures where condition keys contain subpath keys. The PR adds explainSubpathsInConditions, reports the offending path, and explai…
Out of Scope Changes check ✅ Passed The changes stay within the diagnostic scope of issue #325. The shared helper also diagnoses the equivalent unsupported structure in imports. Error wrapping adds the relevant package field path to t…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 5 files.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

lib/util/entrypoints.js

ESLint failed to execute (timeout).


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Codecov flagged one uncovered line in the new code: the condition used in
the suggested example when the offending mapping is the exports field
itself, rather than something nested inside it.

That branch cannot run. It needs a field that mixes condition names with
subpath keys, and `buildExportsField` rejects exactly that before any
processor exists, so the plugin never reaches the diagnosis with one. The
line was not missing a test - it was dead.

Removing it collapses the shorthand special case entirely: both forms of
`exports` and `imports` hold targets in their top-level values, so
descending into the values and never inspecting the field's own keys is
correct for all of them, and the trail can no longer be empty. Behaviour is
unchanged - the issue's field is still explained under "import" and still
resolves "." under "require".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF
@codspeed

codspeed Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 17.45%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 140 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
⚡ Memory node-compare: node require.resolve x 1000 59.3 KB 50.5 KB +17.55%
⚡ Simulation realistic-midsize: mixed batch (cold cache) 18.1 ms 15.4 ms +17.34%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/enhanced-resolve-660-qo14h7 (22bbd7b) with main (e1bdb75)

Open in CodSpeed

Copy link
Copy Markdown
Member Author

CodSpeed is red ("Performance Regression: -7.81%"). This PR does touch lib/, so that deserved checking rather than waving away — but the one regressed benchmark cannot be affected by any change to this package.

The flagged case is node-compare: node require.resolve x 1000 (Memory), and its body is:

bench.add(`node-compare: node require.resolve x ${BATCH_SIZE}`, () => {
	for (let i = 0; i < requests.length; i++) {
		requireAnchor.resolve(requests[i]);
	}
});

requireAnchor is createRequire(path.join(srcDir, "index.js")). That task measures Node's own CJS resolver — it is the control arm of the head-to-head case and never calls enhanced-resolve. No edit to lib/ can move it.

Two further points against reading it as real:

  • The same benchmark's BASE was 78.7 KB on refactor: generate types with typescript instead of webpack/tooling #675 (base 332d999) and is 59.3 KB here (base e1bdb75). Node's require.resolve memory moving 25% between two base commits of our package, neither of which can affect it, is the instrument.
  • The other flagged row is an improvement — realistic-midsize: mixed batch (cold cache), 18.1 ms to 15.5 ms, +16.98%. My diff cannot make an enhanced-resolve benchmark 17% faster either.

Independently of the benchmark: the diagnosis this PR adds runs only where paths.length === 0, i.e. after a request has already failed to match. No successful resolve reaches it. Measured locally, field construction is unchanged against main (0.0648 vs 0.0674 ms on a 1000-subpath field, retained heap identical at 2.83 KB), and interleaved runs of the exports benchmarks gave 9089 and 9217 ops/s on this branch against 8811 and 9141 on main — a gap smaller than the spread within either arm.

So there is nothing to fix and no fix to port. I am re-running the benchmark workflow once; if it comes back red on the same warning, it is measurement noise rather than this diff. Acknowledging it in the CodSpeed dashboard is a maintainer action, so I am leaving that to you.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit f07a030 into main Sep 21, 2026
42 checks passed
@alexander-akait
alexander-akait deleted the claude/enhanced-resolve-660-qo14h7 branch September 21, 2026 07:17
alexander-akait pushed a commit that referenced this pull request Sep 29, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## enhanced-resolve@5.26.0

### Minor Changes

- Add experimental support for [Node.js package
maps](https://nodejs.org/api/packages.html#package-maps) through a new
`packageMap` option, which takes the path of the configuration file (or
a `file:` `URL`) or an already-parsed `packages` object. When it is set,
a bare specifier is resolved through the importing package's
`dependencies` table and the target package's location is handed to the
regular pipeline, instead of walking `node_modules`; relative and
absolute requests and `node:` builtins are unaffected. Because several
package entries may share one `url`, the package a request resolved into
is exposed as `packageId` on the result and can be passed back in as
`context.packageId` to resolve from that package unambiguously. Package
maps are stability 1 (experimental) in Node.js, and this option tracks
that specification and may change with it. (by
[@alexander-akait](https://github.com/alexander-akait) in
[#667](#667))

- Explain an `exports`/`imports` field whose conditions wrap subpaths,
instead of failing with a message that points at the request. A field
shaped like `{ "import": { ".": "./esm/index.js", "./*": "./esm/*.js" },
"require": "./build/bundle.js" }` is not supported by Node.js: the
subpaths inside a condition are read as condition names, so they match
nothing, and every request into the package failed as `"./foo" is not
exported under the conditions [...]` — which reads as though the package
forgot to export `./foo`. Such a failure now names the offending keys
and shows the arrangement that works, with the subpaths at the top level
and the conditions nested inside them. Resolution itself is unchanged:
the diagnosis runs only on a request that has already failed, so nothing
that resolves today starts failing, and a successful resolve does no
extra work. Errors raised while processing either field also name the
`package.json` they came from, which previously only appeared in the
resolver log. (by [@alexander-akait](https://github.com/alexander-akait)
in [#676](#676))

- Generate the published type declarations with TypeScript instead of
`webpack/tooling`, which is no longer a dependency. Every name the
package exported before is still exported, and `types.d.ts` is still the
entry point, but the declarations themselves now live in `types/` and
are emitted by `tsc` from the JSDoc in `lib/`. Two shapes follow the
sources more closely than the previous generator did: the object form of
`Plugin` no longer declares `this: Resolver` on `apply` (it is called as
`plugin.apply(resolver)`, so `this` is the plugin), and the entries of
`ResolveContext.stack` declare `name: string | undefined` rather than an
optional `name`. Class fields that the old generator dropped, such as
the cache backends on `CachedInputFileSystem`, are now part of the
declarations. (by [@alexander-akait](https://github.com/alexander-akait)
in [#675](#675))

### Patch Changes

- Size the ancestor path and segment arrays that `getPathsCached` keeps
to what they actually hold: a `push`-built store keeps room for 17
entries while a path has a handful, and the cache holds these for the
filesystem's lifetime. (by
[@alexander-akait](https://github.com/alexander-akait) in
[#681](#681))

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exports field trees with top-level conditions result in a misleading error message

1 participant