@angular/cli 21.0.0-next.5 → 21.0.0-next.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/lib/config/schema.json +140 -43
  2. package/lib/config/workspace-schema.d.ts +1 -0
  3. package/lib/config/workspace-schema.js +1 -0
  4. package/package.json +17 -17
  5. package/src/command-builder/utilities/json-schema.d.ts +13 -1
  6. package/src/command-builder/utilities/json-schema.js +179 -100
  7. package/src/commands/cache/info/cli.js +35 -11
  8. package/src/commands/mcp/tools/doc-search.js +4 -3
  9. package/src/commands/mcp/tools/projects.d.ts +18 -0
  10. package/src/commands/mcp/tools/projects.js +123 -4
  11. package/src/commands/version/cli.d.ts +3 -7
  12. package/src/commands/version/cli.js +49 -49
  13. package/src/commands/version/version-info.d.ts +28 -10
  14. package/src/commands/version/version-info.js +33 -50
  15. package/src/package-managers/discovery.d.ts +23 -0
  16. package/src/package-managers/discovery.js +109 -0
  17. package/src/package-managers/error.d.ts +31 -0
  18. package/src/package-managers/error.js +40 -0
  19. package/src/package-managers/factory.d.ts +25 -0
  20. package/src/package-managers/factory.js +122 -0
  21. package/src/package-managers/host.d.ts +64 -0
  22. package/src/package-managers/host.js +68 -0
  23. package/src/package-managers/logger.d.ts +27 -0
  24. package/src/package-managers/logger.js +9 -0
  25. package/src/package-managers/package-manager-descriptor.d.ts +204 -0
  26. package/src/package-managers/package-manager-descriptor.js +146 -0
  27. package/src/package-managers/package-manager.d.ts +144 -0
  28. package/src/package-managers/package-manager.js +302 -0
  29. package/src/package-managers/package-metadata.d.ts +85 -0
  30. package/src/package-managers/package-metadata.js +9 -0
  31. package/src/package-managers/package-tree.d.ts +23 -0
  32. package/src/package-managers/package-tree.js +9 -0
  33. package/src/package-managers/parsers.d.ts +92 -0
  34. package/src/package-managers/parsers.js +233 -0
  35. package/src/package-managers/testing/mock-host.d.ts +26 -0
  36. package/src/package-managers/testing/mock-host.js +52 -0
  37. package/src/utilities/version.js +1 -1
@@ -14,6 +14,7 @@ exports.LIST_PROJECTS_TOOL = void 0;
14
14
  const promises_1 = require("node:fs/promises");
15
15
  const node_path_1 = __importDefault(require("node:path"));
16
16
  const node_url_1 = require("node:url");
17
+ const semver_1 = __importDefault(require("semver"));
17
18
  const zod_1 = __importDefault(require("zod"));
18
19
  const config_1 = require("../../../utilities/config");
19
20
  const error_1 = require("../../../utilities/error");
@@ -21,6 +22,10 @@ const tool_registry_1 = require("./tool-registry");
21
22
  const listProjectsOutputSchema = {
22
23
  workspaces: zod_1.default.array(zod_1.default.object({
23
24
  path: zod_1.default.string().describe('The path to the `angular.json` file for this workspace.'),
25
+ frameworkVersion: zod_1.default
26
+ .string()
27
+ .optional()
28
+ .describe('The major version of the Angular framework (`@angular/core`) in this workspace, if found.'),
24
29
  projects: zod_1.default.array(zod_1.default.object({
25
30
  name: zod_1.default
26
31
  .string()
@@ -29,6 +34,10 @@ const listProjectsOutputSchema = {
29
34
  .enum(['application', 'library'])
30
35
  .optional()
31
36
  .describe(`The type of the project, either 'application' or 'library'.`),
37
+ builder: zod_1.default
38
+ .string()
39
+ .optional()
40
+ .describe('The primary builder for the project, typically from the "build" target.'),
32
41
  root: zod_1.default
33
42
  .string()
34
43
  .describe('The root directory of the project, relative to the workspace root.'),
@@ -49,6 +58,15 @@ const listProjectsOutputSchema = {
49
58
  }))
50
59
  .default([])
51
60
  .describe('A list of files that looked like workspaces but failed to parse.'),
61
+ versioningErrors: zod_1.default
62
+ .array(zod_1.default.object({
63
+ filePath: zod_1.default
64
+ .string()
65
+ .describe('The path to the workspace `angular.json` for which versioning failed.'),
66
+ message: zod_1.default.string().describe('The error message detailing why versioning failed.'),
67
+ }))
68
+ .default([])
69
+ .describe('A list of workspaces for which the framework version could not be determined.'),
52
70
  };
53
71
  exports.LIST_PROJECTS_TOOL = (0, tool_registry_1.declareTool)({
54
72
  name: 'list_projects',
@@ -64,6 +82,8 @@ their types, and their locations.
64
82
  * Identifying the \`root\` and \`sourceRoot\` of a project to read, analyze, or modify its files.
65
83
  * Determining if a project is an \`application\` or a \`library\`.
66
84
  * Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions.
85
+ * Identifying the major version of the Angular framework for each workspace, which is crucial for monorepos.
86
+ * Determining a project's primary function by inspecting its builder (e.g., '@angular-devkit/build-angular:browser' for an application).
67
87
  </Use Cases>
68
88
  <Operational Notes>
69
89
  * **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST**
@@ -122,6 +142,64 @@ async function* findAngularJsonFiles(rootDir) {
122
142
  yield* foundFilesInBatch;
123
143
  }
124
144
  }
145
+ /**
146
+ * Searches upwards from a starting directory to find the version of '@angular/core'.
147
+ * It caches results to avoid redundant lookups.
148
+ * @param startDir The directory to start the search from.
149
+ * @param cache A map to store cached results.
150
+ * @param searchRoot The directory at which to stop the search.
151
+ * @returns The major version of '@angular/core' as a string, otherwise undefined.
152
+ */
153
+ async function findAngularCoreVersion(startDir, cache, searchRoot) {
154
+ let currentDir = startDir;
155
+ const dirsToCache = [];
156
+ while (currentDir) {
157
+ dirsToCache.push(currentDir);
158
+ if (cache.has(currentDir)) {
159
+ const cachedResult = cache.get(currentDir);
160
+ // Populate cache for all intermediate directories.
161
+ for (const dir of dirsToCache) {
162
+ cache.set(dir, cachedResult);
163
+ }
164
+ return cachedResult;
165
+ }
166
+ const pkgPath = node_path_1.default.join(currentDir, 'package.json');
167
+ try {
168
+ const pkgContent = await (0, promises_1.readFile)(pkgPath, 'utf-8');
169
+ const pkg = JSON.parse(pkgContent);
170
+ const versionSpecifier = pkg.dependencies?.['@angular/core'] ?? pkg.devDependencies?.['@angular/core'];
171
+ if (versionSpecifier) {
172
+ const minVersion = semver_1.default.minVersion(versionSpecifier);
173
+ const result = minVersion ? String(minVersion.major) : undefined;
174
+ for (const dir of dirsToCache) {
175
+ cache.set(dir, result);
176
+ }
177
+ return result;
178
+ }
179
+ }
180
+ catch (error) {
181
+ (0, error_1.assertIsError)(error);
182
+ if (error.code !== 'ENOENT') {
183
+ // Ignore missing package.json files, but rethrow other errors.
184
+ throw error;
185
+ }
186
+ }
187
+ // Stop if we are at the search root or the filesystem root.
188
+ if (currentDir === searchRoot) {
189
+ break;
190
+ }
191
+ const parentDir = node_path_1.default.dirname(currentDir);
192
+ if (parentDir === currentDir) {
193
+ break; // Reached the filesystem root.
194
+ }
195
+ currentDir = parentDir;
196
+ }
197
+ // Cache the failure for all traversed directories.
198
+ for (const dir of dirsToCache) {
199
+ cache.set(dir, undefined);
200
+ }
201
+ return undefined;
202
+ }
125
203
  /**
126
204
  * Loads, parses, and transforms a single angular.json file into the tool's output format.
127
205
  * It checks a set of seen paths to avoid processing the same workspace multiple times.
@@ -142,6 +220,7 @@ async function loadAndParseWorkspace(configFile, seenPaths) {
142
220
  projects.push({
143
221
  name,
144
222
  type: project.extensions['projectType'],
223
+ builder: project.targets.get('build')?.builder,
145
224
  root: project.root,
146
225
  sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'),
147
226
  selectorPrefix: project.extensions['prefix'],
@@ -160,11 +239,44 @@ async function loadAndParseWorkspace(configFile, seenPaths) {
160
239
  return { workspace: null, error: { filePath: configFile, message } };
161
240
  }
162
241
  }
242
+ /**
243
+ * Processes a single `angular.json` file to extract workspace and framework version information.
244
+ * @param configFile The path to the `angular.json` file.
245
+ * @param searchRoot The directory at which to stop the upward search for `package.json`.
246
+ * @param seenPaths A Set of absolute paths that have already been processed to avoid duplicates.
247
+ * @param versionCache A Map to cache framework version lookups for performance.
248
+ * @returns A promise resolving to an object containing the processed data and any errors.
249
+ */
250
+ async function processConfigFile(configFile, searchRoot, seenPaths, versionCache) {
251
+ const { workspace, error } = await loadAndParseWorkspace(configFile, seenPaths);
252
+ if (error) {
253
+ return { parsingError: error };
254
+ }
255
+ if (!workspace) {
256
+ return {}; // Skipped as it was already seen.
257
+ }
258
+ try {
259
+ const workspaceDir = node_path_1.default.dirname(configFile);
260
+ workspace.frameworkVersion = await findAngularCoreVersion(workspaceDir, versionCache, searchRoot);
261
+ return { workspace };
262
+ }
263
+ catch (e) {
264
+ return {
265
+ workspace,
266
+ versioningError: {
267
+ filePath: workspace.path,
268
+ message: e instanceof Error ? e.message : 'An unknown error occurred.',
269
+ },
270
+ };
271
+ }
272
+ }
163
273
  async function createListProjectsHandler({ server }) {
164
274
  return async () => {
165
275
  const workspaces = [];
166
276
  const parsingErrors = [];
277
+ const versioningErrors = [];
167
278
  const seenPaths = new Set();
279
+ const versionCache = new Map();
168
280
  let searchRoots;
169
281
  const clientCapabilities = server.server.getClientCapabilities();
170
282
  if (clientCapabilities?.roots) {
@@ -177,12 +289,15 @@ async function createListProjectsHandler({ server }) {
177
289
  }
178
290
  for (const root of searchRoots) {
179
291
  for await (const configFile of findAngularJsonFiles(root)) {
180
- const { workspace, error } = await loadAndParseWorkspace(configFile, seenPaths);
292
+ const { workspace, parsingError, versioningError } = await processConfigFile(configFile, root, seenPaths, versionCache);
181
293
  if (workspace) {
182
294
  workspaces.push(workspace);
183
295
  }
184
- if (error) {
185
- parsingErrors.push(error);
296
+ if (parsingError) {
297
+ parsingErrors.push(parsingError);
298
+ }
299
+ if (versioningError) {
300
+ versioningErrors.push(versioningError);
186
301
  }
187
302
  }
188
303
  }
@@ -204,9 +319,13 @@ async function createListProjectsHandler({ server }) {
204
319
  text += `\n\nWarning: The following ${parsingErrors.length} file(s) could not be parsed and were skipped:\n`;
205
320
  text += parsingErrors.map((e) => `- ${e.filePath}: ${e.message}`).join('\n');
206
321
  }
322
+ if (versioningErrors.length > 0) {
323
+ text += `\n\nWarning: The framework version for the following ${versioningErrors.length} workspace(s) could not be determined:\n`;
324
+ text += versioningErrors.map((e) => `- ${e.filePath}: ${e.message}`).join('\n');
325
+ }
207
326
  return {
208
327
  content: [{ type: 'text', text }],
209
- structuredContent: { workspaces, parsingErrors },
328
+ structuredContent: { workspaces, parsingErrors, versioningErrors },
210
329
  };
211
330
  };
212
331
  }
@@ -24,13 +24,9 @@ export default class VersionCommandModule extends CommandModule implements Comma
24
24
  /**
25
25
  * The main execution logic for the `ng version` command.
26
26
  */
27
- run(): Promise<void>;
28
- /**
29
- * Formats the Angular packages section of the version output.
30
- * @param versionInfo An object containing the version information.
31
- * @returns A string containing the formatted Angular packages information.
32
- */
33
- private formatAngularPackages;
27
+ run(options: {
28
+ json?: boolean;
29
+ }): Promise<void>;
34
30
  /**
35
31
  * Formats the package table section of the version output.
36
32
  * @param versions A map of package names to their versions.
@@ -39,55 +39,44 @@ class VersionCommandModule extends command_module_1.CommandModule {
39
39
  * @returns The configured `yargs` instance.
40
40
  */
41
41
  builder(localYargs) {
42
- return localYargs;
42
+ return localYargs.option('json', {
43
+ describe: 'Outputs version information in JSON format.',
44
+ type: 'boolean',
45
+ });
43
46
  }
44
47
  /**
45
48
  * The main execution logic for the `ng version` command.
46
49
  */
47
- async run() {
50
+ async run(options) {
48
51
  const { logger } = this.context;
49
52
  const versionInfo = (0, version_info_1.gatherVersionInfo)(this.context);
50
- const { ngCliVersion, nodeVersion, unsupportedNodeVersion, packageManagerName, packageManagerVersion, os, arch, versions, } = versionInfo;
51
- const header = `
52
- Angular CLI: ${ngCliVersion}
53
- Node: ${nodeVersion}${unsupportedNodeVersion ? ' (Unsupported)' : ''}
54
- Package Manager: ${packageManagerName} ${packageManagerVersion ?? '<error>'}
55
- OS: ${os} ${arch}
56
- `.replace(/^ {6}/gm, '');
57
- const angularPackages = this.formatAngularPackages(versionInfo);
58
- const packageTable = this.formatPackageTable(versions);
59
- logger.info([ASCII_ART, header, angularPackages, packageTable].join('\n\n'));
53
+ if (options.json) {
54
+ // eslint-disable-next-line no-console
55
+ console.log(JSON.stringify(versionInfo, null, 2));
56
+ return;
57
+ }
58
+ const { cli: { version: ngCliVersion }, framework, system: { node: { version: nodeVersion, unsupported: unsupportedNodeVersion }, os: { platform: os, architecture: arch }, packageManager: { name: packageManagerName, version: packageManagerVersion }, }, packages, } = versionInfo;
59
+ const headerInfo = [{ label: 'Angular CLI', value: ngCliVersion }];
60
+ if (framework.version) {
61
+ headerInfo.push({ label: 'Angular', value: framework.version });
62
+ }
63
+ headerInfo.push({
64
+ label: 'Node.js',
65
+ value: `${nodeVersion}${unsupportedNodeVersion ? color_1.colors.yellow(' (Unsupported)') : ''}`,
66
+ }, {
67
+ label: 'Package Manager',
68
+ value: `${packageManagerName} ${packageManagerVersion ?? '<error>'}`,
69
+ }, { label: 'Operating System', value: `${os} ${arch}` });
70
+ const maxHeaderLabelLength = Math.max(...headerInfo.map((l) => l.label.length));
71
+ const header = headerInfo
72
+ .map(({ label, value }) => color_1.colors.bold(label.padEnd(maxHeaderLabelLength + 2)) + `: ${color_1.colors.cyan(value)}`)
73
+ .join('\n');
74
+ const packageTable = this.formatPackageTable(packages);
75
+ logger.info([ASCII_ART, header, packageTable].join('\n\n'));
60
76
  if (unsupportedNodeVersion) {
61
77
  logger.warn(`Warning: The current version of Node (${nodeVersion}) is not supported by Angular.`);
62
78
  }
63
79
  }
64
- /**
65
- * Formats the Angular packages section of the version output.
66
- * @param versionInfo An object containing the version information.
67
- * @returns A string containing the formatted Angular packages information.
68
- */
69
- formatAngularPackages(versionInfo) {
70
- const { angularCoreVersion, angularSameAsCore } = versionInfo;
71
- if (!angularCoreVersion) {
72
- return 'Angular: <error>';
73
- }
74
- const wrappedPackages = angularSameAsCore
75
- .reduce((acc, name) => {
76
- if (acc.length === 0) {
77
- return [name];
78
- }
79
- const line = acc[acc.length - 1] + ', ' + name;
80
- if (line.length > 60) {
81
- acc.push(name);
82
- }
83
- else {
84
- acc[acc.length - 1] = line;
85
- }
86
- return acc;
87
- }, [])
88
- .join('\n... ');
89
- return `Angular: ${angularCoreVersion}\n... ${wrappedPackages}`;
90
- }
91
80
  /**
92
81
  * Formats the package table section of the version output.
93
82
  * @param versions A map of package names to their versions.
@@ -98,19 +87,30 @@ class VersionCommandModule extends command_module_1.CommandModule {
98
87
  if (versionKeys.length === 0) {
99
88
  return '';
100
89
  }
101
- const header = 'Package';
102
- const maxNameLength = Math.max(...versionKeys.map((key) => key.length));
103
- const namePad = ' '.repeat(Math.max(0, maxNameLength - header.length) + 3);
104
- const tableHeader = `${header}${namePad}Version`;
105
- const separator = '-'.repeat(tableHeader.length);
90
+ const headers = {
91
+ name: 'Package',
92
+ installed: 'Installed Version',
93
+ requested: 'Requested Version',
94
+ };
95
+ const maxNameLength = Math.max(headers.name.length, ...versionKeys.map((key) => key.length));
96
+ const maxInstalledLength = Math.max(headers.installed.length, ...versionKeys.map((key) => versions[key].installed.length));
97
+ const maxRequestedLength = Math.max(headers.requested.length, ...versionKeys.map((key) => versions[key].requested.length));
106
98
  const tableRows = versionKeys
107
99
  .map((module) => {
108
- const padding = ' '.repeat(maxNameLength - module.length + 3);
109
- return `${module}${padding}${versions[module]}`;
100
+ const { requested, installed } = versions[module];
101
+ const name = module.padEnd(maxNameLength);
102
+ const coloredInstalled = installed === '<error>' ? color_1.colors.red(installed) : color_1.colors.cyan(installed);
103
+ const installedPadding = ' '.repeat(maxInstalledLength - installed.length);
104
+ return `│ ${name} │ ${coloredInstalled}${installedPadding} │ ${requested.padEnd(maxRequestedLength)} │`;
110
105
  })
111
- .sort()
112
- .join('\n');
113
- return `${tableHeader}\n${separator}\n${tableRows}`;
106
+ .sort();
107
+ const top = `┌─${'─'.repeat(maxNameLength)}─┬─${''.repeat(maxInstalledLength)}─┬─${'─'.repeat(maxRequestedLength)}─┐`;
108
+ const header = `│ ${headers.name.padEnd(maxNameLength)} │ ` +
109
+ `${headers.installed.padEnd(maxInstalledLength)} │ ` +
110
+ `${headers.requested.padEnd(maxRequestedLength)} │`;
111
+ const separator = `├─${'─'.repeat(maxNameLength)}─┼─${'─'.repeat(maxInstalledLength)}─┼─${'─'.repeat(maxRequestedLength)}─┤`;
112
+ const bottom = `└─${'─'.repeat(maxNameLength)}─┴─${'─'.repeat(maxInstalledLength)}─┴─${'─'.repeat(maxRequestedLength)}─┘`;
113
+ return [top, header, separator, ...tableRows, bottom].join('\n');
114
114
  }
115
115
  }
116
116
  exports.default = VersionCommandModule;
@@ -5,20 +5,38 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
+ /**
9
+ * An object containing version information for a single package.
10
+ */
11
+ export interface PackageVersionInfo {
12
+ requested: string;
13
+ installed: string;
14
+ }
8
15
  /**
9
16
  * An object containing all the version information that will be displayed by the command.
10
17
  */
11
18
  export interface VersionInfo {
12
- ngCliVersion: string;
13
- angularCoreVersion: string;
14
- angularSameAsCore: string[];
15
- versions: Record<string, string>;
16
- unsupportedNodeVersion: boolean;
17
- nodeVersion: string;
18
- packageManagerName: string;
19
- packageManagerVersion: string | undefined;
20
- os: string;
21
- arch: string;
19
+ cli: {
20
+ version: string;
21
+ };
22
+ framework: {
23
+ version: string | undefined;
24
+ };
25
+ system: {
26
+ node: {
27
+ version: string;
28
+ unsupported: boolean;
29
+ };
30
+ os: {
31
+ platform: string;
32
+ architecture: string;
33
+ };
34
+ packageManager: {
35
+ name: string;
36
+ version: string | undefined;
37
+ };
38
+ };
39
+ packages: Record<string, PackageVersionInfo>;
22
40
  }
23
41
  /**
24
42
  * Gathers all the version information from the environment and workspace.
@@ -9,7 +9,7 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.gatherVersionInfo = gatherVersionInfo;
11
11
  const node_module_1 = require("node:module");
12
- const node_path_1 = require("node:path");
12
+ const version_1 = require("../../utilities/version");
13
13
  /**
14
14
  * Major versions of Node.js that are officially supported by Angular.
15
15
  * @see https://angular.dev/reference/versions#supported-node-js-versions
@@ -35,10 +35,8 @@ const PACKAGE_PATTERNS = [
35
35
  * @returns An object containing all the version information.
36
36
  */
37
37
  function gatherVersionInfo(context) {
38
- const localRequire = (0, node_module_1.createRequire)((0, node_path_1.resolve)(__filename, '../../../'));
39
38
  // Trailing slash is used to allow the path to be treated as a directory
40
39
  const workspaceRequire = (0, node_module_1.createRequire)(context.root + '/');
41
- const cliPackage = localRequire('./package.json');
42
40
  let workspacePackage;
43
41
  try {
44
42
  workspacePackage = workspaceRequire('./package.json');
@@ -46,46 +44,43 @@ function gatherVersionInfo(context) {
46
44
  catch { }
47
45
  const [nodeMajor] = process.versions.node.split('.').map((part) => Number(part));
48
46
  const unsupportedNodeVersion = !SUPPORTED_NODE_MAJORS.includes(nodeMajor);
49
- const packageNames = new Set(Object.keys({
50
- ...cliPackage.dependencies,
51
- ...cliPackage.devDependencies,
47
+ const allDependencies = {
52
48
  ...workspacePackage?.dependencies,
53
49
  ...workspacePackage?.devDependencies,
54
- }));
55
- const versions = {};
50
+ };
51
+ const packageNames = new Set(Object.keys(allDependencies));
52
+ const packages = {};
56
53
  for (const name of packageNames) {
57
54
  if (PACKAGE_PATTERNS.some((p) => p.test(name))) {
58
- versions[name] = getVersion(name, workspaceRequire, localRequire);
59
- }
60
- }
61
- const ngCliVersion = cliPackage.version;
62
- let angularCoreVersion = '';
63
- const angularSameAsCore = [];
64
- if (workspacePackage) {
65
- // Filter all angular versions that are the same as core.
66
- angularCoreVersion = versions['@angular/core'];
67
- if (angularCoreVersion) {
68
- for (const [name, version] of Object.entries(versions)) {
69
- if (version === angularCoreVersion && name.startsWith('@angular/')) {
70
- angularSameAsCore.push(name.replace(/^@angular\//, ''));
71
- delete versions[name];
72
- }
73
- }
74
- // Make sure we list them in alphabetical order.
75
- angularSameAsCore.sort();
55
+ packages[name] = {
56
+ requested: allDependencies[name] ?? 'error',
57
+ installed: getVersion(name, workspaceRequire),
58
+ };
76
59
  }
77
60
  }
61
+ const angularCoreVersion = packages['@angular/core'];
78
62
  return {
79
- ngCliVersion,
80
- angularCoreVersion,
81
- angularSameAsCore,
82
- versions,
83
- unsupportedNodeVersion,
84
- nodeVersion: process.versions.node,
85
- packageManagerName: context.packageManager.name,
86
- packageManagerVersion: context.packageManager.version,
87
- os: process.platform,
88
- arch: process.arch,
63
+ cli: {
64
+ version: version_1.VERSION.full,
65
+ },
66
+ framework: {
67
+ version: angularCoreVersion?.installed,
68
+ },
69
+ system: {
70
+ node: {
71
+ version: process.versions.node,
72
+ unsupported: unsupportedNodeVersion,
73
+ },
74
+ os: {
75
+ platform: process.platform,
76
+ architecture: process.arch,
77
+ },
78
+ packageManager: {
79
+ name: context.packageManager.name,
80
+ version: context.packageManager.version,
81
+ },
82
+ },
83
+ packages,
89
84
  };
90
85
  }
91
86
  /**
@@ -95,28 +90,16 @@ function gatherVersionInfo(context) {
95
90
  * @param localRequire A `require` function for the CLI.
96
91
  * @returns The version of the package, or `<error>` if it could not be found.
97
92
  */
98
- function getVersion(moduleName, workspaceRequire, localRequire) {
93
+ function getVersion(moduleName, workspaceRequire) {
99
94
  let packageInfo;
100
- let cliOnly = false;
101
95
  // Try to find the package in the workspace
102
96
  try {
103
97
  packageInfo = workspaceRequire(`${moduleName}/package.json`);
104
98
  }
105
99
  catch { }
106
- // If not found, try to find within the CLI
107
- if (!packageInfo) {
108
- try {
109
- packageInfo = localRequire(`${moduleName}/package.json`);
110
- cliOnly = true;
111
- }
112
- catch { }
113
- }
114
100
  // If found, attempt to get the version
115
101
  if (packageInfo) {
116
- try {
117
- return packageInfo.version + (cliOnly ? ' (cli-only)' : '');
118
- }
119
- catch { }
102
+ return packageInfo.version;
120
103
  }
121
104
  return '<error>';
122
105
  }
@@ -0,0 +1,23 @@
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
+ import { Host } from './host';
9
+ import { Logger } from './logger';
10
+ import { PackageManagerName } from './package-manager-descriptor';
11
+ /**
12
+ * Discovers the package manager used in a project by searching for lockfiles.
13
+ *
14
+ * This function searches for lockfiles in the given directory and its ancestors.
15
+ * If multiple lockfiles are found, it uses the precedence array to determine
16
+ * which package manager to use. The search is bounded by the git repository root.
17
+ *
18
+ * @param host A `Host` instance for interacting with the file system.
19
+ * @param startDir The directory to start the search from.
20
+ * @param logger An optional logger instance.
21
+ * @returns A promise that resolves to the name of the discovered package manager, or null if none is found.
22
+ */
23
+ export declare function discover(host: Host, startDir: string, logger?: Logger): Promise<PackageManagerName | null>;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Copyright Google LLC All Rights Reserved.
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://angular.dev/license
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.discover = discover;
11
+ /**
12
+ * @fileoverview This file contains the logic for discovering the package manager
13
+ * used in a project by searching for lockfiles. It is designed to be efficient
14
+ * and to correctly handle monorepo structures.
15
+ */
16
+ const node_path_1 = require("node:path");
17
+ const package_manager_descriptor_1 = require("./package-manager-descriptor");
18
+ /**
19
+ * A map from lockfile names to their corresponding package manager.
20
+ * This is a performance optimization to avoid iterating over all possible
21
+ * lockfiles in every directory.
22
+ */
23
+ const LOCKFILE_TO_PACKAGE_MANAGER = new Map();
24
+ for (const [name, descriptor] of Object.entries(package_manager_descriptor_1.SUPPORTED_PACKAGE_MANAGERS)) {
25
+ for (const lockfile of descriptor.lockfiles) {
26
+ LOCKFILE_TO_PACKAGE_MANAGER.set(lockfile, name);
27
+ }
28
+ }
29
+ /**
30
+ * Searches a directory for lockfiles and returns a set of package managers that correspond to them.
31
+ * @param host A `Host` instance for interacting with the file system.
32
+ * @param directory The directory to search.
33
+ * @param logger An optional logger instance.
34
+ * @returns A promise that resolves to a set of package manager names.
35
+ */
36
+ async function findLockfiles(host, directory, logger) {
37
+ logger?.debug(`Searching for lockfiles in '${directory}'...`);
38
+ try {
39
+ const files = await host.readdir(directory);
40
+ const foundPackageManagers = new Set();
41
+ for (const file of files) {
42
+ const packageManager = LOCKFILE_TO_PACKAGE_MANAGER.get(file);
43
+ if (packageManager) {
44
+ logger?.debug(` Found '${file}'.`);
45
+ foundPackageManagers.add(packageManager);
46
+ }
47
+ }
48
+ return foundPackageManagers;
49
+ }
50
+ catch (e) {
51
+ logger?.debug(` Failed to read directory: ${e}`);
52
+ // Ignore directories that don't exist or can't be read.
53
+ return new Set();
54
+ }
55
+ }
56
+ /**
57
+ * Checks if a given path is a directory.
58
+ * @param host A `Host` instance for interacting with the file system.
59
+ * @param path The path to check.
60
+ * @returns A promise that resolves to true if the path is a directory, false otherwise.
61
+ */
62
+ async function isDirectory(host, path) {
63
+ try {
64
+ return (await host.stat(path)).isDirectory();
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ /**
71
+ * Discovers the package manager used in a project by searching for lockfiles.
72
+ *
73
+ * This function searches for lockfiles in the given directory and its ancestors.
74
+ * If multiple lockfiles are found, it uses the precedence array to determine
75
+ * which package manager to use. The search is bounded by the git repository root.
76
+ *
77
+ * @param host A `Host` instance for interacting with the file system.
78
+ * @param startDir The directory to start the search from.
79
+ * @param logger An optional logger instance.
80
+ * @returns A promise that resolves to the name of the discovered package manager, or null if none is found.
81
+ */
82
+ async function discover(host, startDir, logger) {
83
+ logger?.debug(`Starting package manager discovery in '${startDir}'...`);
84
+ let currentDir = startDir;
85
+ while (true) {
86
+ const found = await findLockfiles(host, currentDir, logger);
87
+ if (found.size > 0) {
88
+ logger?.debug(`Found lockfile(s): [${[...found].join(', ')}]. Applying precedence...`);
89
+ for (const packageManager of package_manager_descriptor_1.PACKAGE_MANAGER_PRECEDENCE) {
90
+ if (found.has(packageManager)) {
91
+ logger?.debug(`Selected '${packageManager}' based on precedence.`);
92
+ return packageManager;
93
+ }
94
+ }
95
+ }
96
+ // Stop searching if we reach the git repository root.
97
+ if (await isDirectory(host, (0, node_path_1.join)(currentDir, '.git'))) {
98
+ logger?.debug(`Reached repository root at '${currentDir}'. Stopping search.`);
99
+ return null;
100
+ }
101
+ const parentDir = (0, node_path_1.dirname)(currentDir);
102
+ if (parentDir === currentDir) {
103
+ // We have reached the filesystem root.
104
+ logger?.debug('Reached filesystem root. No lockfile found.');
105
+ return null;
106
+ }
107
+ currentDir = parentDir;
108
+ }
109
+ }