@angular/cli 21.0.0-next.1 → 21.0.0-next.3

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 (39) hide show
  1. package/lib/code-examples.db +0 -0
  2. package/lib/config/schema.json +34 -3
  3. package/lib/config/workspace-schema.d.ts +39 -0
  4. package/lib/config/workspace-schema.js +12 -1
  5. package/package.json +19 -19
  6. package/src/command-builder/architect-command-module.js +11 -5
  7. package/src/command-builder/utilities/json-schema.js +1 -1
  8. package/src/commands/add/cli.js +65 -26
  9. package/src/commands/mcp/mcp-server.d.ts +3 -3
  10. package/src/commands/mcp/mcp-server.js +36 -4
  11. package/src/commands/mcp/tools/best-practices.js +15 -5
  12. package/src/commands/mcp/tools/doc-search.d.ts +18 -1
  13. package/src/commands/mcp/tools/doc-search.js +94 -37
  14. package/src/commands/mcp/tools/examples.d.ts +34 -1
  15. package/src/commands/mcp/tools/examples.js +295 -44
  16. package/src/commands/mcp/tools/modernize.js +28 -17
  17. package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.d.ts +17 -0
  18. package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.js +61 -0
  19. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.d.ts +12 -0
  20. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.js +72 -0
  21. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.d.ts +11 -0
  22. package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.js +105 -0
  23. package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.d.ts +15 -0
  24. package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.js +236 -0
  25. package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.d.ts +10 -0
  26. package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.js +19 -0
  27. package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.d.ts +36 -0
  28. package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.js +135 -0
  29. package/src/commands/mcp/tools/onpush-zoneless-migration/types.d.ts +13 -0
  30. package/src/commands/mcp/tools/onpush-zoneless-migration/types.js +9 -0
  31. package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.d.ts +14 -0
  32. package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.js +205 -0
  33. package/src/commands/mcp/tools/projects.d.ts +47 -16
  34. package/src/commands/mcp/tools/projects.js +155 -30
  35. package/src/commands/mcp/tools/tool-registry.d.ts +2 -1
  36. package/src/commands/mcp/tools/tool-registry.js +3 -2
  37. package/src/utilities/package-manager.d.ts +12 -0
  38. package/src/utilities/package-manager.js +31 -22
  39. package/src/utilities/version.js +1 -1
@@ -11,15 +11,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.LIST_PROJECTS_TOOL = void 0;
14
+ const promises_1 = require("node:fs/promises");
14
15
  const node_path_1 = __importDefault(require("node:path"));
16
+ const node_url_1 = require("node:url");
15
17
  const zod_1 = __importDefault(require("zod"));
18
+ const config_1 = require("../../../utilities/config");
19
+ const error_1 = require("../../../utilities/error");
16
20
  const tool_registry_1 = require("./tool-registry");
17
- exports.LIST_PROJECTS_TOOL = (0, tool_registry_1.declareTool)({
18
- name: 'list_projects',
19
- title: 'List Angular Projects',
20
- description: 'Lists the names of all applications and libraries defined within an Angular workspace. ' +
21
- 'It reads the `angular.json` configuration file to identify the projects. ',
22
- outputSchema: {
21
+ const listProjectsOutputSchema = {
22
+ workspaces: zod_1.default.array(zod_1.default.object({
23
+ path: zod_1.default.string().describe('The path to the `angular.json` file for this workspace.'),
23
24
  projects: zod_1.default.array(zod_1.default.object({
24
25
  name: zod_1.default
25
26
  .string()
@@ -40,15 +41,152 @@ exports.LIST_PROJECTS_TOOL = (0, tool_registry_1.declareTool)({
40
41
  .describe('The prefix to use for component selectors.' +
41
42
  ` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.`),
42
43
  })),
43
- },
44
+ })),
45
+ parsingErrors: zod_1.default
46
+ .array(zod_1.default.object({
47
+ filePath: zod_1.default.string().describe('The path to the file that could not be parsed.'),
48
+ message: zod_1.default.string().describe('The error message detailing why parsing failed.'),
49
+ }))
50
+ .default([])
51
+ .describe('A list of files that looked like workspaces but failed to parse.'),
52
+ };
53
+ exports.LIST_PROJECTS_TOOL = (0, tool_registry_1.declareTool)({
54
+ name: 'list_projects',
55
+ title: 'List Angular Projects',
56
+ description: `
57
+ <Purpose>
58
+ Provides a comprehensive overview of all Angular workspaces and projects within a monorepo.
59
+ It is essential to use this tool as a first step before performing any project-specific actions to understand the available projects,
60
+ their types, and their locations.
61
+ </Purpose>
62
+ <Use Cases>
63
+ * Finding the correct project name to use in other commands (e.g., \`ng generate component my-comp --project=my-app\`).
64
+ * Identifying the \`root\` and \`sourceRoot\` of a project to read, analyze, or modify its files.
65
+ * Determining if a project is an \`application\` or a \`library\`.
66
+ * Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions.
67
+ </Use Cases>
68
+ <Operational Notes>
69
+ * **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST**
70
+ be executed from the parent directory of the \`path\` field for the relevant workspace.
71
+ * **Disambiguation:** A monorepo may contain multiple workspaces (e.g., for different applications or even in output directories).
72
+ Use the \`path\` of each workspace to understand its context and choose the correct project.
73
+ </Operational Notes>`,
74
+ outputSchema: listProjectsOutputSchema,
44
75
  isReadOnly: true,
45
76
  isLocalOnly: true,
46
- shouldRegister: (context) => !!context.workspace,
47
77
  factory: createListProjectsHandler,
48
78
  });
49
- function createListProjectsHandler({ workspace }) {
79
+ const EXCLUDED_DIRS = new Set(['node_modules', 'dist', 'out', 'coverage']);
80
+ /**
81
+ * Iteratively finds all 'angular.json' files with controlled concurrency and directory exclusions.
82
+ * This non-recursive implementation is suitable for very large directory trees
83
+ * and prevents file descriptor exhaustion (`EMFILE` errors).
84
+ * @param rootDir The directory to start the search from.
85
+ * @returns An async generator that yields the full path of each found 'angular.json' file.
86
+ */
87
+ async function* findAngularJsonFiles(rootDir) {
88
+ const CONCURRENCY_LIMIT = 50;
89
+ const queue = [rootDir];
90
+ while (queue.length > 0) {
91
+ const batch = queue.splice(0, CONCURRENCY_LIMIT);
92
+ const foundFilesInBatch = [];
93
+ const promises = batch.map(async (dir) => {
94
+ try {
95
+ const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
96
+ const subdirectories = [];
97
+ for (const entry of entries) {
98
+ const fullPath = node_path_1.default.join(dir, entry.name);
99
+ if (entry.isDirectory()) {
100
+ // Exclude dot-directories, build/cache directories, and node_modules
101
+ if (entry.name.startsWith('.') || EXCLUDED_DIRS.has(entry.name)) {
102
+ continue;
103
+ }
104
+ subdirectories.push(fullPath);
105
+ }
106
+ else if (entry.name === 'angular.json') {
107
+ foundFilesInBatch.push(fullPath);
108
+ }
109
+ }
110
+ return subdirectories;
111
+ }
112
+ catch (error) {
113
+ (0, error_1.assertIsError)(error);
114
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
115
+ return []; // Silently ignore permission errors.
116
+ }
117
+ throw error;
118
+ }
119
+ });
120
+ const nestedSubdirs = await Promise.all(promises);
121
+ queue.push(...nestedSubdirs.flat());
122
+ yield* foundFilesInBatch;
123
+ }
124
+ }
125
+ /**
126
+ * Loads, parses, and transforms a single angular.json file into the tool's output format.
127
+ * It checks a set of seen paths to avoid processing the same workspace multiple times.
128
+ * @param configFile The path to the angular.json file.
129
+ * @param seenPaths A Set of absolute paths that have already been processed.
130
+ * @returns A promise resolving to the workspace data or a parsing error.
131
+ */
132
+ async function loadAndParseWorkspace(configFile, seenPaths) {
133
+ try {
134
+ const resolvedPath = node_path_1.default.resolve(configFile);
135
+ if (seenPaths.has(resolvedPath)) {
136
+ return { workspace: null, error: null }; // Already processed, skip.
137
+ }
138
+ seenPaths.add(resolvedPath);
139
+ const ws = await config_1.AngularWorkspace.load(configFile);
140
+ const projects = [];
141
+ for (const [name, project] of ws.projects.entries()) {
142
+ projects.push({
143
+ name,
144
+ type: project.extensions['projectType'],
145
+ root: project.root,
146
+ sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'),
147
+ selectorPrefix: project.extensions['prefix'],
148
+ });
149
+ }
150
+ return { workspace: { path: configFile, projects }, error: null };
151
+ }
152
+ catch (error) {
153
+ let message;
154
+ if (error instanceof Error) {
155
+ message = error.message;
156
+ }
157
+ else {
158
+ message = 'An unknown error occurred while parsing the file.';
159
+ }
160
+ return { workspace: null, error: { filePath: configFile, message } };
161
+ }
162
+ }
163
+ async function createListProjectsHandler({ server }) {
50
164
  return async () => {
51
- if (!workspace) {
165
+ const workspaces = [];
166
+ const parsingErrors = [];
167
+ const seenPaths = new Set();
168
+ let searchRoots;
169
+ const clientCapabilities = server.server.getClientCapabilities();
170
+ if (clientCapabilities?.roots) {
171
+ const { roots } = await server.server.listRoots();
172
+ searchRoots = roots?.map((r) => node_path_1.default.normalize((0, node_url_1.fileURLToPath)(r.uri))) ?? [];
173
+ }
174
+ else {
175
+ // Fallback to the current working directory if client does not support roots
176
+ searchRoots = [process.cwd()];
177
+ }
178
+ for (const root of searchRoots) {
179
+ for await (const configFile of findAngularJsonFiles(root)) {
180
+ const { workspace, error } = await loadAndParseWorkspace(configFile, seenPaths);
181
+ if (workspace) {
182
+ workspaces.push(workspace);
183
+ }
184
+ if (error) {
185
+ parsingErrors.push(error);
186
+ }
187
+ }
188
+ }
189
+ if (workspaces.length === 0 && parsingErrors.length === 0) {
52
190
  return {
53
191
  content: [
54
192
  {
@@ -58,30 +196,17 @@ function createListProjectsHandler({ workspace }) {
58
196
  ' could not be located in the current directory or any of its parent directories.',
59
197
  },
60
198
  ],
61
- structuredContent: { projects: [] },
199
+ structuredContent: { workspaces: [] },
62
200
  };
63
201
  }
64
- const projects = [];
65
- // Convert to output format
66
- for (const [name, project] of workspace.projects.entries()) {
67
- projects.push({
68
- name,
69
- type: project.extensions['projectType'],
70
- root: project.root,
71
- sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'),
72
- selectorPrefix: project.extensions['prefix'],
73
- });
202
+ let text = `Found ${workspaces.length} workspace(s).\n${JSON.stringify({ workspaces })}`;
203
+ if (parsingErrors.length > 0) {
204
+ text += `\n\nWarning: The following ${parsingErrors.length} file(s) could not be parsed and were skipped:\n`;
205
+ text += parsingErrors.map((e) => `- ${e.filePath}: ${e.message}`).join('\n');
74
206
  }
75
- // The structuredContent field is newer and may not be supported by all hosts.
76
- // A text representation of the content is also provided for compatibility.
77
207
  return {
78
- content: [
79
- {
80
- type: 'text',
81
- text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`,
82
- },
83
- ],
84
- structuredContent: { projects },
208
+ content: [{ type: 'text', text }],
209
+ structuredContent: { workspaces, parsingErrors },
85
210
  };
86
211
  };
87
212
  }
@@ -10,6 +10,7 @@ import { ZodRawShape } from 'zod';
10
10
  import type { AngularWorkspace } from '../../../utilities/config';
11
11
  type ToolConfig = Parameters<McpServer['registerTool']>[1];
12
12
  export interface McpToolContext {
13
+ server: McpServer;
13
14
  workspace?: AngularWorkspace;
14
15
  logger: {
15
16
  warn(text: string): void;
@@ -31,5 +32,5 @@ export interface McpToolDeclaration<TInput extends ZodRawShape, TOutput extends
31
32
  }
32
33
  export type AnyMcpToolDeclaration = McpToolDeclaration<any, any>;
33
34
  export declare function declareTool<TInput extends ZodRawShape, TOutput extends ZodRawShape>(declaration: McpToolDeclaration<TInput, TOutput>): McpToolDeclaration<TInput, TOutput>;
34
- export declare function registerTools(server: McpServer, context: McpToolContext, declarations: AnyMcpToolDeclaration[]): Promise<void>;
35
+ export declare function registerTools(server: McpServer, context: Omit<McpToolContext, 'server'>, declarations: AnyMcpToolDeclaration[]): Promise<void>;
35
36
  export {};
@@ -14,11 +14,12 @@ function declareTool(declaration) {
14
14
  }
15
15
  async function registerTools(server, context, declarations) {
16
16
  for (const declaration of declarations) {
17
- if (declaration.shouldRegister && !(await declaration.shouldRegister(context))) {
17
+ const toolContext = { ...context, server };
18
+ if (declaration.shouldRegister && !(await declaration.shouldRegister(toolContext))) {
18
19
  continue;
19
20
  }
20
21
  const { name, factory, shouldRegister, isReadOnly, isLocalOnly, ...config } = declaration;
21
- const handler = await factory(context);
22
+ const handler = await factory(toolContext);
22
23
  // Add declarative characteristics to annotations
23
24
  config.annotations ??= {};
24
25
  if (isReadOnly !== undefined) {
@@ -12,8 +12,14 @@ export interface PackageManagerUtilsContext {
12
12
  workspace?: AngularWorkspace;
13
13
  root: string;
14
14
  }
15
+ /**
16
+ * Utilities for interacting with various package managers.
17
+ */
15
18
  export declare class PackageManagerUtils {
16
19
  private readonly context;
20
+ /**
21
+ * @param context The context for the package manager utilities, including workspace and global configuration.
22
+ */
17
23
  constructor(context: PackageManagerUtilsContext);
18
24
  /** Get the package manager name. */
19
25
  get name(): PackageManager;
@@ -32,6 +38,12 @@ export declare class PackageManagerUtils {
32
38
  private run;
33
39
  private getVersion;
34
40
  private getName;
41
+ /**
42
+ * Checks if a lockfile for a specific package manager exists in the root directory.
43
+ * @param packageManager The package manager to check for.
44
+ * @param filesInRoot An array of file names in the root directory.
45
+ * @returns True if the lockfile exists, false otherwise.
46
+ */
35
47
  private hasLockfile;
36
48
  private getConfiguredPackageManager;
37
49
  }
@@ -50,6 +50,18 @@ const node_path_1 = require("node:path");
50
50
  const workspace_schema_1 = require("../../lib/config/workspace-schema");
51
51
  const config_1 = require("./config");
52
52
  const memoize_1 = require("./memoize");
53
+ /**
54
+ * A map of package managers to their corresponding lockfile names.
55
+ */
56
+ const LOCKFILE_NAMES = {
57
+ [workspace_schema_1.PackageManager.Yarn]: 'yarn.lock',
58
+ [workspace_schema_1.PackageManager.Pnpm]: 'pnpm-lock.yaml',
59
+ [workspace_schema_1.PackageManager.Bun]: ['bun.lockb', 'bun.lock'],
60
+ [workspace_schema_1.PackageManager.Npm]: 'package-lock.json',
61
+ };
62
+ /**
63
+ * Utilities for interacting with various package managers.
64
+ */
53
65
  let PackageManagerUtils = (() => {
54
66
  let _instanceExtraInitializers = [];
55
67
  let _getVersion_decorators;
@@ -64,6 +76,9 @@ let PackageManagerUtils = (() => {
64
76
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
65
77
  }
66
78
  context = __runInitializers(this, _instanceExtraInitializers);
79
+ /**
80
+ * @param context The context for the package manager utilities, including workspace and global configuration.
81
+ */
67
82
  constructor(context) {
68
83
  this.context = context;
69
84
  }
@@ -211,10 +226,11 @@ let PackageManagerUtils = (() => {
211
226
  if (packageManager) {
212
227
  return packageManager;
213
228
  }
214
- const hasNpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Npm);
215
- const hasYarnLock = this.hasLockfile(workspace_schema_1.PackageManager.Yarn);
216
- const hasPnpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Pnpm);
217
- const hasBunLock = this.hasLockfile(workspace_schema_1.PackageManager.Bun);
229
+ const filesInRoot = (0, node_fs_1.readdirSync)(this.context.root);
230
+ const hasNpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Npm, filesInRoot);
231
+ const hasYarnLock = this.hasLockfile(workspace_schema_1.PackageManager.Yarn, filesInRoot);
232
+ const hasPnpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Pnpm, filesInRoot);
233
+ const hasBunLock = this.hasLockfile(workspace_schema_1.PackageManager.Bun, filesInRoot);
218
234
  // PERF NOTE: `this.getVersion` spawns the package a the child_process which can take around ~300ms at times.
219
235
  // Therefore, we should only call this method when needed. IE: don't call `this.getVersion(PackageManager.Pnpm)` unless truly needed.
220
236
  // The result of this method is not stored in a variable because it's memoized.
@@ -259,24 +275,17 @@ let PackageManagerUtils = (() => {
259
275
  // Potentially with a prompt to choose and optionally set as the default.
260
276
  return workspace_schema_1.PackageManager.Npm;
261
277
  }
262
- hasLockfile(packageManager) {
263
- let lockfileName;
264
- switch (packageManager) {
265
- case workspace_schema_1.PackageManager.Yarn:
266
- lockfileName = 'yarn.lock';
267
- break;
268
- case workspace_schema_1.PackageManager.Pnpm:
269
- lockfileName = 'pnpm-lock.yaml';
270
- break;
271
- case workspace_schema_1.PackageManager.Bun:
272
- lockfileName = 'bun.lockb';
273
- break;
274
- case workspace_schema_1.PackageManager.Npm:
275
- default:
276
- lockfileName = 'package-lock.json';
277
- break;
278
- }
279
- return (0, node_fs_1.existsSync)((0, node_path_1.join)(this.context.root, lockfileName));
278
+ /**
279
+ * Checks if a lockfile for a specific package manager exists in the root directory.
280
+ * @param packageManager The package manager to check for.
281
+ * @param filesInRoot An array of file names in the root directory.
282
+ * @returns True if the lockfile exists, false otherwise.
283
+ */
284
+ hasLockfile(packageManager, filesInRoot) {
285
+ const lockfiles = LOCKFILE_NAMES[packageManager];
286
+ return typeof lockfiles === 'string'
287
+ ? filesInRoot.includes(lockfiles)
288
+ : lockfiles.some((lockfile) => filesInRoot.includes(lockfile));
280
289
  }
281
290
  getConfiguredPackageManager() {
282
291
  const getPackageManager = (source) => {
@@ -22,4 +22,4 @@ class Version {
22
22
  this.patch = patch;
23
23
  }
24
24
  }
25
- exports.VERSION = new Version('21.0.0-next.1');
25
+ exports.VERSION = new Version('21.0.0-next.3');