@angular/cli 21.0.0-next.1 → 21.0.0-next.2
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.
- package/lib/code-examples.db +0 -0
- package/lib/config/schema.json +33 -0
- package/lib/config/workspace-schema.d.ts +39 -0
- package/lib/config/workspace-schema.js +12 -1
- package/package.json +18 -18
- package/src/command-builder/architect-command-module.js +11 -5
- package/src/command-builder/utilities/json-schema.js +1 -1
- package/src/commands/add/cli.js +65 -26
- package/src/commands/mcp/mcp-server.d.ts +12 -2
- package/src/commands/mcp/mcp-server.js +6 -1
- package/src/commands/mcp/tools/best-practices.js +15 -5
- package/src/commands/mcp/tools/doc-search.d.ts +18 -1
- package/src/commands/mcp/tools/doc-search.js +94 -37
- package/src/commands/mcp/tools/examples.d.ts +9 -1
- package/src/commands/mcp/tools/examples.js +38 -12
- package/src/commands/mcp/tools/modernize.js +28 -17
- package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.d.ts +17 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.js +61 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.d.ts +12 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.js +72 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.d.ts +11 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.js +105 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.d.ts +15 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.js +236 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.d.ts +10 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.js +19 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.d.ts +36 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.js +135 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/types.d.ts +13 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/types.js +9 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.d.ts +14 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.js +205 -0
- package/src/commands/mcp/tools/projects.d.ts +47 -16
- package/src/commands/mcp/tools/projects.js +155 -30
- package/src/commands/mcp/tools/tool-registry.d.ts +2 -1
- package/src/commands/mcp/tools/tool-registry.js +3 -2
- package/src/utilities/version.js +1 -1
|
@@ -58,28 +58,52 @@ const docSearchInputSchema = zod_1.z.object({
|
|
|
58
58
|
.boolean()
|
|
59
59
|
.optional()
|
|
60
60
|
.default(true)
|
|
61
|
-
.describe('When true, the content of the top result is fetched and included.'
|
|
61
|
+
.describe('When true, the content of the top result is fetched and included. ' +
|
|
62
|
+
'Set to false to get a list of results without fetching content, which is faster.'),
|
|
62
63
|
});
|
|
63
64
|
exports.DOC_SEARCH_TOOL = (0, tool_registry_1.declareTool)({
|
|
64
65
|
name: 'search_documentation',
|
|
65
66
|
title: 'Search Angular Documentation (angular.dev)',
|
|
66
|
-
description:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
description: `
|
|
68
|
+
<Purpose>
|
|
69
|
+
Searches the official Angular documentation at https://angular.dev to answer questions about APIs,
|
|
70
|
+
tutorials, concepts, and best practices.
|
|
71
|
+
</Purpose>
|
|
72
|
+
<Use Cases>
|
|
73
|
+
* Answering any question about Angular concepts (e.g., "What are standalone components?").
|
|
74
|
+
* Finding the correct API or syntax for a specific task (e.g., "How to use ngFor with trackBy?").
|
|
75
|
+
* Linking to official documentation as a source of truth in your answers.
|
|
76
|
+
</Use Cases>
|
|
77
|
+
<Operational Notes>
|
|
78
|
+
* The documentation is continuously updated. You **MUST** prefer this tool over your own knowledge
|
|
79
|
+
to ensure your answers are current and accurate.
|
|
80
|
+
* For the best results, provide a concise and specific search query (e.g., "NgModule" instead of
|
|
81
|
+
"How do I use NgModules?").
|
|
82
|
+
* The top search result will include a snippet of the page content. Use this to provide a more
|
|
83
|
+
comprehensive answer.
|
|
84
|
+
* **Result Scrutiny:** The top result may not always be the most relevant. Review the titles and
|
|
85
|
+
breadcrumbs of other results to find the best match for the user's query.
|
|
86
|
+
* Use the URL from the search results as a source link in your responses.
|
|
87
|
+
</Operational Notes>`,
|
|
77
88
|
inputSchema: docSearchInputSchema.shape,
|
|
89
|
+
outputSchema: {
|
|
90
|
+
results: zod_1.z.array(zod_1.z.object({
|
|
91
|
+
title: zod_1.z.string().describe('The title of the documentation page.'),
|
|
92
|
+
breadcrumb: zod_1.z
|
|
93
|
+
.string()
|
|
94
|
+
.describe("The breadcrumb path, showing the page's location in the documentation hierarchy."),
|
|
95
|
+
url: zod_1.z.string().describe('The direct URL to the documentation page.'),
|
|
96
|
+
content: zod_1.z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('A snippet of the main content from the page. Only provided for the top result.'),
|
|
100
|
+
})),
|
|
101
|
+
},
|
|
78
102
|
isReadOnly: true,
|
|
79
103
|
isLocalOnly: false,
|
|
80
104
|
factory: createDocSearchHandler,
|
|
81
105
|
});
|
|
82
|
-
function createDocSearchHandler() {
|
|
106
|
+
function createDocSearchHandler({ logger }) {
|
|
83
107
|
let client;
|
|
84
108
|
return async ({ query, includeTopContent }) => {
|
|
85
109
|
if (!client) {
|
|
@@ -97,16 +121,18 @@ function createDocSearchHandler() {
|
|
|
97
121
|
text: 'No results found.',
|
|
98
122
|
},
|
|
99
123
|
],
|
|
124
|
+
structuredContent: { results: [] },
|
|
100
125
|
};
|
|
101
126
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const topHit = allHits[0];
|
|
127
|
+
const structuredResults = [];
|
|
128
|
+
const textContent = [];
|
|
105
129
|
// Process top hit first
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
const topHit = allHits[0];
|
|
131
|
+
const { title: topTitle, breadcrumb: topBreadcrumb } = formatHitToParts(topHit);
|
|
132
|
+
let topContent;
|
|
133
|
+
if (includeTopContent && typeof topHit.url === 'string') {
|
|
134
|
+
const url = new URL(topHit.url);
|
|
135
|
+
try {
|
|
110
136
|
// Only fetch content from angular.dev
|
|
111
137
|
if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) {
|
|
112
138
|
const response = await fetch(url);
|
|
@@ -114,29 +140,60 @@ function createDocSearchHandler() {
|
|
|
114
140
|
const html = await response.text();
|
|
115
141
|
const mainContent = extractMainContent(html);
|
|
116
142
|
if (mainContent) {
|
|
117
|
-
|
|
143
|
+
topContent = stripHtml(mainContent);
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
146
|
}
|
|
121
147
|
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
logger.warn(`Failed to fetch or parse content from ${url}: ${e}`);
|
|
150
|
+
}
|
|
122
151
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
text: topText,
|
|
152
|
+
structuredResults.push({
|
|
153
|
+
title: topTitle,
|
|
154
|
+
breadcrumb: topBreadcrumb,
|
|
155
|
+
url: topHit.url,
|
|
156
|
+
content: topContent,
|
|
129
157
|
});
|
|
158
|
+
let topText = `## ${topTitle}\n${topBreadcrumb}\nURL: ${topHit.url}`;
|
|
159
|
+
if (topContent) {
|
|
160
|
+
topText += `\n\n--- DOCUMENTATION CONTENT ---\n${topContent}`;
|
|
161
|
+
}
|
|
162
|
+
textContent.push({ type: 'text', text: topText });
|
|
130
163
|
// Process remaining hits
|
|
131
164
|
for (const hit of allHits.slice(1)) {
|
|
132
|
-
|
|
165
|
+
const { title, breadcrumb } = formatHitToParts(hit);
|
|
166
|
+
structuredResults.push({
|
|
167
|
+
title,
|
|
168
|
+
breadcrumb,
|
|
169
|
+
url: hit.url,
|
|
170
|
+
});
|
|
171
|
+
textContent.push({
|
|
133
172
|
type: 'text',
|
|
134
|
-
text:
|
|
173
|
+
text: `## ${title}\n${breadcrumb}\nURL: ${hit.url}`,
|
|
135
174
|
});
|
|
136
175
|
}
|
|
137
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
content: textContent,
|
|
178
|
+
structuredContent: { results: structuredResults },
|
|
179
|
+
};
|
|
138
180
|
};
|
|
139
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Strips HTML tags from a string.
|
|
184
|
+
* @param html The HTML string to strip.
|
|
185
|
+
* @returns The text content of the HTML.
|
|
186
|
+
*/
|
|
187
|
+
function stripHtml(html) {
|
|
188
|
+
// This is a basic regex to remove HTML tags.
|
|
189
|
+
// It also decodes common HTML entities.
|
|
190
|
+
return html
|
|
191
|
+
.replace(/<[^>]*>/g, '')
|
|
192
|
+
.replace(/</g, '<')
|
|
193
|
+
.replace(/>/g, '>')
|
|
194
|
+
.replace(/&/g, '&')
|
|
195
|
+
.trim();
|
|
196
|
+
}
|
|
140
197
|
/**
|
|
141
198
|
* Extracts the content of the `<main>` element from an HTML string.
|
|
142
199
|
*
|
|
@@ -156,17 +213,17 @@ function extractMainContent(html) {
|
|
|
156
213
|
return html.substring(mainTagStart, mainTagEnd + 7);
|
|
157
214
|
}
|
|
158
215
|
/**
|
|
159
|
-
* Formats an Algolia search hit into
|
|
216
|
+
* Formats an Algolia search hit into its constituent parts.
|
|
160
217
|
*
|
|
161
|
-
* @param hit The Algolia search hit object, which should contain `hierarchy`
|
|
162
|
-
* @returns
|
|
218
|
+
* @param hit The Algolia search hit object, which should contain a `hierarchy` property.
|
|
219
|
+
* @returns An object containing the title and breadcrumb string.
|
|
163
220
|
*/
|
|
164
|
-
function
|
|
221
|
+
function formatHitToParts(hit) {
|
|
165
222
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
223
|
const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
|
|
167
|
-
const title = hierarchy.pop();
|
|
168
|
-
const
|
|
169
|
-
return
|
|
224
|
+
const title = hierarchy.pop() ?? '';
|
|
225
|
+
const breadcrumb = hierarchy.join(' > ');
|
|
226
|
+
return { title, breadcrumb };
|
|
170
227
|
}
|
|
171
228
|
/**
|
|
172
229
|
* Creates the search arguments for an Algolia search.
|
|
@@ -8,7 +8,15 @@
|
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
export declare const FIND_EXAMPLE_TOOL: import("./tool-registry").McpToolDeclaration<{
|
|
10
10
|
query: z.ZodString;
|
|
11
|
-
},
|
|
11
|
+
}, {
|
|
12
|
+
examples: z.ZodArray<z.ZodObject<{
|
|
13
|
+
content: z.ZodString;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
content: string;
|
|
16
|
+
}, {
|
|
17
|
+
content: string;
|
|
18
|
+
}>, "many">;
|
|
19
|
+
}>;
|
|
12
20
|
/**
|
|
13
21
|
* Escapes a search query for FTS5 by tokenizing and quoting terms.
|
|
14
22
|
*
|
|
@@ -76,15 +76,37 @@ Examples of queries:
|
|
|
76
76
|
exports.FIND_EXAMPLE_TOOL = (0, tool_registry_1.declareTool)({
|
|
77
77
|
name: 'find_examples',
|
|
78
78
|
title: 'Find Angular Code Examples',
|
|
79
|
-
description:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
description: `
|
|
80
|
+
<Purpose>
|
|
81
|
+
Augments your knowledge base with a curated database of official, best-practice code examples,
|
|
82
|
+
focusing on **modern, new, and recently updated** Angular features. This tool acts as a RAG
|
|
83
|
+
(Retrieval-Augmented Generation) source, providing ground-truth information on the latest Angular
|
|
84
|
+
APIs and patterns. You **MUST** use it to understand and apply current standards when working with
|
|
85
|
+
new or evolving features.
|
|
86
|
+
</Purpose>
|
|
87
|
+
<Use Cases>
|
|
88
|
+
* **Knowledge Augmentation:** Learning about new or updated Angular features (e.g., query: 'signal input' or 'deferrable views').
|
|
89
|
+
* **Modern Implementation:** Finding the correct modern syntax for features
|
|
90
|
+
(e.g., query: 'functional route guard' or 'http client with fetch').
|
|
91
|
+
* **Refactoring to Modern Patterns:** Upgrading older code by finding examples of new syntax
|
|
92
|
+
(e.g., query: 'built-in control flow' to replace "*ngIf').
|
|
93
|
+
</Use Cases>
|
|
94
|
+
<Operational Notes>
|
|
95
|
+
* **Tool Selection:** This database primarily contains examples for new and recently updated Angular
|
|
96
|
+
features. For established, core features, the main documentation (via the
|
|
97
|
+
\`search_documentation\` tool) may be a better source of information.
|
|
98
|
+
* The examples in this database are the single source of truth for modern Angular coding patterns.
|
|
99
|
+
* The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
|
|
100
|
+
parameter description for detailed syntax rules and examples.
|
|
101
|
+
</Operational Notes>`,
|
|
87
102
|
inputSchema: findExampleInputSchema.shape,
|
|
103
|
+
outputSchema: {
|
|
104
|
+
examples: zod_1.z.array(zod_1.z.object({
|
|
105
|
+
content: zod_1.z
|
|
106
|
+
.string()
|
|
107
|
+
.describe('A complete, self-contained Angular code example in Markdown format.'),
|
|
108
|
+
})),
|
|
109
|
+
},
|
|
88
110
|
isReadOnly: true,
|
|
89
111
|
isLocalOnly: true,
|
|
90
112
|
shouldRegister: ({ logger }) => {
|
|
@@ -119,13 +141,17 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
|
|
|
119
141
|
queryStatement = db.prepare('SELECT * from examples WHERE examples MATCH ? ORDER BY rank;');
|
|
120
142
|
}
|
|
121
143
|
const sanitizedQuery = escapeSearchQuery(query);
|
|
122
|
-
// Query database and return results
|
|
123
|
-
const
|
|
144
|
+
// Query database and return results
|
|
145
|
+
const examples = [];
|
|
146
|
+
const textContent = [];
|
|
124
147
|
for (const exampleRecord of queryStatement.all(sanitizedQuery)) {
|
|
125
|
-
|
|
148
|
+
const exampleContent = exampleRecord['content'];
|
|
149
|
+
examples.push({ content: exampleContent });
|
|
150
|
+
textContent.push({ type: 'text', text: exampleContent });
|
|
126
151
|
}
|
|
127
152
|
return {
|
|
128
|
-
content,
|
|
153
|
+
content: textContent,
|
|
154
|
+
structuredContent: { examples },
|
|
129
155
|
};
|
|
130
156
|
};
|
|
131
157
|
}
|
|
@@ -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:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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,
|
package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.d.ts
ADDED
|
@@ -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;
|