@@ -8,6 +8,7 @@ import * as prettier from "prettier";
88import { ComposableStyles , ElementStyles } from '@microsoft/fast-element' ;
99import { CSSDesignToken } from "@microsoft/fast-foundation" ;
1010import { Command } from 'commander' ;
11+ import { kebabCase } from "change-case" ;
1112import { deepmerge } from "deepmerge-ts" ;
1213import { glob } from "glob" ;
1314import postcss , { type Processor } from "postcss" ;
@@ -60,9 +61,9 @@ program.command('compile-style <inFile> <outFile>')
6061
6162 ensureFileExists ( inFilePath ) ;
6263
63- const fileData = await compileFile ( inFilePath , outFilePath , exportNames . s , exportNames . a ) ;
64+ const { css } = await compileFile ( inFilePath , exportNames . s , exportNames . a , outFilePath ) ;
6465
65- writeStyleFile ( fileData , outFilePath )
66+ writeStyleFile ( css , outFilePath )
6667 process . exit ( 0 ) ;
6768 } )
6869
@@ -72,55 +73,44 @@ program.command("compile-styles <glob>")
7273 . option ( "-s <name>, --styles <name>" , "The name of the styles exports. This option supports wildcards." , "styles" )
7374 . option ( "-e <extension>, --extension <extension>" , "The file extension of the file to write." , ".css" )
7475 . action ( async ( globPath , args : { a : string , s : string , e : string } ) => {
75- glob ( globPath , { absolute : true } ) . then ( async ( paths ) => {
76- await Promise . all ( paths . map ( async ( inFilePath ) => {
77- ensureFileExists ( inFilePath )
78- const outFilePath = path . format ( { ...path . parse ( inFilePath ) , base : '' , ext : args . e } ) ;
79- const fileData = await compileFile ( inFilePath , outFilePath , args . s , args . a ) ;
80- writeStyleFile ( fileData , outFilePath ) ;
81- } ) ) ;
82-
83- process . exit ( 0 ) ;
84- } )
76+ const paths = await glob ( globPath , { absolute : true } ) ;
77+
78+ await Promise . all ( paths . map ( async ( inFilePath ) => {
79+ ensureFileExists ( inFilePath ) ;
80+
81+ let outFilePath : string | undefined ;
82+
83+ if ( isJsonAnatomyFile ( inFilePath ) ) {
84+ const jsonData = JSON . parse ( ( await fsp . readFile ( inFilePath ) ) . toString ( ) ) as SerializableAnatomyWithImports ;
85+ if ( ! jsonData . name ) {
86+ console . warn ( warnColor , `Skipping ${ inFilePath } : JSON anatomy has no name property.` ) ;
87+ return ;
88+ }
89+
90+ outFilePath = resolveStylesheetOutputPath ( inFilePath , jsonData . name , args . e ) ;
91+ }
92+
93+ const { css, anatomyName } = await compileFile ( inFilePath , args . s , args . a , outFilePath ) ;
94+
95+ if ( ! outFilePath ) {
96+ outFilePath = resolveStylesheetOutputPath ( inFilePath , anatomyName , args . e ) ;
97+ }
98+
99+ writeStyleFile ( css , outFilePath ) ;
100+ } ) ) ;
101+
102+ process . exit ( 0 ) ;
85103 } ) ;
86104
87105program . command ( "compile-json-anatomy <anatomyPath>" )
88106 . description ( "Compile a stylesheet from a JSON anatomy" )
89107 . action ( async ( jsonPath : string ) => {
90- const data = ( await fsp . readFile ( jsonPath ) ) . toString ( ) ;
91- await import ( "../reference/index.js" ) ;
92-
93- let jsonData = JSON . parse ( data ) as SerializableAnatomyWithImports ;
94-
95- if ( jsonData . imports ) {
96- for ( const imp of jsonData . imports ) {
97- const impWithExt = imp . toLowerCase ( ) . endsWith ( ".json" ) ? imp : `${ imp } .json` ;
98- const impFilePath = path . format ( { ...path . parse ( path . join ( path . parse ( jsonPath ) . dir , impWithExt ) ) } ) ;
99- const impData = ( await fsp . readFile ( impFilePath ) ) . toString ( ) ;
100- const impJsonData = JSON . parse ( impData ) as SerializableAnatomy ;
101-
102- // If `parts` are in the import, they are either for validation/consistency of that file
103- // or additive to the main anatomy definition.
104- // If the part selector is empty, remove it an use the value from the main anatomy definition.
105- for ( const part in impJsonData . parts ) {
106- if ( impJsonData . parts . hasOwnProperty ( part ) ) {
107- if ( impJsonData . parts [ part ] === "" ) {
108- delete impJsonData . parts [ part ] ;
109- }
110- }
111- }
112-
113- jsonData = deepmerge ( jsonData , impJsonData ) ;
114- }
115- }
108+ const jsonData = await readJsonAnatomyWithImports ( jsonPath ) ;
109+ const styles = jsonToAUIStyleSheet ( jsonData ) ;
110+ const outFilePath = resolveStylesheetOutputPath ( jsonPath , jsonData . name , ".css" ) ;
111+ const css = await compileStylesheet ( styles , jsonPath , outFilePath ) ;
116112
117- const compiler = new SheetCompilerImpl ( ) ;
118- const sheet = jsonToAUIStyleSheet ( jsonData ) ;
119- const compiledSheet = compiler . compile ( sheet ) ;
120- const formatted = await prettier . format ( compiledSheet , { filepath : "foo.css" } ) ;
121- const minified = await mergeCSSRules ( formatted ) ;
122- process . stdout . write ( "/* This file is generated. Do not edit directly */\n" , ) ;
123- process . stdout . write ( minified )
113+ process . stdout . write ( css ) ;
124114 process . stdout . end ( ) ;
125115 } ) ;
126116
@@ -139,51 +129,131 @@ function ensureFileExists(inFilePath: string) {
139129}
140130
141131/**
142- * Compile a single style file
132+ * Resolve the output CSS path from an anatomy name, matching the designer convention.
143133 */
144- async function compileFile ( inFilePath : string , outFilePath : string , stylesName : string , anatomyName : string ) : Promise < string > {
145- // Ensure inFile exists
146- ensureFileExists ( inFilePath )
147-
148- const module = await import ( pathToFileURL ( inFilePath ) . href ) ;
149- const exportKeys = Object . keys ( module ) ;
150-
151- const stylesExportName = matcher ( exportKeys , stylesName ) ;
152- const anatomyExportsName = matcher ( exportKeys , anatomyName ) ;
153-
154- if ( stylesExportName . length === 0 ) {
155- console . error ( failColor , `No style rules for export matching '${ stylesName } '.` )
156- process . exit ( 1 ) ;
157- } else if ( stylesExportName . length > 1 ) {
158- console . warn (
159- warnColor ,
160- `Multiple exports for style rules found in ${ inFilePath } .\nConsider re-naming exports or fixing the --styles matcher.`
161- ) ;
134+ function resolveStylesheetOutputPath ( inFilePath : string , anatomyName : string , extension : string ) : string {
135+ return path . resolve ( path . dirname ( inFilePath ) , `../stylesheets/${ kebabCase ( anatomyName ) } ${ extension } ` ) ;
136+ }
137+
138+ function resolveAnatomyName ( anatomy : ComponentAnatomy < any , any > , inFilePath : string ) : string {
139+ if ( anatomy . name ) {
140+ return anatomy . name ;
162141 }
163142
143+ return path . basename ( path . dirname ( inFilePath ) ) ;
144+ }
145+
146+ function isJsonAnatomyFile ( inFilePath : string ) : boolean {
147+ return inFilePath . toLowerCase ( ) . endsWith ( ".json" ) ;
148+ }
164149
165- if ( anatomyExportsName . length === 0 ) {
166- console . error ( failColor , `No anatomy for export matching '${ stylesName } '.` )
167- process . exit ( 1 ) ;
168- } else if ( stylesExportName . length > 1 ) {
169- console . warn (
170- warnColor ,
171- `Multiple exports for anatomy found in ${ inFilePath } .\nConsider re-naming exports or fixing the --anatomy matcher.`
172- ) ;
150+ async function readJsonAnatomyWithImports ( jsonPath : string ) : Promise < SerializableAnatomyWithImports > {
151+ await import ( "../reference/index.js" ) ;
152+
153+ const data = ( await fsp . readFile ( jsonPath ) ) . toString ( ) ;
154+ let jsonData = JSON . parse ( data ) as SerializableAnatomyWithImports ;
155+
156+ if ( jsonData . imports ) {
157+ for ( const imp of jsonData . imports ) {
158+ const impWithExt = imp . toLowerCase ( ) . endsWith ( ".json" ) ? imp : `${ imp } .json` ;
159+ const impFilePath = path . format ( { ...path . parse ( path . join ( path . parse ( jsonPath ) . dir , impWithExt ) ) } ) ;
160+ const impData = ( await fsp . readFile ( impFilePath ) ) . toString ( ) ;
161+ const impJsonData = JSON . parse ( impData ) as SerializableAnatomy ;
162+
163+ // If `parts` are in the import, they are either for validation/consistency of that file
164+ // or additive to the main anatomy definition.
165+ // If the part selector is empty, remove it an use the value from the main anatomy definition.
166+ for ( const part in impJsonData . parts ) {
167+ if ( impJsonData . parts . hasOwnProperty ( part ) ) {
168+ if ( impJsonData . parts [ part ] === "" ) {
169+ delete impJsonData . parts [ part ] ;
170+ }
171+ }
172+ }
173+
174+ jsonData = deepmerge ( jsonData , impJsonData ) ;
175+ }
173176 }
174177
178+ return jsonData ;
179+ }
175180
181+ function createGeneratedStylesheetHeader ( inFilePath : string , outFilePath : string ) : string {
182+ const sourcePath = path . relative ( path . dirname ( outFilePath ) , inFilePath ) ;
176183
177- const styles : AUIStyleSheet = {
178- rules : module [ stylesExportName [ 0 ] ] ,
179- anatomy : module [ anatomyExportsName [ 0 ] ]
180- }
184+ return `/* This file is generated by Adaptive UI from ${ sourcePath } . Do not edit directly. */\n` ;
185+ }
181186
187+ async function compileStylesheet (
188+ styles : AUIStyleSheet ,
189+ inFilePath : string ,
190+ outFilePath : string
191+ ) : Promise < string > {
182192 const compiler = new SheetCompilerImpl ( ) ;
183193 const compiled = compiler . compile ( styles ) ;
184-
185194 const formatted = await prettier . format ( compiled , { filepath : outFilePath } ) ;
186- return ( `/* This file is generated by Adaptive UI from ${ path . relative ( outFilePath , inFilePath ) } */\n` ) + formatted ;
195+ const minified = await mergeCSSRules ( formatted ) ;
196+
197+ return createGeneratedStylesheetHeader ( inFilePath , outFilePath ) + minified ;
198+ }
199+
200+ /**
201+ * Compile a single input file to CSS.
202+ */
203+ async function compileFile (
204+ inFilePath : string ,
205+ stylesName : string ,
206+ anatomyName : string ,
207+ outFilePath ?: string
208+ ) : Promise < { css : string , anatomyName : string } > {
209+ ensureFileExists ( inFilePath ) ;
210+
211+ const formatPath = outFilePath ?? path . format ( { ...path . parse ( inFilePath ) , ext : ".css" } ) ;
212+ let styles : AUIStyleSheet ;
213+ let resolvedAnatomyName : string ;
214+
215+ if ( isJsonAnatomyFile ( inFilePath ) ) {
216+ const jsonData = await readJsonAnatomyWithImports ( inFilePath ) ;
217+ resolvedAnatomyName = jsonData . name ;
218+ styles = jsonToAUIStyleSheet ( jsonData ) ;
219+ } else {
220+ const module = await import ( pathToFileURL ( inFilePath ) . href ) ;
221+ const exportKeys = Object . keys ( module ) ;
222+
223+ const stylesExportName = matcher ( exportKeys , stylesName ) ;
224+ const anatomyExportsName = matcher ( exportKeys , anatomyName ) ;
225+
226+ if ( stylesExportName . length === 0 ) {
227+ console . error ( failColor , `No style rules for export matching '${ stylesName } '.` )
228+ process . exit ( 1 ) ;
229+ } else if ( stylesExportName . length > 1 ) {
230+ console . warn (
231+ warnColor ,
232+ `Multiple exports for style rules found in ${ inFilePath } .\nConsider re-naming exports or fixing the --styles matcher.`
233+ ) ;
234+ }
235+
236+
237+ if ( anatomyExportsName . length === 0 ) {
238+ console . error ( failColor , `No anatomy for export matching '${ anatomyName } '.` )
239+ process . exit ( 1 ) ;
240+ } else if ( anatomyExportsName . length > 1 ) {
241+ console . warn (
242+ warnColor ,
243+ `Multiple exports for anatomy found in ${ inFilePath } .\nConsider re-naming exports or fixing the --anatomy matcher.`
244+ ) ;
245+ }
246+
247+ styles = {
248+ rules : module [ stylesExportName [ 0 ] ] ,
249+ anatomy : module [ anatomyExportsName [ 0 ] ]
250+ } ;
251+ resolvedAnatomyName = resolveAnatomyName ( styles . anatomy , inFilePath ) ;
252+ }
253+
254+ const css = await compileStylesheet ( styles , inFilePath , formatPath ) ;
255+
256+ return { css, anatomyName : resolvedAnatomyName } ;
187257}
188258
189259/**
0 commit comments