diff --git a/packages/angular/cli/bin/ng.js b/packages/angular/cli/bin/ng.js index a95f51b8def4..d503fb895f5f 100755 --- a/packages/angular/cli/bin/ng.js +++ b/packages/angular/cli/bin/ng.js @@ -43,20 +43,19 @@ if (rawCommandName === '--get-yargs-completions' || rawCommandName === 'completi // This node version check ensures that extremely old versions of node are not used. // These may not support ES2015 features such as const/let/async/await/etc. // These would then crash with a hard to diagnose error message. -const [major] = process.versions.node.split('.', 1).map((part) => Number(part)); - -if (major === 23 || major === 25) { - // Allow new odd numbered releases with a warning. +if (nodeUtils.isNodeVersionSupported()) { + require('./bootstrap'); +} else if (nodeUtils.isNodeVersionRunnable()) { console.warn( 'Node.js version ' + process.version + ' detected.\n' + - 'Prior to version 27, odd numbered Node.js versions will not enter LTS status and should not be used for production.' + - ' For more information, please see https://nodejs.org/en/about/previous-releases/.', + 'The Angular CLI has not been officially tested on this Node.js version and it is not supported for production use.\n' + + 'For more information, please see https://nodejs.org/en/about/previous-releases/.', ); require('./bootstrap'); -} else if (!nodeUtils.isNodeVersionSupported()) { +} else { // Error and exit if less than the supported versions. console.error( 'Node.js version ' + @@ -69,6 +68,4 @@ if (major === 23 || major === 25) { ); process.exitCode = 3; -} else { - require('./bootstrap'); } diff --git a/packages/angular/cli/bin/version.js b/packages/angular/cli/bin/version.js index 38edd4e299c7..1755c1ed55c8 100644 --- a/packages/angular/cli/bin/version.js +++ b/packages/angular/cli/bin/version.js @@ -27,21 +27,24 @@ var supportedNodeVersions = SUPPORTED_NODE_VERSIONS.replace(/[\^~<>=]/g, '') }); /** - * Checks if the current Node.js version is supported. - * @returns `true` if the current Node.js version is supported, `false` otherwise. + * Checks if the current Node.js version is officially supported. + * @param {string} [currentVersion] Optional Node.js version string to check. Defaults to `process.versions.node`. + * @param {string[]} [supportedVersions] Optional supported versions array. Defaults to `supportedNodeVersions`. + * @returns {boolean} `true` if the current Node.js version is officially supported, `false` otherwise. */ -function isNodeVersionSupported() { - if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') { +function isNodeVersionSupported(currentVersion, supportedVersions) { + if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0' && !supportedVersions && !currentVersion) { return true; } - var parts = process.versions.node.split('.', 3).map(Number); + var parts = (currentVersion || process.versions.node).split('.', 3).map(Number); var processMajor = parts[0]; var processMinor = parts[1]; var processPatch = parts[2]; - for (var i = 0; i < supportedNodeVersions.length; i++) { - var vParts = supportedNodeVersions[i].split('.', 3).map(Number); + var versions = supportedVersions || supportedNodeVersions; + for (var i = 0; i < versions.length; i++) { + var vParts = versions[i].split('.', 3).map(Number); var major = vParts[0]; var minor = vParts[1]; var patch = vParts[2]; @@ -56,9 +59,40 @@ function isNodeVersionSupported() { return false; } +/** + * Checks if the current Node.js version is runnable (newer than supported LTS or an interim odd release). + * @param {string} [currentVersion] Optional Node.js version string to check. Defaults to `process.versions.node`. + * @param {string[]} [supportedVersions] Optional supported versions array. Defaults to `supportedNodeVersions`. + * @returns {boolean} `true` if the current Node.js version is runnable, `false` otherwise. + */ +function isNodeVersionRunnable(currentVersion, supportedVersions) { + if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0' && !supportedVersions && !currentVersion) { + return false; + } + + var processMajor = Number((currentVersion || process.versions.node).split('.', 1)[0]); + var versions = supportedVersions || supportedNodeVersions; + + var minMajor = Number(versions[0].split('.', 1)[0]); + var maxMajor = Number(versions[versions.length - 1].split('.', 1)[0]); + + // Versions newer than the maximum supported major (e.g. Node 27, 28+) + if (processMajor > maxMajor) { + return true; + } + + // Odd-numbered interim releases between min and max (e.g. Node 23, 25) + if (processMajor > minMajor && processMajor % 2 === 1) { + return true; + } + + return false; +} + module.exports = { VERSION: VERSION, SUPPORTED_NODE_VERSIONS: SUPPORTED_NODE_VERSIONS, supportedNodeVersions: supportedNodeVersions, isNodeVersionSupported: isNodeVersionSupported, + isNodeVersionRunnable: isNodeVersionRunnable, }; diff --git a/packages/angular/cli/src/typings.d.ts b/packages/angular/cli/src/typings.d.ts index 948d7b1d2280..43b81c1b0229 100644 --- a/packages/angular/cli/src/typings.d.ts +++ b/packages/angular/cli/src/typings.d.ts @@ -15,5 +15,12 @@ declare module '#version' { export const VERSION: string; export const SUPPORTED_NODE_VERSIONS: string; export const supportedNodeVersions: string[]; - export function isNodeVersionSupported(): boolean; + export function isNodeVersionSupported( + currentVersion?: string, + supportedVersions?: string[], + ): boolean; + export function isNodeVersionRunnable( + currentVersion?: string, + supportedVersions?: string[], + ): boolean; } diff --git a/packages/angular/cli/src/utilities/node-version.ts b/packages/angular/cli/src/utilities/node-version.ts index 18f06b960369..87eb4fa377b6 100644 --- a/packages/angular/cli/src/utilities/node-version.ts +++ b/packages/angular/cli/src/utilities/node-version.ts @@ -13,22 +13,32 @@ import { SUPPORTED_NODE_VERSIONS, supportedNodeVersions } from '#version'; -export { SUPPORTED_NODE_VERSIONS, supportedNodeVersions, isNodeVersionSupported } from '#version'; +export { + SUPPORTED_NODE_VERSIONS, + supportedNodeVersions, + isNodeVersionSupported, + isNodeVersionRunnable, +} from '#version'; /** * Checks if the current Node.js version is the minimum supported version. + * @param currentVersion Optional Node.js version string to check. Defaults to `process.versions.node`. + * @param supportedVersions Optional supported versions array. Defaults to `supportedNodeVersions`. * @returns `true` if the current Node.js version is the minimum supported version, `false` otherwise. */ -export function isNodeVersionMinSupported(): boolean { - if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0') { +export function isNodeVersionMinSupported( + currentVersion = process.versions.node, + supportedVersions = supportedNodeVersions, +): boolean { + if (SUPPORTED_NODE_VERSIONS.charAt(0) === '0' && currentVersion === process.versions.node) { // Unlike `pkg_npm`, `ts_library` which is used to run unit tests does not support substitutions. return true; } - const [processMajor, processMinor, processPatch] = process.versions.node + const [processMajor, processMinor, processPatch] = currentVersion .split('.', 3) .map((part) => Number(part)); - const [major, minor, patch] = supportedNodeVersions[0].split('.', 3).map((part) => Number(part)); + const [major, minor, patch] = supportedVersions[0].split('.', 3).map((part) => Number(part)); return ( processMajor > major || diff --git a/packages/angular/cli/src/utilities/node-version_spec.ts b/packages/angular/cli/src/utilities/node-version_spec.ts new file mode 100644 index 000000000000..9426521faf65 --- /dev/null +++ b/packages/angular/cli/src/utilities/node-version_spec.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { + isNodeVersionMinSupported, + isNodeVersionRunnable, + isNodeVersionSupported, +} from './node-version'; + +describe('node-version', () => { + const supportedVersions = ['22.22.3', '24.15.0', '26.0.0']; + + describe('isNodeVersionSupported', () => { + it('should accept valid LTS versions', () => { + expect(isNodeVersionSupported('22.22.3', supportedVersions)).toBeTrue(); + expect(isNodeVersionSupported('22.23.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionSupported('24.15.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionSupported('24.16.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionSupported('26.0.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionSupported('26.1.0', supportedVersions)).toBeTrue(); + }); + + it('should reject versions below the minimum for a supported major', () => { + expect(isNodeVersionSupported('22.22.2', supportedVersions)).toBeFalse(); + expect(isNodeVersionSupported('22.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionSupported('24.14.9', supportedVersions)).toBeFalse(); + expect(isNodeVersionSupported('20.18.0', supportedVersions)).toBeFalse(); + }); + + it('should reject odd-numbered major versions', () => { + expect(isNodeVersionSupported('23.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionSupported('25.0.0', supportedVersions)).toBeFalse(); + }); + + it('should reject versions higher than the maximum supported LTS version', () => { + expect(isNodeVersionSupported('27.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionSupported('28.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionSupported('29.1.2', supportedVersions)).toBeFalse(); + }); + + it('should return true when unstamped (0.0.0-ENGINES-NODE)', () => { + expect(isNodeVersionSupported()).toBeTrue(); + }); + }); + + describe('isNodeVersionRunnable', () => { + it('should allow odd-numbered interim releases between min and max major', () => { + expect(isNodeVersionRunnable('23.0.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionRunnable('25.0.0', supportedVersions)).toBeTrue(); + }); + + it('should allow versions higher than the maximum supported major', () => { + expect(isNodeVersionRunnable('27.0.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionRunnable('28.0.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionRunnable('29.1.2', supportedVersions)).toBeTrue(); + }); + + it('should return false for supported LTS versions', () => { + expect(isNodeVersionRunnable('22.22.3', supportedVersions)).toBeFalse(); + expect(isNodeVersionRunnable('24.15.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionRunnable('26.0.0', supportedVersions)).toBeFalse(); + }); + + it('should return false for versions below the minimum major', () => { + expect(isNodeVersionRunnable('18.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionRunnable('20.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionRunnable('21.0.0', supportedVersions)).toBeFalse(); + }); + + it('should return false for outdated minors of supported majors', () => { + expect(isNodeVersionRunnable('22.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionRunnable('24.10.0', supportedVersions)).toBeFalse(); + }); + }); + + describe('isNodeVersionMinSupported', () => { + it('should accept versions at or above the minimum supported version', () => { + expect(isNodeVersionMinSupported('22.22.3', supportedVersions)).toBeTrue(); + expect(isNodeVersionMinSupported('22.25.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionMinSupported('23.0.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionMinSupported('24.0.0', supportedVersions)).toBeTrue(); + expect(isNodeVersionMinSupported('28.0.0', supportedVersions)).toBeTrue(); + }); + + it('should reject versions below the minimum supported version', () => { + expect(isNodeVersionMinSupported('22.22.2', supportedVersions)).toBeFalse(); + expect(isNodeVersionMinSupported('20.0.0', supportedVersions)).toBeFalse(); + expect(isNodeVersionMinSupported('18.19.0', supportedVersions)).toBeFalse(); + }); + }); +});