diff --git a/packages/angular/cli/src/commands/version/cli.ts b/packages/angular/cli/src/commands/version/cli.ts index 80f3f87d2e92..b3624a9a06ce 100644 --- a/packages/angular/cli/src/commands/version/cli.ts +++ b/packages/angular/cli/src/commands/version/cli.ts @@ -6,37 +6,30 @@ * found in the LICENSE file at https://angular.dev/license */ -import nodeModule from 'node:module'; -import { resolve } from 'node:path'; -import { Argv } from 'yargs'; +import type { Argv } from 'yargs'; import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; import { colors } from '../../utilities/color'; import { RootCommands } from '../command-config'; - -interface PartialPackageInfo { - name: string; - version: string; - dependencies?: Record; - devDependencies?: Record; -} +import { VersionInfo, gatherVersionInfo } from './version-info'; /** - * Major versions of Node.js that are officially supported by Angular. + * The Angular CLI logo, displayed as ASCII art. */ -const SUPPORTED_NODE_MAJORS = [20, 22, 24]; - -const PACKAGE_PATTERNS = [ - /^@angular\/.*/, - /^@angular-devkit\/.*/, - /^@ngtools\/.*/, - /^@schematics\/.*/, - /^rxjs$/, - /^typescript$/, - /^ng-packagr$/, - /^webpack$/, - /^zone\.js$/, -]; +const ASCII_ART = ` + _ _ ____ _ ___ + / \\ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _| + / △ \\ | '_ \\ / _\` | | | | |/ _\` | '__| | | | | | | + / ___ \\| | | | (_| | |_| | | (_| | | | |___| |___ | | + /_/ \\_\\_| |_|\\__, |\\__,_|_|\\__,_|_| \\____|_____|___| + |___/ + ` + .split('\n') + .map((x) => colors.red(x)) + .join('\n'); +/** + * The command-line module for the `ng version` command. + */ export default class VersionCommandModule extends CommandModule implements CommandModuleImplementation @@ -46,146 +39,108 @@ export default class VersionCommandModule describe = 'Outputs Angular CLI version.'; longDescriptionPath?: string | undefined; + /** + * Builds the command-line options for the `ng version` command. + * @param localYargs The `yargs` instance to configure. + * @returns The configured `yargs` instance. + */ builder(localYargs: Argv): Argv { return localYargs; } + /** + * The main execution logic for the `ng version` command. + */ async run(): Promise { - const { packageManager, logger, root } = this.context; - const localRequire = nodeModule.createRequire(resolve(__filename, '../../../')); - // Trailing slash is used to allow the path to be treated as a directory - const workspaceRequire = nodeModule.createRequire(root + '/'); - - const cliPackage: PartialPackageInfo = localRequire('./package.json'); - let workspacePackage: PartialPackageInfo | undefined; - try { - workspacePackage = workspaceRequire('./package.json'); - } catch {} - - const [nodeMajor] = process.versions.node.split('.').map((part) => Number(part)); - const unsupportedNodeVersion = !SUPPORTED_NODE_MAJORS.includes(nodeMajor); - - const packageNames = new Set( - Object.keys({ - ...cliPackage.dependencies, - ...cliPackage.devDependencies, - ...workspacePackage?.dependencies, - ...workspacePackage?.devDependencies, - }), - ); - - const versions: Record = {}; - for (const name of packageNames) { - if (PACKAGE_PATTERNS.some((p) => p.test(name))) { - versions[name] = this.getVersion(name, workspaceRequire, localRequire); - } - } + const { logger } = this.context; + const versionInfo = gatherVersionInfo(this.context); + const { + ngCliVersion, + nodeVersion, + unsupportedNodeVersion, + packageManagerName, + packageManagerVersion, + os, + arch, + versions, + } = versionInfo; + + const header = ` + Angular CLI: ${ngCliVersion} + Node: ${nodeVersion}${unsupportedNodeVersion ? ' (Unsupported)' : ''} + Package Manager: ${packageManagerName} ${packageManagerVersion ?? ''} + OS: ${os} ${arch} + `.replace(/^ {6}/gm, ''); - const ngCliVersion = cliPackage.version; - let angularCoreVersion = ''; - const angularSameAsCore: string[] = []; - - if (workspacePackage) { - // Filter all angular versions that are the same as core. - angularCoreVersion = versions['@angular/core']; - if (angularCoreVersion) { - for (const [name, version] of Object.entries(versions)) { - if (version === angularCoreVersion && name.startsWith('@angular/')) { - angularSameAsCore.push(name.replace(/^@angular\//, '')); - delete versions[name]; - } - } + const angularPackages = this.formatAngularPackages(versionInfo); + const packageTable = this.formatPackageTable(versions); - // Make sure we list them in alphabetical order. - angularSameAsCore.sort(); - } - } - - const namePad = ' '.repeat( - Object.keys(versions).sort((a, b) => b.length - a.length)[0].length + 3, - ); - const asciiArt = ` - _ _ ____ _ ___ - / \\ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _| - / △ \\ | '_ \\ / _\` | | | | |/ _\` | '__| | | | | | | - / ___ \\| | | | (_| | |_| | | (_| | | | |___| |___ | | - /_/ \\_\\_| |_|\\__, |\\__,_|_|\\__,_|_| \\____|_____|___| - |___/ - ` - .split('\n') - .map((x) => colors.red(x)) - .join('\n'); - - logger.info(asciiArt); - logger.info( - ` - Angular CLI: ${ngCliVersion} - Node: ${process.versions.node}${unsupportedNodeVersion ? ' (Unsupported)' : ''} - Package Manager: ${packageManager.name} ${packageManager.version ?? ''} - OS: ${process.platform} ${process.arch} - - Angular: ${angularCoreVersion} - ... ${angularSameAsCore - .reduce((acc, name) => { - // Perform a simple word wrap around 60. - if (acc.length == 0) { - return [name]; - } - const line = acc[acc.length - 1] + ', ' + name; - if (line.length > 60) { - acc.push(name); - } else { - acc[acc.length - 1] = line; - } - - return acc; - }, []) - .join('\n... ')} - - Package${namePad.slice(7)}Version - -------${namePad.replace(/ /g, '-')}------------------ - ${Object.keys(versions) - .map((module) => `${module}${namePad.slice(module.length)}${versions[module]}`) - .sort() - .join('\n')} - `.replace(/^ {6}/gm, ''), - ); + logger.info([ASCII_ART, header, angularPackages, packageTable].join('\n\n')); if (unsupportedNodeVersion) { logger.warn( - `Warning: The current version of Node (${process.versions.node}) is not supported by Angular.`, + `Warning: The current version of Node (${nodeVersion}) is not supported by Angular.`, ); } } - private getVersion( - moduleName: string, - workspaceRequire: NodeRequire, - localRequire: NodeRequire, - ): string { - let packageInfo: PartialPackageInfo | undefined; - let cliOnly = false; - - // Try to find the package in the workspace - try { - packageInfo = workspaceRequire(`${moduleName}/package.json`); - } catch {} - - // If not found, try to find within the CLI - if (!packageInfo) { - try { - packageInfo = localRequire(`${moduleName}/package.json`); - cliOnly = true; - } catch {} + /** + * Formats the Angular packages section of the version output. + * @param versionInfo An object containing the version information. + * @returns A string containing the formatted Angular packages information. + */ + private formatAngularPackages(versionInfo: VersionInfo): string { + const { angularCoreVersion, angularSameAsCore } = versionInfo; + if (!angularCoreVersion) { + return 'Angular: '; } - // If found, attempt to get the version - if (packageInfo) { - try { - return packageInfo.version + (cliOnly ? ' (cli-only)' : ''); - } catch {} + const wrappedPackages = angularSameAsCore + .reduce((acc, name) => { + if (acc.length === 0) { + return [name]; + } + const line = acc[acc.length - 1] + ', ' + name; + if (line.length > 60) { + acc.push(name); + } else { + acc[acc.length - 1] = line; + } + + return acc; + }, []) + .join('\n... '); + + return `Angular: ${angularCoreVersion}\n... ${wrappedPackages}`; + } + + /** + * Formats the package table section of the version output. + * @param versions A map of package names to their versions. + * @returns A string containing the formatted package table. + */ + private formatPackageTable(versions: Record): string { + const versionKeys = Object.keys(versions); + if (versionKeys.length === 0) { + return ''; } - return ''; + const header = 'Package'; + const maxNameLength = Math.max(...versionKeys.map((key) => key.length)); + const namePad = ' '.repeat(Math.max(0, maxNameLength - header.length) + 3); + + const tableHeader = `${header}${namePad}Version`; + const separator = '-'.repeat(tableHeader.length); + + const tableRows = versionKeys + .map((module) => { + const padding = ' '.repeat(maxNameLength - module.length + 3); + + return `${module}${padding}${versions[module]}`; + }) + .sort() + .join('\n'); + + return `${tableHeader}\n${separator}\n${tableRows}`; } } diff --git a/packages/angular/cli/src/commands/version/version-info.ts b/packages/angular/cli/src/commands/version/version-info.ts new file mode 100644 index 000000000000..cc7c99a6232e --- /dev/null +++ b/packages/angular/cli/src/commands/version/version-info.ts @@ -0,0 +1,167 @@ +/** + * @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 { createRequire } from 'node:module'; +import { resolve } from 'node:path'; + +/** + * A subset of `package.json` fields that are relevant for the version command. + */ +interface PartialPackageInfo { + name: string; + version: string; + dependencies?: Record; + devDependencies?: Record; +} + +/** + * An object containing all the version information that will be displayed by the command. + */ +export interface VersionInfo { + ngCliVersion: string; + angularCoreVersion: string; + angularSameAsCore: string[]; + versions: Record; + unsupportedNodeVersion: boolean; + nodeVersion: string; + packageManagerName: string; + packageManagerVersion: string | undefined; + os: string; + arch: string; +} + +/** + * Major versions of Node.js that are officially supported by Angular. + * @see https://angular.dev/reference/versions#supported-node-js-versions + */ +const SUPPORTED_NODE_MAJORS = [20, 22, 24]; + +/** + * A list of regular expression patterns that match package names that should be included in the + * version output. + */ +const PACKAGE_PATTERNS = [ + /^@angular\/.*/, + /^@angular-devkit\/.*/, + /^@ngtools\/.*/, + /^@schematics\/.*/, + /^rxjs$/, + /^typescript$/, + /^ng-packagr$/, + /^webpack$/, + /^zone\.js$/, +]; + +/** + * Gathers all the version information from the environment and workspace. + * @returns An object containing all the version information. + */ +export function gatherVersionInfo(context: { + packageManager: { name: string; version: string | undefined }; + root: string; +}): VersionInfo { + const localRequire = createRequire(resolve(__filename, '../../../')); + // Trailing slash is used to allow the path to be treated as a directory + const workspaceRequire = createRequire(context.root + '/'); + + const cliPackage: PartialPackageInfo = localRequire('./package.json'); + let workspacePackage: PartialPackageInfo | undefined; + try { + workspacePackage = workspaceRequire('./package.json'); + } catch {} + + const [nodeMajor] = process.versions.node.split('.').map((part) => Number(part)); + const unsupportedNodeVersion = !SUPPORTED_NODE_MAJORS.includes(nodeMajor); + + const packageNames = new Set( + Object.keys({ + ...cliPackage.dependencies, + ...cliPackage.devDependencies, + ...workspacePackage?.dependencies, + ...workspacePackage?.devDependencies, + }), + ); + + const versions: Record = {}; + for (const name of packageNames) { + if (PACKAGE_PATTERNS.some((p) => p.test(name))) { + versions[name] = getVersion(name, workspaceRequire, localRequire); + } + } + + const ngCliVersion = cliPackage.version; + let angularCoreVersion = ''; + const angularSameAsCore: string[] = []; + + if (workspacePackage) { + // Filter all angular versions that are the same as core. + angularCoreVersion = versions['@angular/core']; + if (angularCoreVersion) { + for (const [name, version] of Object.entries(versions)) { + if (version === angularCoreVersion && name.startsWith('@angular/')) { + angularSameAsCore.push(name.replace(/^@angular\//, '')); + delete versions[name]; + } + } + + // Make sure we list them in alphabetical order. + angularSameAsCore.sort(); + } + } + + return { + ngCliVersion, + angularCoreVersion, + angularSameAsCore, + versions, + unsupportedNodeVersion, + nodeVersion: process.versions.node, + packageManagerName: context.packageManager.name, + packageManagerVersion: context.packageManager.version, + os: process.platform, + arch: process.arch, + }; +} + +/** + * Gets the version of a package. + * @param moduleName The name of the package. + * @param workspaceRequire A `require` function for the workspace. + * @param localRequire A `require` function for the CLI. + * @returns The version of the package, or `` if it could not be found. + */ +function getVersion( + moduleName: string, + workspaceRequire: NodeJS.Require, + localRequire: NodeJS.Require, +): string { + let packageInfo: PartialPackageInfo | undefined; + let cliOnly = false; + + // Try to find the package in the workspace + try { + packageInfo = workspaceRequire(`${moduleName}/package.json`); + } catch {} + + // If not found, try to find within the CLI + if (!packageInfo) { + try { + packageInfo = localRequire(`${moduleName}/package.json`); + cliOnly = true; + } catch {} + } + + // If found, attempt to get the version + if (packageInfo) { + try { + return packageInfo.version + (cliOnly ? ' (cli-only)' : ''); + } catch {} + } + + return ''; +}