Skip to content

Commit d795ba2

Browse files
committed
feat(@angular/cli): add --json output to ng version
This commit introduces a `--json` option to the `ng version` command. When this flag is used, the command will output all version information in a machine-readable JSON format. The JSON structure is designed to be a stable, well-organized public API, with information grouped into `cli`, `system`, and `packages` categories. This is useful for scripting, tool integration, and creating detailed, parsable bug reports. The standard, human-readable output is still the default.
1 parent 7b9020c commit d795ba2

2 files changed

Lines changed: 103 additions & 102 deletions

File tree

packages/angular/cli/src/commands/version/cli.ts

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Argv } from 'yargs';
1010
import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module';
1111
import { colors } from '../../utilities/color';
1212
import { RootCommands } from '../command-config';
13-
import { VersionInfo, gatherVersionInfo } from './version-info';
13+
import { gatherVersionInfo } from './version-info';
1414

1515
/**
1616
* The Angular CLI logo, displayed as ASCII art.
@@ -45,37 +45,61 @@ export default class VersionCommandModule
4545
* @returns The configured `yargs` instance.
4646
*/
4747
builder(localYargs: Argv): Argv {
48-
return localYargs;
48+
return localYargs.option('json', {
49+
describe: 'Outputs version information in JSON format.',
50+
type: 'boolean',
51+
});
4952
}
5053

5154
/**
5255
* The main execution logic for the `ng version` command.
5356
*/
54-
async run(): Promise<void> {
57+
async run(options: { json?: boolean }): Promise<void> {
5558
const { logger } = this.context;
5659
const versionInfo = gatherVersionInfo(this.context);
60+
61+
if (options.json) {
62+
// eslint-disable-next-line no-console
63+
console.log(JSON.stringify(versionInfo, null, 2));
64+
65+
return;
66+
}
67+
5768
const {
58-
ngCliVersion,
59-
nodeVersion,
60-
unsupportedNodeVersion,
61-
packageManagerName,
62-
packageManagerVersion,
63-
os,
64-
arch,
65-
versions,
69+
cli: { version: ngCliVersion },
70+
system: {
71+
node: { version: nodeVersion, unsupported: unsupportedNodeVersion },
72+
os: { platform: os, architecture: arch },
73+
packageManager: { name: packageManagerName, version: packageManagerVersion },
74+
},
75+
packages,
6676
} = versionInfo;
6777

68-
const header = `
69-
Angular CLI: ${ngCliVersion}
70-
Node: ${nodeVersion}${unsupportedNodeVersion ? ' (Unsupported)' : ''}
71-
Package Manager: ${packageManagerName} ${packageManagerVersion ?? '<error>'}
72-
OS: ${os} ${arch}
73-
`.replace(/^ {6}/gm, '');
78+
const headerInfo = [
79+
{ label: 'Angular CLI', value: ngCliVersion },
80+
{
81+
label: 'Node.js',
82+
value: `${nodeVersion}${unsupportedNodeVersion ? colors.yellow(' (Unsupported)') : ''}`,
83+
},
84+
{
85+
label: 'Package Manager',
86+
value: `${packageManagerName} ${packageManagerVersion ?? '<error>'}`,
87+
},
88+
{ label: 'Operating System', value: `${os} ${arch}` },
89+
];
90+
91+
const maxHeaderLabelLength = Math.max(...headerInfo.map((l) => l.label.length));
92+
93+
const header = headerInfo
94+
.map(
95+
({ label, value }) =>
96+
`${colors.bold(label)}`.padEnd(maxHeaderLabelLength + 11) + `: ${colors.cyan(value)}`,
97+
)
98+
.join('\n');
7499

75-
const angularPackages = this.formatAngularPackages(versionInfo);
76-
const packageTable = this.formatPackageTable(versions);
100+
const packageTable = this.formatPackageTable(packages);
77101

78-
logger.info([ASCII_ART, header, angularPackages, packageTable].join('\n\n'));
102+
logger.info([ASCII_ART, header, packageTable].join('\n\n'));
79103

80104
if (unsupportedNodeVersion) {
81105
logger.warn(
@@ -84,36 +108,6 @@ export default class VersionCommandModule
84108
}
85109
}
86110

87-
/**
88-
* Formats the Angular packages section of the version output.
89-
* @param versionInfo An object containing the version information.
90-
* @returns A string containing the formatted Angular packages information.
91-
*/
92-
private formatAngularPackages(versionInfo: VersionInfo): string {
93-
const { angularCoreVersion, angularSameAsCore } = versionInfo;
94-
if (!angularCoreVersion) {
95-
return 'Angular: <error>';
96-
}
97-
98-
const wrappedPackages = angularSameAsCore
99-
.reduce<string[]>((acc, name) => {
100-
if (acc.length === 0) {
101-
return [name];
102-
}
103-
const line = acc[acc.length - 1] + ', ' + name;
104-
if (line.length > 60) {
105-
acc.push(name);
106-
} else {
107-
acc[acc.length - 1] = line;
108-
}
109-
110-
return acc;
111-
}, [])
112-
.join('\n... ');
113-
114-
return `Angular: ${angularCoreVersion}\n... ${wrappedPackages}`;
115-
}
116-
117111
/**
118112
* Formats the package table section of the version output.
119113
* @param versions A map of package names to their versions.
@@ -125,22 +119,33 @@ export default class VersionCommandModule
125119
return '';
126120
}
127121

128-
const header = 'Package';
129-
const maxNameLength = Math.max(...versionKeys.map((key) => key.length));
130-
const namePad = ' '.repeat(Math.max(0, maxNameLength - header.length) + 3);
122+
const nameHeader = 'Package';
123+
const versionHeader = 'Version';
131124

132-
const tableHeader = `${header}${namePad}Version`;
133-
const separator = '-'.repeat(tableHeader.length);
125+
const maxNameLength = Math.max(nameHeader.length, ...versionKeys.map((key) => key.length));
126+
const maxVersionLength = Math.max(
127+
versionHeader.length,
128+
...versionKeys.map((key) => versions[key].length),
129+
);
134130

135131
const tableRows = versionKeys
136132
.map((module) => {
137-
const padding = ' '.repeat(maxNameLength - module.length + 3);
133+
const name = module.padEnd(maxNameLength);
134+
const version = versions[module];
135+
const coloredVersion = version === '<error>' ? colors.red(version) : colors.cyan(version);
136+
const padding = ' '.repeat(maxVersionLength - version.length);
138137

139-
return `${module}${padding}${versions[module]}`;
138+
return `${name}${coloredVersion}${padding}`;
140139
})
141-
.sort()
142-
.join('\n');
140+
.sort();
141+
142+
const top = `┌─${'─'.repeat(maxNameLength)}─┬─${'─'.repeat(maxVersionLength)}─┐`;
143+
const header = `│ ${nameHeader.padEnd(maxNameLength)}${versionHeader.padEnd(
144+
maxVersionLength,
145+
)} │`;
146+
const separator = `├─${'─'.repeat(maxNameLength)}─┼─${'─'.repeat(maxVersionLength)}─┤`;
147+
const bottom = `└─${'─'.repeat(maxNameLength)}─┴─${'─'.repeat(maxVersionLength)}─┘`;
143148

144-
return `${tableHeader}\n${separator}\n${tableRows}`;
149+
return [top, header, separator, ...tableRows, bottom].join('\n');
145150
}
146151
}

packages/angular/cli/src/commands/version/version-info.ts

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,24 @@ interface PartialPackageInfo {
2323
* An object containing all the version information that will be displayed by the command.
2424
*/
2525
export interface VersionInfo {
26-
ngCliVersion: string;
27-
angularCoreVersion: string;
28-
angularSameAsCore: string[];
29-
versions: Record<string, string>;
30-
unsupportedNodeVersion: boolean;
31-
nodeVersion: string;
32-
packageManagerName: string;
33-
packageManagerVersion: string | undefined;
34-
os: string;
35-
arch: string;
26+
cli: {
27+
version: string;
28+
};
29+
system: {
30+
node: {
31+
version: string;
32+
unsupported: boolean;
33+
};
34+
os: {
35+
platform: string;
36+
architecture: string;
37+
};
38+
packageManager: {
39+
name: string;
40+
version: string | undefined;
41+
};
42+
};
43+
packages: Record<string, string>;
3644
}
3745

3846
/**
@@ -83,44 +91,32 @@ export function gatherVersionInfo(context: {
8391
}),
8492
);
8593

86-
const versions: Record<string, string> = {};
94+
const packages: Record<string, string> = {};
8795
for (const name of packageNames) {
8896
if (PACKAGE_PATTERNS.some((p) => p.test(name))) {
89-
versions[name] = getVersion(name, workspaceRequire);
90-
}
91-
}
92-
93-
const ngCliVersion = VERSION.full;
94-
let angularCoreVersion = '';
95-
const angularSameAsCore: string[] = [];
96-
97-
if (workspacePackage) {
98-
// Filter all angular versions that are the same as core.
99-
angularCoreVersion = versions['@angular/core'];
100-
if (angularCoreVersion) {
101-
for (const [name, version] of Object.entries(versions)) {
102-
if (version === angularCoreVersion && name.startsWith('@angular/')) {
103-
angularSameAsCore.push(name.replace(/^@angular\//, ''));
104-
delete versions[name];
105-
}
106-
}
107-
108-
// Make sure we list them in alphabetical order.
109-
angularSameAsCore.sort();
97+
packages[name] = getVersion(name, workspaceRequire);
11098
}
11199
}
112100

113101
return {
114-
ngCliVersion,
115-
angularCoreVersion,
116-
angularSameAsCore,
117-
versions,
118-
unsupportedNodeVersion,
119-
nodeVersion: process.versions.node,
120-
packageManagerName: context.packageManager.name,
121-
packageManagerVersion: context.packageManager.version,
122-
os: process.platform,
123-
arch: process.arch,
102+
cli: {
103+
version: VERSION.full,
104+
},
105+
system: {
106+
node: {
107+
version: process.versions.node,
108+
unsupported: unsupportedNodeVersion,
109+
},
110+
os: {
111+
platform: process.platform,
112+
architecture: process.arch,
113+
},
114+
packageManager: {
115+
name: context.packageManager.name,
116+
version: context.packageManager.version,
117+
},
118+
},
119+
packages,
124120
};
125121
}
126122

0 commit comments

Comments
 (0)