@@ -15,6 +15,26 @@ import { AngularWorkspace } from '../../../utilities/config';
1515import { assertIsError } from '../../../utilities/error' ;
1616import { 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+
1838const 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