@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
@@ -59,7 +59,9 @@ const modernizeInputSchema = zod_1.z.object({
59
59
  // Casting to [string, ...string[]] since the enum definition requires a nonempty array.
60
60
  transformations: zod_1.z
61
61
  .array(zod_1.z.enum(TRANSFORMATIONS.map((t) => t.name)))
62
- .optional(),
62
+ .optional()
63
+ .describe('A list of specific transformations to get instructions for. ' +
64
+ 'If omitted, general guidance is provided.'),
63
65
  });
64
66
  function generateInstructions(transformationNames) {
65
67
  if (transformationNames.length === 0) {
@@ -97,27 +99,36 @@ async function runModernization(input) {
97
99
  exports.MODERNIZE_TOOL = (0, tool_registry_1.declareTool)({
98
100
  name: 'modernize',
99
101
  title: 'Modernize Angular Code',
100
- description: '<Purpose>\n' +
101
- 'This tool modernizes Angular code by applying the latest best practices and syntax improvements, ' +
102
- 'ensuring it is idiomatic, readable, and maintainable.\n\n' +
103
- '</Purpose>\n' +
104
- '<Use Cases>\n' +
105
- '* After generating new code: Run this tool immediately after creating new Angular components, directives, ' +
106
- 'or services to ensure they adhere to modern standards.\n' +
107
- '* On existing code: Apply to existing TypeScript files (.ts) and Angular templates (.html) to update ' +
108
- 'them with the latest features, such as the new built-in control flow syntax.\n\n' +
109
- '* When the user asks for a specific transformation: When the transformation list is populated, ' +
110
- 'these specific ones will be ran on the inputs.\n' +
111
- '</Use Cases>\n' +
112
- '<Transformations>\n' +
113
- TRANSFORMATIONS.map((t) => `* ${t.name}: ${t.description}`).join('\n') +
114
- '\n</Transformations>\n',
102
+ description: `
103
+ <Purpose>
104
+ Provides instructions and commands for modernizing Angular code to align with the latest best
105
+ practices and syntax. This tool helps ensure code is idiomatic, readable, and maintainable by
106
+ generating the exact steps needed to perform specific migrations.
107
+ </Purpose>
108
+ <Use Cases>
109
+ * **Applying Specific Migrations:** Get the precise commands to update code to modern patterns
110
+ (e.g., selecting 'control-flow-migration' to replace *ngIf with @if).
111
+ * **Upgrading Existing Code:** Modernize an entire project by running the 'standalone' migration,
112
+ which provides a multi-step command sequence.
113
+ * **Discovering Available Migrations:** Call the tool with no transformations to get a link to the
114
+ general best practices guide.
115
+ </Use Cases>
116
+ <Operational Notes>
117
+ * **Execution:** This tool **provides instructions**, which you **MUST** then execute as shell commands.
118
+ It does not modify code directly.
119
+ * **Standalone Migration:** The 'standalone' transformation is a special, multi-step process.
120
+ You **MUST** execute the commands in the exact order provided and validate your application
121
+ between each step.
122
+ * **Transformation List:** The following transformations are available:
123
+ ${TRANSFORMATIONS.map((t) => ` * ${t.name}: ${t.description}`).join('\n')}
124
+ </Operational Notes>`,
115
125
  inputSchema: modernizeInputSchema.shape,
116
126
  outputSchema: {
117
127
  instructions: zod_1.z
118
128
  .array(zod_1.z.string())
119
129
  .optional()
120
- .describe('A list of instructions on how to run the migrations.'),
130
+ .describe('A list of instructions and shell commands to run the requested modernizations. ' +
131
+ 'Each string in the array is a separate step or command.'),
121
132
  },
122
133
  isLocalOnly: true,
123
134
  isReadOnly: true,
@@ -0,0 +1,17 @@
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 type { ImportSpecifier, Node, SourceFile } from 'typescript';
9
+ import { MigrationResponse } from './types';
10
+ export declare function analyzeForUnsupportedZoneUses(sourceFile: SourceFile): Promise<MigrationResponse | null>;
11
+ /**
12
+ * Finds usages of `NgZone` that are not supported in zoneless applications.
13
+ * @param sourceFile The source file to check.
14
+ * @param ngZoneImport The import specifier for `NgZone`.
15
+ * @returns A list of nodes that are unsupported `NgZone` usages.
16
+ */
17
+ export declare function findUnsupportedZoneUsages(sourceFile: SourceFile, ngZoneImport: ImportSpecifier): Promise<Node[]>;
@@ -0,0 +1,61 @@
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.analyzeForUnsupportedZoneUses = analyzeForUnsupportedZoneUses;
11
+ exports.findUnsupportedZoneUsages = findUnsupportedZoneUsages;
12
+ const prompts_1 = require("./prompts");
13
+ const ts_utils_1 = require("./ts_utils");
14
+ async function analyzeForUnsupportedZoneUses(sourceFile) {
15
+ const ngZoneImport = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core', 'NgZone');
16
+ if (!ngZoneImport) {
17
+ return null;
18
+ }
19
+ const unsupportedUsages = await findUnsupportedZoneUsages(sourceFile, ngZoneImport);
20
+ if (unsupportedUsages.length === 0) {
21
+ return null;
22
+ }
23
+ const locations = unsupportedUsages.map((node) => {
24
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
25
+ return `line ${line + 1}, character ${character + 1}: ${node.getText()}`;
26
+ });
27
+ return (0, prompts_1.createUnsupportedZoneUsagesMessage)(locations, sourceFile.fileName);
28
+ }
29
+ /**
30
+ * Finds usages of `NgZone` that are not supported in zoneless applications.
31
+ * @param sourceFile The source file to check.
32
+ * @param ngZoneImport The import specifier for `NgZone`.
33
+ * @returns A list of nodes that are unsupported `NgZone` usages.
34
+ */
35
+ async function findUnsupportedZoneUsages(sourceFile, ngZoneImport) {
36
+ const unsupportedUsages = [];
37
+ const ngZoneClassName = ngZoneImport.name.text;
38
+ const staticMethods = new Set([
39
+ 'isInAngularZone',
40
+ 'assertInAngularZone',
41
+ 'assertNotInAngularZone',
42
+ ]);
43
+ const instanceMethods = new Set(['onMicrotaskEmpty', 'onStable']);
44
+ const ts = await (0, ts_utils_1.loadTypescript)();
45
+ ts.forEachChild(sourceFile, function visit(node) {
46
+ if (ts.isPropertyAccessExpression(node)) {
47
+ const propertyName = node.name.text;
48
+ const expressionText = node.expression.getText(sourceFile);
49
+ // Static: NgZone.method()
50
+ if (expressionText === ngZoneClassName && staticMethods.has(propertyName)) {
51
+ unsupportedUsages.push(node);
52
+ }
53
+ // Instance: zone.method() or this.zone.method()
54
+ if (instanceMethods.has(propertyName)) {
55
+ unsupportedUsages.push(node);
56
+ }
57
+ }
58
+ ts.forEachChild(node, visit);
59
+ });
60
+ return unsupportedUsages;
61
+ }
@@ -0,0 +1,12 @@
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 { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
9
+ import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
10
+ import type { SourceFile } from 'typescript';
11
+ import { MigrationResponse } from './types';
12
+ export declare function migrateSingleFile(sourceFile: SourceFile, extras: RequestHandlerExtra<ServerRequest, ServerNotification>): Promise<MigrationResponse | null>;
@@ -0,0 +1,72 @@
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.migrateSingleFile = migrateSingleFile;
11
+ const analyze_for_unsupported_zone_uses_1 = require("./analyze_for_unsupported_zone_uses");
12
+ const migrate_test_file_1 = require("./migrate_test_file");
13
+ const prompts_1 = require("./prompts");
14
+ const send_debug_message_1 = require("./send_debug_message");
15
+ const ts_utils_1 = require("./ts_utils");
16
+ async function migrateSingleFile(sourceFile, extras) {
17
+ const testBedSpecifier = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core/testing', 'TestBed');
18
+ const isTestFile = sourceFile.fileName.endsWith('.spec.ts') || !!testBedSpecifier;
19
+ if (isTestFile) {
20
+ return (0, migrate_test_file_1.migrateTestFile)(sourceFile);
21
+ }
22
+ const unsupportedZoneUseResponse = await (0, analyze_for_unsupported_zone_uses_1.analyzeForUnsupportedZoneUses)(sourceFile);
23
+ if (unsupportedZoneUseResponse) {
24
+ return unsupportedZoneUseResponse;
25
+ }
26
+ let detectedStrategy;
27
+ let hasComponentDecorator = false;
28
+ const componentSpecifier = await (0, ts_utils_1.getImportSpecifier)(sourceFile, '@angular/core', 'Component');
29
+ if (!componentSpecifier) {
30
+ (0, send_debug_message_1.sendDebugMessage)(`No component decorator found in file: ${sourceFile.fileName}`, extras);
31
+ return null;
32
+ }
33
+ const ts = await (0, ts_utils_1.loadTypescript)();
34
+ ts.forEachChild(sourceFile, function visit(node) {
35
+ if (detectedStrategy) {
36
+ return; // Already found, no need to traverse further
37
+ }
38
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
39
+ const callExpr = node.expression;
40
+ if (callExpr.expression.getText(sourceFile) === 'Component') {
41
+ hasComponentDecorator = true;
42
+ if (callExpr.arguments.length > 0 && ts.isObjectLiteralExpression(callExpr.arguments[0])) {
43
+ const componentMetadata = callExpr.arguments[0];
44
+ for (const prop of componentMetadata.properties) {
45
+ if (ts.isPropertyAssignment(prop) &&
46
+ prop.name.getText(sourceFile) === 'changeDetection') {
47
+ if (ts.isPropertyAccessExpression(prop.initializer) &&
48
+ prop.initializer.expression.getText(sourceFile) === 'ChangeDetectionStrategy') {
49
+ const strategy = prop.initializer.name.text;
50
+ if (strategy === 'OnPush' || strategy === 'Default') {
51
+ detectedStrategy = strategy;
52
+ return;
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ts.forEachChild(node, visit);
61
+ });
62
+ if (!hasComponentDecorator ||
63
+ // component uses OnPush. We don't have anything more to do here.
64
+ detectedStrategy === 'OnPush' ||
65
+ // Explicit default strategy, assume there's a reason for it (already migrated, or is a library that hosts Default components) and skip.
66
+ detectedStrategy === 'Default') {
67
+ (0, send_debug_message_1.sendDebugMessage)(`Component decorator found with strategy: ${detectedStrategy} in file: ${sourceFile.fileName}. Skipping migration for file.`, extras);
68
+ return null;
69
+ }
70
+ // Component decorator found, but no change detection strategy.
71
+ return (0, prompts_1.generateZonelessMigrationInstructionsForComponent)(sourceFile.fileName);
72
+ }
@@ -0,0 +1,11 @@
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 type { SourceFile } from 'typescript';
9
+ import { MigrationResponse } from './types';
10
+ export declare function migrateTestFile(sourceFile: SourceFile): Promise<MigrationResponse | null>;
11
+ export declare function searchForGlobalZoneless(startPath: string): Promise<boolean>;
@@ -0,0 +1,105 @@
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
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.migrateTestFile = migrateTestFile;
44
+ exports.searchForGlobalZoneless = searchForGlobalZoneless;
45
+ const fs = __importStar(require("node:fs"));
46
+ const promises_1 = require("node:fs/promises");
47
+ const node_path_1 = require("node:path");
48
+ const prompts_1 = require("./prompts");
49
+ const ts_utils_1 = require("./ts_utils");
50
+ async function migrateTestFile(sourceFile) {
51
+ const ts = await (0, ts_utils_1.loadTypescript)();
52
+ // Check if tests use zoneless either by default through `initTestEnvironment` or by explicitly calling `provideZonelessChangeDetection`.
53
+ let testsUseZonelessChangeDetection = await searchForGlobalZoneless(sourceFile.fileName);
54
+ if (!testsUseZonelessChangeDetection) {
55
+ ts.forEachChild(sourceFile, function visit(node) {
56
+ if (ts.isCallExpression(node) &&
57
+ node.expression.getText(sourceFile) === 'provideZonelessChangeDetection') {
58
+ testsUseZonelessChangeDetection = true;
59
+ return;
60
+ }
61
+ ts.forEachChild(node, visit);
62
+ });
63
+ }
64
+ if (!testsUseZonelessChangeDetection) {
65
+ // Tests do not use zoneless, so we provide instructions to set it up.
66
+ return (0, prompts_1.createProvideZonelessForTestsSetupPrompt)(sourceFile.fileName);
67
+ }
68
+ // At this point, tests are using zoneless, so we look for any explicit uses of `provideZoneChangeDetection` that need to be fixed.
69
+ return (0, prompts_1.createFixResponseForZoneTests)(sourceFile);
70
+ }
71
+ async function searchForGlobalZoneless(startPath) {
72
+ const angularJsonDir = findAngularJsonDir(startPath);
73
+ if (!angularJsonDir) {
74
+ // Cannot determine project root, fallback to original behavior or assume false.
75
+ // For now, let's assume no global setup if angular.json is not found.
76
+ return false;
77
+ }
78
+ try {
79
+ const files = (0, promises_1.glob)(`${angularJsonDir}/**/*.ts`);
80
+ for await (const file of files) {
81
+ const content = fs.readFileSync(file, 'utf-8');
82
+ if (content.includes('initTestEnvironment') &&
83
+ content.includes('provideZonelessChangeDetection')) {
84
+ return true;
85
+ }
86
+ }
87
+ }
88
+ catch (e) {
89
+ return false;
90
+ }
91
+ return false;
92
+ }
93
+ function findAngularJsonDir(startDir) {
94
+ let currentDir = startDir;
95
+ while (true) {
96
+ if (fs.existsSync((0, node_path_1.join)(currentDir, 'angular.json'))) {
97
+ return currentDir;
98
+ }
99
+ const parentDir = (0, node_path_1.dirname)(currentDir);
100
+ if (parentDir === currentDir) {
101
+ return null;
102
+ }
103
+ currentDir = parentDir;
104
+ }
105
+ }
@@ -0,0 +1,15 @@
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 type { SourceFile } from 'typescript';
9
+ import { MigrationResponse } from './types';
10
+ export declare function createProvideZonelessForTestsSetupPrompt(testFilePath: string): MigrationResponse;
11
+ export declare function createUnsupportedZoneUsagesMessage(usages: string[], filePath: string): MigrationResponse;
12
+ export declare function generateZonelessMigrationInstructionsForComponent(filePath: string): MigrationResponse;
13
+ export declare function createTestDebuggingGuideForNonActionableInput(fileOrDirPath: string): MigrationResponse;
14
+ export declare function createFixResponseForZoneTests(sourceFile: SourceFile): Promise<MigrationResponse | null>;
15
+ export declare function createResponse(text: string): MigrationResponse;
@@ -0,0 +1,236 @@
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.createProvideZonelessForTestsSetupPrompt = createProvideZonelessForTestsSetupPrompt;
11
+ exports.createUnsupportedZoneUsagesMessage = createUnsupportedZoneUsagesMessage;
12
+ exports.generateZonelessMigrationInstructionsForComponent = generateZonelessMigrationInstructionsForComponent;
13
+ exports.createTestDebuggingGuideForNonActionableInput = createTestDebuggingGuideForNonActionableInput;
14
+ exports.createFixResponseForZoneTests = createFixResponseForZoneTests;
15
+ exports.createResponse = createResponse;
16
+ const ts_utils_1 = require("./ts_utils");
17
+ /* eslint-disable max-len */
18
+ function createProvideZonelessForTestsSetupPrompt(testFilePath) {
19
+ const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to update the test file at \`${testFilePath}\` to enable zoneless change detection and identify tests that are not yet compatible.
20
+
21
+ Follow these instructions precisely.
22
+
23
+ ### Refactoring Guide
24
+
25
+ The test file \`${testFilePath}\` is not yet configured for zoneless change detection. You need to enable it for the entire test suite and then identify which specific tests fail.
26
+
27
+ #### Step 1: Enable Zoneless Change Detection for the Suite
28
+
29
+ In the main \`beforeEach\` block for the test suite (the one inside the top-level \`describe\`), add \`provideZonelessChangeDetection()\` to the providers array in \`TestBed.configureTestingModule\`.
30
+
31
+ * If there is already an import from \`@angular/core\`, add \`provideZonelessChangeDetection\` to the existing import.
32
+ * Otherwise, add a new import statement for \`provideZonelessChangeDetection\` from \`@angular/core\`.
33
+
34
+ \`\`\`diff
35
+ - import {{ SomeImport }} from '@angular/core';
36
+ + import {{ SomeImport, provideZonelessChangeDetection }} from '@angular/core';
37
+
38
+ describe('MyComponent', () => {
39
+ + beforeEach(() => {
40
+ + TestBed.configureTestingModule({providers: [provideZonelessChangeDetection()]});
41
+ + });
42
+ });
43
+ \`\`\`
44
+
45
+ #### Step 2: Identify and fix Failing Tests
46
+
47
+ After enabling zoneless detection for the suite, some tests will likely fail. Your next task is to identify these failing tests and fix them.
48
+
49
+ ${testDebuggingGuideText(testFilePath)}
50
+ 8. **DO** add \`provideZonelessChangeDetection()\` _once_ to the top-most \`describe\` in a \`beforeEach\` block as instructed in Step 1.
51
+ 9. **DO** run the tests after adding \`provideZonelessChangeDetection\` to see which ones fail. **DO NOT** make assumptions about which tests will might fail.
52
+
53
+ ### Final Step
54
+ After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.`;
55
+ return createResponse(text);
56
+ }
57
+ function createUnsupportedZoneUsagesMessage(usages, filePath) {
58
+ const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to refactor the component in ${filePath} to remove unsupported NgZone APIs.
59
+
60
+ The component uses NgZone APIs that are incompatible with zoneless applications. The only permitted NgZone APIs are \`NgZone.run\` and \`NgZone.runOutsideAngular\`.
61
+
62
+ The following usages are unsupported and must be fixed:
63
+ ${usages.map((usage) => `- ${usage}`).join('\n')}
64
+
65
+ Follow these instructions precisely to refactor the code.
66
+
67
+ ### Refactoring Guide
68
+
69
+ #### 1. APIs to Remove (No Replacement)
70
+ The following methods have no replacement in a zoneless context and must be removed entirely:
71
+ - \`NgZone.assertInAngularZone\`
72
+ - \`NgZone.assertNotInAngularZone\`
73
+ - \`NgZone.isInAngularZone\`
74
+
75
+ #### 2. APIs to Replace
76
+ The \`onMicrotaskEmpty\` and \`onStable\` observables must be replaced with modern Angular APIs.
77
+
78
+ - **For single-event subscriptions** (e.g., using \`.pipe(take(1))\` or \`.pipe(first())\`), use \`afterNextRender\` from \`@angular/core\`.
79
+
80
+ \`\`\`diff
81
+ - this.zone.onMicrotaskEmpty.pipe(take(1)).subscribe(() => {});
82
+ - this.zone.onStable.pipe(take(1)).subscribe(() => {});
83
+ + import { afterNextRender, Injector } from '@angular/core';
84
+ + afterNextRender(() => {}, {injector: this.injector});
85
+ \`\`\`
86
+
87
+ - **For continuous subscriptions**, use \`afterEveryRender\` from \`@angular/core\`.
88
+
89
+ \`\`\`diff
90
+ - this.zone.onMicrotaskEmpty.subscribe(() => {});
91
+ - this.zone.onStable.subscribe(() => {});
92
+ + import { afterEveryRender, Injector } from '@angular/core';
93
+ + afterEveryRender(() => {}, {injector: this.injector});
94
+ \`\`\`
95
+
96
+ - If the code checks \`this.zone.isStable\` before subscribing, you can remove the \`isStable\` check. \`afterNextRender\` handles this case correctly.
97
+
98
+ ### IMPORTANT: Rules and Constraints
99
+ You must follow these rules without exception:
100
+ 1. **DO NOT** make any changes to the component that are unrelated to removing the unsupported NgZone APIs listed above.
101
+ 2. **DO NOT** remove or modify usages of \`NgZone.run\` or \`NgZone.runOutsideAngular\`. These are still required.
102
+ 3. **DO** ensure that you replace \`onMicrotaskEmpty\` and \`onStable\` with the correct replacements (\`afterNextRender\` or \`afterEveryRender\`) as described in the guide.
103
+ 4. **DO** add the necessary imports for \`afterNextRender\`, \`afterEveryRender\`, and \`Injector\` when you use them.
104
+
105
+ ### Final Step
106
+ After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.
107
+ `;
108
+ return createResponse(text);
109
+ }
110
+ function generateZonelessMigrationInstructionsForComponent(filePath) {
111
+ const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to refactor the component in \`${filePath}\` to be compatible with zoneless change detection by ensuring Angular is notified of all state changes that affect the view.
112
+
113
+ The component does not currently use a change detection strategy, which means it may rely on Zone.js. To prepare it for zoneless, you must manually trigger change detection when its state changes.
114
+
115
+ Follow these instructions precisely.
116
+
117
+ ### Refactoring Guide
118
+
119
+ #### Step 1: Identify and Refactor State
120
+ Your primary goal is to ensure that every time a component property used in the template is updated, Angular knows it needs to run change detection.
121
+
122
+ 1. **Identify Properties**: Find all component properties that are read by the template.
123
+ 2. **Choose a Strategy**: For each property identified, choose one of the following refactoring strategies:
124
+ * **(Preferred) Convert to Signal**: The best approach is to convert the property to an Angular Signal. This is the most idiomatic and future-proof way to handle state in zoneless applications.
125
+ * **(Alternative) Use \`markForCheck()\`**: If converting to a signal is too complex or would require extensive refactoring, you can instead inject \`ChangeDetectorRef\` and call \`this.cdr.markForCheck()\` immediately after the property is updated.
126
+
127
+ #### Step 2: Add \`ChangeDetectionStrategy.Default\`
128
+ After you have refactored all necessary properties, you must update the component's decorator to explicitly set the change detection strategy.
129
+
130
+ 1. Add \`ChangeDetectionStrategy\` to the import from \`@angular/core\`.
131
+ 2. In the \`@Component\` decorator, add the property \`changeDetection: ChangeDetectionStrategy.Default\`.
132
+ 3. Add a \`// TODO\` comment above this line explaining that the component should be fully migrated to \`OnPush\` after the application has been tested with these changes.
133
+
134
+ Example:
135
+ \`\`\`typescript
136
+ @Component({
137
+ ...
138
+ // TODO: This component has been partially migrated to be zoneless-compatible.
139
+ // After testing, this should be updated to ChangeDetectionStrategy.OnPush.
140
+ changeDetection: ChangeDetectionStrategy.Default,
141
+ })
142
+ \`\`\`
143
+
144
+ ### IMPORTANT: Rules and Constraints
145
+ You must follow these rules without exception:
146
+ 1. **DO** apply one of the two refactoring strategies (signals or \`markForCheck()\`) for all relevant component properties.
147
+ 2. **DO** add \`changeDetection: ChangeDetectionStrategy.Default\` with the specified TODO comment as the final code change.
148
+ 3. **DO NOT** use \`ChangeDetectionStrategy.OnPush\`. This will be the next step in the migration, but it is not part of this task.
149
+ 4. **DO NOT** modify properties that are already signals or are used with the \`async\` pipe in the template, as they are already zoneless-compatible.
150
+ 5. **DO NOT** make any changes to files other than the component file at \`${filePath}\` and its direct template/style files if necessary.
151
+
152
+ ### Final Step
153
+ After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.`;
154
+ return createResponse(text);
155
+ }
156
+ function createTestDebuggingGuideForNonActionableInput(fileOrDirPath) {
157
+ const text = `You are an expert Angular developer assisting with a migration to zoneless.
158
+
159
+ No actionable migration steps were found in the application code for \`${fileOrDirPath}\`. However, if the tests for this code are failing with zoneless enabled, the tests themselves likely need to be updated.
160
+
161
+ Your task is to investigate and fix any failing tests related to the code in \`${fileOrDirPath}\`.
162
+
163
+ ${testDebuggingGuideText(fileOrDirPath)}
164
+ `;
165
+ return createResponse(text);
166
+ }
167
+ async function createFixResponseForZoneTests(sourceFile) {
168
+ const ts = await (0, ts_utils_1.loadTypescript)();
169
+ const usages = [];
170
+ ts.forEachChild(sourceFile, function visit(node) {
171
+ if (ts.isCallExpression(node) &&
172
+ node.expression.getText(sourceFile) === 'provideZoneChangeDetection') {
173
+ usages.push(node);
174
+ }
175
+ ts.forEachChild(node, visit);
176
+ });
177
+ if (usages.length === 0) {
178
+ // No usages of provideZoneChangeDetection found, so no fix needed.
179
+ return null;
180
+ }
181
+ const locations = usages.map((node) => {
182
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
183
+ return `line ${line + 1}, character ${character + 1}`;
184
+ });
185
+ const text = `You are an expert Angular developer assisting with a migration to zoneless. Your task is to update the test file at \`${sourceFile.fileName}\` to be fully zoneless-compatible.
186
+
187
+ The test suite has been partially migrated, but some tests were incompatible and are still using Zone.js-based change detection via \`provideZoneChangeDetection\`. You must refactor these tests to work in a zoneless environment and remove the \`provideZoneChangeDetection\` calls.
188
+
189
+ The following usages of \`provideZoneChangeDetection\` must be removed:
190
+ ${locations.map((loc) => `- ${loc}`).join('\n')}
191
+
192
+ After removing \`provideZoneChangeDetection\`, the tests will likely fail. Use this guide to diagnose and fix the failures.
193
+
194
+ ${testDebuggingGuideText(sourceFile.fileName)}
195
+
196
+ ### Final Step
197
+ After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.`;
198
+ return createResponse(text);
199
+ }
200
+ function testDebuggingGuideText(fileName) {
201
+ return `
202
+ ### Test Debugging Guide
203
+
204
+ 1. **\`ExpressionChangedAfterItHasBeenCheckedError\`**:
205
+ * **Cause**: This error indicates that a value in a component's template was updated, but Angular was not notified to run change detection.
206
+ * **Solution**:
207
+ * If the value is in a test-only wrapper component, update the property to be a signal.
208
+ * For application components, either convert the property to a signal or call \`ChangeDetectorRef.markForCheck()\` immediately after the property is updated.
209
+
210
+ 2. **Asynchronous Operations and Timing**:
211
+ * **Cause**: Without Zone.js, change detection is always scheduled asynchronously. Tests that previously relied on synchronous updates might now fail. The \`fixture.whenStable()\` utility also no longer waits for timers (like \`setTimeout\` or \`setInterval\`).
212
+ * **Solution**:
213
+ * Avoid relying on synchronous change detection.
214
+ * To wait for asynchronous operations to complete, you may need to poll for an expected state, use \`fakeAsync\` with \`tick()\`, or use a mock clock to flush timers.
215
+
216
+ 3. **Indirect Dependencies**:
217
+ * **Cause**: The component itself might be zoneless-compatible, but it could be using a service or another dependency that is not.
218
+ * **Solution**: Investigate the services and dependencies used by the component and its tests. Run this tool on those dependencies to identify and fix any issues.
219
+
220
+ ### IMPORTANT: Rules and Constraints
221
+
222
+ You must follow these rules without exception:
223
+ 1. **DO** focus only on fixing the tests for the code in \`${fileName}\`.
224
+ 2. **DO** remove all usages of \`provideZoneChangeDetection\` from the test file.
225
+ 3. **DO** apply the solutions described in the debugging guide to fix any resulting test failures.
226
+ 4. **DO** update properties of test components and directives to use signals. Tests often use plain objects and values and update the component state directly before calling \`fixture.detectChanges\`. This will not work and will result in \`ExpressionChangedAfterItHasBeenCheckedError\` because Angular was not notifed of the change.
227
+ 5. **DO NOT** make changes to application code unless it is to fix a bug revealed by the zoneless migration (e.g., converting a property to a signal to fix an \`ExpressionChangedAfterItHasBeenCheckedError\`).
228
+ 6. **DO NOT** make any changes unrelated to fixing the failing tests in \`${fileName}\`.
229
+ 7. **DO NOT** re-introduce \`provideZoneChangeDetection()\` into tests that are already using \`provideZonelessChangeDetection()\`.`;
230
+ }
231
+ /* eslint-enable max-len */
232
+ function createResponse(text) {
233
+ return {
234
+ content: [{ type: 'text', text }],
235
+ };
236
+ }
@@ -0,0 +1,10 @@
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 { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol';
9
+ import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types';
10
+ export declare function sendDebugMessage(message: string, { sendNotification }: RequestHandlerExtra<ServerRequest, ServerNotification>): void;
@@ -0,0 +1,19 @@
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.sendDebugMessage = sendDebugMessage;
11
+ function sendDebugMessage(message, { sendNotification }) {
12
+ void sendNotification({
13
+ method: 'notifications/message',
14
+ params: {
15
+ level: 'debug',
16
+ data: message,
17
+ },
18
+ });
19
+ }