fix(npm): raise engines.node floor to 24.15.0 so npm ci resolves - #226
Merged
Conversation
`npm ci` failed on a clean checkout with EUSAGE, naming a version that
appears nowhere in package-lock.json:
npm error Missing: esbuild@0.28.2 from lock file
... (+26 @esbuild/* platform packages, all 0.28.2)
while `npm install --package-lock-only --dry-run` reported "up to date".
The lockfile is not at fault and is unchanged here. `vitest`'s nested
`vite@8.2.2` declares esbuild as an *optional* peerDependency
(`^0.27.0 || ^0.28.0`, `peerDependenciesMeta.esbuild.optional = true`),
and nothing in the lock satisfies that edge at that location — which is
correct, because an unsatisfied optional peer should be skipped. Older
npm instead resolves it against the registry (arborist's `loadPeerSet`),
picks up whatever is current, then rejects the lock as out of sync. That
is also the install-vs-ci contradiction: the two commands build the tree
by different paths and disagree about optional peers.
Fixed upstream by npm/cli#8981 (npm 11.10.1) and npm/cli#9083
(npm 11.11.1). Node 24.15.0 is the first 24.x bundling npm >= 11.11.1.
CI was never affected: `.nvmrc` is bare `24`, so setup-node floats to
v24.19.0 / npm 11.17.0. The failure only reached environments sitting on
an old-but-in-range Node 24, which `engines.node: ">=24"` permitted.
Regenerating the lockfile under npm 11.17.0 reproduces it byte for byte;
regenerating under an affected npm instead commits esbuild 0.28.2 and 26
platform packages that CI does not install.
`.nvmrc` deliberately stays bare `24` — the repo pins by major, and
scripts/check-node-version.sh compares majors only, so it still passes.
Also corrects two references the new floor invalidated: the Dockerfile
comment that quoted "node >=24" (now states the floor without repeating
a value that can drift) and the docs/ops.md local-dev requirement.
|
Warning Review limit reachedNext included review available in 8 minutes. View limit detailsLimit details: You’ve used all 2 included reviews currently available. Your 82 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed and why
npm cifailed on a clean checkout with EUSAGE, naming a version that appearsnowhere in
package-lock.json:…while
npm install --package-lock-only --dry-runsaidup to date in 1s.The lockfile is not at fault and is unchanged in this PR.
vitest's nestedvite@8.2.2declares esbuild as an optional peer:Nothing in the lock satisfies that edge at that location — top-level esbuild is
0.25.12, and the 0.28.1 is nested under
tsx. That is correct: an unsatisfiedoptional peer should be skipped. Older npm instead resolves it against the
registry (arborist's
loadPeerSet), picks up whatever is current — 0.28.2 —and then rejects the lock. That is also the install-vs-ci contradiction: the two
commands build the tree by different paths and disagree about optional peers.
npm does not document this difference;
npm-ci's docs never mention optionalpeers.
Fixed upstream by npm/cli#8981 (npm
11.10.1,
peerOptionalresolution inbuildIdealTree) andnpm/cli#9083 (npm 11.11.1,
cicountingthe lockfile's optional deps as missing). Node 24.15.0 is the first 24.x
bundling npm ≥ 11.11.1 (it ships 11.12.1).
This regressed at
7142704(vitest 3.2.7 → 4.1.10), which took vite 7.3.6 → 8.vite 7 had esbuild as a hard dependency and the lock carried
node_modules/vite/node_modules/esbuild@0.28.1; vite 8 made it an optionalpeer, and the entry correctly went away.
.nvmrcdeliberately stays bare24. The repo pins by major andscripts/check-node-version.shcompares majors only, so it still passes —pinning
.nvmrcto a minor is a known way to break every CI job atnpm ci.What CI cannot check
CI was never broken by this — that is the main thing to know.
.nvmrcisbare
24, sosetup-nodefloats to the newest 24.x. The last green run onanother PR:
Only environments on an old-but-in-range Node 24 were affected, which
engines.node: ">=24"permitted. Reproduced on node v24.0.0 / npm 11.3.0.The lockfile is already canonical. Regenerated with CI's npm on a pristine
clone:
Lockfile diff review — the trap. "Just regenerate the lock" is the wrong
fix. Regenerating under the affected npm 11.3.0:
That commits 27 packages CI does not install, to satisfy a peer that is
optional. Hence: no lockfile change here, and an explicit "don't" in
.npmrc.Why 24.15.0 and not the empirical boundary. Measured
npm ci --dry-runagainst this exact lockfile, pristine clone per run:
Today's lock in fact survives down to 24.13.1, but the passing range is not
contiguous (11.5.x/11.6.0 pass, 11.6.1/11.6.2 fail), so that boundary is
incidental to this tree shape — and dependabot reshapes it weekly. The floor is
set where npm documents the fix instead. Verified none of the passing npms
silently deviate from the lock (no 0.28.2 in any install plan).
The fix produces the intended failure. Same tree, node v24.0.0:
.npmrcalready hadengine-strict=true, so this is a hard error, not awarning — and it names Node instead of a phantom esbuild version.
Docker gate.
.npmrcis COPYed beforenpm ciin both stages withengine-strict on, so a floor above the base image would break the image build.
node:24-alpineresolves to v24.19.0;docker buildrun three times acrossthe edits, all
exited with code 0.Full suite, node 24.19.0 locally:
The one e2e failure is
e2e/not-found.spec.ts:229onexpect(seen.focused).toBe(true)— the documented ~40% pre-existing flake, notthis change. Alternated branch/base runs of the filtered spec,
--repeat-each=6each, per
docs/e2e-flake-triage.md:Both sides fail; indistinguishable. Mechanically a JSON
enginesstring cannotaffect a focus ring.
Deploy notes
None. No secret, no migration, no
fly.tomlchange. The engines floor isenforced at
npm citime only;node:24-alpineis already v24.19.0, so theimage build and the
db:migraterelease command are unaffected — verified bybuilding the image.
Flags
before anyone treats this as urgent: every job was green throughout. This is
a local-developer-experience fix plus a correctness fix to an understated
floor.
docs/ops.mdand theDockerfilecomment both stated the old>=24andwere invalidated by this diff. Both updated. The Dockerfile comment now
states "the engines floor" without repeating a value, so it cannot re-drift on
the next bump.
npm/cli#9846, affecting 11.16.0 and
12.0.2 for unavailable optional deps. It prints an empty version rather than
a concrete one, so it is a different variant, and CI's 11.17.0 is unaffected
today. Worth remembering if a similar EUSAGE reappears: raising the floor
further would not necessarily help.
not-foundfocus-ring flake (own change,per the triage doc) and the four moderate
npm auditadvisories thatnpm cireports.