@angular/cli 20.2.0-next.0 → 20.2.0-next.1

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.
@@ -4272,7 +4272,7 @@
4272
4272
  ]
4273
4273
  },
4274
4274
  "browsers": {
4275
- "description": "A list of browsers to use for test execution. If undefined, jsdom on Node.js will be used instead of a browser.",
4275
+ "description": "A list of browsers to use for test execution. If undefined, jsdom on Node.js will be used instead of a browser. For Vitest and Karma, browser names ending with 'Headless' (e.g., 'ChromeHeadless') will enable headless mode for that browser.",
4276
4276
  "type": "array",
4277
4277
  "items": {
4278
4278
  "type": "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/cli",
3
- "version": "20.2.0-next.0",
3
+ "version": "20.2.0-next.1",
4
4
  "description": "CLI tool for Angular",
5
5
  "main": "lib/cli/index.js",
6
6
  "bin": {
@@ -25,15 +25,15 @@
25
25
  },
26
26
  "homepage": "https://github.com/angular/angular-cli",
27
27
  "dependencies": {
28
- "@angular-devkit/architect": "0.2002.0-next.0",
29
- "@angular-devkit/core": "20.2.0-next.0",
30
- "@angular-devkit/schematics": "20.2.0-next.0",
31
- "@inquirer/prompts": "7.6.0",
28
+ "@angular-devkit/architect": "0.2002.0-next.1",
29
+ "@angular-devkit/core": "20.2.0-next.1",
30
+ "@angular-devkit/schematics": "20.2.0-next.1",
31
+ "@inquirer/prompts": "7.7.1",
32
32
  "@listr2/prompt-adapter-inquirer": "3.0.1",
33
- "@modelcontextprotocol/sdk": "1.15.1",
34
- "@schematics/angular": "20.2.0-next.0",
33
+ "@modelcontextprotocol/sdk": "1.16.0",
34
+ "@schematics/angular": "20.2.0-next.1",
35
35
  "@yarnpkg/lockfile": "1.1.0",
36
- "algoliasearch": "5.32.0",
36
+ "algoliasearch": "5.34.0",
37
37
  "ini": "5.0.0",
38
38
  "jsonc-parser": "3.3.1",
39
39
  "listr2": "9.0.1",
@@ -48,14 +48,14 @@
48
48
  "ng-update": {
49
49
  "migrations": "@schematics/angular/migrations/migration-collection.json",
50
50
  "packageGroup": {
51
- "@angular/cli": "20.2.0-next.0",
52
- "@angular/build": "20.2.0-next.0",
53
- "@angular/ssr": "20.2.0-next.0",
54
- "@angular-devkit/architect": "0.2002.0-next.0",
55
- "@angular-devkit/build-angular": "20.2.0-next.0",
56
- "@angular-devkit/build-webpack": "0.2002.0-next.0",
57
- "@angular-devkit/core": "20.2.0-next.0",
58
- "@angular-devkit/schematics": "20.2.0-next.0"
51
+ "@angular/cli": "20.2.0-next.1",
52
+ "@angular/build": "20.2.0-next.1",
53
+ "@angular/ssr": "20.2.0-next.1",
54
+ "@angular-devkit/architect": "0.2002.0-next.1",
55
+ "@angular-devkit/build-angular": "20.2.0-next.1",
56
+ "@angular-devkit/build-webpack": "0.2002.0-next.1",
57
+ "@angular-devkit/core": "20.2.0-next.1",
58
+ "@angular-devkit/schematics": "20.2.0-next.1"
59
59
  }
60
60
  },
61
61
  "packageManager": "pnpm@9.15.9",
@@ -19,20 +19,23 @@ function jsonHelpUsage(localYargs) {
19
19
  const descriptions = usageInstance.getDescriptions();
20
20
  const groups = localYargsInstance.getGroups();
21
21
  const positional = groups[usageInstance.getPositionalGroupName()];
22
+ const seen = new Set();
22
23
  const hidden = new Set(hiddenOptions);
23
24
  const normalizeOptions = [];
24
25
  const allAliases = new Set([...Object.values(aliases).flat()]);
26
+ // Reverted order of https://github.com/yargs/yargs/blob/971e351705f0fbc5566c6ed1dfd707fa65e11c0d/lib/usage.ts#L419-L424
25
27
  for (const [names, type] of [
28
+ [number, 'number'],
26
29
  [array, 'array'],
27
30
  [string, 'string'],
28
31
  [boolean, 'boolean'],
29
- [number, 'number'],
30
32
  ]) {
31
33
  for (const name of names) {
32
- if (allAliases.has(name) || hidden.has(name)) {
34
+ if (allAliases.has(name) || hidden.has(name) || seen.has(name)) {
33
35
  // Ignore hidden, aliases and already visited option.
34
36
  continue;
35
37
  }
38
+ seen.add(name);
36
39
  const positionalIndex = positional?.indexOf(name) ?? -1;
37
40
  const alias = aliases[name];
38
41
  normalizeOptions.push({
@@ -25,11 +25,15 @@ function subscribeToWorkflow(workflow, logger) {
25
25
  logger.error(`ERROR! ${eventPath} ${event.description == 'alreadyExist' ? 'already exists' : 'does not exist'}.`);
26
26
  break;
27
27
  case 'update':
28
- logs.push(`${color_1.colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`);
28
+ logs.push(
29
+ // TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution.
30
+ `${color_1.colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`);
29
31
  files.add(eventPath);
30
32
  break;
31
33
  case 'create':
32
- logs.push(`${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`);
34
+ logs.push(
35
+ // TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution.
36
+ `${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`);
33
37
  files.add(eventPath);
34
38
  break;
35
39
  case 'delete':
@@ -14,9 +14,10 @@ exports.createMcpServer = createMcpServer;
14
14
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
15
15
  const promises_1 = require("node:fs/promises");
16
16
  const node_path_1 = __importDefault(require("node:path"));
17
- const zod_1 = require("zod");
18
17
  const version_1 = require("../../utilities/version");
18
+ const best_practices_1 = require("./tools/best-practices");
19
19
  const doc_search_1 = require("./tools/doc-search");
20
+ const projects_1 = require("./tools/projects");
20
21
  async function createMcpServer(context) {
21
22
  const server = new mcp_js_1.McpServer({
22
23
  name: 'angular-cli-server',
@@ -37,72 +38,8 @@ async function createMcpServer(context) {
37
38
  const text = await (0, promises_1.readFile)(node_path_1.default.join(__dirname, 'instructions', 'best-practices.md'), 'utf-8');
38
39
  return { contents: [{ uri: 'instructions://best-practices', text }] };
39
40
  });
40
- server.registerTool('list_projects', {
41
- title: 'List Angular Projects',
42
- description: 'Lists the names of all applications and libraries defined within an Angular workspace. ' +
43
- 'It reads the `angular.json` configuration file to identify the projects. ',
44
- annotations: {
45
- readOnlyHint: true,
46
- },
47
- outputSchema: {
48
- projects: zod_1.z.array(zod_1.z.object({
49
- name: zod_1.z
50
- .string()
51
- .describe('The name of the project, as defined in the `angular.json` file.'),
52
- type: zod_1.z
53
- .enum(['application', 'library'])
54
- .optional()
55
- .describe(`The type of the project, either 'application' or 'library'.`),
56
- root: zod_1.z
57
- .string()
58
- .describe('The root directory of the project, relative to the workspace root.'),
59
- sourceRoot: zod_1.z
60
- .string()
61
- .describe(`The root directory of the project's source files, relative to the workspace root.`),
62
- selectorPrefix: zod_1.z
63
- .string()
64
- .optional()
65
- .describe('The prefix to use for component selectors.' +
66
- ` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.`),
67
- })),
68
- },
69
- }, async () => {
70
- const { workspace } = context;
71
- if (!workspace) {
72
- return {
73
- content: [
74
- {
75
- type: 'text',
76
- text: 'No Angular workspace found.' +
77
- ' An `angular.json` file, which marks the root of a workspace,' +
78
- ' could not be located in the current directory or any of its parent directories.',
79
- },
80
- ],
81
- };
82
- }
83
- const projects = [];
84
- // Convert to output format
85
- for (const [name, project] of workspace.projects.entries()) {
86
- projects.push({
87
- name,
88
- type: project.extensions['projectType'],
89
- root: project.root,
90
- sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'),
91
- selectorPrefix: project.extensions['prefix'],
92
- });
93
- }
94
- // The structuredContent field is newer and may not be supported by all hosts.
95
- // A text representation of the content is also provided for compatibility.
96
- return {
97
- content: [
98
- {
99
- type: 'text',
100
- text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`,
101
- },
102
- ],
103
- structuredContent: { projects },
104
- };
105
- });
41
+ (0, best_practices_1.registerBestPracticesTool)(server);
42
+ (0, projects_1.registerListProjectsTool)(server, context);
106
43
  await (0, doc_search_1.registerDocSearchTool)(server);
107
44
  return server;
108
45
  }
@@ -0,0 +1,9 @@
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 { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ export declare function registerBestPracticesTool(server: McpServer): void;
@@ -0,0 +1,39 @@
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 __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.registerBestPracticesTool = registerBestPracticesTool;
14
+ const promises_1 = require("node:fs/promises");
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ function registerBestPracticesTool(server) {
17
+ server.registerTool('get_best_practices', {
18
+ title: 'Get Angular Coding Best Practices Guide',
19
+ description: 'You **MUST** use this tool to retrieve the Angular Best Practices Guide ' +
20
+ 'before any interaction with Angular code (creating, analyzing, modifying). ' +
21
+ 'It is mandatory to follow this guide to ensure all code adheres to ' +
22
+ 'modern standards, including standalone components, typed forms, and ' +
23
+ 'modern control flow. This is the first step for any Angular task.',
24
+ annotations: {
25
+ readOnlyHint: true,
26
+ openWorldHint: false,
27
+ },
28
+ }, async () => {
29
+ const text = await (0, promises_1.readFile)(node_path_1.default.join(__dirname, '..', 'instructions', 'best-practices.md'), 'utf-8');
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text,
35
+ },
36
+ ],
37
+ };
38
+ });
39
+ }
@@ -60,39 +60,118 @@ async function registerDocSearchTool(server) {
60
60
  let client;
61
61
  server.registerTool('search_documentation', {
62
62
  title: 'Search Angular Documentation (angular.dev)',
63
- description: 'Searches the official Angular documentation on https://angular.dev.' +
64
- ' This tool is useful for finding the most up-to-date information on Angular, including APIs, tutorials, and best practices.' +
65
- ' Use this when creating Angular specific code or answering questions that require knowledge of the latest Angular features.',
63
+ description: 'Searches the official Angular documentation at https://angular.dev. Use this tool to answer any questions about Angular, ' +
64
+ 'such as for APIs, tutorials, and best practices. Because the documentation is continuously updated, you should **always** ' +
65
+ 'prefer this tool over your own knowledge to ensure your answers are current.\n\n' +
66
+ 'The results will be a list of content entries, where each entry has the following structure:\n' +
67
+ '```\n' +
68
+ '## {Result Title}\n' +
69
+ '{Breadcrumb path to the content}\n' +
70
+ 'URL: {Direct link to the documentation page}\n' +
71
+ '```\n' +
72
+ 'Use the title and breadcrumb to understand the context of the result and use the URL as a source link. For the best results, ' +
73
+ "provide a concise and specific search query (e.g., 'NgModule' instead of 'How do I use NgModules?').",
66
74
  annotations: {
67
75
  readOnlyHint: true,
68
76
  },
69
77
  inputSchema: {
70
78
  query: zod_1.z
71
79
  .string()
72
- .describe('The search query to use when searching the Angular documentation.' +
73
- ' This should be a concise and specific query to get the most relevant results.'),
80
+ .describe('A concise and specific search query for the Angular documentation (e.g., "NgModule" or "standalone components").'),
81
+ includeTopContent: zod_1.z
82
+ .boolean()
83
+ .optional()
84
+ .default(true)
85
+ .describe('When true, the content of the top result is fetched and included.'),
74
86
  },
75
- }, async ({ query }) => {
87
+ }, async ({ query, includeTopContent }) => {
76
88
  if (!client) {
77
89
  const dcip = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', (constants_1.k1 + ALGOLIA_APP_ID).padEnd(32, '^'), constants_1.iv).setAuthTag(Buffer.from(constants_1.at, 'base64'));
78
90
  const { searchClient } = await Promise.resolve().then(() => __importStar(require('algoliasearch')));
79
91
  client = searchClient(ALGOLIA_APP_ID, dcip.update(ALGOLIA_API_E, 'hex', 'utf-8') + dcip.final('utf-8'));
80
92
  }
81
93
  const { results } = await client.search(createSearchArguments(query));
82
- // Convert results into text content entries instead of stringifying the entire object
83
- const content = results.flatMap((result) => result.hits.map((hit) => {
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
86
- const title = hierarchy.pop();
87
- const description = hierarchy.join(' > ');
94
+ const allHits = results.flatMap((result) => result.hits);
95
+ if (allHits.length === 0) {
88
96
  return {
89
- type: 'text',
90
- text: `## ${title}\n${description}\nURL: ${hit.url}`,
97
+ content: [
98
+ {
99
+ type: 'text',
100
+ text: 'No results found.',
101
+ },
102
+ ],
91
103
  };
92
- }));
104
+ }
105
+ const content = [];
106
+ // The first hit is the top search result
107
+ const topHit = allHits[0];
108
+ // Process top hit first
109
+ let topText = formatHitToText(topHit);
110
+ try {
111
+ if (includeTopContent && typeof topHit.url === 'string') {
112
+ const url = new URL(topHit.url);
113
+ // Only fetch content from angular.dev
114
+ if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) {
115
+ const response = await fetch(url);
116
+ if (response.ok) {
117
+ const html = await response.text();
118
+ const mainContent = extractBodyContent(html);
119
+ if (mainContent) {
120
+ topText += `\n\n--- DOCUMENTATION CONTENT ---\n${mainContent}`;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // Ignore errors fetching content. The basic info is still returned.
128
+ }
129
+ content.push({
130
+ type: 'text',
131
+ text: topText,
132
+ });
133
+ // Process remaining hits
134
+ for (const hit of allHits.slice(1)) {
135
+ content.push({
136
+ type: 'text',
137
+ text: formatHitToText(hit),
138
+ });
139
+ }
93
140
  return { content };
94
141
  });
95
142
  }
143
+ /**
144
+ * Extracts the content of the `<body>` element from an HTML string.
145
+ *
146
+ * @param html The HTML content of a page.
147
+ * @returns The content of the `<body>` element, or `undefined` if not found.
148
+ */
149
+ function extractBodyContent(html) {
150
+ // TODO: Use '<main>' element instead of '<body>' when available in angular.dev HTML.
151
+ const mainTagStart = html.indexOf('<body');
152
+ if (mainTagStart === -1) {
153
+ return undefined;
154
+ }
155
+ const mainTagEnd = html.lastIndexOf('</body>');
156
+ if (mainTagEnd <= mainTagStart) {
157
+ return undefined;
158
+ }
159
+ // Add 7 to include '</body>'
160
+ return html.substring(mainTagStart, mainTagEnd + 7);
161
+ }
162
+ /**
163
+ * Formats an Algolia search hit into a text representation.
164
+ *
165
+ * @param hit The Algolia search hit object, which should contain `hierarchy` and `url` properties.
166
+ * @returns A formatted string with title, description, and URL.
167
+ */
168
+ function formatHitToText(hit) {
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
171
+ const title = hierarchy.pop();
172
+ const description = hierarchy.join(' > ');
173
+ return `## ${title}\n${description}\nURL: ${hit.url}`;
174
+ }
96
175
  /**
97
176
  * Creates the search arguments for an Algolia search.
98
177
  *
@@ -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 type { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
9
+ import type { AngularWorkspace } from '../../../utilities/config';
10
+ export declare function registerListProjectsTool(server: McpServer, context: {
11
+ workspace?: AngularWorkspace;
12
+ }): void;
@@ -0,0 +1,85 @@
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 __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.registerListProjectsTool = registerListProjectsTool;
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const zod_1 = __importDefault(require("zod"));
16
+ function registerListProjectsTool(server, context) {
17
+ server.registerTool('list_projects', {
18
+ title: 'List Angular Projects',
19
+ description: 'Lists the names of all applications and libraries defined within an Angular workspace. ' +
20
+ 'It reads the `angular.json` configuration file to identify the projects. ',
21
+ annotations: {
22
+ readOnlyHint: true,
23
+ openWorldHint: false,
24
+ },
25
+ outputSchema: {
26
+ projects: zod_1.default.array(zod_1.default.object({
27
+ name: zod_1.default
28
+ .string()
29
+ .describe('The name of the project, as defined in the `angular.json` file.'),
30
+ type: zod_1.default
31
+ .enum(['application', 'library'])
32
+ .optional()
33
+ .describe(`The type of the project, either 'application' or 'library'.`),
34
+ root: zod_1.default
35
+ .string()
36
+ .describe('The root directory of the project, relative to the workspace root.'),
37
+ sourceRoot: zod_1.default
38
+ .string()
39
+ .describe(`The root directory of the project's source files, relative to the workspace root.`),
40
+ selectorPrefix: zod_1.default
41
+ .string()
42
+ .optional()
43
+ .describe('The prefix to use for component selectors.' +
44
+ ` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.`),
45
+ })),
46
+ },
47
+ }, async () => {
48
+ const { workspace } = context;
49
+ if (!workspace) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: 'No Angular workspace found.' +
55
+ ' An `angular.json` file, which marks the root of a workspace,' +
56
+ ' could not be located in the current directory or any of its parent directories.',
57
+ },
58
+ ],
59
+ structuredContent: { projects: [] },
60
+ };
61
+ }
62
+ const projects = [];
63
+ // Convert to output format
64
+ for (const [name, project] of workspace.projects.entries()) {
65
+ projects.push({
66
+ name,
67
+ type: project.extensions['projectType'],
68
+ root: project.root,
69
+ sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'),
70
+ selectorPrefix: project.extensions['prefix'],
71
+ });
72
+ }
73
+ // The structuredContent field is newer and may not be supported by all hosts.
74
+ // A text representation of the content is also provided for compatibility.
75
+ return {
76
+ content: [
77
+ {
78
+ type: 'text',
79
+ text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`,
80
+ },
81
+ ],
82
+ structuredContent: { projects },
83
+ };
84
+ });
85
+ }
@@ -22,4 +22,4 @@ class Version {
22
22
  this.patch = patch;
23
23
  }
24
24
  }
25
- exports.VERSION = new Version('20.2.0-next.0');
25
+ exports.VERSION = new Version('20.2.0-next.1');