Skip to content

Commit 466b933

Browse files
committed
feat: Infrastructure for single pass analyzer & rewriter.
This commit adds the basic end-to-end flow of the ember-cli addon that compiles css-block templates and stylesheets. Includes: * Parsing blocks that are used by the templates in the input tree. * Compiling in-tree blocks that are used by the templates (blocks outside the tree are not compiled yet). * Writing serialized analysis files for each handlebars file output. * Broccoli importer to read from the broccoli merged filesystem abstraction. * New analysis and template types to reduce the coupling with the css-blocks glimmer package. * AnalyzingRewriteManager to coordinate block parsing and ast plugin creation. * Fixes for the latest release of @glimmer/syntax. * Various refactorings to facilitate code reuse and/or reduce coupling.
1 parent a7a3e8f commit 466b933

36 files changed

+784
-138
lines changed

packages/@css-blocks/broccoli/src/Analyze.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class CSSBlocksAnalyze extends BroccoliPlugin {
106106
let blocks = this.analyzer.transitiveBlockDependencies();
107107
for (let block of blocks) {
108108
if (block.stylesheet) {
109-
let root = blockCompiler.compile(block, block.stylesheet, this.analyzer);
109+
let root = blockCompiler.compile(block, block.stylesheet, this.analyzer.reservedClassNames());
110110
let result = root.toResult({ to: this.outputFileName, map: { inline: false, annotation: false } });
111111
let filesystemPath = options.importer.filesystemPath(block.identifier, options);
112112
let filename = filesystemPath || options.importer.debugIdentifier(block.identifier, options);

packages/@css-blocks/core/.vscode/cSpell.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"rulesets",
2727
"shorthands",
2828
"stateful",
29+
"Stmnt",
2930
"truthy",
3031
"Validators"
3132
],

packages/@css-blocks/core/src/Analyzer/Analysis.ts

+16-12
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export interface SerializedAnalysis<K extends keyof TemplateTypes> {
3636
elements: ObjectDictionary<SerializedElementAnalysis>;
3737
}
3838

39+
// tslint:disable-next-line:prefer-unknown-to-any
40+
type ElementAnalyzedCallback<BooleanExpression, StringExpression, TernaryExpression> = (element: ElementAnalysis<BooleanExpression, StringExpression, TernaryExpression>) => void;
41+
3942
/**
4043
* An Analysis performs book keeping and ensures internal consistency of the block objects referenced
4144
* within a single template. It is designed to be used as part of an AST walk over a template.
@@ -48,7 +51,6 @@ export interface SerializedAnalysis<K extends keyof TemplateTypes> {
4851
export class Analysis<K extends keyof TemplateTypes> {
4952

5053
idGenerator: IdentGenerator;
51-
parent?: Analyzer<K>;
5254
template: TemplateTypes[K];
5355

5456
/**
@@ -77,16 +79,22 @@ export class Analysis<K extends keyof TemplateTypes> {
7779
*/
7880
private validator: TemplateValidator;
7981

82+
/**
83+
* Callback when an element is done being analyzed.
84+
* The element analysis will be sealed.
85+
*/
86+
onElementAnalyzed?: ElementAnalyzedCallback<any, any, any>;
87+
8088
/**
8189
* @param template The template being analyzed.
8290
*/
83-
constructor(parent: Analyzer<K>, template: TemplateTypes[K], options?: TemplateValidatorOptions) {
91+
constructor(template: TemplateTypes[K], options?: TemplateValidatorOptions, onElementAnalyzed?: ElementAnalyzedCallback<any, any, any>) {
8492
this.idGenerator = new IdentGenerator();
85-
this.parent = parent;
8693
this.template = template;
8794
this.blocks = {};
8895
this.elements = new Map();
8996
this.validator = new TemplateValidator(options);
97+
this.onElementAnalyzed = onElementAnalyzed;
9098
}
9199

92100
/**
@@ -205,13 +213,8 @@ export class Analysis<K extends keyof TemplateTypes> {
205213
if (!element.sealed) { element.seal(); }
206214
this.validator.validate(this, element);
207215
this.elements.set(element.id, element);
208-
if (this.parent) {
209-
for (let s of [...element.classesFound(false), ...element.attributesFound(false)]) {
210-
this.parent.saveStaticStyle(s, this);
211-
}
212-
for (let s of [...element.classesFound(true), ...element.attributesFound(true)]) {
213-
this.parent.saveDynamicStyle(s, this);
214-
}
216+
if (this.onElementAnalyzed) {
217+
this.onElementAnalyzed(element);
215218
}
216219
this.currentElement = undefined;
217220
}
@@ -295,7 +298,7 @@ export class Analysis<K extends keyof TemplateTypes> {
295298
/**
296299
* Generates a [[SerializedTemplateAnalysis]] for this analysis.
297300
*/
298-
serialize(): SerializedAnalysis<K> {
301+
serialize(blockPaths?: Map<Block, string>): SerializedAnalysis<K> {
299302
let blocks = {};
300303
let stylesFound: string[] = [];
301304
let elements: ObjectDictionary<SerializedElementAnalysis> = {};
@@ -321,7 +324,8 @@ export class Analysis<K extends keyof TemplateTypes> {
321324

322325
// Serialize our blocks to a map of their local names.
323326
Object.keys(this.blocks).forEach((localName) => {
324-
blocks[localName] = this.blocks[localName].identifier;
327+
let block = this.blocks[localName];
328+
blocks[localName] = blockPaths && blockPaths.get(block) || block.identifier;
325329
});
326330

327331
// Serialize all discovered Elements.

packages/@css-blocks/core/src/Analyzer/Analyzer.ts

+12-12
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,18 @@ export abstract class Analyzer<K extends keyof TemplateTypes> {
6565
}
6666

6767
newAnalysis(info: TemplateTypes[K]): Analysis<K> {
68-
let analysis = new Analysis<K>(this, info, this.validatorOptions);
68+
let analysis = new Analysis<K>(info, this.validatorOptions, (element) => {
69+
for (let s of [...element.classesFound(false), ...element.attributesFound(false)]) {
70+
this.staticStyles.set(s, analysis);
71+
}
72+
for (let s of [...element.classesFound(true), ...element.attributesFound(true)]) {
73+
this.dynamicStyles.set(s, analysis);
74+
}
75+
});
6976
this.analysisMap.set(info.identifier, analysis);
7077
return analysis;
7178
}
7279

73-
saveStaticStyle(style: Style, analysis: Analysis<K>) {
74-
this.staticStyles.set(style, analysis);
75-
}
76-
77-
saveDynamicStyle(style: Style, analysis: Analysis<K>) {
78-
this.dynamicStyles.set(style, analysis);
79-
}
80-
8180
getAnalysis(idx: number): Analysis<K> { return this.analyses()[idx]; }
8281

8382
analysisCount(): number { return this.analysisMap.size; }
@@ -113,9 +112,10 @@ export abstract class Analyzer<K extends keyof TemplateTypes> {
113112
}
114113

115114
/**
116-
* Iterates through all the analyses objects for all the templates and creates
117-
* a set of reservedClassNames here. This is what the block compiler calls to
118-
* get the list of reservedClassNames.
115+
* Iterates through all the analyses objects for all the templates and
116+
* creates a set of reservedClassNames here. These are used by the block
117+
* compiler to ensure the classnames that are output don't collide with user
118+
* specified style aliases.
119119
*/
120120
reservedClassNames(): Set<string> {
121121
let allReservedClassNames = new Set<string>();

packages/@css-blocks/core/src/Analyzer/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { IAnalysis as Analysis, SerializedAnalysis } from "./Analysis";
1+
export { IAnalysis as Analysis, SerializedAnalysis, Analysis as AnalysisImpl } from "./Analysis";
22
export { Analyzer, AnalysisOptions, SerializedAnalyzer } from "./Analyzer";
33
export * from "./ElementAnalysis";
44
export {

packages/@css-blocks/core/src/BlockCompiler/index.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { TemplateTypes } from "@opticss/template-api";
21
import { postcss } from "opticss";
32

4-
import { Analyzer } from "../Analyzer";
53
import {
64
BLOCK_ALIAS,
75
BLOCK_AT_RULES,
@@ -31,12 +29,10 @@ export class BlockCompiler {
3129
this.postcss = postcssImpl;
3230
}
3331

34-
compile(block: Block, root: postcss.Root, analyzer?: Analyzer<keyof TemplateTypes>): postcss.Root {
35-
let resolver = new ConflictResolver(this.config, analyzer ? analyzer.reservedClassNames() : new Set());
32+
compile(block: Block, root: postcss.Root, reservedClassNames: Set<string>): postcss.Root {
33+
let resolver = new ConflictResolver(this.config, reservedClassNames);
3634
let filename = this.config.importer.debugIdentifier(block.identifier, this.config);
3735

38-
if (analyzer) { /* Do something smart with the Analyzer here */ }
39-
4036
// Process all debug statements for this block.
4137
this.processDebugStatements(filename, root, block);
4238

@@ -61,7 +57,7 @@ export class BlockCompiler {
6157
resolver.resolveInheritance(root, block);
6258
root.walkRules((rule) => {
6359
let parsedSelectors = block.getParsedSelectors(rule);
64-
rule.selector = parsedSelectors.map(s => block.rewriteSelectorToString(s, this.config, analyzer ? analyzer.reservedClassNames() : new Set())).join(",\n");
60+
rule.selector = parsedSelectors.map(s => block.rewriteSelectorToString(s, this.config, reservedClassNames)).join(",\n");
6561
});
6662

6763
resolver.resolve(root, block);

packages/@css-blocks/core/src/BlockParser/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export {
1515
OptionalPreprocessor,
1616
Preprocessors,
1717
ProcessedFile,
18+
syntaxFromExtension,
1819
} from "./preprocessing";

packages/@css-blocks/core/src/BlockParser/preprocessing.ts

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ export function syntaxName(syntax: Syntax): string {
2222
return Object.keys(Syntax).find(s => Syntax[s] === syntax) || "other";
2323
}
2424

25+
export function syntaxFromExtension(extname: string): Syntax {
26+
extname = extname.startsWith(".") ? extname.substring(1) : extname;
27+
if (extname === "styl") {
28+
return Syntax.stylus;
29+
} else {
30+
return Syntax[extname] || Syntax.other;
31+
}
32+
}
33+
2534
export interface ProcessedFile {
2635
/**
2736
* The result of processing the file.

packages/@css-blocks/core/test/opticss-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class TemplateAnalysisTests {
9999
let compiler = new BlockCompiler(postcss, config);
100100
let optimizer = new Optimizer({}, { rewriteIdents: { id: false, class: true} });
101101
let block = await analyzer.blockFactory.getBlock(filename);
102-
let compiled = compiler.compile(block, block.stylesheet!, analyzer);
102+
let compiled = compiler.compile(block, block.stylesheet!, analyzer.reservedClassNames());
103103
for (let analysis of analyzer.analyses()) {
104104
let optimizerAnalysis = analysis.forOptimizer(config);
105105
optimizer.addSource({

packages/@css-blocks/core/test/util/postcss-helper.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Plugin {
4444

4545
await factory.parseRoot(root, sourceFile, defaultName).then((block) => {
4646
let compiler = new BlockCompiler(postcss, this.config);
47-
compiler.compile(block, root);
47+
compiler.compile(block, root, new Set());
4848
});
4949
}
5050
}

packages/@css-blocks/ember/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"broccoli-node-api": "^1.7.0",
4040
"broccoli-test-helper": "^2.0.0",
4141
"ember-cli-htmlbars": "^4.3.1",
42+
"fs-merger": "^3.0.2",
43+
"broccoli-output-wrapper": "^3.2.1",
4244
"typescript": "~3.8.3",
4345
"watch": "^1.0.2"
4446
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
AnalysisOptions,
3+
Block,
4+
BlockFactory,
5+
CssBlockError,
6+
Options as CSSBlocksOptions,
7+
ResolvedConfiguration as CSSBlocksConfiguration,
8+
StyleMapping,
9+
TemplateValidatorOptions,
10+
resolveConfiguration,
11+
} from "@css-blocks/core";
12+
import { Syntax } from "@glimmer/syntax";
13+
import { ObjectDictionary, unionInto } from "@opticss/util";
14+
15+
import { EmberAnalysis } from "./EmberAnalysis";
16+
import { FileLocator } from "./FileLocator";
17+
import { HandlebarsTemplate, TEMPLATE_TYPE } from "./HandlebarsTemplate";
18+
import { TemplateAnalyzingRewriter } from "./TemplateAnalyzingRewriter";
19+
20+
export type GlimmerStyleMapping = StyleMapping<TEMPLATE_TYPE>;
21+
22+
export interface AnalyzedTemplate {
23+
template: HandlebarsTemplate;
24+
block: Block;
25+
analysis: EmberAnalysis;
26+
}
27+
28+
export class AnalyzingRewriteManager {
29+
elementCount: number;
30+
cssBlocksOpts: CSSBlocksConfiguration;
31+
validationOptions: TemplateValidatorOptions;
32+
analysisOptions: AnalysisOptions;
33+
templateBlocks: ObjectDictionary<Block | undefined>;
34+
analyses: Map<string, EmberAnalysis>;
35+
fileLocator: FileLocator;
36+
blockFactory: BlockFactory;
37+
possibleStylesheetExtensions: Array<string>;
38+
templates: Map<string, HandlebarsTemplate>;
39+
40+
constructor(
41+
blockFactory: BlockFactory,
42+
fileLocator: FileLocator,
43+
analysisOptions: AnalysisOptions,
44+
cssBlocksOpts: CSSBlocksOptions,
45+
) {
46+
this.validationOptions = analysisOptions && analysisOptions.validations || {};
47+
this.blockFactory = blockFactory;
48+
this.fileLocator = fileLocator;
49+
this.analysisOptions = analysisOptions;
50+
this.cssBlocksOpts = resolveConfiguration(cssBlocksOpts);
51+
let extensions = new Set(Object.keys(this.cssBlocksOpts.preprocessors));
52+
extensions.add("css");
53+
this.possibleStylesheetExtensions = [...extensions];
54+
this.elementCount = 0;
55+
this.templateBlocks = {};
56+
this.analyses = new Map();
57+
this.templates = new Map();
58+
}
59+
60+
async discoverTemplatesWithBlocks(): Promise<number> {
61+
let count = 0;
62+
for (let templatePath of this.fileLocator.possibleTemplatePaths()) {
63+
let stylesheet = this.fileLocator.findStylesheetForTemplate(templatePath, this.possibleStylesheetExtensions);
64+
if (stylesheet) {
65+
let block = await this.blockFactory.getBlock(this.fileLocator.blockIdentifier(stylesheet));
66+
this.registerTemplate(templatePath, block);
67+
count++;
68+
}
69+
}
70+
return count;
71+
}
72+
73+
registerTemplate(template: string, block: Block) {
74+
this.templateBlocks[template] = block;
75+
}
76+
77+
/**
78+
* @param templatePath relative path to template
79+
*/
80+
templateAnalyzerAndRewriter(templatePath: string, syntax: Syntax): TemplateAnalyzingRewriter {
81+
if (this.analyses.get(templatePath)) {
82+
throw new CssBlockError(`Internal Error: Template at ${templatePath} was already analyzed.`);
83+
}
84+
let block = this.templateBlocks[templatePath];
85+
let template = new HandlebarsTemplate(templatePath, templatePath);
86+
this.templates.set(templatePath, template);
87+
let analysis = new EmberAnalysis(template, this.validationOptions);
88+
this.analyses.set(templatePath, analysis);
89+
return new TemplateAnalyzingRewriter(template, block, analysis, this.cssBlocksOpts, syntax);
90+
}
91+
92+
/**
93+
* Iterates through all the analyses objects for all the templates and
94+
* creates a set of reservedClassNames here. These are used by the block
95+
* compiler to ensure the classnames that are output don't collide with user
96+
* specified style aliases.
97+
*/
98+
reservedClassNames(): Set<string> {
99+
let allReservedClassNames = new Set<string>();
100+
for (let analysis of this.analyses.values()) {
101+
unionInto(allReservedClassNames, analysis.reservedClassNames());
102+
}
103+
return allReservedClassNames;
104+
}
105+
106+
*analyzedTemplates(): Generator<AnalyzedTemplate, void> {
107+
let templatePaths = Object.keys(this.templateBlocks);
108+
for (let templatePath of templatePaths) {
109+
let block = this.templateBlocks[templatePath]!;
110+
let template = this.templates.get(templatePath);
111+
if (!template) {
112+
throw new CssBlockError(`Internal Error: Template at ${templatePath} was not yet analyzed.`);
113+
}
114+
let analysis = this.analyses.get(templatePath);
115+
if (!analysis) {
116+
throw new CssBlockError(`Internal Error: Template at ${templatePath} was not yet analyzed.`);
117+
}
118+
yield {
119+
template,
120+
analysis,
121+
block,
122+
};
123+
}
124+
}
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { FS as MergedFS } from "fs-merger";
2+
3+
import { FileLocator } from "./FileLocator";
4+
5+
export type FS = Pick<MergedFS, "existsSync" | "entries">;
6+
7+
export class BroccoliFileLocator implements FileLocator {
8+
fs: FS;
9+
constructor(fs: FS) {
10+
this.fs = fs;
11+
}
12+
findStylesheetForTemplate(relativePathToTemplate: string, extensions: Array<string>): string | null {
13+
let possibleStylesheets = this.possibleStylesheetPathsForTemplate(relativePathToTemplate, extensions);
14+
return possibleStylesheets.find((s) => this.fs.existsSync(s)) || null;
15+
}
16+
blockIdentifier(relativePathToStylesheet: string): string {
17+
return `broccoli-tree:${relativePathToStylesheet}`;
18+
}
19+
possibleTemplatePaths(): Array<string> {
20+
return this.fs.entries(".", ["**/*.hbs"]).map(e => e.basePath);
21+
}
22+
possibleStylesheetPathsForTemplate(templatePath: string, extensions: Array<string>): Array<string> {
23+
let path = templatePath.replace("/templates/", "/styles/");
24+
return extensions.map(ext => path.replace(/\.hbs$/, `.block.${ext}`));
25+
}
26+
}

0 commit comments

Comments
 (0)