Files
reverse-engineering-anything/scripts/lib/runtime-admission.mjs
T
dbfab4a65d fix(cli): refuse an unsupported Node.js release before linking (#9297)
* fix(cli): refuse an unsupported Node.js release before linking

The dispatcher imported the compiled runtime before checking the runtime
range, so a release outside `engines.node` failed while linking a module
that imports `createZstdDecompress`. Callers saw a module-linking stack
trace from every command, including `--version` and `doctor`, instead of
the refusal the installer and `engines` already promise.

Check `engines.node` from the package read the dispatcher already performs,
before the first compiled import. The helper has no imports of its own: the
dispatcher must admit the runtime before any dependency can be resolved.

Fixes #9296

* fix(cli): ship and align runtime admission

* fix(ci): cover packaged runtime admission helper

* test(cli): assert Node admission with structured range results

Export satisfiesNodeRange and cover caret, minimum, union, and
non-release versions. The dispatcher proves the compiled runtime never
loads by the absence of its marker file, and compares stderr to the
helper's own message.

---------

Co-authored-by: 2SinceJuly <202121119+2SinceJuly@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-10 13:53:58 -05:00

55 lines
2.2 KiB
JavaScript

/**
* Reject a Node.js release the compiled runtime cannot link against.
*
* The dispatcher imports compiled modules before it can report anything, and
* those modules import Node APIs that some releases do not export. Checking the
* published range here keeps that failure a readable refusal instead of a
* module-linking stack trace.
*
* `range` is the package's own `engines.node`, so the dispatcher and the
* installer cannot drift apart. This module deliberately has no imports: the
* dispatcher must admit the runtime before any dependency, including `semver`,
* can be resolved.
*/
export const unsupportedRuntimeMessage = (range, version) => {
if (typeof range !== "string" || range.length === 0) return undefined;
if (satisfiesNodeRange(range, version)) return undefined;
return (
`REA requires ${supportedReleaseProse(range)}; this process runs Node.js ${version}. ` +
"Install a supported release, then retry."
);
};
/** Human-readable release families for an `engines.node` range. */
export const supportedReleaseProse = (range) =>
`Node.js ${range
.split("||")
.map((clause) => {
const floor = /^(\^|>=)\s*(\d+\.\d+\.\d+)$/.exec(clause.trim());
if (floor === null) return clause.trim();
return floor[1] === ">="
? `${floor[2]}+`
: `${floor[2].split(".")[0]}.x (>=${floor[2]})`;
})
.join(", ")}`;
const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
const FLOOR = /^(>=|\^)\s*(\d+)\.(\d+)\.(\d+)$/;
/** Every clause of the published range is `^major.minor.patch` or `>=`. */
export const satisfiesNodeRange = (range, version) => {
const found = VERSION.exec(version);
// A prerelease or a non-release spelling is never an admitted release.
if (found === null) return false;
const [, major, minor, patch] = found;
return range.split("||").some((clause) => {
const floor = FLOOR.exec(clause.trim());
if (floor === null) return false;
const [, operator, floorMajor, floorMinor, floorPatch] = floor;
if (Number(major) !== Number(floorMajor))
return operator === ">=" && Number(major) > Number(floorMajor);
if (minor !== floorMinor) return Number(minor) > Number(floorMinor);
return Number(patch) >= Number(floorPatch);
});
};