Skip to content

Commit bf9bd06

Browse files
committed
fix: Fix dev build performance issue.
This fix makes the block GUID stable across build processes so that cached templates remain valid. Closes #357.
1 parent 6dc7397 commit bf9bd06

File tree

6 files changed

+59
-46
lines changed

6 files changed

+59
-46
lines changed

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

+34-30
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { Analyzer, BlockCompiler, StyleMapping } from "@css-blocks/core";
22
import { TemplateTypes } from "@opticss/template-api";
33
import * as debugGenerator from "debug";
4-
import * as fs from "fs-extra";
4+
import * as fs from "fs";
55
import * as FSTree from "fs-tree-diff";
66
import * as glob from "glob";
7+
import * as minimatch from "minimatch";
78
import { OptiCSSOptions, Optimizer, postcss } from "opticss";
89
import * as path from "path";
910
import * as walkSync from "walk-sync";
1011

1112
import { Transport } from "./Transport";
12-
import { BroccoliPlugin, symlinkOrCopy } from "./utils";
13+
import { BroccoliPlugin } from "./utils";
1314

1415
const debug = debugGenerator("css-blocks:broccoli");
1516

@@ -32,11 +33,9 @@ export class CSSBlocksAnalyze extends BroccoliPlugin {
3233
private analyzer: Analyzer<keyof TemplateTypes>;
3334
private entries: string[];
3435
private output: string;
35-
private root: string;
3636
private transport: Transport;
3737
private optimizationOptions: Partial<OptiCSSOptions>;
3838
private previousInput: FSTree = new FSTree();
39-
private previousOutput: FSTree = new FSTree();
4039

4140
/**
4241
* Initialize this new instance with the app tree, transport, and analysis options.
@@ -55,48 +54,52 @@ export class CSSBlocksAnalyze extends BroccoliPlugin {
5554
this.output = options.output || "css-blocks.css";
5655
this.optimizationOptions = options.optimization || {};
5756
this.analyzer = options.analyzer;
58-
this.root = options.root || process.cwd();
5957
this.transport.css = this.transport.css ? this.transport.css : "";
6058
}
6159

6260
/**
6361
* Re-run the broccoli build over supplied inputs.
6462
*/
6563
async build() {
66-
let input = this.inputPaths[0];
64+
// We currently rely on the fact that the input path is a source directory
65+
// and not a temp directory from a previous build step.
66+
// When the input directory is a temp directory, the block guid
67+
// becomes unstable across brocolli build processes which can cause
68+
// persistent cache incoherence within the templates.
69+
let input = path.resolve(this.inputPaths[0]);
6770
let output = this.outputPath;
6871
let options = this.analyzer.cssBlocksOptions;
6972
let blockCompiler = new BlockCompiler(postcss, options);
7073
let optimizer = new Optimizer(this.optimizationOptions, this.analyzer.optimizationOptions);
7174

75+
let isBlockFile = minimatch.makeRe("**/*.block.*");
76+
7277
// Test if anything has changed since last time. If not, skip all analysis work.
7378
let newFsTree = FSTree.fromEntries(walkSync.entries(input));
7479
let diff = this.previousInput.calculatePatch(newFsTree);
80+
this.previousInput = newFsTree;
7581
if (!diff.length) { return; }
7682

77-
// Save the current state of our output dir for future diffs.
78-
this.previousOutput = FSTree.fromEntries(walkSync.entries(output));
79-
FSTree.applyPatch(input, output, this.previousOutput.calculatePatch(newFsTree));
80-
this.previousInput = newFsTree;
83+
// Get all the operations for files that aren't related to CSS Block files
84+
// So they can be performed on the output directory.
85+
let nonBlockFileChanges = new Array<FSTree.Operation>();
86+
for (let op of diff) {
87+
let entry = op[2];
88+
if (entry && entry.relativePath.match(isBlockFile)) {
89+
continue;
90+
}
91+
nonBlockFileChanges.push(op);
92+
}
93+
94+
FSTree.applyPatch(input, output, nonBlockFileChanges);
8195

8296
// When no entry points are passed, we treat *every* template as an entry point.
83-
this.entries = this.entries.length ? this.entries : glob.sync("**/*.hbs", { cwd: output });
84-
85-
// The glimmer-analyzer package tries to require() package.json
86-
// in the root of the directory it is passed. We pass it our broccoli
87-
// tree, so it needs to contain package.json too.
88-
// TODO: Ideally this is configurable in glimmer-analyzer. We can
89-
// contribute that option back to the project. However,
90-
// other template integrations may want this available too...
91-
let pjsonLink = path.join(output, "package.json");
92-
if (!fs.existsSync(pjsonLink)) {
93-
symlinkOrCopy(path.join(this.root, "package.json"), pjsonLink);
94-
}
97+
this.entries = this.entries.length ? this.entries : glob.sync("**/*.hbs", { cwd: input });
9598

9699
// Oh hey look, we're analyzing.
97100
this.analyzer.reset();
98101
this.transport.reset();
99-
await this.analyzer.analyze(output, this.entries);
102+
await this.analyzer.analyze(input, this.entries);
100103

101104
// Compile all Blocks and add them as sources to the Optimizer.
102105
// TODO: handle a sourcemap from compiling the block file via a preprocessor.
@@ -108,10 +111,14 @@ export class CSSBlocksAnalyze extends BroccoliPlugin {
108111
let filesystemPath = options.importer.filesystemPath(block.identifier, options);
109112
let filename = filesystemPath || options.importer.debugIdentifier(block.identifier, options);
110113

111-
// If this Block has a representation on disk, remove it from our output tree.
112-
if (filesystemPath && !!~filesystemPath.indexOf(output)) {
113-
debug(`Removing block file ${filesystemPath} from output.`);
114-
if (fs.existsSync(filesystemPath)) { fs.removeSync(filesystemPath); }
114+
if (filesystemPath && filesystemPath.startsWith(input)) {
115+
let relativePath = path.relative(input, filesystemPath);
116+
let outputPath = path.resolve(output, relativePath);
117+
try {
118+
fs.unlinkSync(outputPath);
119+
} catch (e) {
120+
// ignore
121+
}
115122
}
116123

117124
// Add the compiled Block file to the optimizer.
@@ -123,9 +130,6 @@ export class CSSBlocksAnalyze extends BroccoliPlugin {
123130
}
124131
}
125132

126-
// Save the current state of our output dir for future diffs.
127-
this.previousOutput = FSTree.fromEntries(walkSync.entries(output));
128-
129133
// Add each Analysis to the Optimizer.
130134
this.analyzer.eachAnalysis((a) => optimizer.addAnalysis(a.forOptimizer(options)));
131135

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

-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { BlockFactory } from "@css-blocks/core";
22
import { GlimmerAnalyzer } from "@css-blocks/glimmer";
33
import * as assert from "assert";
44
import { TempDir, createBuilder, createTempDir } from "broccoli-test-helper";
5-
import * as fs from "fs";
65
import * as FSTree from "fs-tree-diff";
76
import * as walkSync from "walk-sync";
87

@@ -80,8 +79,6 @@ describe("Broccoli Analyze Plugin Test", function () {
8079
});
8180

8281
// Modifications to block files trigger build but result in no output tree changes.
83-
// Accidental modification of output directory does not make the plugin explode.
84-
fs.unlinkSync(output.path("src/ui/components/Chrisrng/template.hbs"));
8582
input.write({
8683
src: {
8784
ui: {

packages/@css-blocks/core/src/BlockTree/Block.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { MultiMap, ObjectDictionary } from "@opticss/util";
22
import * as crypto from "crypto";
3+
import * as debugGenerator from "debug";
34
import {
45
CompoundSelector,
56
ParsedSelector,
67
parseSelector,
78
postcss,
89
postcssSelectorParser as selectorParser,
910
} from "opticss";
11+
import * as process from "process";
1012

1113
import { isAttributeNode, isClassNode } from "../BlockParser";
1214
import { isRootNode, toAttrToken } from "../BlockParser";
@@ -20,22 +22,24 @@ import { BlockClass } from "./BlockClass";
2022
import { Inheritable } from "./Inheritable";
2123
import { Styles } from "./Styles";
2224

23-
// Random seed for `get_guid`. Consistent for life of process.
24-
const RAND_SEED = Math.floor(Math.random() * 1000);
25+
const DEBUG = debugGenerator("css-blocks:caching");
2526

2627
/**
27-
* Generates a 5 digit, process-wide globally unique
28-
* identifier from a given input identifier. This
29-
* generated identifier hash will remain consistent
30-
* for the life of the process that spawned it.
31-
* @prop identifier string Input Block identifier.
28+
* Generates a 5 digit, unique identifier from a given input identifier. This
29+
* generated identifier hash will remain consistent for the tuple of (machine,
30+
* user, css blocks installation location).
31+
* @param identifier Input Block identifier.
3232
* @returns This Block's guid hash.
3333
*/
3434
function gen_guid(identifier: string): string {
35-
return crypto.createHash("md5")
36-
.update(identifier + RAND_SEED)
35+
let hash = crypto.createHash("md5")
36+
.update(process.getuid().toString())
37+
.update(__filename)
38+
.update(identifier)
3739
.digest("hex")
3840
.slice(0, 5);
41+
DEBUG("guid is %s for %s", hash, identifier);
42+
return hash;
3943
}
4044

4145
export class Block

packages/@css-blocks/ember-cli/index.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,15 @@ module.exports = {
196196
// Analyze all templates and block files from `/src` in Glimmer apps.
197197
if (parent.trees) {
198198
let treeName = env.isEmber ? "app" : "src";
199-
parent.trees[treeName] = this.genTreeWrapper(env, options)(parent.trees[treeName]);
199+
let tree = parent.trees[treeName];
200+
if (!env.isEmber) {
201+
// Glimmer apps use the glimmer dependency analyzer which expects to find
202+
// the package.json file for the project in the tree that also includes the app's
203+
// src directory
204+
let packageJsonTree = BroccoliFunnel(env.rootDir, {include: ["package.json"]});
205+
tree = BroccoliMerge([tree, packageJsonTree]);
206+
}
207+
parent.trees[treeName] = this.genTreeWrapper(env, options)(tree);
200208
}
201209

202210
},
@@ -304,7 +312,6 @@ module.exports = {
304312
},
305313

306314
optionsForCacheInvalidation() {
307-
let pid = process.pid; // Because of the per-process randomization of css block names.
308315
let aliases = this._options.aliases;
309316
let analysisOpts = this._options.analysisOpts;
310317
let optimization = this._options.optimization;
@@ -318,7 +325,6 @@ module.exports = {
318325
}
319326

320327
return {
321-
pid,
322328
aliases,
323329
analysisOpts,
324330
optimization,

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class GlimmerAnalyzer extends Analyzer<TEMPLATE_TYPE> {
2828
constructor(
2929
blockFactory: BlockFactory,
3030
analysisOpts: AnalysisOptions,
31-
moduleConfig: ResolverConfiguration,
31+
moduleConfig?: ResolverConfiguration | undefined,
3232
) {
3333
super(blockFactory, analysisOpts);
3434
this.blockFactory = blockFactory;

packages/@css-blocks/glimmer/src/Resolver.ts

+2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export class Resolver {
8282
* @return The DependencyAnalyzer, or undefined.
8383
*/
8484
private dependencyAnalyzerFor(base: string): DependencyAnalyzer | undefined {
85+
DEBUG("Base directory for dependency analysis is %s", base);
86+
8587
if (!this.moduleConfig) { return undefined; }
8688
if (this.depAnalyzers.has(base)) {
8789
return this.depAnalyzers.get(base)!;

0 commit comments

Comments
 (0)