Skip to content

Commit 7d6fd23

Browse files
authored
Consolidate style generation and enable bulk update. (#296)
1 parent f9ce9bc commit 7d6fd23

4 files changed

Lines changed: 170 additions & 92 deletions

File tree

.changeset/green-moons-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@adaptive-web/adaptive-ui": patch
3+
---
4+
5+
Consolidate style generation and enable bulk update.

package-lock.json

Lines changed: 16 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/adaptive-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@happy-dom/global-registrator": "^14.7.1",
4141
"@microsoft/fast-element": "2.0.0-beta.26",
4242
"@microsoft/fast-foundation": "3.0.0-alpha.31",
43+
"change-case": "^5.4.4",
4344
"commander": "^12.0.0",
4445
"culori": "^3.2.0",
4546
"deepmerge-ts": "^7.1.3",

packages/adaptive-ui/src/bin/aui.ts

Lines changed: 148 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as prettier from "prettier";
88
import { ComposableStyles, ElementStyles } from '@microsoft/fast-element';
99
import { CSSDesignToken } from "@microsoft/fast-foundation";
1010
import { Command } from 'commander';
11+
import { kebabCase } from "change-case";
1112
import { deepmerge } from "deepmerge-ts";
1213
import { glob } from "glob";
1314
import 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

87105
program.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

Comments
 (0)