Skip to content

refactor(@angular/cli): separate the version command's analysis logic from logging #30813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
249 changes: 102 additions & 147 deletions packages/angular/cli/src/commands/version/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
devDependencies?: Record<string, string>;
}
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
Expand All @@ -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<void> {
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<string, string> = {};
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 ?? '<error>'}
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 ?? '<error>'}
OS: ${process.platform} ${process.arch}

Angular: ${angularCoreVersion}
... ${angularSameAsCore
.reduce<string[]>((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: <error>';
}

// If found, attempt to get the version
if (packageInfo) {
try {
return packageInfo.version + (cliOnly ? ' (cli-only)' : '');
} catch {}
const wrappedPackages = angularSameAsCore
.reduce<string[]>((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, string>): string {
const versionKeys = Object.keys(versions);
if (versionKeys.length === 0) {
return '';
}

return '<error>';
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}`;
}
}
Loading