Skip to content

feat: experimental support for Node.js package maps - #667

Merged
alexander-akait merged 7 commits into
mainfrom
feat/package-map
Sep 18, 2026
Merged

alexander-akait merged 7 commits into
mainfrom
feat/package-map

Conversation

@alexander-akait

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

Copy link
Copy Markdown
Member

Closes #653

Adds a packageMap option implementing Node.js package maps (added in v26.4.0 behind --experimental-package-map). The whole surface is marked @experimental, since the Node.js feature is stability 1 and this tracks its specification.

API

const resolver = resolve.create({ packageMap: path.resolve("./package-map.json") });

Also accepts a file: URL, or an already-parsed map alongside the configFile its relative url values resolve against:

resolve.create({ packageMap: { configFile, packages: { /* ... */ } } });

Semantics

Bare specifiers resolve through the importing package's dependencies table; the target package's location is then handed to the regular pipeline, so exports, main, mainFiles and extension resolution keep working unchanged. Relative/absolute requests and node: builtins are untouched, per the spec's "Interaction with other resolution".

The map is authoritative: a specifier the importing package does not declare is reported as unresolved rather than falling back to node_modules.

Package identity

The spec allows several package ids to share one url — the same sources consumed with different dependency tables — so the importing package cannot always be derived from the file path. The package a request resolved into is exposed as packageId on the result and accepted back as context.packageId:

resolver({}, parent, specifier, {}, (err, result, request) => {
  // request.packageId → pass back as context.packageId for resolutions from `result`
});

Where the answer would otherwise be a guess, resolution fails rather than picking one, matching Node.js ("Node.js will throw an error rather than guess"):

situation code
importer outside every mapped package ERR_PACKAGE_MAP_EXTERNAL_FILE
several package ids share the importer's location, no id given ERR_PACKAGE_MAP_AMBIGUOUS_PACKAGE
explicit package id not in the map ERR_PACKAGE_MAP_UNKNOWN_PACKAGE
malformed map (bad url, non-file: scheme, dangling dependency target) ERR_INVALID_PACKAGE_MAP

Only ERR_PACKAGE_MAP_EXTERNAL_FILE is named by the Node.js docs; the other three are this package's, for conditions Node.js describes but does not give a code for. The map is static, so dangling dependency targets are rejected eagerly at parse time rather than on the request that happens to hit one.

Implementation

file role
lib/util/packageMap.js parses/validates the config into lookup tables; findPackageIds locates an importer by path, most deeply nested package first
lib/PackageMapPlugin.js taps raw-module → undescribed-resolve-in-package, reads the config lazily through resolver.fileSystem and queues concurrent first requests so a cold start reads it once
lib/util/pathToFileURL.js runtime-agnostic port of Node's url.pathToFileURL, needed to use the config file as the WHATWG URL base like Node does; mirrors the existing fileURLToPath port
lib/ResolverFactory.js option normalization and wiring, ahead of the modules plugins

pathToFileURL output was diffed against Node's own url.pathToFileURL across spaces, #, ?, tabs, backslashes, non-ASCII and trailing separators — all identical.

Inline packages require a configFile to resolve relative urls against: there is no working directory to fall back on, since the resolver runs in browsers too. That is enforced eagerly with a clear message.

Tests

20 new tests in test/package-map.test.js covering resolution and sub-path forwarding, per-importer version selection, the no-node_modules-fallback rule, both option shapes, each of the four error codes, packageId propagation in and out (including the two-ids-one-url case from the spec's "Multiple packages for the same URL"), and the lookup-table edges (shared locations, nested packages, sibling directories sharing a prefix).

Full suite: 1542 pass, 0 fail on Node, and green under Bun (1551) and Deno + jest (1551) via the cross-runtime workflow's own commands. lint:code, lint:types, lint:types-test, fmt:check, lint:spellcheck and test:browser all pass.

⚠️ lint:special is the one check I could not run locally — it needs the tooling dependency, which this sandbox's proxy blocks from codeload.github.com. types.d.ts is therefore hand-written to match the generator's conventions (alphabetical interface order, JSDoc-derived comments, named typedefs for the index-signature types so nothing has to be guessed). CI's lint job is the real check here; if it disagrees, npm run fix:special will settle it and I'll push the regenerated file.

🤖 Generated with Claude Code

https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added experimental Node.js package map support for resolving bare package specifiers.
    • The packageMap option accepts configuration paths, file: URLs, or inline package data.
    • Supports dependency-aware subpath resolution and disambiguation for packages sharing locations.
    • Added package identity information to resolution results and context.
    • Relative, absolute, and built-in node: requests continue using existing resolution behavior.
  • Documentation

    • Documented configuration, usage examples, plugin behavior, limitations, and error cases.
    • Added TypeScript declarations for package map options and package identity fields.

Node.js v26.4.0 added package maps behind `--experimental-package-map`: a
static table that says where each package lives and which package id every
bare specifier of a given package maps to, so resolution never has to walk
`node_modules`.

Add a `packageMap` option taking the configuration file path (or a `file:`
URL), or an already-parsed `packages` object alongside the `configFile` its
relative urls resolve against. When set, `PackageMapPlugin` resolves bare
specifiers through the importing package's `dependencies` table and hands
the target package's location to the regular pipeline, so `exports`, `main`
and extension resolution keep working unchanged. Relative and absolute
requests and `node:` builtins are untouched, and the map is authoritative:
an undeclared specifier is reported as unresolved rather than looked up in
`node_modules`.

A package map may point several package ids at one `url` — the same sources
consumed with different dependency tables — so the importing package cannot
always be derived from the file path. The package a request resolved into is
exposed as `packageId` on the result and accepted back as `context.packageId`.
Where that is missing and the answer would be a guess, resolution fails:
`ERR_PACKAGE_MAP_EXTERNAL_FILE` for an importer outside every mapped package
and `ERR_PACKAGE_MAP_AMBIGUOUS_PACKAGE` for a shared location.

The whole surface is marked `@experimental`: package maps are stability 1 in
Node.js and this implementation tracks that specification.

Closes #653

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

changeset-bot Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 78b7d87

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 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.43262% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.44%. Comparing base (e913bc5) to head (78b7d87).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
lib/ResolverFactory.js 95.78% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #667      +/-   ##
==========================================
+ Coverage   98.14%   98.44%   +0.29%     
==========================================
  Files          49       52       +3     
  Lines       10046    10749     +703     
==========================================
+ Hits         9860    10582     +722     
+ Misses        186      167      -19     
Flag Coverage Δ
integration 98.44% <99.43%> (+0.29%) ⬆️

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.

@codspeed

codspeed Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ 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

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 140 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Simulation extensions-many: 6-extension list (warm) 2.2 ms 2.8 ms -22.08%
⚡ Memory array-alias: @ -> [preferred, fallback] (warm) 3.3 KB 2 KB +62.6%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/package-map (78b7d87) with main (e913bc5)

Open in CodSpeed

alexander-akait and others added 2 commits September 18, 2026 14:21
The package map declarations were written by hand because the `tooling`
dependency could not be installed here, and `lint:special` rejected them:
`PackageMapOptions` sorts before `PackageMapPackage`, and the generator
drops the description of a `@typedef` rather than emitting it above the
interface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF
Codecov reported 88.5% patch coverage: the validation branches, the
Windows/UNC and percent-encoding branches of `pathToFileURL`, and the
plugin's config-loading paths were all unexercised.

Add a `pathToFileURL` test mirroring the `fileURLToPath` one (pinned
literals, both platform branches, UNC errors, a round-trip), cover every
`ERR_INVALID_PACKAGE_MAP` branch, and exercise the plugin's file-dependency
recording, its log line for an undeclared specifier, the read-once queueing
of concurrent first resolutions, `node:` specifiers, and both the read and
the parse failure of the configuration file.

The three new files are now at 100% line coverage.

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

linux-foundation-easycla Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via:

Co-authored-by: name <email>

Supported Co-authored-by: formats include:

  1. Anything <id+login@users.noreply.github.com> - it will locate your GitHub user by id part.
  2. Anything <login@users.noreply.github.com> - it will locate your GitHub user by login part.
  3. Anything <public-email> - it will locate your GitHub user by public-email part. Note that this email must be made public on Github.
  4. Anything <other-email> - it will locate your GitHub user by other-email part but only if that email was used before for any other CLA as a main commit author.
  5. login <any-valid-email> - it will locate your GitHub user by login part, note that login part must be at least 3 characters long.

Alternatively, if the co-author should not be included, remove the Co-authored-by: line from the commit message.

Please update your commit message(s) by doing git commit --amend and then git push [--force] and then request re-running CLA check via commenting on this pull request:

/easycla

alexander-akait and others added 2 commits September 18, 2026 14:33
main gained `file:` URL string support wherever a path is expected (#668),
which the `packageMap` option picks up through `toPath`. Cover it the way
the other path options are covered: a URL instance and a URL string as the
config file, a URL instance as `options.configFile`, and `file:` URLs as
package entry urls.

Drops the narrower URL test from the package map suite, which this replaces.

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

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

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: f0b77d23-bb7f-4917-bbc1-f7e9a9fe8956

📥 Commits

Reviewing files that changed from the base of the PR and between 8581b75 and 78b7d87.

📒 Files selected for processing (3)
  • lib/PackageMapPlugin.js
  • lib/ResolverFactory.js
  • test/package-map.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/package-map.test.js
  • lib/ResolverFactory.js
  • lib/PackageMapPlugin.js

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


Walkthrough

Adds experimental packageMap support for Node.js-style dependency resolution. The change validates and parses package maps, supports filesystem paths and file: URLs, tracks package identities, and integrates PackageMapPlugin before node_modules resolution. It handles shared package locations, configuration errors, concurrent loading, builtins, and unresolved dependencies. It also adds public type declarations, documentation, fixtures, and comprehensive tests.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 78b7d

The previously reported legacy Node.js test failure is addressed by the existing compatibility polyfills, so no actionable merge risk remains.

🚥 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 experimental Node.js package map support.
Linked Issues check ✅ Passed Issue #653 requires opt-in package-map support with path, file: URL, and parsed-map inputs. The PR adds packageMap normalization, lazy loading, map validation, importer-specific dependency resolut…
Out of Scope Changes check ✅ Passed The changes remain within Issue #653. PackageMapPlugin, package-map utilities, pathToFileURL, type declarations, README content, fixtures, changeset data, and tests support package-map configurati…
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 10 functions across 16 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/PackageMapPlugin.js

ESLint failed to execute (timeout).

lib/ResolverFactory.js

ESLint skipped: the matched ESLint configuration already failed (timeout).

test/package-map.test.js

ESLint skipped: the matched ESLint configuration already failed (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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: e19556a8-d3b7-4be3-ad56-eea4fde20903

📥 Commits

Reviewing files that changed from the base of the PR and between e913bc5 and 6b5c2bf.

📒 Files selected for processing (25)
  • .changeset/experimental-package-map-support.md
  • README.md
  • lib/PackageMapPlugin.js
  • lib/Resolver.js
  • lib/ResolverFactory.js
  • lib/util/packageMap.js
  • lib/util/pathToFileURL.js
  • test/file-url-options.test.js
  • test/fixtures/package-map/invalid-map.json
  • test/fixtures/package-map/lib/index.js
  • test/fixtures/package-map/lib/package.json
  • test/fixtures/package-map/outside/index.js
  • test/fixtures/package-map/package-map.json
  • test/fixtures/package-map/packages/app/index.js
  • test/fixtures/package-map/packages/app/package.json
  • test/fixtures/package-map/packages/utils/index.js
  • test/fixtures/package-map/packages/utils/package.json
  • test/fixtures/package-map/packages/utils/sub.js
  • test/fixtures/package-map/vendor/component-v1/index.js
  • test/fixtures/package-map/vendor/component-v1/package.json
  • test/fixtures/package-map/vendor/component-v2/index.js
  • test/fixtures/package-map/vendor/component-v2/package.json
  • test/package-map.test.js
  • test/pathToFileURL.test.js
  • types.d.ts

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

Comment thread lib/PackageMapPlugin.js Outdated
Comment thread lib/PackageMapPlugin.js Outdated
Comment thread lib/ResolverFactory.js
Comment thread lib/util/packageMap.js Outdated
Comment thread test/package-map.test.js
const resolver = createResolver();
const err = catchError(() => resolver(appDir, "m1"));

assert.match(err.message, /Can't resolve 'm1'/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use assertion methods supported by Node.js 10.13.0.

The package declares Node.js >=10.13.0. assert.match and assert.doesNotMatch were added in Node.js 12.16.0 and 13.6.0. Node.js 10 therefore throws a TypeError at these calls. (nodejs.org)

Replace these methods with Node.js 10-compatible assertions such as assert.ok(pattern.test(value)).

Also applies to: 116-116, 168-168, 180-180, 192-192, 254-254, 264-264, 283-283, 297-297, 375-375, 395-395

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not acting on this one — the premise doesn't hold for this repository.

test/_runner.js, which every test file here imports for describe/it, polyfills both methods at module load precisely for the legacy legs:

// `assert.match` and `assert.doesNotMatch` were added in Node.js 13.6. Legacy
// jest CI also runs on Node.js 10/12, so polyfill them when missing.
if (typeof assert.match !== "function") { … }
if (typeof assert.doesNotMatch !== "function") { … }

It mutates the shared assert module object, so the patch is in place before any test body runs regardless of require order. Six existing suites already rely on this — exportsField, importsField, tsconfig-paths, description-file, file-url-options, resolve-context-stack.

The empirical check agrees: test (ubuntu-latest, 10.x) and test (macos-latest, 10.x) both passed on the commit this review looked at (6b5c2bf), running these exact assertions through jest on Node 10. Switching to assert.ok(pattern.test(value)) would lose the failure diff for no gain.


Generated by Claude Code

alexander-akait and others added 2 commits September 18, 2026 14:59
Four fixes from the automated review:

- Run the package map ahead of `SelfReferencePlugin`. A package whose
  `package.json` has a `name` and `exports` could import itself through the
  self-reference shortcut without declaring the dependency, which
  contradicts the map being authoritative for bare specifiers. Node lists
  only relative, absolute and builtin specifiers as unaffected by package
  maps, so a self-reference goes through the dependency table too.
- Make a relative `configFile` absolute against the working directory, like
  `tsconfig` does. It was previously handed to the URL parser as-is, which
  silently resolved package locations against the filesystem root. Where
  there is no working directory (the resolver also runs in browsers), say so
  instead.
- Reject an empty package `url`. It resolved to the configuration file
  itself rather than to a package directory.
- Keep an empty string as a package id: the id is any key of `packages`, so
  "not provided" has to be distinguished from falsy, or a propagated empty
  id would be reported as ambiguous instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF
A configuration file reached through a symlink produced package locations
spelled through that symlink, while the pipeline hands over importer paths
with their symlinks already resolved. The two never matched, so a perfectly
valid importer was rejected with ERR_PACKAGE_MAP_EXTERNAL_FILE.

Resolve the configuration file to its real path when `symlinks` is on, and
leave it as given when it is off, so both sides are always spelled alike.
Canonicalizing unconditionally would have been wrong the other way round: it
would move package locations for resolvers that deliberately keep symlinked
paths.

Tests cover both directions, guarded by the same privilege probe
`test/symlink.test.js` uses.

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

Copy link
Copy Markdown
Member Author

CodSpeed Performance Analysis is failing on 78b7d87 with one regression — extensions-many: 6-extension list (warm), 2.2 ms → 2.8 ms. I do not think it is this PR's, and here is the evidence rather than an assertion.

The resolve pipeline is byte-identical. Outside the three new packageMap files, this PR changes only module-level requires in ResolverFactory.js, two functions called at resolver-creation time, and two JSDoc lines in Resolver.js. PackageMapPlugin is registered only under if (packageMap), so with the option unset nothing is added to any hook. Dumping every hook and its taps for a resolver built without packageMap gives the same 32 lines on both commits:

$ diff <(node dump-pipeline.js @main) <(node dump-pipeline.js @78b7d87)
(no output)

The failing case is the warm one: it builds one resolver outside the loop and resolves repeatedly, so per-resolve work dominates — and this PR adds none.

Measured both commits locally, same machine, interleaved, with the repo's own npm run benchmark V8 flags:

run 78b7d87 main
1 0.3384 ms 0.3413 ms
2 0.2926 ms 0.3955 ms
3 0.2721 ms 0.3444 ms

The branch is never slower. More to the point, the spread within each arm is about 24% — the same order as the 22% CodSpeed attributes to the change, which is what you would expect from a benchmark this short (sub-millisecond mean) on shared infrastructure.

The report contradicts itself in the same run. Alongside the regression it reports a 62.6% memory improvement on array-alias: @ -> [preferred, fallback] (warm). This PR cannot explain a third less memory on the alias path any more than it can explain the slowdown; two large opposite-signed swings in unrelated benchmarks is the signature of measurement noise. CodSpeed also flags it itself: "Different runtime environments detected — some benchmarks with significant performance changes were compared across different runtime environments, which may affect the accuracy of the results."

On the environment mismatch, one contributing factor may be worth fixing regardless of this PR: benchmarks.yml has cancel-in-progress: true with no condition, so a push to main cancels the in-flight benchmark run for the previous main commit. Two merges in quick succession leave a commit with no benchmark result for later comparisons to use as a baseline. #673 makes that cancellation apply to pull requests only. I have not proven that is the cause here, but it is the same class of problem the warning describes.

Re-run already spent. The Benchmarks run for this head (35360874309) was first cancelled while queued 31 minutes without starting — account-wide runner starvation — and I re-ran it once; this result is that re-run. I am not re-running again.

Nothing here is actionable in the code, so I am leaving the diff alone. If you would rather see the check green, the regression can be acknowledged in the CodSpeed dashboard — that is a maintainer action and not one I would take unasked.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

Follow-up on the CodSpeed failure above — a control turned up that settles it better than my reasoning did.

#673 changes three YAML files under .github/workflows/ and nothing else: no lib/, no test/, no dependencies. CodSpeed reports it as "Performance Regression: -85.11%":

Mode Benchmark BASE HEAD
❌ Memory extensions-many: 6-extension list (warm) 2.8 KB 100.4 KB -97.23%
❌ Memory node-compare: node require.resolve x 1000 76 KB 95.1 KB -20.09%

A workflow-YAML diff cannot make a benchmark allocate 36× more memory. And it is the same extensions-many: 6-extension list (warm) case that this PR was charged with regressing by 22%, and credited with improving by 62.6%, in a single run.

One benchmark swinging between -97% and +62% across three branches, one of which contains no executable change at all, is the measurement rather than the code. That is a stronger statement than the argument I made earlier from the diff and from local timings, so I wanted it on the record here rather than only on #673.

Nothing changes on this PR as a result — still no code to fix, still not re-running (that was spent). Everything else on 78b7d87 is green: lint, codecov/patch (99.43%), codecov/project (98.44%, +0.29%), bun, deno, browser, both benchmark jobs after the re-run, and all nine Ubuntu plus all nine Windows test legs including the legacy jest ones. The macOS legs are still queued behind the shared runner backlog.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit e638bc9 into main Sep 18, 2026
39 of 42 checks passed
@alexander-akait
alexander-akait deleted the feat/package-map branch September 18, 2026 17:21
alexander-akait added a commit that referenced this pull request Sep 18, 2026
Pushing a new commit to a pull request currently leaves the previous
commit's jobs running to completion, even though nobody will read their
results.

`Test` alone is **28 jobs** per push — three operating systems × nine
Node.js versions, plus `lint` — and `Cross-runtime` adds three more. PR
#667 went through five pushes this afternoon, so roughly 130 job-runs
were obsolete the moment they started.

## Change

`Test` and `Cross-runtime` get the concurrency group `Benchmarks`
already had:

```yaml
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
```

`github.workflow` is in the group key, so the workflows never cancel
each other. On a `pull_request` event `github.ref` is
`refs/pull/<n>/merge`, which is unique per pull request, so one PR's
pushes never cancel another's.

## Why `main` is exempt

The condition is the point of the change, not decoration. `Benchmarks`
cancelled `main` runs until now, and this stops that too:

- CodSpeed compares a pull request against the benchmark result of its
**base commit**. Two merges in quick succession would cancel the first
one's run and leave later comparisons without a baseline.
- Codecov's project check compares against `main` coverage in the same
way.
- A cancelled `main` run hides a breakage that is already on the default
branch.

## Left alone deliberately

- **`Release`** — already has `concurrency: ${{ github.workflow }}-${{
github.ref }}`. The string form defaults `cancel-in-progress` to false,
so releases queue instead of cancelling, which is what you want for
something that publishes.
- **`Dependabot`** — one short job that approves and enables auto-merge.
Cancelling it halfway could leave a pull request without the auto-merge
it was about to turn on, and there is nothing to save.

## Verification

`actionlint` 1.7.7 passes on all five workflows. I checked that this
means something by typo'ing the expression to `github.event_nam` and
confirming it is caught:

```
.github/workflows/test.yml:16:27: property "event_nam" is not defined in object type {…}
   |
16 |   cancel-in-progress: ${{ github.event_nam == 'pull_request' }}
   |                           ^~~~~~~~~~~~~~~~
```

All five workflows also still parse as YAML and are Prettier-clean. No
changeset: CI-only change, matching how previous `ci:` commits were
handled here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF

---
_Generated by [Claude
Code](https://claude.ai/code/session_016aGHrb1YaEvaGwNELGEHjF)_

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Improved automated workflow run management by grouping pull request
runs by workflow and Git reference.
- In-progress pull request runs are cancelled when a newer run starts,
reducing redundant checks.
- Pushes to the main branch and manually triggered runs now use
independent run groups and are not cancelled, preserving benchmark
baselines and ongoing validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

[Feature Request] Support Node.js package maps

1 participant