From 1ad15394f7a7a4e8bfd2f57afaeb635a5f1dff18 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:05:19 -0400 Subject: [PATCH] feat(@angular/cli): add style language detection to list_projects tool This commit enhances the `list_projects` MCP tool by adding a `styleLanguage` field to the project output. This provides critical context for an AI model, enabling it to generate components with the correct stylesheet format (e.g., `.scss`, `.css`) without needing to parse additional configuration files. This makes the AI more autonomous and its code generation more accurate. The detection logic uses a prioritized heuristic to determine the most likely style language: 1. Checks for a project-specific schematic setting in `angular.json`. 2. Checks for a workspace-level schematic setting. 3. Infers from the `build` target's `inlineStyleLanguage` option. 4. Infers from file extensions in the `build` target's `styles` array. 5. As a future-proof fallback, checks for the existence of an implicit `styles.{ext}` file in the project's source root. The implementation was also refactored to use a single Zod schema as the source of truth for valid style languages, improving maintainability and eliminating repetitive code. --- .../cli/src/commands/mcp/tools/projects.ts | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index a2d82ee55858..e3990b1b74c5 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -15,6 +15,26 @@ import { AngularWorkspace } from '../../../utilities/config'; import { assertIsError } from '../../../utilities/error'; import { McpToolContext, declareTool } from './tool-registry'; +// Single source of truth for what constitutes a valid style language. +const styleLanguageSchema = z.enum(['css', 'scss', 'sass', 'less']); +type StyleLanguage = z.infer; +const VALID_STYLE_LANGUAGES = styleLanguageSchema.options; + +// Explicitly ordered for the file system search heuristic. +const STYLE_LANGUAGE_SEARCH_ORDER: ReadonlyArray = ['scss', 'sass', 'less', 'css']; + +function isStyleLanguage(value: unknown): value is StyleLanguage { + return ( + typeof value === 'string' && (VALID_STYLE_LANGUAGES as ReadonlyArray).includes(value) + ); +} + +function getStyleLanguageFromExtension(extension: string): StyleLanguage | undefined { + const style = extension.toLowerCase().substring(1); // remove leading '.' + + return isStyleLanguage(style) ? style : undefined; +} + const listProjectsOutputSchema = { workspaces: z.array( z.object({ @@ -61,6 +81,12 @@ const listProjectsOutputSchema = { 'This field is critical for generating correct and idiomatic unit tests. ' + 'When writing or modifying tests, you MUST use the APIs corresponding to this framework.', ), + styleLanguage: styleLanguageSchema + .optional() + .describe( + 'The default style language for the project (e.g., "scss"). ' + + 'This determines the file extension for new component styles.', + ), }), ), }), @@ -100,6 +126,7 @@ their types, and their locations. * Finding the correct project name to use in other commands (e.g., \`ng generate component my-comp --project=my-app\`). * Identifying the \`root\` and \`sourceRoot\` of a project to read, analyze, or modify its files. * Determining a project's unit test framework (\`unitTestFramework\`) before writing or modifying tests. +* Identifying the project's style language (\`styleLanguage\`) to use the correct file extension (e.g., \`.scss\`). * Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions. * Identifying the major version of the Angular framework for each workspace, which is crucial for monorepos. * Determining a project's primary function by inspecting its builder (e.g., '@angular-devkit/build-angular:browser' for an application). @@ -317,6 +344,74 @@ function getUnitTestFramework( return undefined; } +/** + * Determines the style language for a project using a prioritized heuristic. + * It checks project-specific schematics, then workspace-level schematics, + * and finally infers from the build target's inlineStyleLanguage option. + * @param project The project definition from the workspace configuration. + * @param workspace The loaded Angular workspace. + * @returns The determined style language ('css', 'scss', 'sass', 'less'). + */ +async function getProjectStyleLanguage( + project: import('@angular-devkit/core').workspaces.ProjectDefinition, + workspace: AngularWorkspace, + fullSourceRoot: string, +): Promise { + const projectSchematics = project.extensions.schematics as + | Record> + | undefined; + const workspaceSchematics = workspace.extensions.schematics as + | Record> + | undefined; + + // 1. Check for a project-specific schematic setting. + let style = projectSchematics?.['@schematics/angular:component']?.['style']; + if (isStyleLanguage(style)) { + return style; + } + + // 2. Check for a workspace-level schematic setting. + style = workspaceSchematics?.['@schematics/angular:component']?.['style']; + if (isStyleLanguage(style)) { + return style; + } + + const buildTarget = project.targets.get('build'); + if (buildTarget?.options) { + // 3. Infer from the build target's inlineStyleLanguage option. + style = buildTarget.options['inlineStyleLanguage']; + if (isStyleLanguage(style)) { + return style; + } + + // 4. Infer from the 'styles' array (explicit). + const styles = buildTarget.options['styles'] as string[] | undefined; + if (Array.isArray(styles)) { + for (const stylePath of styles) { + const style = getStyleLanguageFromExtension(path.extname(stylePath)); + if (style) { + return style; + } + } + } + } + + // 5. Infer from implicit default styles file (future-proofing). + for (const ext of STYLE_LANGUAGE_SEARCH_ORDER) { + try { + await stat(path.join(fullSourceRoot, `styles.${ext}`)); + + return ext; + } catch { + // Silently ignore all errors (e.g., file not found, permissions). + // If we can't read the file, we can't use it for detection. + } + } + + // 6. Fallback to 'css'. + return 'css'; +} + /** * Loads, parses, and transforms a single angular.json file into the tool's output format. * It checks a set of seen paths to avoid processing the same workspace multiple times. @@ -337,17 +432,22 @@ async function loadAndParseWorkspace( const ws = await AngularWorkspace.load(configFile); const projects = []; + const workspaceRoot = path.dirname(configFile); for (const [name, project] of ws.projects.entries()) { + const sourceRoot = path.posix.join(project.root, project.sourceRoot ?? 'src'); + const fullSourceRoot = path.join(workspaceRoot, sourceRoot); const unitTestFramework = getUnitTestFramework(project.targets.get('test')); + const styleLanguage = await getProjectStyleLanguage(project, ws, fullSourceRoot); projects.push({ name, type: project.extensions['projectType'] as 'application' | 'library' | undefined, builder: project.targets.get('build')?.builder, root: project.root, - sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), + sourceRoot, selectorPrefix: project.extensions['prefix'] as string, unitTestFramework, + styleLanguage, }); }