Files
OpenMAIC/scripts/check-node-engine-contract.mjs
T
Zhenghan Songandwyuc ca3d785221 build(node): enforce direct dependency engine floor (#1337)
Why:
- The root Node 20.9 contract predates direct runtime dependencies whose declared minimums now reach Node 22.19.
- README, contributor, localized docs, and the OpenMAIC extension skill repeated the stale supported-version claim.

What:
- Raise the root Node minimum to 22.19 and align every operator, contributor, locale, and skill prerequisite.
- Add a check that compares the root minimum with installed direct production dependency engine minimums.
- Run the new contract check in CI after the frozen dependency install.

Risk:
- This changes the declared minimum only; no upper bound is added and Node 24 compatibility remains a separate concern.
- The localized docs build was validated with the independent #1306 boundary fix from PR #1307, which is not included here.

Tests:
- RED on the base: engine check reported pi-agent-core, pi-ai, svg-pathdata, and undici floors
- GREEN: root minimum 22.19 satisfies 35 engine-constrained direct dependencies
- Node 20 lockfile-only install reports the root unsupported-engine warning
- Node 22 frozen install and postinstall
- Docs build with PR #1307 boundary: 34 pages and all locale postexport checks; docs types:check
- Root Prettier, ESLint, TypeScript, i18n, package-version, and internal-dependency gates
- Root pnpm test: 7137 passed, 81 skipped

Live Docs:
- GitHub issue #1304 tracks the Node contract; #1306 / PR #1307 tracks the separate docs-build prerequisite.

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-02 04:29:52 -04:00

74 lines
2.3 KiB
JavaScript

import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import semver from 'semver';
const repositoryRoot = dirname(fileURLToPath(new URL('../package.json', import.meta.url)));
function readManifest(path) {
return JSON.parse(readFileSync(path, 'utf8'));
}
function fail(headline, problems) {
console.error([headline, ...problems.map((problem) => `- ${problem}`)].join('\n'));
process.exit(1);
}
const rootManifest = readManifest(join(repositoryRoot, 'package.json'));
const rootRange = rootManifest.engines?.node;
const rootMinimum = rootRange ? semver.minVersion(rootRange) : null;
if (!rootRange || rootMinimum === null) {
fail('The root Node engine contract is invalid:', [
`package.json engines.node must be a valid semver range, got ${JSON.stringify(rootRange)}.`,
]);
}
const failures = [];
let constrainedDependencies = 0;
for (const dependency of Object.keys(rootManifest.dependencies ?? {}).sort()) {
const manifestPath = join(
repositoryRoot,
'node_modules',
...dependency.split('/'),
'package.json',
);
let manifest;
try {
manifest = readManifest(manifestPath);
} catch (error) {
failures.push(
`cannot read installed manifest for ${dependency} at ${manifestPath}: ${String(error)}. ` +
'Run pnpm install before this check.',
);
continue;
}
const dependencyRange = manifest.engines?.node;
if (!dependencyRange) continue;
constrainedDependencies += 1;
const dependencyMinimum = semver.minVersion(dependencyRange);
if (dependencyMinimum === null) {
failures.push(
`${dependency} declares an invalid engines.node range: ${JSON.stringify(dependencyRange)}.`,
);
continue;
}
if (semver.lt(rootMinimum, dependencyMinimum)) {
failures.push(
`root engines.node ${JSON.stringify(rootRange)} starts at ${rootMinimum.version}, below ` +
`${dependency}@${manifest.version} engines.node ${JSON.stringify(dependencyRange)} ` +
`(minimum ${dependencyMinimum.version}).`,
);
}
}
if (failures.length > 0) {
fail('The root Node engine minimum is below its direct production dependencies:', failures);
}
console.log(
`Node engine contract passed: root minimum ${rootMinimum.version} satisfies ` +
`${constrainedDependencies} engine-constrained direct production dependencies.`,
);