Skip to content

Commit 3ec0216

Browse files
Tim Lindvalltimlindvall
Tim Lindvall
authored andcommitted
feat: Scan app CSS for classes.
- Write found classes to log file for now. - Also moves some common classes and interfaces to utility files.
1 parent a663364 commit 3ec0216

File tree

4 files changed

+153
-36
lines changed

4 files changed

+153
-36
lines changed

packages/@css-blocks/ember-app/src/broccoli-plugin.ts

+78-14
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import Plugin = require("broccoli-plugin");
88
import type { PluginOptions } from "broccoli-plugin/dist/interfaces";
99
import debugGenerator from "debug";
1010
import * as FSTree from "fs-tree-diff";
11-
import { OptiCSSOptions, Optimizer, postcss } from "opticss";
11+
import { OptiCSSOptions, Optimizer, parseSelector, postcss } from "opticss";
1212
import * as path from "path";
1313

1414
import { RuntimeDataGenerator } from "./RuntimeDataGenerator";
15+
import { cssBlocksPostprocessFilename, cssBlocksPreprocessFilename } from "./utils/filepaths";
16+
import { AddonEnvironment } from "./utils/interfaces";
1517

1618
const debug = debugGenerator("css-blocks:ember-app");
1719

@@ -83,7 +85,7 @@ export class CSSBlocksApplicationPlugin extends Filter {
8385
}
8486
debug(`Loaded ${blocksUsed.size} blocks.`);
8587
debug(`Loaded ${optimizer.analyses.length} analyses.`);
86-
let cssFileName = cssBlocksOutputFilename(this.cssBlocksOptions);
88+
let cssFileName = cssBlocksPreprocessFilename(this.cssBlocksOptions);
8789
let optLogFileName = `${cssFileName}.optimization.log`;
8890
let optimizationResult = await optimizer.optimize(cssFileName);
8991
debug(`Optimized CSS. There were ${optimizationResult.actions.performed.length} optimizations performed.`);
@@ -164,7 +166,7 @@ export class CSSBlocksStylesPreprocessorPlugin extends Plugin {
164166
}
165167
async build() {
166168
// Are there any changes to make? If not, bail out early.
167-
let stylesheetPath = cssBlocksOutputFilename(this.cssBlocksOptions);
169+
let stylesheetPath = cssBlocksPreprocessFilename(this.cssBlocksOptions);
168170
let entries = this.input.entries(".", {globs: [stylesheetPath]});
169171
let currentFSTree = FSTree.fromEntries(entries);
170172
let patch = this.previousSourceTree.calculatePatch(currentFSTree);
@@ -190,6 +192,79 @@ export class CSSBlocksStylesPreprocessorPlugin extends Plugin {
190192
}
191193
}
192194

195+
/**
196+
* Plugin for the CSS postprocess tree. This plugin scans for classes declared
197+
* in application CSS (outside of CSS Blocks) and checks if there are any
198+
* duplicates between the app code and the classes generated by the optimizer.
199+
*
200+
* This plugin is only run for builds where the optimizer is enabled.
201+
*/
202+
export class CSSBlocksStylesPostprocessorPlugin extends Filter {
203+
env: AddonEnvironment;
204+
205+
constructor(env: AddonEnvironment, inputNodes: InputNode[]) {
206+
super(mergeTrees(inputNodes), {});
207+
this.env = env;
208+
}
209+
210+
processString(contents: string, _relativePath: string): string {
211+
return contents;
212+
}
213+
214+
async build() {
215+
await super.build();
216+
217+
// Look up all the application style content that's already present.
218+
const blocksCssFile = cssBlocksPostprocessFilename(this.env.config);
219+
const appStyles: string[] = [];
220+
const foundFiles: string[] = [];
221+
222+
const walkEntries = this.input.entries(undefined, {
223+
globs: ["**/*.css"],
224+
});
225+
walkEntries.forEach(entry => {
226+
if (entry.relativePath === blocksCssFile) return;
227+
try {
228+
appStyles.push(this.input.readFileSync(entry.relativePath).toString("utf8"));
229+
foundFiles.push(entry.relativePath);
230+
} catch (e) {
231+
// broccoli-concat will complain about this later. let's move on.
232+
}
233+
});
234+
235+
// Now, read in each of these sources and check there are no class name conflicts.
236+
const foundClasses: Set<string> = new Set<string>();
237+
const errorLog: string[] = [];
238+
appStyles.forEach(content => {
239+
try {
240+
const parsed = postcss.parse(content);
241+
parsed.walkRules(rule => {
242+
const selectors = parseSelector(rule.selector);
243+
selectors.forEach(sel => {
244+
sel.eachSelectorNode(node => {
245+
if (node.type === "class") {
246+
foundClasses.add(node.value);
247+
}
248+
});
249+
});
250+
});
251+
} catch (e) {
252+
// TODO: Can't parse CSS? Gracefully fail or crash and burn?
253+
errorLog.push(e.toString());
254+
}
255+
});
256+
257+
// Build a logfile for the output tree, for debugging.
258+
let logfile = "FOUND CLASSES:\n";
259+
foundClasses.forEach(cssClass => { logfile += `${cssClass}\n`; });
260+
logfile += "\nFOUND FILES:\n";
261+
foundFiles.forEach(file => { logfile += `${file}\n`; });
262+
logfile += "\nERRORS:\n";
263+
errorLog.forEach(err => { logfile += `${err}\n`; });
264+
this.output.writeFileSync("assets/app-classes.log", logfile);
265+
}
266+
}
267+
193268
/**
194269
* Given CSS and a sourcemap, append an embedded sourcemap (base64 encoded)
195270
* to the end of the css.
@@ -205,14 +280,3 @@ function addSourcemapInfoToOptimizedCss(css: string, sourcemap?: string) {
205280
const encodedSourcemap = Buffer.from(sourcemap).toString("base64");
206281
return `${css}\n/*# sourceMappingURL=data:application/json;base64,${encodedSourcemap} */`;
207282
}
208-
209-
/**
210-
* Generate the output path for the compiled CSS Blocks content, using the
211-
* preferred filename given by the user. If none is given, the default
212-
* path is "app/styles/css-blocks.css".
213-
* @param options - The options passed to the Ember plugin.
214-
* @returns - The path for the CSS Blocks compiled content.
215-
*/
216-
function cssBlocksOutputFilename(options: ResolvedCSSBlocksEmberOptions) {
217-
return `app/styles/${options.output}`;
218-
}

packages/@css-blocks/ember-app/src/index.ts

+22-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BroccoliConcatOptions, CSSBlocksEmberOptions, ResolvedCSSBlocksEmberOptions, getConfig } from "@css-blocks/ember-utils";
1+
import { BroccoliConcatOptions, CSSBlocksEmberOptions, getConfig } from "@css-blocks/ember-utils";
22
import broccoliConcat = require("broccoli-concat");
33
import BroccoliDebug = require("broccoli-debug");
44
import funnel = require("broccoli-funnel");
@@ -8,23 +8,9 @@ import type Addon from "ember-cli/lib/models/addon";
88
import type { AddonImplementation, ThisAddon } from "ember-cli/lib/models/addon";
99
import Project from "ember-cli/lib/models/project";
1010

11-
import { CSSBlocksApplicationPlugin, CSSBlocksStylesPreprocessorPlugin } from "./broccoli-plugin";
12-
13-
interface AddonEnvironment {
14-
parent: Addon | EmberApp;
15-
app: EmberApp;
16-
rootDir: string;
17-
isApp: boolean;
18-
modulePrefix: string;
19-
config: ResolvedCSSBlocksEmberOptions;
20-
}
21-
22-
interface CSSBlocksApplicationAddon {
23-
_modulePrefix(): string;
24-
env: AddonEnvironment | undefined;
25-
getEnv(parent): AddonEnvironment;
26-
broccoliAppPluginInstance: CSSBlocksApplicationPlugin | undefined;
27-
}
11+
import { CSSBlocksApplicationPlugin, CSSBlocksStylesPostprocessorPlugin, CSSBlocksStylesPreprocessorPlugin } from "./broccoli-plugin";
12+
import { appStylesPostprocessFilename, cssBlocksPostprocessFilename } from "./utils/filepaths";
13+
import { AddonEnvironment, CSSBlocksApplicationAddon } from "./utils/interfaces";
2814

2915
/**
3016
* An ember-cli addon for Ember applications using CSS Blocks in its
@@ -202,15 +188,27 @@ const EMBER_ADDON: AddonImplementation<CSSBlocksApplicationAddon> = {
202188
return tree;
203189
}
204190

191+
// Verify there are no selector conflicts...
192+
// (Only for builds with optimization enabled.)
193+
let scannerTree;
194+
if (env.config.optimization.enabled) {
195+
scannerTree = new BroccoliDebug(
196+
new CSSBlocksStylesPostprocessorPlugin(env, [tree]),
197+
"css-blocks:css-postprocess-preconcat",
198+
);
199+
} else {
200+
scannerTree = tree;
201+
}
202+
205203
// Create the concatenated file...
206204
const concatTree = broccoliConcat(
207-
tree,
205+
scannerTree,
208206
buildBroccoliConcatOptions(env),
209207
);
210208

211209
// Then overwrite the original file with our final build artifact.
212210
const mergedTree = funnel(mergeTrees([tree, concatTree], { overwrite: true }), {
213-
exclude: [`assets/${env.config.output}`],
211+
exclude: [cssBlocksPostprocessFilename(env.config)],
214212
});
215213
return new BroccoliDebug(mergedTree, "css-blocks:css-postprocess");
216214
}
@@ -261,9 +259,11 @@ function buildBroccoliConcatOptions(env: AddonEnvironment): BroccoliConcatOption
261259
* @returns - Default broccoli-concat options, accounting for current env settings.
262260
*/
263261
function buildDefaultBroccoliConcatOptions(env: AddonEnvironment): BroccoliConcatOptions {
262+
const appCssPath = appStylesPostprocessFilename(env);
263+
const blocksCssPath = cssBlocksPostprocessFilename(env.config);
264264
return {
265-
inputFiles: [`assets/${env.config.output}`, `assets/${env.modulePrefix}.css`],
266-
outputFile: `assets/${env.modulePrefix}.css`,
265+
inputFiles: [blocksCssPath, appCssPath],
266+
outputFile: appCssPath,
267267
sourceMapConfig: {
268268
enabled: true,
269269
extensions: ["css"],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ResolvedCSSBlocksEmberOptions } from "@css-blocks/ember-utils";
2+
3+
import { AddonEnvironment } from "./interfaces";
4+
5+
/**
6+
* Generate the output path for the compiled CSS Blocks content, using the
7+
* preferred filename given by the user. If none is given, the default
8+
* path is "app/styles/css-blocks.css".
9+
* @param options - The options passed to the Ember plugin.
10+
* @returns - The path for the CSS Blocks compiled content.
11+
*/
12+
export function cssBlocksPreprocessFilename(options: ResolvedCSSBlocksEmberOptions) {
13+
return `app/styles/${options.output}`;
14+
}
15+
16+
/**
17+
* Get the path to the compiled css blocks file in the postprocess tree.
18+
* @param env - The current resolved addon configuration.
19+
* @returns - Filepath in postprocess tree.
20+
*/
21+
export function cssBlocksPostprocessFilename(config: ResolvedCSSBlocksEmberOptions) {
22+
return `assets/${config.output}`;
23+
}
24+
25+
/**
26+
* Get the path to the compiled app css file in the postprocess tree.
27+
* @param env - The current addon environment information
28+
* @returns - Filepath in postprocess tree.
29+
*/
30+
export function appStylesPostprocessFilename(env: AddonEnvironment) {
31+
return `assets/${env.modulePrefix}.css`;
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ResolvedCSSBlocksEmberOptions } from "@css-blocks/ember-utils";
2+
import EmberApp from "ember-cli/lib/broccoli/ember-app";
3+
import type Addon from "ember-cli/lib/models/addon";
4+
5+
import { CSSBlocksApplicationPlugin } from "../broccoli-plugin";
6+
7+
export interface AddonEnvironment {
8+
parent: Addon | EmberApp;
9+
app: EmberApp;
10+
rootDir: string;
11+
isApp: boolean;
12+
modulePrefix: string;
13+
config: ResolvedCSSBlocksEmberOptions;
14+
}
15+
16+
export interface CSSBlocksApplicationAddon {
17+
_modulePrefix(): string;
18+
env: AddonEnvironment | undefined;
19+
getEnv(parent): AddonEnvironment;
20+
broccoliAppPluginInstance: CSSBlocksApplicationPlugin | undefined;
21+
}

0 commit comments

Comments
 (0)