@angular/cli 20.2.0-next.0 → 20.2.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 +1 -1
- package/package.json +17 -18
- package/src/command-builder/utilities/json-help.js +5 -2
- package/src/command-builder/utilities/schematic-workflow.js +6 -2
- package/src/commands/mcp/cli.js +1 -1
- package/src/commands/mcp/instructions/best-practices.md +9 -1
- package/src/commands/mcp/mcp-server.d.ts +2 -0
- package/src/commands/mcp/mcp-server.js +20 -67
- package/src/commands/mcp/tools/best-practices.d.ts +9 -0
- package/src/commands/mcp/tools/best-practices.js +39 -0
- package/src/commands/mcp/tools/doc-search.js +94 -15
- package/src/commands/mcp/tools/examples.d.ts +31 -0
- package/src/commands/mcp/tools/examples.js +190 -0
- package/src/commands/mcp/tools/projects.d.ts +12 -0
- package/src/commands/mcp/tools/projects.js +85 -0
- package/src/commands/update/cli.js +36 -31
- package/src/utilities/version.js +1 -1
- package/src/typings.d.ts +0 -15
|
Binary file
|
package/lib/config/schema.json
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "20.2.0-next.2",
|
|
4
4
|
"description": "CLI tool for Angular",
|
|
5
5
|
"main": "lib/cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,20 +25,19 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/angular/angular-cli",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@angular-devkit/architect": "0.2002.0-next.
|
|
29
|
-
"@angular-devkit/core": "20.2.0-next.
|
|
30
|
-
"@angular-devkit/schematics": "20.2.0-next.
|
|
31
|
-
"@inquirer/prompts": "7.
|
|
28
|
+
"@angular-devkit/architect": "0.2002.0-next.2",
|
|
29
|
+
"@angular-devkit/core": "20.2.0-next.2",
|
|
30
|
+
"@angular-devkit/schematics": "20.2.0-next.2",
|
|
31
|
+
"@inquirer/prompts": "7.7.1",
|
|
32
32
|
"@listr2/prompt-adapter-inquirer": "3.0.1",
|
|
33
|
-
"@modelcontextprotocol/sdk": "1.
|
|
34
|
-
"@schematics/angular": "20.2.0-next.
|
|
33
|
+
"@modelcontextprotocol/sdk": "1.17.0",
|
|
34
|
+
"@schematics/angular": "20.2.0-next.2",
|
|
35
35
|
"@yarnpkg/lockfile": "1.1.0",
|
|
36
|
-
"algoliasearch": "5.
|
|
36
|
+
"algoliasearch": "5.34.1",
|
|
37
37
|
"ini": "5.0.0",
|
|
38
38
|
"jsonc-parser": "3.3.1",
|
|
39
39
|
"listr2": "9.0.1",
|
|
40
|
-
"npm-package-arg": "
|
|
41
|
-
"npm-pick-manifest": "10.0.0",
|
|
40
|
+
"npm-package-arg": "13.0.0",
|
|
42
41
|
"pacote": "21.0.0",
|
|
43
42
|
"resolve": "1.22.10",
|
|
44
43
|
"semver": "7.7.2",
|
|
@@ -48,14 +47,14 @@
|
|
|
48
47
|
"ng-update": {
|
|
49
48
|
"migrations": "@schematics/angular/migrations/migration-collection.json",
|
|
50
49
|
"packageGroup": {
|
|
51
|
-
"@angular/cli": "20.2.0-next.
|
|
52
|
-
"@angular/build": "20.2.0-next.
|
|
53
|
-
"@angular/ssr": "20.2.0-next.
|
|
54
|
-
"@angular-devkit/architect": "0.2002.0-next.
|
|
55
|
-
"@angular-devkit/build-angular": "20.2.0-next.
|
|
56
|
-
"@angular-devkit/build-webpack": "0.2002.0-next.
|
|
57
|
-
"@angular-devkit/core": "20.2.0-next.
|
|
58
|
-
"@angular-devkit/schematics": "20.2.0-next.
|
|
50
|
+
"@angular/cli": "20.2.0-next.2",
|
|
51
|
+
"@angular/build": "20.2.0-next.2",
|
|
52
|
+
"@angular/ssr": "20.2.0-next.2",
|
|
53
|
+
"@angular-devkit/architect": "0.2002.0-next.2",
|
|
54
|
+
"@angular-devkit/build-angular": "20.2.0-next.2",
|
|
55
|
+
"@angular-devkit/build-webpack": "0.2002.0-next.2",
|
|
56
|
+
"@angular-devkit/core": "20.2.0-next.2",
|
|
57
|
+
"@angular-devkit/schematics": "20.2.0-next.2"
|
|
59
58
|
}
|
|
60
59
|
},
|
|
61
60
|
"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(
|
|
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(
|
|
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':
|
package/src/commands/mcp/cli.js
CHANGED
|
@@ -37,7 +37,7 @@ class McpCommandModule extends command_module_1.CommandModule {
|
|
|
37
37
|
this.context.logger.info(INTERACTIVE_MESSAGE);
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
const server = await (0, mcp_server_1.createMcpServer)({ workspace: this.context.workspace });
|
|
40
|
+
const server = await (0, mcp_server_1.createMcpServer)({ workspace: this.context.workspace }, this.context.logger);
|
|
41
41
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
42
42
|
await server.connect(transport);
|
|
43
43
|
}
|
|
@@ -9,10 +9,12 @@ You are an expert in TypeScript, Angular, and scalable web application developme
|
|
|
9
9
|
## Angular Best Practices
|
|
10
10
|
|
|
11
11
|
- Always use standalone components over NgModules
|
|
12
|
-
-
|
|
12
|
+
- Must NOT set `standalone: true` inside Angular decorators. It's the default.
|
|
13
13
|
- Use signals for state management
|
|
14
14
|
- Implement lazy loading for feature routes
|
|
15
|
+
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
|
|
15
16
|
- Use `NgOptimizedImage` for all static images.
|
|
17
|
+
- `NgOptimizedImage` does not work for inline base64 images.
|
|
16
18
|
|
|
17
19
|
## Components
|
|
18
20
|
|
|
@@ -30,6 +32,7 @@ You are an expert in TypeScript, Angular, and scalable web application developme
|
|
|
30
32
|
- Use signals for local component state
|
|
31
33
|
- Use `computed()` for derived state
|
|
32
34
|
- Keep state transformations pure and predictable
|
|
35
|
+
- Do NOT use `mutate` on signals, use `update` or `set` instead
|
|
33
36
|
|
|
34
37
|
## Templates
|
|
35
38
|
|
|
@@ -42,3 +45,8 @@ You are an expert in TypeScript, Angular, and scalable web application developme
|
|
|
42
45
|
- Design services around a single responsibility
|
|
43
46
|
- Use the `providedIn: 'root'` option for singleton services
|
|
44
47
|
- Use the `inject()` function instead of constructor injection
|
|
48
|
+
|
|
49
|
+
## Common pitfalls
|
|
50
|
+
|
|
51
|
+
- Control flow (`@if`):
|
|
52
|
+
- You cannot use `as` expressions in `@else if (...)`. E.g. invalid code: `@else if (bla(); as x)`.
|
|
@@ -9,4 +9,6 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
9
9
|
import type { AngularWorkspace } from '../../utilities/config';
|
|
10
10
|
export declare function createMcpServer(context: {
|
|
11
11
|
workspace?: AngularWorkspace;
|
|
12
|
+
}, logger: {
|
|
13
|
+
warn(text: string): void;
|
|
12
14
|
}): Promise<McpServer>;
|
|
@@ -14,10 +14,12 @@ 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
|
-
|
|
20
|
+
const examples_1 = require("./tools/examples");
|
|
21
|
+
const projects_1 = require("./tools/projects");
|
|
22
|
+
async function createMcpServer(context, logger) {
|
|
21
23
|
const server = new mcp_js_1.McpServer({
|
|
22
24
|
name: 'angular-cli-server',
|
|
23
25
|
version: version_1.VERSION.full,
|
|
@@ -37,72 +39,23 @@ async function createMcpServer(context) {
|
|
|
37
39
|
const text = await (0, promises_1.readFile)(node_path_1.default.join(__dirname, 'instructions', 'best-practices.md'), 'utf-8');
|
|
38
40
|
return { contents: [{ uri: 'instructions://best-practices', text }] };
|
|
39
41
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
};
|
|
42
|
+
(0, best_practices_1.registerBestPracticesTool)(server);
|
|
43
|
+
// If run outside an Angular workspace (e.g., globally) skip the workspace specific tools.
|
|
44
|
+
// Currently only the `list_projects` tool.
|
|
45
|
+
if (!context.workspace) {
|
|
46
|
+
(0, projects_1.registerListProjectsTool)(server, context);
|
|
47
|
+
}
|
|
48
|
+
await (0, doc_search_1.registerDocSearchTool)(server);
|
|
49
|
+
if (process.env['NG_MCP_CODE_EXAMPLES'] === '1') {
|
|
50
|
+
// sqlite database support requires Node.js 22.16+
|
|
51
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(Number);
|
|
52
|
+
if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 16)) {
|
|
53
|
+
logger.warn(`MCP tool 'find_examples' requires Node.js 22.16 (or higher). ` +
|
|
54
|
+
' Registration of this tool has been skipped.');
|
|
82
55
|
}
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
});
|
|
56
|
+
else {
|
|
57
|
+
(0, examples_1.registerFindExampleTool)(server, node_path_1.default.join(__dirname, '../../../lib/code-examples.db'));
|
|
93
58
|
}
|
|
94
|
-
|
|
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
|
-
});
|
|
106
|
-
await (0, doc_search_1.registerDocSearchTool)(server);
|
|
59
|
+
}
|
|
107
60
|
return server;
|
|
108
61
|
}
|
|
@@ -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
|
|
64
|
-
'
|
|
65
|
-
'
|
|
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('
|
|
73
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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,31 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Registers the `find_examples` tool with the MCP server.
|
|
11
|
+
*
|
|
12
|
+
* This tool allows users to search for best-practice Angular code examples
|
|
13
|
+
* from a local SQLite database.
|
|
14
|
+
*
|
|
15
|
+
* @param server The MCP server instance.
|
|
16
|
+
* @param exampleDatabasePath The path to the SQLite database file containing the examples.
|
|
17
|
+
*/
|
|
18
|
+
export declare function registerFindExampleTool(server: McpServer, exampleDatabasePath: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Escapes a search query for FTS5 by tokenizing and quoting terms.
|
|
21
|
+
*
|
|
22
|
+
* This function processes a raw search string and prepares it for an FTS5 full-text search.
|
|
23
|
+
* It correctly handles quoted phrases, logical operators (AND, OR, NOT), parentheses,
|
|
24
|
+
* and prefix searches (ending with an asterisk), ensuring that individual search
|
|
25
|
+
* terms are properly quoted to be treated as literals by the search engine.
|
|
26
|
+
* This is primarily intended to avoid unintentional usage of FTS5 query syntax by consumers.
|
|
27
|
+
*
|
|
28
|
+
* @param query The raw search query string.
|
|
29
|
+
* @returns A sanitized query string suitable for FTS5.
|
|
30
|
+
*/
|
|
31
|
+
export declare function escapeSearchQuery(query: string): string;
|
|
@@ -0,0 +1,190 @@
|
|
|
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.registerFindExampleTool = registerFindExampleTool;
|
|
44
|
+
exports.escapeSearchQuery = escapeSearchQuery;
|
|
45
|
+
const zod_1 = require("zod");
|
|
46
|
+
/**
|
|
47
|
+
* Registers the `find_examples` tool with the MCP server.
|
|
48
|
+
*
|
|
49
|
+
* This tool allows users to search for best-practice Angular code examples
|
|
50
|
+
* from a local SQLite database.
|
|
51
|
+
*
|
|
52
|
+
* @param server The MCP server instance.
|
|
53
|
+
* @param exampleDatabasePath The path to the SQLite database file containing the examples.
|
|
54
|
+
*/
|
|
55
|
+
function registerFindExampleTool(server, exampleDatabasePath) {
|
|
56
|
+
let db;
|
|
57
|
+
let queryStatement;
|
|
58
|
+
server.registerTool('find_examples', {
|
|
59
|
+
title: 'Find Angular Code Examples',
|
|
60
|
+
description: 'Before writing or modifying any Angular code including templates, ' +
|
|
61
|
+
'**ALWAYS** use this tool to find current best-practice examples. ' +
|
|
62
|
+
'This is critical for ensuring code quality and adherence to modern Angular standards. ' +
|
|
63
|
+
'This tool searches a curated database of approved Angular code examples and returns the most relevant results for your query. ' +
|
|
64
|
+
'Example Use Cases: ' +
|
|
65
|
+
"1) Creating new components, directives, or services (e.g., query: 'standalone component' or 'signal input'). " +
|
|
66
|
+
"2) Implementing core features (e.g., query: 'lazy load route', 'httpinterceptor', or 'route guard'). " +
|
|
67
|
+
"3) Refactoring existing code to use modern patterns (e.g., query: 'ngfor trackby' or 'form validation').",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
query: zod_1.z.string().describe(`Performs a full-text search using FTS5 syntax. The query should target relevant Angular concepts.
|
|
70
|
+
|
|
71
|
+
Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):
|
|
72
|
+
- AND (default): Space-separated terms are combined with AND.
|
|
73
|
+
- Example: 'standalone component' (finds results with both "standalone" and "component")
|
|
74
|
+
- OR: Use the OR operator to find results with either term.
|
|
75
|
+
- Example: 'validation OR validator'
|
|
76
|
+
- NOT: Use the NOT operator to exclude terms.
|
|
77
|
+
- Example: 'forms NOT reactive'
|
|
78
|
+
- Grouping: Use parentheses () to group expressions.
|
|
79
|
+
- Example: '(validation OR validator) AND forms'
|
|
80
|
+
- Phrase Search: Use double quotes "" for exact phrases.
|
|
81
|
+
- Example: '"template-driven forms"'
|
|
82
|
+
- Prefix Search: Use an asterisk * for prefix matching.
|
|
83
|
+
- Example: 'rout*' (matches "route", "router", "routing")
|
|
84
|
+
|
|
85
|
+
Examples of queries:
|
|
86
|
+
- Find standalone components: 'standalone component'
|
|
87
|
+
- Find ngFor with trackBy: 'ngFor trackBy'
|
|
88
|
+
- Find signal inputs: 'signal input'
|
|
89
|
+
- Find lazy loading a route: 'lazy load route'
|
|
90
|
+
- Find forms with validation: 'form AND (validation OR validator)'`),
|
|
91
|
+
},
|
|
92
|
+
annotations: {
|
|
93
|
+
readOnlyHint: true,
|
|
94
|
+
openWorldHint: false,
|
|
95
|
+
},
|
|
96
|
+
}, async ({ query }) => {
|
|
97
|
+
if (!db || !queryStatement) {
|
|
98
|
+
suppressSqliteWarning();
|
|
99
|
+
const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
|
|
100
|
+
db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
|
|
101
|
+
queryStatement = db.prepare('SELECT * from examples WHERE examples MATCH ? ORDER BY rank;');
|
|
102
|
+
}
|
|
103
|
+
const sanitizedQuery = escapeSearchQuery(query);
|
|
104
|
+
// Query database and return results as text content
|
|
105
|
+
const content = [];
|
|
106
|
+
for (const exampleRecord of queryStatement.all(sanitizedQuery)) {
|
|
107
|
+
content.push({ type: 'text', text: exampleRecord['content'] });
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
content,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Escapes a search query for FTS5 by tokenizing and quoting terms.
|
|
116
|
+
*
|
|
117
|
+
* This function processes a raw search string and prepares it for an FTS5 full-text search.
|
|
118
|
+
* It correctly handles quoted phrases, logical operators (AND, OR, NOT), parentheses,
|
|
119
|
+
* and prefix searches (ending with an asterisk), ensuring that individual search
|
|
120
|
+
* terms are properly quoted to be treated as literals by the search engine.
|
|
121
|
+
* This is primarily intended to avoid unintentional usage of FTS5 query syntax by consumers.
|
|
122
|
+
*
|
|
123
|
+
* @param query The raw search query string.
|
|
124
|
+
* @returns A sanitized query string suitable for FTS5.
|
|
125
|
+
*/
|
|
126
|
+
function escapeSearchQuery(query) {
|
|
127
|
+
// This regex tokenizes the query string into parts:
|
|
128
|
+
// 1. Quoted phrases (e.g., "foo bar")
|
|
129
|
+
// 2. Parentheses ( and )
|
|
130
|
+
// 3. FTS5 operators (AND, OR, NOT, NEAR)
|
|
131
|
+
// 4. Words, which can include a trailing asterisk for prefix search (e.g., foo*)
|
|
132
|
+
const tokenizer = /"([^"]*)"|([()])|\b(AND|OR|NOT|NEAR)\b|([^\s()]+)/g;
|
|
133
|
+
let match;
|
|
134
|
+
const result = [];
|
|
135
|
+
let lastIndex = 0;
|
|
136
|
+
while ((match = tokenizer.exec(query)) !== null) {
|
|
137
|
+
// Add any whitespace or other characters between tokens
|
|
138
|
+
if (match.index > lastIndex) {
|
|
139
|
+
result.push(query.substring(lastIndex, match.index));
|
|
140
|
+
}
|
|
141
|
+
const [, quoted, parenthesis, operator, term] = match;
|
|
142
|
+
if (quoted !== undefined) {
|
|
143
|
+
// It's a quoted phrase, keep it as is.
|
|
144
|
+
result.push(`"${quoted}"`);
|
|
145
|
+
}
|
|
146
|
+
else if (parenthesis) {
|
|
147
|
+
// It's a parenthesis, keep it as is.
|
|
148
|
+
result.push(parenthesis);
|
|
149
|
+
}
|
|
150
|
+
else if (operator) {
|
|
151
|
+
// It's an operator, keep it as is.
|
|
152
|
+
result.push(operator);
|
|
153
|
+
}
|
|
154
|
+
else if (term) {
|
|
155
|
+
// It's a term that needs to be quoted.
|
|
156
|
+
if (term.endsWith('*')) {
|
|
157
|
+
result.push(`"${term.slice(0, -1)}"*`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
result.push(`"${term}"`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
lastIndex = tokenizer.lastIndex;
|
|
164
|
+
}
|
|
165
|
+
// Add any remaining part of the string
|
|
166
|
+
if (lastIndex < query.length) {
|
|
167
|
+
result.push(query.substring(lastIndex));
|
|
168
|
+
}
|
|
169
|
+
return result.join('');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Suppresses the experimental warning emitted by Node.js for the `node:sqlite` module.
|
|
173
|
+
*
|
|
174
|
+
* This is a workaround to prevent the console from being cluttered with warnings
|
|
175
|
+
* about the experimental status of the SQLite module, which is used by this tool.
|
|
176
|
+
*/
|
|
177
|
+
function suppressSqliteWarning() {
|
|
178
|
+
const originalProcessEmit = process.emit;
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
180
|
+
process.emit = function (event, error) {
|
|
181
|
+
if (event === 'warning' &&
|
|
182
|
+
error instanceof Error &&
|
|
183
|
+
error.name === 'ExperimentalWarning' &&
|
|
184
|
+
error.message.includes('SQLite')) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-rest-params
|
|
188
|
+
return originalProcessEmit.apply(process, arguments);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -52,7 +52,6 @@ const node_module_1 = require("node:module");
|
|
|
52
52
|
const path = __importStar(require("node:path"));
|
|
53
53
|
const node_path_1 = require("node:path");
|
|
54
54
|
const npm_package_arg_1 = __importDefault(require("npm-package-arg"));
|
|
55
|
-
const npm_pick_manifest_1 = __importDefault(require("npm-pick-manifest"));
|
|
56
55
|
const semver = __importStar(require("semver"));
|
|
57
56
|
const workspace_schema_1 = require("../../../lib/config/workspace-schema");
|
|
58
57
|
const command_module_1 = require("../../command-builder/command-module");
|
|
@@ -189,9 +188,11 @@ class UpdateCommandModule extends command_module_1.CommandModule {
|
|
|
189
188
|
if (options.migrateOnly && packageIdentifier.rawSpec !== '*') {
|
|
190
189
|
logger.warn('Package specifier has no effect when using "migrate-only" option.');
|
|
191
190
|
}
|
|
192
|
-
//
|
|
193
|
-
if
|
|
194
|
-
|
|
191
|
+
// Wildcard uses the next tag if next option is used otherwise use latest tag.
|
|
192
|
+
// Wildcard is present if no selector is provided on the command line.
|
|
193
|
+
if (packageIdentifier.rawSpec === '*') {
|
|
194
|
+
packageIdentifier.fetchSpec = options.next ? 'next' : 'latest';
|
|
195
|
+
packageIdentifier.type = 'tag';
|
|
195
196
|
}
|
|
196
197
|
packages.push(packageIdentifier);
|
|
197
198
|
}
|
|
@@ -480,35 +481,39 @@ class UpdateCommandModule extends command_module_1.CommandModule {
|
|
|
480
481
|
// Try to find a package version based on the user requested package specifier
|
|
481
482
|
// registry specifier types are either version, range, or tag
|
|
482
483
|
let manifest;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
// If not found and next was used and user did not provide a specifier, try latest.
|
|
493
|
-
// Package may not have a next tag.
|
|
494
|
-
if (requestIdentifier.type === 'tag' &&
|
|
495
|
-
requestIdentifier.fetchSpec === 'next' &&
|
|
496
|
-
!requestIdentifier.rawSpec) {
|
|
497
|
-
try {
|
|
498
|
-
manifest = (0, npm_pick_manifest_1.default)(metadata, 'latest');
|
|
499
|
-
}
|
|
500
|
-
catch (e) {
|
|
501
|
-
(0, error_1.assertIsError)(e);
|
|
502
|
-
if (e.code !== 'ETARGET' && e.code !== 'ENOVERSIONS') {
|
|
503
|
-
throw e;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
484
|
+
switch (requestIdentifier.type) {
|
|
485
|
+
case 'tag':
|
|
486
|
+
manifest = metadata.tags[requestIdentifier.fetchSpec];
|
|
487
|
+
// If not found and next option was used and user did not provide a specifier, try latest.
|
|
488
|
+
// Package may not have a next tag.
|
|
489
|
+
if (!manifest &&
|
|
490
|
+
requestIdentifier.fetchSpec === 'next' &&
|
|
491
|
+
requestIdentifier.rawSpec === '*') {
|
|
492
|
+
manifest = metadata.tags['latest'];
|
|
507
493
|
}
|
|
508
|
-
|
|
509
|
-
|
|
494
|
+
break;
|
|
495
|
+
case 'version':
|
|
496
|
+
manifest = metadata.versions[requestIdentifier.fetchSpec];
|
|
497
|
+
break;
|
|
498
|
+
case 'range':
|
|
499
|
+
for (const potentialManifest of Object.values(metadata.versions)) {
|
|
500
|
+
// Ignore deprecated package versions
|
|
501
|
+
if (potentialManifest.deprecated) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
// Only consider versions that are within the range
|
|
505
|
+
if (!semver.satisfies(potentialManifest.version, requestIdentifier.fetchSpec, {
|
|
506
|
+
loose: true,
|
|
507
|
+
})) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
// Update the used manifest if current potential is newer than existing or there is not one yet
|
|
511
|
+
if (!manifest ||
|
|
512
|
+
semver.gt(potentialManifest.version, manifest.version, { loose: true })) {
|
|
513
|
+
manifest = potentialManifest;
|
|
514
|
+
}
|
|
510
515
|
}
|
|
511
|
-
|
|
516
|
+
break;
|
|
512
517
|
}
|
|
513
518
|
if (!manifest) {
|
|
514
519
|
logger.error(`Package specified by '${requestIdentifier.raw}' does not exist within the registry.`);
|
package/src/utilities/version.js
CHANGED
package/src/typings.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
|
|
9
|
-
declare module 'npm-pick-manifest' {
|
|
10
|
-
function pickManifest(
|
|
11
|
-
metadata: import('./utilities/package-metadata').PackageMetadata,
|
|
12
|
-
selector: string,
|
|
13
|
-
): import('./utilities/package-metadata').PackageManifest;
|
|
14
|
-
export = pickManifest;
|
|
15
|
-
}
|