Skip to content

Commit 3040b77

Browse files
committed
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.
1 parent de17f18 commit 3040b77

File tree

1 file changed

+101
-1
lines changed

1 file changed

+101
-1
lines changed

packages/angular/cli/src/commands/mcp/tools/projects.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ import { AngularWorkspace } from '../../../utilities/config';
1515
import { assertIsError } from '../../../utilities/error';
1616
import { McpToolContext, declareTool } from './tool-registry';
1717

18+
// Single source of truth for what constitutes a valid style language.
19+
const styleLanguageSchema = z.enum(['css', 'scss', 'sass', 'less']);
20+
type StyleLanguage = z.infer<typeof styleLanguageSchema>;
21+
const VALID_STYLE_LANGUAGES = styleLanguageSchema.options;
22+
23+
// Explicitly ordered for the file system search heuristic.
24+
const STYLE_LANGUAGE_SEARCH_ORDER: ReadonlyArray<StyleLanguage> = ['scss', 'sass', 'less', 'css'];
25+
26+
function isStyleLanguage(value: unknown): value is StyleLanguage {
27+
return (
28+
typeof value === 'string' && (VALID_STYLE_LANGUAGES as ReadonlyArray<string>).includes(value)
29+
);
30+
}
31+
32+
function getStyleLanguageFromExtension(extension: string): StyleLanguage | undefined {
33+
const style = extension.toLowerCase().substring(1); // remove leading '.'
34+
35+
return isStyleLanguage(style) ? style : undefined;
36+
}
37+
1838
const listProjectsOutputSchema = {
1939
workspaces: z.array(
2040
z.object({
@@ -61,6 +81,12 @@ const listProjectsOutputSchema = {
6181
'This field is critical for generating correct and idiomatic unit tests. ' +
6282
'When writing or modifying tests, you MUST use the APIs corresponding to this framework.',
6383
),
84+
styleLanguage: styleLanguageSchema
85+
.optional()
86+
.describe(
87+
'The default style language for the project (e.g., "scss"). ' +
88+
'This determines the file extension for new component styles.',
89+
),
6490
}),
6591
),
6692
}),
@@ -100,6 +126,7 @@ their types, and their locations.
100126
* Finding the correct project name to use in other commands (e.g., \`ng generate component my-comp --project=my-app\`).
101127
* Identifying the \`root\` and \`sourceRoot\` of a project to read, analyze, or modify its files.
102128
* Determining a project's unit test framework (\`unitTestFramework\`) before writing or modifying tests.
129+
* Identifying the project's style language (\`styleLanguage\`) to use the correct file extension (e.g., \`.scss\`).
103130
* Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions.
104131
* Identifying the major version of the Angular framework for each workspace, which is crucial for monorepos.
105132
* 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(
317344
return undefined;
318345
}
319346

347+
/**
348+
* Determines the style language for a project using a prioritized heuristic.
349+
* It checks project-specific schematics, then workspace-level schematics,
350+
* and finally infers from the build target's inlineStyleLanguage option.
351+
* @param project The project definition from the workspace configuration.
352+
* @param workspace The loaded Angular workspace.
353+
* @returns The determined style language ('css', 'scss', 'sass', 'less').
354+
*/
355+
async function getProjectStyleLanguage(
356+
project: import('@angular-devkit/core').workspaces.ProjectDefinition,
357+
workspace: AngularWorkspace,
358+
fullSourceRoot: string,
359+
): Promise<StyleLanguage> {
360+
const projectSchematics = project.extensions.schematics as
361+
| Record<string, Record<string, unknown>>
362+
| undefined;
363+
const workspaceSchematics = workspace.extensions.schematics as
364+
| Record<string, Record<string, unknown>>
365+
| undefined;
366+
367+
// 1. Check for a project-specific schematic setting.
368+
let style = projectSchematics?.['@schematics/angular:component']?.['style'];
369+
if (isStyleLanguage(style)) {
370+
return style;
371+
}
372+
373+
// 2. Check for a workspace-level schematic setting.
374+
style = workspaceSchematics?.['@schematics/angular:component']?.['style'];
375+
if (isStyleLanguage(style)) {
376+
return style;
377+
}
378+
379+
const buildTarget = project.targets.get('build');
380+
if (buildTarget?.options) {
381+
// 3. Infer from the build target's inlineStyleLanguage option.
382+
style = buildTarget.options['inlineStyleLanguage'];
383+
if (isStyleLanguage(style)) {
384+
return style;
385+
}
386+
387+
// 4. Infer from the 'styles' array (explicit).
388+
const styles = buildTarget.options['styles'] as string[] | undefined;
389+
if (Array.isArray(styles)) {
390+
for (const stylePath of styles) {
391+
const style = getStyleLanguageFromExtension(path.extname(stylePath));
392+
if (style) {
393+
return style;
394+
}
395+
}
396+
}
397+
}
398+
399+
// 5. Infer from implicit default styles file (future-proofing).
400+
for (const ext of STYLE_LANGUAGE_SEARCH_ORDER) {
401+
try {
402+
await stat(path.join(fullSourceRoot, `styles.${ext}`));
403+
404+
return ext;
405+
} catch {
406+
// Silently ignore all errors (e.g., file not found, permissions).
407+
// If we can't read the file, we can't use it for detection.
408+
}
409+
}
410+
411+
// 6. Fallback to 'css'.
412+
return 'css';
413+
}
414+
320415
/**
321416
* Loads, parses, and transforms a single angular.json file into the tool's output format.
322417
* It checks a set of seen paths to avoid processing the same workspace multiple times.
@@ -337,17 +432,22 @@ async function loadAndParseWorkspace(
337432

338433
const ws = await AngularWorkspace.load(configFile);
339434
const projects = [];
435+
const workspaceRoot = path.dirname(configFile);
340436
for (const [name, project] of ws.projects.entries()) {
437+
const sourceRoot = path.posix.join(project.root, project.sourceRoot ?? 'src');
438+
const fullSourceRoot = path.join(workspaceRoot, sourceRoot);
341439
const unitTestFramework = getUnitTestFramework(project.targets.get('test'));
440+
const styleLanguage = await getProjectStyleLanguage(project, ws, fullSourceRoot);
342441

343442
projects.push({
344443
name,
345444
type: project.extensions['projectType'] as 'application' | 'library' | undefined,
346445
builder: project.targets.get('build')?.builder,
347446
root: project.root,
348-
sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'),
447+
sourceRoot,
349448
selectorPrefix: project.extensions['prefix'] as string,
350449
unitTestFramework,
450+
styleLanguage,
351451
});
352452
}
353453

0 commit comments

Comments
 (0)