Skip to content

Commit f52fb53

Browse files
committed
fix(@angular/cli): decouple official Node.js support check from execution gate
The `isNodeVersionSupported` function strictly validates whether a Node.js runtime matches an officially supported LTS track (such as `^22.22.3`, `^24.15.0`, or `^26.0.0`). Non-LTS odd releases (Node 23, 25) and uncertified newer releases (Node 27, 28+) return `false`, ensuring that `ng version` accurately marks them as unsupported. The `isNodeVersionRunnable` function dynamically determines whether an unsupported runtime can safely execute the CLI by checking if the major is an odd release between the minimum and maximum supported versions or higher than the maximum supported LTS major. The `bin/ng.js` bootstrap script now warns and continues execution on runnable unsupported runtimes instead of hardcoding specific major versions or aborting execution with an exit code of 3.
1 parent 5e05373 commit f52fb53

5 files changed

Lines changed: 166 additions & 22 deletions

File tree

‎packages/angular/cli/bin/ng.js‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,19 @@ if (rawCommandName === '--get-yargs-completions' || rawCommandName === 'completi
4343
// This node version check ensures that extremely old versions of node are not used.
4444
// These may not support ES2015 features such as const/let/async/await/etc.
4545
// These would then crash with a hard to diagnose error message.
46-
const [major] = process.versions.node.split('.', 1).map((part) => Number(part));
47-
48-
if (major === 23 || major === 25) {
49-
// Allow new odd numbered releases with a warning.
46+
if (nodeUtils.isNodeVersionSupported()) {
47+
require('./bootstrap');
48+
} else if (nodeUtils.isNodeVersionRunnable()) {
5049
console.warn(
5150
'Node.js version ' +
5251
process.version +
5352
' detected.\n' +
54-
'Prior to version 27, odd numbered Node.js versions will not enter LTS status and should not be used for production.' +
55-
' For more information, please see https://nodejs.org/en/about/previous-releases/.',
53+
'The Angular CLI has not been officially tested on this Node.js version and it is not supported for production use.\n' +
54+
'For more information, please see https://nodejs.org/en/about/previous-releases/.',
5655
);
5756

5857
require('./bootstrap');
59-
} else if (!nodeUtils.isNodeVersionSupported()) {
58+
} else {
6059
// Error and exit if less than the supported versions.
6160
console.error(
6261
'Node.js version ' +
@@ -69,6 +68,4 @@ if (major === 23 || major === 25) {
6968
);
7069

7170
process.exitCode = 3;
72-
} else {
73-
require('./bootstrap');
7471
}

‎packages/angular/cli/bin/version.js‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,24 @@ var supportedNodeVersions = SUPPORTED_NODE_VERSIONS.replace(/[\^~<>=]/g, '')
2727
});
2828

2929
/**
30-
* Checks if the current Node.js version is supported.
31-
* @returns `true` if the current Node.js version is supported, `false` otherwise.
30+
* Checks if the current Node.js version is officially supported.
31+
* @param {string} [currentVersion] Optional Node.js version string to check. Defaults to `process.versions.node`.
32+
* @param {string[]} [supportedVersions] Optional supported versions array. Defaults to `supportedNodeVersions`.
33+
* @returns {boolean} `true` if the current Node.js version is officially supported, `false` otherwise.
3234
*/
33-
function isNodeVersionSupported() {
34-
if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') {
35+
function isNodeVersionSupported(currentVersion, supportedVersions) {
36+
if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0' && !supportedVersions && !currentVersion) {
3537
return true;
3638
}
3739

38-
var parts = process.versions.node.split('.', 3).map(Number);
40+
var parts = (currentVersion || process.versions.node).split('.', 3).map(Number);
3941
var processMajor = parts[0];
4042
var processMinor = parts[1];
4143
var processPatch = parts[2];
4244

43-
for (var i = 0; i < supportedNodeVersions.length; i++) {
44-
var vParts = supportedNodeVersions[i].split('.', 3).map(Number);
45+
var versions = supportedVersions || supportedNodeVersions;
46+
for (var i = 0; i < versions.length; i++) {
47+
var vParts = versions[i].split('.', 3).map(Number);
4548
var major = vParts[0];
4649
var minor = vParts[1];
4750
var patch = vParts[2];
@@ -56,9 +59,40 @@ function isNodeVersionSupported() {
5659
return false;
5760
}
5861

62+
/**
63+
* Checks if the current Node.js version is runnable (newer than supported LTS or an interim odd release).
64+
* @param {string} [currentVersion] Optional Node.js version string to check. Defaults to `process.versions.node`.
65+
* @param {string[]} [supportedVersions] Optional supported versions array. Defaults to `supportedNodeVersions`.
66+
* @returns {boolean} `true` if the current Node.js version is runnable, `false` otherwise.
67+
*/
68+
function isNodeVersionRunnable(currentVersion, supportedVersions) {
69+
if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0' && !supportedVersions && !currentVersion) {
70+
return false;
71+
}
72+
73+
var processMajor = Number((currentVersion || process.versions.node).split('.', 1)[0]);
74+
var versions = supportedVersions || supportedNodeVersions;
75+
76+
var minMajor = Number(versions[0].split('.', 1)[0]);
77+
var maxMajor = Number(versions[versions.length - 1].split('.', 1)[0]);
78+
79+
// Versions newer than the maximum supported major (e.g. Node 27, 28+)
80+
if (processMajor > maxMajor) {
81+
return true;
82+
}
83+
84+
// Odd-numbered interim releases between min and max (e.g. Node 23, 25)
85+
if (processMajor > minMajor && processMajor % 2 === 1) {
86+
return true;
87+
}
88+
89+
return false;
90+
}
91+
5992
module.exports = {
6093
VERSION: VERSION,
6194
SUPPORTED_NODE_VERSIONS: SUPPORTED_NODE_VERSIONS,
6295
supportedNodeVersions: supportedNodeVersions,
6396
isNodeVersionSupported: isNodeVersionSupported,
97+
isNodeVersionRunnable: isNodeVersionRunnable,
6498
};

‎packages/angular/cli/src/typings.d.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,12 @@ declare module '#version' {
1515
export const VERSION: string;
1616
export const SUPPORTED_NODE_VERSIONS: string;
1717
export const supportedNodeVersions: string[];
18-
export function isNodeVersionSupported(): boolean;
18+
export function isNodeVersionSupported(
19+
currentVersion?: string,
20+
supportedVersions?: string[],
21+
): boolean;
22+
export function isNodeVersionRunnable(
23+
currentVersion?: string,
24+
supportedVersions?: string[],
25+
): boolean;
1926
}

‎packages/angular/cli/src/utilities/node-version.ts‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,32 @@
1313

1414
import { SUPPORTED_NODE_VERSIONS, supportedNodeVersions } from '#version';
1515

16-
export { SUPPORTED_NODE_VERSIONS, supportedNodeVersions, isNodeVersionSupported } from '#version';
16+
export {
17+
SUPPORTED_NODE_VERSIONS,
18+
supportedNodeVersions,
19+
isNodeVersionSupported,
20+
isNodeVersionRunnable,
21+
} from '#version';
1722

1823
/**
1924
* Checks if the current Node.js version is the minimum supported version.
25+
* @param currentVersion Optional Node.js version string to check. Defaults to `process.versions.node`.
26+
* @param supportedVersions Optional supported versions array. Defaults to `supportedNodeVersions`.
2027
* @returns `true` if the current Node.js version is the minimum supported version, `false` otherwise.
2128
*/
22-
export function isNodeVersionMinSupported(): boolean {
23-
if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') {
29+
export function isNodeVersionMinSupported(
30+
currentVersion = process.versions.node,
31+
supportedVersions = supportedNodeVersions,
32+
): boolean {
33+
if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0' && currentVersion === process.versions.node) {
2434
// Unlike `pkg_npm`, `ts_library` which is used to run unit tests does not support substitutions.
2535
return true;
2636
}
2737

28-
const [processMajor, processMinor, processPatch] = process.versions.node
38+
const [processMajor, processMinor, processPatch] = currentVersion
2939
.split('.', 3)
3040
.map((part) => Number(part));
31-
const [major, minor, patch] = supportedNodeVersions[0].split('.', 3).map((part) => Number(part));
41+
const [major, minor, patch] = supportedVersions[0].split('.', 3).map((part) => Number(part));
3242

3343
return (
3444
processMajor > major ||
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
isNodeVersionMinSupported,
11+
isNodeVersionRunnable,
12+
isNodeVersionSupported,
13+
} from './node-version';
14+
15+
describe('node-version', () => {
16+
const supportedVersions = ['22.22.3', '24.15.0', '26.0.0'];
17+
18+
describe('isNodeVersionSupported', () => {
19+
it('should accept valid LTS versions', () => {
20+
expect(isNodeVersionSupported('22.22.3', supportedVersions)).toBeTrue();
21+
expect(isNodeVersionSupported('22.23.0', supportedVersions)).toBeTrue();
22+
expect(isNodeVersionSupported('24.15.0', supportedVersions)).toBeTrue();
23+
expect(isNodeVersionSupported('24.16.0', supportedVersions)).toBeTrue();
24+
expect(isNodeVersionSupported('26.0.0', supportedVersions)).toBeTrue();
25+
expect(isNodeVersionSupported('26.1.0', supportedVersions)).toBeTrue();
26+
});
27+
28+
it('should reject versions below the minimum for a supported major', () => {
29+
expect(isNodeVersionSupported('22.22.2', supportedVersions)).toBeFalse();
30+
expect(isNodeVersionSupported('22.0.0', supportedVersions)).toBeFalse();
31+
expect(isNodeVersionSupported('24.14.9', supportedVersions)).toBeFalse();
32+
expect(isNodeVersionSupported('20.18.0', supportedVersions)).toBeFalse();
33+
});
34+
35+
it('should reject odd-numbered major versions', () => {
36+
expect(isNodeVersionSupported('23.0.0', supportedVersions)).toBeFalse();
37+
expect(isNodeVersionSupported('25.0.0', supportedVersions)).toBeFalse();
38+
});
39+
40+
it('should reject versions higher than the maximum supported LTS version', () => {
41+
expect(isNodeVersionSupported('27.0.0', supportedVersions)).toBeFalse();
42+
expect(isNodeVersionSupported('28.0.0', supportedVersions)).toBeFalse();
43+
expect(isNodeVersionSupported('29.1.2', supportedVersions)).toBeFalse();
44+
});
45+
46+
it('should return true when unstamped (0.0.0-ENGINES-NODE)', () => {
47+
expect(isNodeVersionSupported()).toBeTrue();
48+
});
49+
});
50+
51+
describe('isNodeVersionRunnable', () => {
52+
it('should allow odd-numbered interim releases between min and max major', () => {
53+
expect(isNodeVersionRunnable('23.0.0', supportedVersions)).toBeTrue();
54+
expect(isNodeVersionRunnable('25.0.0', supportedVersions)).toBeTrue();
55+
});
56+
57+
it('should allow versions higher than the maximum supported major', () => {
58+
expect(isNodeVersionRunnable('27.0.0', supportedVersions)).toBeTrue();
59+
expect(isNodeVersionRunnable('28.0.0', supportedVersions)).toBeTrue();
60+
expect(isNodeVersionRunnable('29.1.2', supportedVersions)).toBeTrue();
61+
});
62+
63+
it('should return false for supported LTS versions', () => {
64+
expect(isNodeVersionRunnable('22.22.3', supportedVersions)).toBeFalse();
65+
expect(isNodeVersionRunnable('24.15.0', supportedVersions)).toBeFalse();
66+
expect(isNodeVersionRunnable('26.0.0', supportedVersions)).toBeFalse();
67+
});
68+
69+
it('should return false for versions below the minimum major', () => {
70+
expect(isNodeVersionRunnable('18.0.0', supportedVersions)).toBeFalse();
71+
expect(isNodeVersionRunnable('20.0.0', supportedVersions)).toBeFalse();
72+
expect(isNodeVersionRunnable('21.0.0', supportedVersions)).toBeFalse();
73+
});
74+
75+
it('should return false for outdated minors of supported majors', () => {
76+
expect(isNodeVersionRunnable('22.0.0', supportedVersions)).toBeFalse();
77+
expect(isNodeVersionRunnable('24.10.0', supportedVersions)).toBeFalse();
78+
});
79+
});
80+
81+
describe('isNodeVersionMinSupported', () => {
82+
it('should accept versions at or above the minimum supported version', () => {
83+
expect(isNodeVersionMinSupported('22.22.3', supportedVersions)).toBeTrue();
84+
expect(isNodeVersionMinSupported('22.25.0', supportedVersions)).toBeTrue();
85+
expect(isNodeVersionMinSupported('23.0.0', supportedVersions)).toBeTrue();
86+
expect(isNodeVersionMinSupported('24.0.0', supportedVersions)).toBeTrue();
87+
expect(isNodeVersionMinSupported('28.0.0', supportedVersions)).toBeTrue();
88+
});
89+
90+
it('should reject versions below the minimum supported version', () => {
91+
expect(isNodeVersionMinSupported('22.22.2', supportedVersions)).toBeFalse();
92+
expect(isNodeVersionMinSupported('20.0.0', supportedVersions)).toBeFalse();
93+
expect(isNodeVersionMinSupported('18.19.0', supportedVersions)).toBeFalse();
94+
});
95+
});
96+
});

0 commit comments

Comments
 (0)