4.2 KiB
RFC: Lower the Node engines floor to 22.18
Status: implemented
Problem
The root engines.node was >=24, which excluded the entire Node 22 LTS line for no runtime reason. The harness has exactly two Node features whose availability gates the floor, and both are satisfied well below Node 24 — so the floor was higher than the code actually requires. Pinning it honestly widens the supported install base (Node 22 LTS is in service until 2027) without weakening any guarantee, provided CI proves the claim on the floor version rather than merely asserting it in a manifest.
Decision
Set engines.node to >=22.18 and treat 22.18 as the tested floor everywhere (CI matrix ['22.18', 24, 26], the real-API e2e job on ['22.18', 24] — floor plus primary line, since the keyless matrix exercises only the mock adapter and the live fetch/SSE path runs only in e2e). 22.18 is the later of the two feature boundaries the code depends on, so it is the earliest Node version where everything the repo ships and tests runs unflagged:
node:sqlite— Node 22.13.packages/session-persistence/session-persistence-sqlitedoes a top-levelimport { DatabaseSync } from 'node:sqlite'. The module dropped its--experimental-sqliteflag requirement in Node 22.13 (backport of the 23.4 change), so any floor ≥ 22.13 loads it without a flag.- Native TypeScript type-stripping — Node 22.18. The
packages/ui/stdio-agent/tests/built-bin.e2e.tssmoke boots the publishedlib/bin.jsunder plainnodeand loads the example's.tsplugins (mock-llm.ts,echo-tool.ts) with no tsx. Native type-stripping — which makes that work — was unflagged in the 22.x LTS line only in 22.18 (before that it needed--experimental-strip-types). This is the binding constraint, so it sets the floor.
@types/node is pinned to the 22.x line (^22.20.0) to match the floor: reaching for a Node 23+/24+/25+ API then fails tsc on every machine and in the typecheck gate, rather than compiling clean and surviving to a runtime failure only the 22.18 matrix leg could catch. The whole tree typechecks clean against the Node 22 type surface today, so the pin costs nothing.
Consequences
- The supported base widens to the Node 22 LTS line, and the
['22.18', 24, 26]matrix proves it on every push and PR rather than trusting the manifest. - The built-bin smoke needs no version-conditional flag: at 22.18 type-stripping is already the default, so the test stays the plain
node lib/bin.jspath it documents. - A future change reaching for a Node 23+ API fails
tscimmediately (the@types/nodepin); one reaching for an API added in 22.19/22.20 — inside the 22.x type surface but above the floor — is caught instead by the 22.18 matrix leg. Either way the floor must move in the same change. - The
vendor/hmrandvendor/loadercomments about Node 24 module-cache internals are unaffected — they describe dev-time HMR loader behavior, not the shipped runtime contract, and are pinned vendored source.
Alternatives considered
- Floor
>=22.13(thenode:sqliteboundary) plus--experimental-strip-typesin the built-bin smoke on 22.13–22.17. Rejected: it adds a version-conditional test flag for one narrow range and dresses up an experimental-flag dependency as first-class support. 22.18 clears both boundaries with zero test special-casing, and the five-patch gap below it buys nothing real. - Keep
>=24. Rejected: it excludes Node 22 LTS with no runtime justification once the two boundaries above are known. - Matrix
[22, 24, 26](latest 22.x) instead of pinning22.18. Rejected: "latest 22.x" drifts upward over time and would silently stop exercising the declared floor. Pinning the floor version is what makes the matrix a proof of the claim rather than a proof of some newer 22.x. - Keep
@types/nodeahead of the floor (^25). Rejected: types ahead of the runtime floor let a Node 24/25-only API compile clean and fail only at runtime on 22.18 — exactly the "green types, broken product" gap. Pinning@types/nodeto the 22.x line turns that into a compile error everywhere, and the tree already typechecks clean against the Node 22 surface, so the pin is free.