Skip to content

Commit 06532fe

Browse files
committed
feat(@angular/cli): Expand the transformation list for the modernize tool
This includes a refactor that centralizes the transformation list, and makes it possible to pick specific ones.
1 parent 58f1669 commit 06532fe

1 file changed

Lines changed: 112 additions & 25 deletions

File tree

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

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,70 +4,157 @@ import { Tree } from '@angular-devkit/schematics';
44
import { z } from 'zod';
55
import path from 'node:path';
66

7+
export const SCHEMATICS_ROOT = '../../../../../../node_modules/@angular/core/schematics';
8+
9+
enum SchematicTarget {
10+
Code,
11+
Template,
12+
}
13+
14+
enum SchematicRunner {
15+
Migration,
16+
Collection,
17+
}
18+
19+
const TRANSFORMATIONS = [
20+
{
21+
name: 'control-flow-migration',
22+
description:
23+
'Migrates from `*ngIf`, `*ngFor`, and `*ngSwitch` to the new `@if`, `@for`, and `@switch` block syntax in templates.',
24+
target: SchematicTarget.Template,
25+
runner: SchematicRunner.Collection,
26+
},
27+
{
28+
name: 'self-closing-tags-migration',
29+
description:
30+
'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).',
31+
target: SchematicTarget.Template,
32+
runner: SchematicRunner.Collection,
33+
},
34+
{
35+
name: 'test-bed-get',
36+
description:
37+
'Updates `TestBed.get` to the preferred and type-safe `TestBed.inject` in TypeScript test files.',
38+
target: SchematicTarget.Code,
39+
runner: SchematicRunner.Migration,
40+
},
41+
{
42+
name: 'inject-flags',
43+
description:
44+
'Updates `inject` calls from using the InjectFlags enum to a more modern and readable options object.',
45+
target: SchematicTarget.Code,
46+
runner: SchematicRunner.Migration,
47+
},
48+
{
49+
name: 'output-migration',
50+
description: 'Converts `@Output` declarations to the new functional `output()` syntax.',
51+
target: SchematicTarget.Code,
52+
runner: SchematicRunner.Collection,
53+
},
54+
{
55+
name: 'signal-input-migration',
56+
description: 'Migrates `@Input` declarations to the new signal-based `input()` syntax.',
57+
target: SchematicTarget.Code,
58+
runner: SchematicRunner.Collection,
59+
},
60+
{
61+
name: 'signal-queries-migration',
62+
description:
63+
'Migrates `@ViewChild` and `@ContentChild` queries to their signal-based `viewChild` and `contentChild` versions.',
64+
target: SchematicTarget.Code,
65+
runner: SchematicRunner.Collection,
66+
},
67+
] as const;
68+
69+
const ALL_TRANSFORMATIONS = TRANSFORMATIONS.map((t) => t.name);
70+
771
export function registerModernizeTool(server: McpServer): void {
872
server.registerTool(
973
'modernize',
1074
{
1175
title: 'Modernize Angular Code',
1276
description:
13-
'Runs migrations on Angular code to make it more modern and idiomatic. ' +
14-
'This tool should be run when creating new Angular code, or when existing Angular code needs to be updated. ' +
15-
'It can apply transformations for things like control flow, self-closing tags, and dependency injection.',
77+
'<Purpose>\n' +
78+
'This tool modernizes Angular code by applying the latest best practices and syntax improvements, ensuring it is idiomatic, readable, and maintainable.\n\n' +
79+
'</Purpose>\n' +
80+
'<Use Cases>\n' +
81+
'- After generating new code: Run this tool immediately after creating new Angular components, directives, or services to ensure they adhere to modern standards.\n' +
82+
'- 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' +
83+
'- When the user asks for a specific transformation: When the transformation list is populated, these specific ones will be ran on the inputs.\n' +
84+
'</Use Cases>\n' +
85+
'<Transformations>\n' +
86+
TRANSFORMATIONS.map((t) => `- ${t.name}: ${t.description}`).join('\n') +
87+
'\n</Transformations>\n',
1688
inputSchema: {
1789
files: z.array(
1890
z.object({
1991
name: z.string().describe('The name of the file.'),
2092
content: z.string().describe('The content of the file.'),
2193
}),
2294
),
95+
// Zod's `enum` requires a non-empty array of string literals, but TypeScript
96+
// infers `ALL_TRANSFORMATIONS` as `string[]`. The `as` cast is a
97+
// workaround to satisfy Zod's type checker.
98+
transformations: z.array(z.enum(ALL_TRANSFORMATIONS as [string, ...string[]])).optional(),
2399
},
24100
outputSchema: {
25101
files: z.array(
26102
z.object({
27103
name: z.string().describe('The name of the file.'),
28-
content: z.string().describe('The updated content of the file.'),
104+
content: z.string().optional().describe('The updated content of the file.'),
105+
changed: z.boolean().describe('Whether the file was changed.'),
29106
}),
30107
),
31108
},
32109
},
33110
async (input) => {
34111
try {
35-
const migrationRunner = new SchematicTestRunner(
36-
'@angular/core',
37-
path.join(
38-
__dirname,
39-
'../../../../../../node_modules/@angular/core/schematics/migrations.json',
40-
),
41-
);
112+
// We don't have access to the file system to run schematics on it directly. Instead we
113+
// use the test runner to create a virtual filesystem, populate it with the input files,
114+
// and run the schematics on them.
42115

43-
const collectionRunner = new SchematicTestRunner(
44-
'@angular/core',
45-
path.join(
46-
__dirname,
47-
'../../../../../../node_modules/@angular/core/schematics/collection.json',
116+
const runners = {
117+
[SchematicRunner.Migration]: new SchematicTestRunner(
118+
'@angular/core',
119+
path.join(__dirname, SCHEMATICS_ROOT, 'migrations.json'),
120+
),
121+
[SchematicRunner.Collection]: new SchematicTestRunner(
122+
'@angular/core',
123+
path.join(__dirname, SCHEMATICS_ROOT, 'collection.json'),
48124
),
125+
};
126+
127+
const transformationsToRun =
128+
input.transformations && input.transformations.length > 0
129+
? TRANSFORMATIONS.filter((t) => input.transformations!.includes(t.name))
130+
: TRANSFORMATIONS;
131+
132+
const hasCodeFiles = input.files.some((f) => f.name.endsWith('.ts'));
133+
const hasTemplateFiles = input.files.some(
134+
(f) => f.name.endsWith('.html') || f.name.endsWith('.ng.html'),
49135
);
50136

51137
let tree: Tree = new UnitTestTree(Tree.empty());
52138
for (const file of input.files) {
53139
tree.create(file.name, file.content);
54140
}
55141

56-
for (const file of input.files) {
57-
if (file.name.endsWith('.ts')) {
58-
tree = await migrationRunner.runSchematic('test-bed-get', {}, tree);
59-
tree = await migrationRunner.runSchematic('inject-flags', {}, tree);
60-
} else if (file.name.endsWith('.html') || file.name.endsWith('.ng.html')) {
61-
tree = await migrationRunner.runSchematic('control-flow-migration', {}, tree);
62-
tree = await collectionRunner.runSchematic('self-closing-tags-migration', {}, tree);
142+
for (const transformation of transformationsToRun) {
143+
if (transformation.target === SchematicTarget.Code && hasCodeFiles) {
144+
tree = await runners[transformation.runner].runSchematic(transformation.name, {}, tree);
145+
} else if (transformation.target === SchematicTarget.Template && hasTemplateFiles) {
146+
tree = await runners[transformation.runner].runSchematic(transformation.name, {}, tree);
63147
}
64148
}
65149

66150
const updatedFiles = input.files.map((file) => {
67-
const updatedContent = tree.read(file.name)?.toString() ?? file.content;
151+
const updatedContent = tree.read(file.name)?.toString();
152+
const changed = updatedContent !== undefined && updatedContent !== file.content;
153+
68154
return {
69155
name: file.name,
70-
content: updatedContent,
156+
content: changed ? updatedContent : undefined,
157+
changed,
71158
};
72159
});
73160

0 commit comments

Comments
 (0)