Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion npm/packages/cli/bin/xgen.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

const { spawnSync } = require('node:child_process');
const catalog = require('../platforms.json');
const { resolvePlatformBinary, selectPlatform } = require('../lib/platform.cjs');
const manifest = require('../package.json');
const { checkNodeVersion, resolvePlatformBinary, selectPlatform } = require('../lib/platform.cjs');

let specification;
let binary;
try {
checkNodeVersion(manifest.engines.node, process.version);
specification = selectPlatform(catalog, process.platform, process.arch);
binary = resolvePlatformBinary(specification);
} catch (error) {
Expand Down
28 changes: 27 additions & 1 deletion npm/packages/cli/lib/platform.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
'use strict';

// Optional platform packages declare the same engines range, so npm skips them
// silently on an older Node.js. Check first so that case is not reported as
// a missing optional dependency.
function checkNodeVersion(engine, version) {
const required = /^>=(\d+)\.(\d+)\.(\d+)$/.exec(engine);
if (!required) {
throw new Error('XGEN npm launcher engine range is invalid');
}
const current = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
if (current) {
const difference = [1, 2, 3]
.map((index) => Number(current[index]) - Number(required[index]))
.find((value) => value !== 0);
if (difference === undefined || difference > 0) {
return;
}
}
const minimum = engine.slice(2);
const error = new Error(
`XGEN requires Node.js ${minimum} or later (current: ${version}). ` +
'Upgrade Node.js, then reinstall @xgen-ai/cli.',
);
error.code = 'XGEN_UNSUPPORTED_NODE';
throw error;
}

function selectPlatform(catalog, platform, architecture) {
if (!catalog || catalog.schemaVersion !== 1 || !Array.isArray(catalog.platforms)) {
throw new Error('XGEN npm platform catalog is invalid');
Expand Down Expand Up @@ -31,4 +57,4 @@ function resolvePlatformBinary(specification, resolver = require.resolve) {
}
}

module.exports = { resolvePlatformBinary, selectPlatform };
module.exports = { checkNodeVersion, resolvePlatformBinary, selectPlatform };
40 changes: 39 additions & 1 deletion npm/tests/platform.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ const require = createRequire(import.meta.url);
const catalog = JSON.parse(
await readFile(new URL('../platforms.json', import.meta.url), 'utf8'),
);
const { resolvePlatformBinary, selectPlatform } = require('../packages/cli/lib/platform.cjs');
const { checkNodeVersion, resolvePlatformBinary, selectPlatform } = require(
'../packages/cli/lib/platform.cjs',
);
const launcher = JSON.parse(
await readFile(new URL('../packages/cli/package.json', import.meta.url), 'utf8'),
);

const expected = [
['linux', 'x64', '@xgen-ai/cli-linux-x64-musl', 'bin/xgen'],
Expand Down Expand Up @@ -62,3 +67,36 @@ test('missing optional dependency produces a bounded recovery message', () => {
!error.message.includes('host-specific'),
);
});

test('launcher accepts Node.js versions at or above the declared engine', () => {
const engine = launcher.engines.node;
for (const version of ['v22.14.0', 'v22.14.1', 'v22.22.1', 'v23.0.0', 'v24.1.0']) {
assert.doesNotThrow(() => checkNodeVersion(engine, version), version);
}
assert.doesNotThrow(() => checkNodeVersion(engine, process.version));
});

test('older Node.js is reported before the missing native package', () => {
const engine = launcher.engines.node;
for (const version of ['v22.13.1', 'v22.0.0', 'v20.18.0', 'v18.20.4', 'not-a-version']) {
assert.throws(
() => checkNodeVersion(engine, version),
(error) =>
error.code === 'XGEN_UNSUPPORTED_NODE' &&
error.message ===
`XGEN requires Node.js 22.14.0 or later (current: ${version}). ` +
'Upgrade Node.js, then reinstall @xgen-ai/cli.',
version,
);
}
});

test('launcher rejects an engine range it cannot evaluate', () => {
for (const engine of ['^22.14.0', '>=22.14', '>=22.14.0 <25']) {
assert.throws(
() => checkNodeVersion(engine, 'v22.14.0'),
/XGEN npm launcher engine range is invalid/,
engine,
);
}
});