@angular/cli
Version:
CLI tool for Angular
190 lines • 9.17 kB
JavaScript
;
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BEST_PRACTICES_TOOL = void 0;
/**
* @fileoverview
* This file defines the `get_best_practices` MCP tool. The tool is designed to be version-aware,
* dynamically resolving the best practices guide from the user's installed version of
* `@angular/core`. It achieves this by reading a custom `angular` metadata block in the
* framework's `package.json`. If this resolution fails, it gracefully falls back to a generic
* guide bundled with the Angular CLI.
*/
const promises_1 = require("node:fs/promises");
const node_module_1 = require("node:module");
const node_path_1 = __importDefault(require("node:path"));
const zod_1 = require("zod");
const version_1 = require("../../../utilities/version");
const tool_registry_1 = require("./tool-registry");
const bestPracticesInputSchema = zod_1.z.object({
workspacePath: zod_1.z
.string()
.optional()
.describe('The absolute path to the `angular.json` file for the workspace. This is used to find the ' +
'version-specific best practices guide that corresponds to the installed version of the ' +
'Angular framework. You **MUST** get this path from the `list_projects` tool. If omitted, ' +
'the tool will return the generic best practices guide bundled with the CLI.'),
});
exports.BEST_PRACTICES_TOOL = (0, tool_registry_1.declareTool)({
name: 'get_best_practices',
title: 'Get Angular Coding Best Practices Guide',
description: `
<Purpose>
Retrieves the official Angular Best Practices Guide. This guide contains the essential rules and conventions
that **MUST** be followed for any task involving the creation, analysis, or modification of Angular code.
</Purpose>
<Use Cases>
* As a mandatory first step before writing or modifying any Angular code to ensure adherence to modern standards.
* To learn about key concepts like standalone components, typed forms, and modern control flow syntax (@if, @for, @switch).
* To verify that existing code aligns with current Angular conventions before making changes.
</Use Cases>
<Operational Notes>
* **Project-Specific Use (Recommended):** For tasks inside a user's project, you **MUST** provide the
\`workspacePath\` argument to get the guide that matches the project's Angular version. Get this
path from \`list_projects\`.
* **General Use:** If no project context is available (e.g., for general questions or learning),
you can call the tool without the \`workspacePath\` argument. It will return the latest
generic best practices guide.
* The content of this guide is non-negotiable and reflects the official, up-to-date standards for Angular development.
* You **MUST** internalize and apply the principles from this guide in all subsequent Angular-related tasks.
* Failure to adhere to these best practices will result in suboptimal and outdated code.
</Operational Notes>`,
inputSchema: bestPracticesInputSchema.shape,
isReadOnly: true,
isLocalOnly: true,
factory: createBestPracticesHandler,
});
/**
* Retrieves the content of the generic best practices guide that is bundled with the CLI.
* This serves as a fallback when a version-specific guide cannot be found.
* @returns A promise that resolves to the string content of the bundled markdown file.
*/
async function getBundledBestPractices() {
return (0, promises_1.readFile)(node_path_1.default.join(__dirname, '..', 'resources', 'best-practices.md'), 'utf-8');
}
/**
* Attempts to find and read a version-specific best practices guide from the user's installed
* version of `@angular/core`. It looks for a custom `angular` metadata property in the
* framework's `package.json` to locate the guide.
*
* @example A sample `package.json` `angular` field:
* ```json
* {
* "angular": {
* "bestPractices": {
* "format": "markdown",
* "path": "./resources/best-practices.md"
* }
* }
* }
* ```
*
* @param workspacePath The absolute path to the user's `angular.json` file.
* @param logger The MCP tool context logger for reporting warnings.
* @returns A promise that resolves to an object containing the guide's content and source,
* or `undefined` if the guide could not be resolved.
*/
async function getVersionSpecificBestPractices(workspacePath, logger) {
// 1. Resolve the path to package.json
let pkgJsonPath;
try {
const workspaceRequire = (0, node_module_1.createRequire)(workspacePath);
pkgJsonPath = workspaceRequire.resolve('@angular/core/package.json');
}
catch (e) {
logger.warn(`Could not resolve '@angular/core/package.json' from '${workspacePath}'. ` +
'Is Angular installed in this project? Falling back to the bundled guide.');
return undefined;
}
// 2. Read and parse package.json, then find and read the guide.
try {
const pkgJsonContent = await (0, promises_1.readFile)(pkgJsonPath, 'utf-8');
const pkgJson = JSON.parse(pkgJsonContent);
const bestPracticesInfo = pkgJson['angular']?.bestPractices;
if (bestPracticesInfo &&
bestPracticesInfo.format === 'markdown' &&
typeof bestPracticesInfo.path === 'string') {
const packageDirectory = node_path_1.default.dirname(pkgJsonPath);
const guidePath = node_path_1.default.resolve(packageDirectory, bestPracticesInfo.path);
// Ensure the resolved guide path is within the package boundary.
// Uses path.relative to create a cross-platform, case-insensitive check.
// If the relative path starts with '..' or is absolute, it is a traversal attempt.
const relativePath = node_path_1.default.relative(packageDirectory, guidePath);
if (relativePath.startsWith('..') || node_path_1.default.isAbsolute(relativePath)) {
logger.warn(`Detected a potential path traversal attempt in '${pkgJsonPath}'. ` +
`The path '${bestPracticesInfo.path}' escapes the package boundary. ` +
'Falling back to the bundled guide.');
return undefined;
}
// Check the file size to prevent reading a very large file.
const stats = await (0, promises_1.stat)(guidePath);
if (stats.size > 1024 * 1024) {
// 1MB
logger.warn(`The best practices guide at '${guidePath}' is larger than 1MB (${stats.size} bytes). ` +
'This is unexpected and the file will not be read. Falling back to the bundled guide.');
return undefined;
}
const content = await (0, promises_1.readFile)(guidePath, 'utf-8');
const source = `framework version ${pkgJson.version}`;
return { content, source };
}
else {
logger.warn(`Did not find valid 'angular.bestPractices' metadata in '${pkgJsonPath}'. ` +
'Falling back to the bundled guide.');
}
}
catch (e) {
logger.warn(`Failed to read or parse version-specific best practices referenced in '${pkgJsonPath}': ${e instanceof Error ? e.message : e}. Falling back to the bundled guide.`);
}
return undefined;
}
/**
* Creates the handler function for the `get_best_practices` tool.
* The handler orchestrates the process of first attempting to get a version-specific guide
* and then falling back to the bundled guide if necessary.
* @param context The MCP tool context, containing the logger.
* @returns An async function that serves as the tool's executor.
*/
function createBestPracticesHandler({ logger }) {
let bundledBestPractices;
return async (input) => {
let content;
let source;
// First, try to get the version-specific guide.
if (input.workspacePath) {
const versionSpecific = await getVersionSpecificBestPractices(input.workspacePath, logger);
if (versionSpecific) {
content = versionSpecific.content;
source = versionSpecific.source;
}
}
// If the version-specific guide was not found for any reason, fall back to the bundled version.
if (content === undefined) {
content = await (bundledBestPractices ??= getBundledBestPractices());
source = `bundled (CLI v${version_1.VERSION.full})`;
}
return {
content: [
{
type: 'text',
text: content,
annotations: {
audience: ['assistant'],
priority: 0.9,
source,
},
},
],
};
};
}
//# sourceMappingURL=best-practices.js.map