forked from linkedin/css-blocks
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPlugin.ts
328 lines (293 loc) · 12.1 KB
/
Plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
import { TemplateTypes } from "@opticss/template-api";
import { ObjectDictionary, objectValues, whatever } from "@opticss/util";
import * as debugGenerator from "debug";
import { postcss } from "opticss";
import * as path from "path";
import { RawSourceMap } from "source-map";
import * as Tapable from "tapable";
import { Compiler as WebpackCompiler, Plugin as WebpackPlugin } from "webpack";
import { RawSource, Source, SourceMapSource } from "webpack-sources";
import {
Analysis,
Analyzer as AnalyzerType,
Block,
BlockCompiler,
Options as CSSBlocksOptions,
StyleMapping,
resolveConfiguration,
} from "@css-blocks/core";
import {
Actions,
DEFAULT_OPTIONS,
OptiCSSOptions,
OptimizationResult,
Optimizer,
} from "opticss";
import { LoaderContext } from "./context";
export type TmpType = keyof TemplateTypes;
export type Analyzer = AnalyzerType<TmpType>;
export type PendingResult = Promise<StyleMapping<TmpType> | void>;
export interface CssBlocksWebpackOptions {
/// The name of the instance of the plugin. Defaults to outputCssFile.
name?: string;
/// The analyzer that decides what templates are analyzed and what blocks will be compiled.
analyzer: Analyzer;
/// The output css file for all compiled CSS Blocks. Defaults to "css-blocks.css"
outputCssFile?: string;
/// Compilation options pass to css-blocks
compilationOptions?: CSSBlocksOptions;
/// Optimization options passed to opticss
optimization?: OptiCSSOptions;
}
// there's not any good types for webpack's internals.
// tslint:disable-next-line:prefer-whatever-to-any
export type WebpackAny = any;
export interface BlockCompilationError {
compilation: WebpackAny;
assetPath: string;
error: Error;
mapping?: StyleMapping<TmpType>;
optimizerActions?: Actions;
}
export interface BlockCompilationComplete {
compilation: WebpackAny;
assetPath: string;
mapping: StyleMapping<TmpType>;
optimizerActions: Actions;
}
type Assets = ObjectDictionary<Source>;
type EntryTypes = string | string[] | ObjectDictionary<string>;
interface CompilationResult {
optimizationResult: OptimizationResult;
blocks: Set<Block>;
analyses: Array<Analysis<TmpType>>;
}
export class CssBlocksPlugin
extends Tapable
implements WebpackPlugin
{
optimizationOptions: OptiCSSOptions;
name: string;
analyzer: Analyzer;
projectDir: string;
outputCssFile: string;
compilationOptions: CSSBlocksOptions;
pendingResult?: PendingResult;
debug: debugGenerator.IDebugger;
constructor(options: CssBlocksWebpackOptions) {
super();
this.debug = debugGenerator("css-blocks:webpack");
this.analyzer = options.analyzer;
this.outputCssFile = options.outputCssFile || "css-blocks.css";
this.name = options.name || this.outputCssFile;
this.compilationOptions = options.compilationOptions || {};
this.projectDir = process.cwd();
this.optimizationOptions = Object.assign({}, DEFAULT_OPTIONS, options.optimization);
}
private async handleMake(outputPath: string, assets: Assets, compilation: WebpackAny, cb: (error?: Error) => void) {
// Start analysis with a clean analysis object
this.trace(`starting analysis.`);
this.analyzer.reset();
// Fetch our app's entry points.
let webpackEntry = compilation.options.entry as EntryTypes;
let entries: string[] = [];
// Zomg webpack, so many config format options.
if (typeof webpackEntry === "string") {
entries = [ webpackEntry ];
}
else if (Array.isArray(webpackEntry)) {
entries = webpackEntry;
}
else if (typeof webpackEntry === "object") {
entries = flatten(objectValues(webpackEntry)) as string[];
}
let pending: PendingResult = this.analyzer.analyze(...entries)
// If analysis fails, drain our BlockFactory, add error to compilation error list and propagate.
.catch((err: Error) => {
this.trace(`Error during analysis. Draining queue.`);
return this.analyzer.blockFactory.prepareForExit().then(() => {
this.trace(`Drained. Raising error.`);
throw err; // We're done, throw to skip the rest of the plugin steps below.
});
})
// If analysis finished successfully, compile our blocks to output.
.then((analysis: Analyzer) => {
return this.compileBlocks(analysis, path.join(outputPath, this.outputCssFile));
})
// Add the resulting css output to our build.
.then((result: CompilationResult) => {
this.trace(`setting css asset: ${this.outputCssFile}`);
let source: Source;
if (result.optimizationResult.output.sourceMap) {
let resultMap = result.optimizationResult.output.sourceMap;
let rawSourceMap: RawSourceMap;
if (typeof resultMap === "string") {
rawSourceMap = JSON.parse(resultMap);
} else {
rawSourceMap = resultMap;
}
source = new SourceMapSource(
result.optimizationResult.output.content.toString(),
"optimized css",
rawSourceMap);
} else {
source = new RawSource(result.optimizationResult.output.content.toString());
}
assets[`${this.outputCssFile}.log`] = new RawSource(result.optimizationResult.actions.performed.map(a => a.logString()).join("\n"));
assets[this.outputCssFile] = source;
let completion: BlockCompilationComplete = {
compilation: compilation,
assetPath: this.outputCssFile,
mapping: new StyleMapping(result.optimizationResult.styleMapping, result.blocks, resolveConfiguration(this.compilationOptions), result.analyses),
optimizerActions: result.optimizationResult.actions,
};
return completion;
})
// Notify the world when complete.
.then((completion: BlockCompilationComplete) => {
this.trace(`notifying of completion`);
this.notifyComplete(completion, cb);
this.trace(`notified of completion`);
return completion;
})
// Return just the mapping object from this promise.
.then((compilationResult: BlockCompilationComplete): StyleMapping<TmpType> => {
return compilationResult.mapping;
})
// If something bad happened, log the error and pretend like nothing happened
// by notifying deps of completion and returning an empty MetaStyleMapping
// so compilation can continue.
.catch((error: Error) => {
this.trace(`notifying of compilation failure`);
compilation.errors.push(error);
this.notifyComplete(
{
error,
compilation,
assetPath: this.outputCssFile,
},
cb);
this.trace(`notified of compilation failure`);
});
this.trace(`notifying of pending compilation`);
this.notifyPendingCompilation(pending);
this.trace(`notified of pending compilation`);
}
apply(compiler: WebpackCompiler) {
this.projectDir = compiler.options.context || this.projectDir;
let outputPath = compiler.options.output && compiler.options.output.path || this.projectDir; // TODO What is the webpack default output directory?
let assets: Assets = {};
compiler.plugin("this-compilation", (compilation) => {
this.notifyCompilationExpiration();
compilation.plugin("additional-assets", (cb: () => void) => {
Object.assign(compilation.assets, assets);
cb();
});
});
compiler.plugin("make", this.handleMake.bind(this, outputPath, assets));
// Once we're done, add all discovered block files to the build dependencies
// so this plugin is re-evaluated when they change.
// TODO: We get timestamp data here. We can probably intelligently re-build.
compiler.plugin("emit", (compilation, callback) => {
let discoveredFiles = [...this.analyzer.transitiveBlockDependencies()].map((b) => b.identifier);
compilation.fileDependencies.push(...discoveredFiles);
callback();
});
this.onCompilationExpiration(() => {
this.trace(`resetting pending compilation.`);
this.pendingResult = undefined;
});
this.onPendingCompilation((pendingResult) => {
this.trace(`received pending compilation.`);
this.pendingResult = pendingResult;
});
compiler.plugin("compilation", (compilation: WebpackAny) => {
compilation.plugin("normal-module-loader", (context: LoaderContext, mod: WebpackAny) => {
this.trace(`preparing normal-module-loader for ${mod.resource}`);
context.cssBlocks = context.cssBlocks || { mappings: {}, compilationOptions: this.compilationOptions };
// If we're already waiting for a css file of this name to finish compiling, throw.
if (context.cssBlocks.mappings[this.outputCssFile]) {
throw new Error(`css conflict detected. Multiple compiles writing to ${this.outputCssFile}?`);
}
if (this.pendingResult === undefined) {
throw new Error(`No pending result is available yet.`);
}
context.cssBlocks.mappings[this.outputCssFile] = this.pendingResult;
});
});
}
private compileBlocks(analyzer: Analyzer, cssOutputName: string): Promise<CompilationResult> {
let options = resolveConfiguration(this.compilationOptions);
let blockCompiler = new BlockCompiler(postcss, options);
let numBlocks = 0;
let optimizer = new Optimizer(this.optimizationOptions, analyzer.optimizationOptions);
let blocks = analyzer.transitiveBlockDependencies();
for (let block of blocks) {
if (block.stylesheet && block.identifier) {
blocks.add(block);
this.trace(`compiling ${block.identifier}.`);
let root = blockCompiler.compile(block, block.stylesheet, analyzer);
let result = root.toResult({to: cssOutputName, map: { inline: false, annotation: false }});
// TODO: handle a sourcemap from compiling the block file via a preprocessor.
let filename = options.importer.filesystemPath(block.identifier, options) || options.importer.debugIdentifier(block.identifier, options);
optimizer.addSource({
content: result.css,
filename,
sourceMap: result.map.toJSON(),
});
numBlocks++;
}
}
let analyses = analyzer.analyses();
for (let a of analyses) {
this.trace(`Adding analysis for ${a.template.identifier} to optimizer.`);
this.trace(`Analysis for ${a.template.identifier} has ${a.elementCount()} elements.`);
optimizer.addAnalysis(a.forOptimizer(options));
}
this.trace(`compiled ${numBlocks} blocks.`);
this.debug("optimization starting.");
return optimizer.optimize(cssOutputName).then(optimizationResult => {
this.debug("optimization complete.");
return {
optimizationResult,
blocks,
analyses,
};
});
}
trace(message: string) {
message = message.replace(this.projectDir + "/", "");
this.debug(`[${this.name}] ${message}`);
}
/**
* Fires when the compilation promise is available.
*/
onPendingCompilation(handler: (pendingResult: PendingResult) => void): void {
this.plugin("block-compilation-pending", handler);
}
private notifyPendingCompilation(pendingResult: PendingResult): void {
this.applyPlugins("block-compilation-pending", pendingResult);
}
/**
* Fires when the compilation is first started to let any listeners know that
* their current promise is no longer valid.
*/
onCompilationExpiration(handler: () => void): void {
this.plugin("block-compilation-expired", handler);
}
private notifyCompilationExpiration(): void {
this.applyPlugins("block-compilation-expired");
}
/**
* Fires when the compilation is done.
*/
onComplete(handler: (result: BlockCompilationComplete | BlockCompilationError, cb: (err: Error) => void) => void): void {
this.plugin("block-compilation-complete", handler);
}
private notifyComplete(result: BlockCompilationComplete | BlockCompilationError, cb: (err: Error) => void): void {
this.applyPluginsAsync("block-compilation-complete", result, cb);
}
}
function flatten(arr: whatever[]): whatever[] {
return arr.reduce((acc, val) => (acc as whatever[]).concat(Array.isArray(val) ? flatten(val) : val), []) as whatever[];
}