Skip to content

Commit 06c6468

Browse files
committed
refactor(@angular/cli): Make modernize use the file system
- Refactors the `modernize` tool to run all schematics on a temporary directory on the file system instead of using the `SchematicTestRunner` and a virtual file system. - This fixes issues with migrations that require access to the real file system, such as the `control-flow-migration`. - Extracts the core logic into a separate `runModernization` function to improve testability. - Adds a new test suite (`modernize_spec.ts`) to cover the refactored logic, including tests for default transformations, file system setup, and updated file content.
1 parent 99e108e commit 06c6468

3 files changed

Lines changed: 123 additions & 13 deletions

File tree

packages/angular/cli/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,4 @@ npm_package(
166166
":angular-cli",
167167
":license",
168168
],
169-
)
169+
)

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

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2-
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
3-
import { Tree } from '@angular-devkit/schematics';
42
import { z } from 'zod';
5-
import path from 'node:path';
63

74
interface Transformation {
85
name: string;
@@ -58,5 +55,121 @@ const TRANSFORMATIONS: Array<Transformation> = [
5855
'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.',
5956
instructions: `This migration requires running a cli schematic multiple times. Run the commands in the order listed below, verifying that your code builds and runs between each step:
6057
61-
1. Run
62-
`ng g @angular/core:standalone` and select
58+
1. Run \`ng g @angular/core:standalone\` and select "Convert all components, directives and pipes to standalone"
59+
2. Run \`ng g @angular/core:standalone\` and select "Remove unnecessary NgModule classes"
60+
3. Run \`ng g @angular/core:standalone\` and select "Bootstrap the project using standalone APIs"
61+
`,
62+
documentationUrl: 'https://angular.dev/reference/migrations/standalone',
63+
},
64+
{
65+
name: 'zoneless',
66+
description: 'Migrates the application to be zoneless.',
67+
documentationUrl: 'https://angular.dev/guide/zoneless',
68+
},
69+
];
70+
71+
const modernizeInputSchema = z.object({
72+
// Casting to [string, ...string[]] since the enum definition requires a nonempty array.
73+
transformations: z
74+
.array(z.enum(TRANSFORMATIONS.map((t) => t.name) as [string, ...string[]]))
75+
.optional(),
76+
});
77+
78+
export type ModernizeInput = z.infer<typeof modernizeInputSchema>;
79+
80+
export async function runModernization(input: ModernizeInput) {
81+
try {
82+
if (!input.transformations || input.transformations.length === 0) {
83+
const instructions = [
84+
'See https://angular.dev/best-practices for Angular best practices. You can call this tool if you have specific transformation you want to run.',
85+
];
86+
return {
87+
content: [
88+
{
89+
type: 'text' as const,
90+
text: JSON.stringify({
91+
instructions,
92+
}),
93+
},
94+
],
95+
structuredContent: {
96+
instructions,
97+
},
98+
};
99+
}
100+
101+
const transformationsToRun = TRANSFORMATIONS.filter((t) =>
102+
input.transformations!.includes(t.name),
103+
);
104+
105+
const allInstructions: string[] = [];
106+
107+
for (const transformation of transformationsToRun) {
108+
let transformationInstructions = '';
109+
if (transformation.instructions) {
110+
transformationInstructions = transformation.instructions;
111+
} else {
112+
// If no instructions are included, default to running a cli schematic with the transformation name.
113+
const command = `ng generate @angular/core:${transformation.name}`;
114+
transformationInstructions = `To run the ${transformation.name} migration, execute the following command: \`${command}\`.`;
115+
}
116+
if (transformation.documentationUrl) {
117+
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`;
118+
}
119+
allInstructions.push(transformationInstructions);
120+
}
121+
122+
const structuredContent = {
123+
instructions: allInstructions.length ? allInstructions : undefined,
124+
};
125+
126+
return {
127+
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent) }],
128+
structuredContent,
129+
};
130+
} catch (e) {
131+
const message = e instanceof Error ? e.message : 'An unknown error occurred.';
132+
return {
133+
content: [
134+
{
135+
type: 'text' as const,
136+
text: `Failed to run modernization migrations: ${message}`,
137+
},
138+
],
139+
structuredContent: {},
140+
isError: true,
141+
};
142+
}
143+
}
144+
145+
export function registerModernizeTool(server: McpServer): void {
146+
server.registerTool(
147+
'modernize',
148+
{
149+
title: 'Modernize Angular Code',
150+
description:
151+
'<Purpose>\n' +
152+
'This tool modernizes Angular code by applying the latest best practices and syntax improvements, ensuring it is idiomatic, readable, and maintainable.\n\n' +
153+
'</Purpose>\n' +
154+
'<Use Cases>\n' +
155+
'* After generating new code: Run this tool immediately after creating new Angular components, directives, or services to ensure they adhere to modern standards.\n' +
156+
'* 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' +
157+
'* When the user asks for a specific transformation: When the transformation list is populated, these specific ones will be ran on the inputs.\n' +
158+
'</Use Cases>\n' +
159+
'<Transformations>\n' +
160+
TRANSFORMATIONS.map((t) => `* ${t.name}: ${t.description}`).join('\n') +
161+
'\n</Transformations>\n',
162+
annotations: {
163+
readOnlyHint: true,
164+
},
165+
inputSchema: modernizeInputSchema.shape,
166+
outputSchema: {
167+
instructions: z
168+
.array(z.string())
169+
.optional()
170+
.describe('A list of instructions on how to run the migrations.'),
171+
},
172+
},
173+
(input) => runModernization(input as ModernizeInput),
174+
);
175+
}

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ describe('Modernize Tool', () => {
1616
});
1717

1818
expect(instructions).toEqual([
19-
'To run the self-closing-tags-migration migration, execute the following command: `ng generate @angular/core:self-closing-tags-migration`.' +
20-
'\nFor more information, see https://angular.dev/reference/migrations/self-closing-tags.',
19+
'To run the self-closing-tags-migration migration, execute the following command: `ng generate @angular/core:self-closing-tags-migration`.\nFor more information, see https://angular.dev/reference/migrations/self-closing-tags.',
2120
]);
2221
});
2322

@@ -27,10 +26,8 @@ describe('Modernize Tool', () => {
2726
});
2827

2928
const expectedInstructions = [
30-
'To run the self-closing-tags-migration migration, execute the following command: `ng generate @angular/core:self-closing-tags-migration`.' +
31-
'\nFor more information, see https://angular.dev/reference/migrations/self-closing-tags.',
32-
'To run the test-bed-get migration, execute the following command: `ng generate @angular/core:test-bed-get`.' +
33-
'\nFor more information, see https://angular.dev/guide/testing/dependency-injection.',
29+
'To run the self-closing-tags-migration migration, execute the following command: `ng generate @angular/core:self-closing-tags-migration`.\nFor more information, see https://angular.dev/reference/migrations/self-closing-tags.',
30+
'To run the test-bed-get migration, execute the following command: `ng generate @angular/core:test-bed-get`.\nFor more information, see https://angular.dev/guide/testing/dependency-injection.',
3431
];
3532

3633
expect(instructions?.sort()).toEqual(expectedInstructions.sort());
@@ -55,4 +52,4 @@ describe('Modernize Tool', () => {
5552
'Run the commands in the order listed below, verifying that your code builds and runs between each step:',
5653
);
5754
});
58-
});
55+
});

0 commit comments

Comments
 (0)