@@ -4,70 +4,157 @@ import { Tree } from '@angular-devkit/schematics';
44import { z } from 'zod' ;
55import 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+
771export 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