Skip to content

Commit ffa1c4b

Browse files
committed
feat(@angular/cli): Add standalone and zoneless transformations to the modernize tool
- The transformation is now a three-step process guided by the output field. - A input was added to the tool to specify which step of the transformation to run. - A output field was added to provide links to relevant migration guides. - A transformation was added, which provides a documentation link and instructions for the manual migration process.
1 parent 7515b61 commit ffa1c4b

2 files changed

Lines changed: 78 additions & 10 deletions

File tree

packages/angular/cli/src/commands/mcp/mcp-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function createMcpServer(context: {
3131

3232
registerInstructionsResource(server);
3333
registerListProjectsTool(server, context);
34-
registerModernizeTool(server);
34+
registerModernizeTool(server);
3535

3636
await registerDocSearchTool(server);
3737

packages/angular/cli/src/commands/mcp/tools/modernize.ts

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,50 +23,78 @@ const TRANSFORMATIONS = [
2323
'Migrates from `*ngIf`, `*ngFor`, and `*ngSwitch` to the new `@if`, `@for`, and `@switch` block syntax in templates.',
2424
target: SchematicTarget.Template,
2525
runner: SchematicRunner.Collection,
26+
includedByDefault: true,
27+
documentation: 'https://angular.dev/reference/migrations/control-flow',
2628
},
2729
{
2830
name: 'self-closing-tags-migration',
2931
description:
3032
'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).',
3133
target: SchematicTarget.Template,
3234
runner: SchematicRunner.Collection,
35+
includedByDefault: true,
36+
documentation: 'https://angular.dev/reference/migrations/self-closing-tags',
3337
},
3438
{
3539
name: 'test-bed-get',
3640
description:
3741
'Updates `TestBed.get` to the preferred and type-safe `TestBed.inject` in TypeScript test files.',
3842
target: SchematicTarget.Code,
3943
runner: SchematicRunner.Migration,
44+
includedByDefault: true,
45+
documentation: 'https://angular.dev/guide/testing/dependency-injection',
4046
},
4147
{
4248
name: 'inject-flags',
4349
description:
4450
'Updates `inject` calls from using the InjectFlags enum to a more modern and readable options object.',
4551
target: SchematicTarget.Code,
4652
runner: SchematicRunner.Migration,
53+
includedByDefault: true,
54+
documentation: 'https://angular.dev/reference/migrations/inject-function',
4755
},
4856
{
4957
name: 'output-migration',
5058
description: 'Converts `@Output` declarations to the new functional `output()` syntax.',
5159
target: SchematicTarget.Code,
5260
runner: SchematicRunner.Collection,
61+
includedByDefault: true,
62+
documentation: 'https://angular.dev/reference/migrations/outputs',
5363
},
5464
{
5565
name: 'signal-input-migration',
5666
description: 'Migrates `@Input` declarations to the new signal-based `input()` syntax.',
5767
target: SchematicTarget.Code,
5868
runner: SchematicRunner.Collection,
69+
includedByDefault: true,
70+
documentation: 'https://angular.dev/reference/migrations/signal-inputs',
5971
},
6072
{
6173
name: 'signal-queries-migration',
6274
description:
6375
'Migrates `@ViewChild` and `@ContentChild` queries to their signal-based `viewChild` and `contentChild` versions.',
6476
target: SchematicTarget.Code,
6577
runner: SchematicRunner.Collection,
78+
includedByDefault: true,
79+
documentation: 'https://angular.dev/reference/migrations/signal-queries',
80+
},
81+
{
82+
name: 'standalone',
83+
description:
84+
'Converts the application to use standalone components, directives, and pipes. This is a three-step process. After each step, you should verify that your application builds and runs correctly. Full instructions at https://angular.dev/reference/migrations/standalone',
85+
target: SchematicTarget.Code,
86+
runner: SchematicRunner.Collection,
87+
includedByDefault: false,
88+
documentation: 'https://angular.dev/reference/migrations/standalone',
89+
},
90+
{
91+
name: 'zoneless',
92+
description: 'Migrates the application to be zoneless.',
93+
includedByDefault: false,
94+
documentation: 'https://angular.dev/guide/zoneless',
6695
},
6796
] as const;
6897

69-
const ALL_TRANSFORMATIONS = TRANSFORMATIONS.map((t) => t.name);
7098
export function registerModernizeTool(server: McpServer): void {
7199
server.registerTool(
72100
'modernize',
@@ -77,12 +105,21 @@ export function registerModernizeTool(server: McpServer): void {
77105
'This tool modernizes Angular code by applying the latest best practices and syntax improvements, ensuring it is idiomatic, readable, and maintainable.\n\n' +
78106
'</Purpose>\n' +
79107
'<Use Cases>\n' +
80-
'- After generating new code: Run this tool immediately after creating new Angular components, directives, or services to ensure they adhere to modern standards.\n' +
81-
'- On existing code: Apply to existing TypeScript files (.ts) and Angular templates (.ng.html) to update them with the latest features, such as the new built-in control flow syntax.\n\n' +
82-
'- When the user asks for a specific transformation: When the transformation list is populated, these specific ones will be ran on the inputs.\n' +
108+
'* After generating new code: Run this tool immediately after creating new Angular components, directives, or services to ensure they adhere to modern standards.\n' +
109+
'* On existing code: Apply to existing TypeScript files (.ts) and Angular templates (.ng.html) to update them with the latest features, such as the new built-in control flow syntax.\n\n' +
110+
'* When the user asks for a specific transformation: When the transformation list is populated, these specific ones will be ran on the inputs.\n' +
83111
'</Use Cases>\n' +
84112
'<Transformations>\n' +
85-
TRANSFORMATIONS.map((t) => `- ${t.name}: ${t.description}`).join('\n') +
113+
'<Default Transformations>\n' +
114+
TRANSFORMATIONS.filter((t) => t.includedByDefault)
115+
.map((t) => `* ${t.name}: ${t.description}`)
116+
.join('\n') +
117+
'\n</Default Transformations>\n' +
118+
'<On-Request Transformations>\n' +
119+
TRANSFORMATIONS.filter((t) => !t.includedByDefault)
120+
.map((t) => `* ${t.name}: ${t.description}`)
121+
.join('\n') +
122+
'/<On-Request Transformations>\n' +
86123
'\n</Transformations>\n',
87124
inputSchema: {
88125
files: z.array(
@@ -94,7 +131,13 @@ export function registerModernizeTool(server: McpServer): void {
94131
// Zod's `enum` requires a non-empty array of string literals, but TypeScript
95132
// infers `ALL_TRANSFORMATIONS` as `string[]`. The `as` cast is a
96133
// workaround to satisfy Zod's type checker.
97-
transformations: z.array(z.enum(ALL_TRANSFORMATIONS as [string, ...string[]])).optional(),
134+
transformations: z
135+
.array(z.enum(TRANSFORMATIONS.map((t) => t.name) as [string, ...string[]]))
136+
.optional(),
137+
mode: z
138+
.enum(['convert-to-standalone', 'prune-ng-modules', 'standalone-bootstrap'])
139+
.optional()
140+
.describe('The mode to use for the standalone transformation.'),
98141
},
99142
outputSchema: {
100143
files: z.array(
@@ -104,8 +147,9 @@ export function registerModernizeTool(server: McpServer): void {
104147
changed: z.boolean().describe('Whether the file was changed.'),
105148
}),
106149
),
150+
instructions: z.string().optional().describe('Additional instructions.'),
151+
documentation: z.string().optional().describe('A link to relevant documentation.'),
107152
},
108-
109153
},
110154
async (input) => {
111155
try {
@@ -127,7 +171,7 @@ export function registerModernizeTool(server: McpServer): void {
127171
const transformationsToRun =
128172
input.transformations && input.transformations.length > 0
129173
? TRANSFORMATIONS.filter((t) => input.transformations!.includes(t.name))
130-
: TRANSFORMATIONS;
174+
: TRANSFORMATIONS.filter((t) => t.includedByDefault);
131175

132176
const hasCodeFiles = input.files.some((f) => f.name.endsWith('.ts'));
133177
const hasTemplateFiles = input.files.some(
@@ -139,8 +183,30 @@ export function registerModernizeTool(server: McpServer): void {
139183
tree.create(file.name, file.content);
140184
}
141185

186+
let instructions: string | undefined;
187+
const documentation = new Set<string>();
188+
142189
for (const transformation of transformationsToRun) {
143-
if (transformation.target === SchematicTarget.Code && hasCodeFiles) {
190+
if (transformation.documentation) {
191+
documentation.add(transformation.documentation);
192+
}
193+
if (transformation.name === 'standalone') {
194+
const mode = input.mode ?? 'convert-to-standalone';
195+
tree = await runners[transformation.runner].runSchematic('standalone', { mode }, tree);
196+
197+
if (mode === 'convert-to-standalone') {
198+
instructions =
199+
'The first step of the `standalone` migration has been performed. Please verify that your application builds and runs correctly. Then, run this tool again with `mode: "prune-ng-modules"` to continue.';
200+
} else if (mode === 'prune-ng-modules') {
201+
instructions =
202+
'The second step of the `standalone` migration has been performed. Please verify that your application builds and runs correctly. Then, run this tool again with `mode: "standalone-bootstrap"` to complete the migration.';
203+
} else {
204+
instructions = 'The `standalone` migration has been completed.';
205+
}
206+
} else if (transformation.name === 'zoneless') {
207+
instructions =
208+
'The `zoneless` migration is a manual process. Please follow the instructions at https://angular.dev/guide/zoneless to complete the migration.';
209+
} else if (transformation.target === SchematicTarget.Code && hasCodeFiles) {
144210
tree = await runners[transformation.runner].runSchematic(transformation.name, {}, tree);
145211
} else if (transformation.target === SchematicTarget.Template && hasTemplateFiles) {
146212
tree = await runners[transformation.runner].runSchematic(transformation.name, {}, tree);
@@ -167,6 +233,8 @@ export function registerModernizeTool(server: McpServer): void {
167233
],
168234
structuredContent: {
169235
files: updatedFiles,
236+
instructions,
237+
documentation: documentation.size > 0 ? [...documentation].join('\n') : undefined,
170238
},
171239
};
172240
} catch (e) {

0 commit comments

Comments
 (0)