@angular/cli 20.2.0-rc.1 → 20.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/cli",
3
- "version": "20.2.0-rc.1",
3
+ "version": "20.2.1",
4
4
  "description": "CLI tool for Angular",
5
5
  "main": "lib/cli/index.js",
6
6
  "bin": {
@@ -25,13 +25,13 @@
25
25
  },
26
26
  "homepage": "https://github.com/angular/angular-cli",
27
27
  "dependencies": {
28
- "@angular-devkit/architect": "0.2002.0-rc.1",
29
- "@angular-devkit/core": "20.2.0-rc.1",
30
- "@angular-devkit/schematics": "20.2.0-rc.1",
28
+ "@angular-devkit/architect": "0.2002.1",
29
+ "@angular-devkit/core": "20.2.1",
30
+ "@angular-devkit/schematics": "20.2.1",
31
31
  "@inquirer/prompts": "7.8.2",
32
32
  "@listr2/prompt-adapter-inquirer": "3.0.1",
33
- "@modelcontextprotocol/sdk": "1.17.2",
34
- "@schematics/angular": "20.2.0-rc.1",
33
+ "@modelcontextprotocol/sdk": "1.17.3",
34
+ "@schematics/angular": "20.2.1",
35
35
  "@yarnpkg/lockfile": "1.1.0",
36
36
  "algoliasearch": "5.35.0",
37
37
  "ini": "5.0.0",
@@ -47,17 +47,17 @@
47
47
  "ng-update": {
48
48
  "migrations": "@schematics/angular/migrations/migration-collection.json",
49
49
  "packageGroup": {
50
- "@angular/cli": "20.2.0-rc.1",
51
- "@angular/build": "20.2.0-rc.1",
52
- "@angular/ssr": "20.2.0-rc.1",
53
- "@angular-devkit/architect": "0.2002.0-rc.1",
54
- "@angular-devkit/build-angular": "20.2.0-rc.1",
55
- "@angular-devkit/build-webpack": "0.2002.0-rc.1",
56
- "@angular-devkit/core": "20.2.0-rc.1",
57
- "@angular-devkit/schematics": "20.2.0-rc.1"
50
+ "@angular/cli": "20.2.1",
51
+ "@angular/build": "20.2.1",
52
+ "@angular/ssr": "20.2.1",
53
+ "@angular-devkit/architect": "0.2002.1",
54
+ "@angular-devkit/build-angular": "20.2.1",
55
+ "@angular-devkit/build-webpack": "0.2002.1",
56
+ "@angular-devkit/core": "20.2.1",
57
+ "@angular-devkit/schematics": "20.2.1"
58
58
  }
59
59
  },
60
- "packageManager": "pnpm@10.14.0",
60
+ "packageManager": "pnpm@10.15.0",
61
61
  "engines": {
62
62
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
63
63
  "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
@@ -218,11 +218,17 @@ let CommandModule = (() => {
218
218
  ...Object.values(analytics_parameters_1.EventCustomMetric),
219
219
  ]);
220
220
  for (const [name, ua] of this.optionsWithAnalytics) {
221
+ if (!validEventCustomDimensionAndMetrics.has(ua)) {
222
+ continue;
223
+ }
221
224
  const value = options[name];
222
- if ((typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') &&
223
- validEventCustomDimensionAndMetrics.has(ua)) {
225
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
224
226
  parameters[ua] = value;
225
227
  }
228
+ else if (Array.isArray(value)) {
229
+ // GA doesn't allow array as values.
230
+ parameters[ua] = value.sort().join(', ');
231
+ }
226
232
  }
227
233
  return parameters;
228
234
  }
@@ -224,11 +224,18 @@ let SchematicsCommandModule = (() => {
224
224
  ? {
225
225
  name: item,
226
226
  value: item,
227
+ checked: definition.multiselect && Array.isArray(definition.default)
228
+ ? definition.default?.includes(item)
229
+ : item === definition.default,
227
230
  }
228
231
  : {
229
232
  ...item,
230
233
  name: item.label,
231
234
  value: item.value,
235
+ checked: definition.multiselect && Array.isArray(definition.default)
236
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
237
+ definition.default?.includes(item.value)
238
+ : item.value === definition.default,
232
239
  }),
233
240
  });
234
241
  break;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { json } from '@angular-devkit/core';
9
9
  import type { Argv, Options as YargsOptions } from 'yargs';
10
+ import { EventCustomDimension } from '../../analytics/analytics-parameters';
10
11
  /**
11
12
  * An option description.
12
13
  */
@@ -48,4 +49,4 @@ export declare function parseJsonSchemaToOptions(registry: json.schema.SchemaReg
48
49
  *
49
50
  * @returns A map from option name to analytics configuration.
50
51
  */
51
- export declare function addSchemaOptionsToCommand<T>(localYargs: Argv<T>, options: Option[], includeDefaultValues: boolean): Map<string, string>;
52
+ export declare function addSchemaOptionsToCommand<T>(localYargs: Argv<T>, options: Option[], includeDefaultValues: boolean): Map<string, EventCustomDimension>;
@@ -10,7 +10,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.parseJsonSchemaToOptions = parseJsonSchemaToOptions;
11
11
  exports.addSchemaOptionsToCommand = addSchemaOptionsToCommand;
12
12
  const core_1 = require("@angular-devkit/core");
13
- function coerceToStringMap(dashedName, value) {
13
+ function checkStringMap(keyValuePairOptions, args) {
14
+ for (const key of keyValuePairOptions) {
15
+ const value = args[key];
16
+ if (!Array.isArray(value)) {
17
+ // Value has been parsed.
18
+ continue;
19
+ }
20
+ for (const pair of value) {
21
+ if (pair === undefined) {
22
+ continue;
23
+ }
24
+ if (!pair.includes('=')) {
25
+ throw new Error(`Invalid value for argument: ${key}, Given: '${pair}', Expected key=value pair`);
26
+ }
27
+ }
28
+ }
29
+ return true;
30
+ }
31
+ function coerceToStringMap(value) {
14
32
  const stringMap = {};
15
33
  for (const pair of value) {
16
34
  // This happens when the flag isn't passed at all.
@@ -19,14 +37,11 @@ function coerceToStringMap(dashedName, value) {
19
37
  }
20
38
  const eqIdx = pair.indexOf('=');
21
39
  if (eqIdx === -1) {
22
- // TODO: Remove workaround once yargs properly handles thrown errors from coerce.
23
- // Right now these sometimes end up as uncaught exceptions instead of proper validation
24
- // errors with usage output.
25
- return Promise.reject(new Error(`Invalid value for argument: ${dashedName}, Given: '${pair}', Expected key=value pair`));
40
+ // In the case it is not valid skip processing this option and handle the error in `checkStringMap`
41
+ return value;
26
42
  }
27
43
  const key = pair.slice(0, eqIdx);
28
- const value = pair.slice(eqIdx + 1);
29
- stringMap[key] = value;
44
+ stringMap[key] = pair.slice(eqIdx + 1);
30
45
  }
31
46
  return stringMap;
32
47
  }
@@ -79,7 +94,7 @@ async function parseJsonSchemaToOptions(registry, schema, interactive = true) {
79
94
  // Only include arrays if they're boolean, string or number.
80
95
  if (core_1.json.isJsonObject(current.items) &&
81
96
  typeof current.items.type == 'string' &&
82
- ['boolean', 'number', 'string'].includes(current.items.type)) {
97
+ isValidTypeForEnum(current.items.type)) {
83
98
  return true;
84
99
  }
85
100
  return false;
@@ -94,16 +109,13 @@ async function parseJsonSchemaToOptions(registry, schema, interactive = true) {
94
109
  return;
95
110
  }
96
111
  // Only keep enum values we support (booleans, numbers and strings).
97
- const enumValues = ((core_1.json.isJsonArray(current.enum) && current.enum) || []).filter((x) => {
98
- switch (typeof x) {
99
- case 'boolean':
100
- case 'number':
101
- case 'string':
102
- return true;
103
- default:
104
- return false;
105
- }
106
- });
112
+ const enumValues = ((core_1.json.isJsonArray(current.enum) && current.enum) ||
113
+ (core_1.json.isJsonObject(current.items) &&
114
+ core_1.json.isJsonArray(current.items.enum) &&
115
+ current.items.enum) ||
116
+ [])
117
+ .filter((value) => isValidTypeForEnum(typeof value))
118
+ .sort();
107
119
  let defaultValue = undefined;
108
120
  if (current.default !== undefined) {
109
121
  switch (types[0]) {
@@ -112,6 +124,11 @@ async function parseJsonSchemaToOptions(registry, schema, interactive = true) {
112
124
  defaultValue = current.default;
113
125
  }
114
126
  break;
127
+ case 'array':
128
+ if (Array.isArray(current.default)) {
129
+ defaultValue = current.default;
130
+ }
131
+ break;
115
132
  case 'number':
116
133
  if (typeof current.default == 'number') {
117
134
  defaultValue = current.default;
@@ -199,7 +216,7 @@ function addSchemaOptionsToCommand(localYargs, options, includeDefaultValues) {
199
216
  booleanOptionsWithNoPrefix.add(dashedName);
200
217
  }
201
218
  if (itemValueType) {
202
- keyValuePairOptions.add(name);
219
+ keyValuePairOptions.add(dashedName);
203
220
  }
204
221
  const sharedOptions = {
205
222
  alias,
@@ -207,7 +224,7 @@ function addSchemaOptionsToCommand(localYargs, options, includeDefaultValues) {
207
224
  description,
208
225
  deprecated,
209
226
  choices,
210
- coerce: itemValueType ? coerceToStringMap.bind(null, dashedName) : undefined,
227
+ coerce: itemValueType ? coerceToStringMap : undefined,
211
228
  // This should only be done when `--help` is used otherwise default will override options set in angular.json.
212
229
  ...(includeDefaultValues ? { default: defaultVal } : {}),
213
230
  };
@@ -229,6 +246,10 @@ function addSchemaOptionsToCommand(localYargs, options, includeDefaultValues) {
229
246
  optionsWithAnalytics.set(name, userAnalytics);
230
247
  }
231
248
  }
249
+ // Valid key/value options
250
+ if (keyValuePairOptions.size) {
251
+ localYargs.check(checkStringMap.bind(null, keyValuePairOptions), false);
252
+ }
232
253
  // Handle options which have been defined in the schema with `no` prefix.
233
254
  if (booleanOptionsWithNoPrefix.size) {
234
255
  localYargs.middleware((options) => {
@@ -242,3 +263,7 @@ function addSchemaOptionsToCommand(localYargs, options, includeDefaultValues) {
242
263
  }
243
264
  return optionsWithAnalytics;
244
265
  }
266
+ const VALID_ENUM_TYPES = new Set(['boolean', 'number', 'string']);
267
+ function isValidTypeForEnum(value) {
268
+ return VALID_ENUM_TYPES.has(value);
269
+ }
@@ -47,11 +47,9 @@ function shouldWrapSchematic(schematicFile, schematicEncapsulation) {
47
47
  }
48
48
  // Check for first-party Angular schematic packages
49
49
  // Angular schematics are safe to use in the wrapped VM context
50
- if (/\/node_modules\/@(?:angular|schematics|nguniversal)\//.test(normalizedSchematicFile)) {
51
- return true;
52
- }
53
- // Otherwise use the value of the schematic collection's encapsulation option (current default of false)
54
- return schematicEncapsulation;
50
+ const isFirstParty = /\/node_modules\/@(?:angular|schematics|nguniversal)\//.test(normalizedSchematicFile);
51
+ // Use value of defined option if present, otherwise default to first-party usage.
52
+ return schematicEncapsulation ?? isFirstParty;
55
53
  }
56
54
  class SchematicEngineHost extends tools_1.NodeModulesEngineHost {
57
55
  _resolveReferenceString(refString, parentPath, collectionDescription) {
@@ -60,7 +58,7 @@ class SchematicEngineHost extends tools_1.NodeModulesEngineHost {
60
58
  const fullPath = path[0] === '.' ? (0, node_path_1.resolve)(parentPath ?? process.cwd(), path) : path;
61
59
  const referenceRequire = (0, node_module_1.createRequire)(__filename);
62
60
  const schematicFile = referenceRequire.resolve(fullPath, { paths: [parentPath] });
63
- if (shouldWrapSchematic(schematicFile, !!collectionDescription?.encapsulation)) {
61
+ if (shouldWrapSchematic(schematicFile, collectionDescription?.encapsulation)) {
64
62
  const schematicPath = (0, node_path_1.dirname)(schematicFile);
65
63
  const moduleCache = new Map();
66
64
  const factoryInitializer = wrap(schematicFile, schematicPath, moduleCache, name || 'default');
@@ -24,6 +24,8 @@ To start using the Angular CLI MCP Server, add this configuration to your host:
24
24
  }
25
25
 
26
26
  Exact configuration may differ depending on the host.
27
+
28
+ For more information and documentation, visit: https://angular.dev/ai/mcp
27
29
  `;
28
30
  class McpCommandModule extends command_module_1.CommandModule {
29
31
  command = 'mcp';
@@ -46,6 +48,8 @@ class McpCommandModule extends command_module_1.CommandModule {
46
48
  alias: 'E',
47
49
  array: true,
48
50
  describe: 'Enable an experimental tool.',
51
+ choices: mcp_server_1.EXPERIMENTAL_TOOLS.map(({ name }) => name),
52
+ hidden: true,
49
53
  });
50
54
  }
51
55
  async run(options) {
@@ -8,6 +8,17 @@
8
8
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
9
  import type { AngularWorkspace } from '../../utilities/config';
10
10
  import { AnyMcpToolDeclaration } from './tools/tool-registry';
11
+ /**
12
+ * The set of tools that are available but not enabled by default.
13
+ * These tools are considered experimental and may have limitations.
14
+ */
15
+ export declare const EXPERIMENTAL_TOOLS: readonly [import("./tools/tool-registry").McpToolDeclaration<{
16
+ query: import("zod").ZodString;
17
+ }, import("zod").ZodRawShape>, import("./tools/tool-registry").McpToolDeclaration<{
18
+ transformations: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodEnum<[string, ...string[]]>, "many">>;
19
+ }, {
20
+ instructions: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
21
+ }>];
11
22
  export declare function createMcpServer(options: {
12
23
  workspace?: AngularWorkspace;
13
24
  readOnly?: boolean;
@@ -10,6 +10,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  return (mod && mod.__esModule) ? mod : { "default": mod };
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.EXPERIMENTAL_TOOLS = void 0;
13
14
  exports.createMcpServer = createMcpServer;
14
15
  exports.assembleToolDeclarations = assembleToolDeclarations;
15
16
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -31,7 +32,7 @@ const STABLE_TOOLS = [best_practices_1.BEST_PRACTICES_TOOL, doc_search_1.DOC_SEA
31
32
  * The set of tools that are available but not enabled by default.
32
33
  * These tools are considered experimental and may have limitations.
33
34
  */
34
- const EXPERIMENTAL_TOOLS = [examples_1.FIND_EXAMPLE_TOOL, modernize_1.MODERNIZE_TOOL];
35
+ exports.EXPERIMENTAL_TOOLS = [examples_1.FIND_EXAMPLE_TOOL, modernize_1.MODERNIZE_TOOL];
35
36
  async function createMcpServer(options, logger) {
36
37
  const server = new mcp_js_1.McpServer({
37
38
  name: 'angular-cli-server',
@@ -46,7 +47,7 @@ async function createMcpServer(options, logger) {
46
47
  'When writing or modifying Angular code, use the MCP server and its tools instead of direct shell commands where possible.',
47
48
  });
48
49
  (0, instructions_1.registerInstructionsResource)(server);
49
- const toolDeclarations = assembleToolDeclarations(STABLE_TOOLS, EXPERIMENTAL_TOOLS, {
50
+ const toolDeclarations = assembleToolDeclarations(STABLE_TOOLS, exports.EXPERIMENTAL_TOOLS, {
50
51
  ...options,
51
52
  logger,
52
53
  });
@@ -112,7 +112,7 @@ function createDocSearchHandler() {
112
112
  const response = await fetch(url);
113
113
  if (response.ok) {
114
114
  const html = await response.text();
115
- const mainContent = extractBodyContent(html);
115
+ const mainContent = extractMainContent(html);
116
116
  if (mainContent) {
117
117
  topText += `\n\n--- DOCUMENTATION CONTENT ---\n${mainContent}`;
118
118
  }
@@ -138,22 +138,21 @@ function createDocSearchHandler() {
138
138
  };
139
139
  }
140
140
  /**
141
- * Extracts the content of the `<body>` element from an HTML string.
141
+ * Extracts the content of the `<main>` element from an HTML string.
142
142
  *
143
143
  * @param html The HTML content of a page.
144
- * @returns The content of the `<body>` element, or `undefined` if not found.
144
+ * @returns The content of the `<main>` element, or `undefined` if not found.
145
145
  */
146
- function extractBodyContent(html) {
147
- // TODO: Use '<main>' element instead of '<body>' when available in angular.dev HTML.
148
- const mainTagStart = html.indexOf('<body');
146
+ function extractMainContent(html) {
147
+ const mainTagStart = html.indexOf('<main');
149
148
  if (mainTagStart === -1) {
150
149
  return undefined;
151
150
  }
152
- const mainTagEnd = html.lastIndexOf('</body>');
151
+ const mainTagEnd = html.lastIndexOf('</main>');
153
152
  if (mainTagEnd <= mainTagStart) {
154
153
  return undefined;
155
154
  }
156
- // Add 7 to include '</body>'
155
+ // Add 7 to include '</main>'
157
156
  return html.substring(mainTagStart, mainTagEnd + 7);
158
157
  }
159
158
  /**
@@ -22,11 +22,6 @@ const TRANSFORMATIONS = [
22
22
  description: 'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).',
23
23
  documentationUrl: 'https://angular.dev/reference/migrations/self-closing-tags',
24
24
  },
25
- {
26
- name: 'test-bed-get',
27
- description: 'Updates `TestBed.get` to the preferred and type-safe `TestBed.inject` in TypeScript test files.',
28
- documentationUrl: 'https://angular.dev/guide/testing/dependency-injection',
29
- },
30
25
  {
31
26
  name: 'inject',
32
27
  description: 'Converts usages of constructor-based injection to the inject() function.',
@@ -59,11 +54,6 @@ const TRANSFORMATIONS = [
59
54
  '3. Run `ng g @angular/core:standalone` and select "Bootstrap the project using standalone APIs"',
60
55
  documentationUrl: 'https://angular.dev/reference/migrations/standalone',
61
56
  },
62
- {
63
- name: 'zoneless',
64
- description: 'Migrates the application to be zoneless.',
65
- documentationUrl: 'https://angular.dev/guide/zoneless',
66
- },
67
57
  ];
68
58
  const modernizeInputSchema = zod_1.z.object({
69
59
  // Casting to [string, ...string[]] since the enum definition requires a nonempty array.
@@ -171,7 +171,7 @@ let PackageManagerUtils = (() => {
171
171
  const { cwd = process.cwd(), silent = false } = options;
172
172
  return new Promise((resolve) => {
173
173
  const bufferedOutput = [];
174
- const childProcess = (0, node_child_process_1.spawn)(this.name, args, {
174
+ const childProcess = (0, node_child_process_1.spawn)(`${this.name} ${args.join(' ')}`, {
175
175
  // Always pipe stderr to allow for failures to be reported
176
176
  stdio: silent ? ['ignore', 'ignore', 'pipe'] : 'pipe',
177
177
  shell: true,
@@ -22,4 +22,4 @@ class Version {
22
22
  this.patch = patch;
23
23
  }
24
24
  }
25
- exports.VERSION = new Version('20.2.0-rc.1');
25
+ exports.VERSION = new Version('20.2.1');