/
compiler.js
383 lines (359 loc) · 13.3 KB
/
compiler.js
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
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
// @ts-check
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
/** @typedef {import("webpack/lib/Chunk.js")} WebpackChunk */
'use strict';
/**
* @file
* This file uses webpack to compile a template with a child compiler.
*
* [TEMPLATE] -> [JAVASCRIPT]
*
*/
'use strict';
const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
/**
* The HtmlWebpackChildCompiler is a helper to allow resusing one childCompiler
* for multile HtmlWebpackPlugin instances to improve the compilation performance.
*/
class HtmlWebpackChildCompiler {
constructor () {
/**
* @type {string[]} templateIds
* The template array will allow us to keep track which input generated which output
*/
this.templates = [];
/**
* @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
*/
this.compilationPromise;
/**
* @type {number}
*/
this.compilationStartedTimestamp;
/**
* @type {number}
*/
this.compilationEndedTimestamp;
/**
* All file dependencies of the child compiler
* @type {string[]}
*/
this.fileDependencies = [];
}
/**
* Add a templatePath to the child compiler
* The given template will be compiled by `compileTemplates`
* @param {string} template - The webpack path to the template e.g. `'!!html-loader!index.html'`
* @returns {boolean} true if the template is new
*/
addTemplate (template) {
const templateId = this.templates.indexOf(template);
// Don't add the template to the compiler if a similar template was already added
if (templateId !== -1) {
return false;
}
// A child compiler can compile only once
// throw an error if a new template is added after the compilation started
if (this.isCompiling()) {
throw new Error('New templates can only be added before `compileTemplates` was called.');
}
// Add the template to the childCompiler
this.templates.push(template);
// Mark the cache invalid
return true;
}
/**
* Returns true if the childCompiler is currently compiling
* @retuns {boolean}
*/
isCompiling () {
return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
}
/**
* Returns true if the childCOmpiler is done compiling
*/
didCompile () {
return this.compilationEndedTimestamp !== undefined;
}
/**
* This function will start the template compilation
* once it is started no more templates can be added
*
* @param {WebpackCompilation} mainCompilation
* @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
*/
compileTemplates (mainCompilation) {
// To prevent multiple compilations for the same template
// the compilation is cached in a promise.
// If it already exists return
if (this.compilationPromise) {
return this.compilationPromise;
}
// The entry file is just an empty helper as the dynamic template
// require is added in "loader.js"
const outputOptions = {
filename: '__child-[name]',
publicPath: mainCompilation.outputOptions.publicPath
};
const compilerName = 'HtmlWebpackCompiler';
// Create an additional child compiler which takes the template
// and turns it into an Node.JS html factory.
// This allows us to use loaders during the compilation
const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
// The file path context which webpack uses to resolve all relative files to
childCompiler.context = mainCompilation.compiler.context;
// Compile the template to nodejs javascript
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
new NodeTargetPlugin().apply(childCompiler);
new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
new LoaderTargetPlugin('node').apply(childCompiler);
// Fix for "Uncaught TypeError: __webpack_require__(...) is not a function"
// Hot module replacement requires that every child compiler has its own
// cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179
childCompiler.hooks.compilation.tap('HtmlWebpackPlugin', compilation => {
if (compilation.cache) {
if (!compilation.cache[compilerName]) {
compilation.cache[compilerName] = {};
}
compilation.cache = compilation.cache[compilerName];
}
});
// Add all templates
this.templates.forEach((template, index) => {
new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
});
this.compilationStartedTimestamp = new Date().getTime();
this.compilationPromise = new Promise((resolve, reject) => {
childCompiler.runAsChild((err, entries, childCompilation) => {
// Extract templates
const compiledTemplates = entries
? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
: [];
// Extract file dependencies
if (entries) {
this.fileDependencies = extractFileDependenciesFilesFromCompilation(entries);
}
// Reject the promise if the childCompilation contains error
if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n');
reject(new Error('Child compilation failed:\n' + errorDetails));
return;
}
// Reject if the error object contains errors
if (err) {
reject(err);
return;
}
/**
* @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
*/
const result = {};
compiledTemplates.forEach((templateSource, entryIndex) => {
// The compiledTemplates are generated from the entries added in
// the addTemplate function.
// Therefore the array index of this.templates should be the as entryIndex.
result[this.templates[entryIndex]] = {
content: templateSource,
hash: childCompilation.hash,
entry: entries[entryIndex]
};
});
this.compilationEndedTimestamp = new Date().getTime();
resolve(result);
});
});
return this.compilationPromise;
}
}
/**
* The webpack child compilation will create files as a side effect.
* This function will extract them and clean them up so they won't be written to disk.
*
* Returns the source code of the compiled templates as string
*
* @returns Array<string>
*/
function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
return mainCompilation.mainTemplate.hooks.assetPath.call(filename, {
hash: childCompilation.hash,
chunk: entryChunk,
name: `HtmlWebpackPlugin_${index}`
});
});
helperAssetNames.forEach((helperFileName) => {
delete mainCompilation.assets[helperFileName];
});
const helperContents = helperAssetNames.map((helperFileName) => {
return childCompilation.assets[helperFileName].source();
});
return helperContents;
}
/**
* Return all file dependencies from the given set of entries.
* @param {WebpackChunk[]} entries
* @returns {string[]}
*/
function extractFileDependenciesFilesFromCompilation (entries) {
const fileDependencies = new Map();
entries.forEach((entry) => {
entry.entryModule.buildInfo.fileDependencies.forEach((fileDependency) => {
fileDependencies.set(fileDependency, true);
});
});
return Array.from(fileDependencies.keys());
}
/**
* @type {WeakMap<WebpackCompiler, HtmlWebpackChildCompiler>}}
*/
const childCompilerCache = new WeakMap();
/**
* Get child compiler from cache or a new child compiler for the given mainCompilation
*
* @param {WebpackCompiler} mainCompiler
*/
function getChildCompiler (mainCompiler) {
const cachedChildCompiler = childCompilerCache.get(mainCompiler);
if (cachedChildCompiler) {
return cachedChildCompiler;
}
const newCompiler = new HtmlWebpackChildCompiler();
childCompilerCache.set(mainCompiler, newCompiler);
return newCompiler;
}
/**
* Remove the childCompiler from the cache
*
* @param {WebpackCompiler} mainCompiler
*/
function clearCache (mainCompiler) {
const childCompiler = getChildCompiler(mainCompiler);
// If this childCompiler was already used
// remove the entire childCompiler from the cache
if (childCompiler.isCompiling() || childCompiler.didCompile()) {
childCompilerCache.delete(mainCompiler);
}
}
/**
* Register a template for the current main compiler
* @param {WebpackCompiler} mainCompiler
* @param {string} templatePath
*/
function addTemplateToCompiler (mainCompiler, templatePath) {
const childCompiler = getChildCompiler(mainCompiler);
const isNew = childCompiler.addTemplate(templatePath);
if (isNew) {
clearCache(mainCompiler);
}
}
/**
* Starts the compilation for all templates.
* This has to be called once all templates where added.
*
* If this function is called multiple times it will use a cache inside
* the childCompiler
*
* @param {string} templatePath
* @param {string} outputFilename
* @param {WebpackCompilation} mainCompilation
*/
function compileTemplate (templatePath, outputFilename, mainCompilation) {
const childCompiler = getChildCompiler(mainCompilation.compiler);
return childCompiler.compileTemplates(mainCompilation).then((compiledTemplates) => {
if (!compiledTemplates[templatePath]) console.log(Object.keys(compiledTemplates), templatePath);
const compiledTemplate = compiledTemplates[templatePath];
// Replace [hash] placeholders in filename
const outputName = mainCompilation.mainTemplate.hooks.assetPath.call(outputFilename, {
hash: compiledTemplate.hash,
chunk: compiledTemplate.entry
});
return {
// Hash of the template entry point
hash: compiledTemplate.hash,
// Output name
outputName: outputName,
// Compiled code
content: compiledTemplate.content
};
});
}
/**
* Return all file dependencies of the last child compilation
*
* @param {WebpackCompiler} compiler
* @returns {Array<string>}
*/
function getFileDependencies (compiler) {
const childCompiler = getChildCompiler(compiler);
return childCompiler.fileDependencies;
}
/**
* @type {WeakMap<WebpackCompilation, WeakMap<HtmlWebpackChildCompiler, boolean>>}}
*/
const hasOutdatedCompilationDependenciesMap = new WeakMap();
/**
* Returns `true` if the file dependencies of the current childCompiler
* for the given mainCompilation are outdated.
*
* Uses the `hasOutdatedCompilationDependenciesMap` cache if possible.
*
* @param {WebpackCompilation} mainCompilation
* @returns {boolean}
*/
function hasOutDatedTemplateCache (mainCompilation) {
const childCompiler = getChildCompiler(mainCompilation.compiler);
/**
* @type {WeakMap<HtmlWebpackChildCompiler, boolean>|undefined}
*/
let hasOutdatedChildCompilerDependenciesMap = hasOutdatedCompilationDependenciesMap.get(mainCompilation);
// Create map for childCompiler if none exist
if (!hasOutdatedChildCompilerDependenciesMap) {
hasOutdatedChildCompilerDependenciesMap = new WeakMap();
hasOutdatedCompilationDependenciesMap.set(mainCompilation, hasOutdatedChildCompilerDependenciesMap);
}
// Try to get the `checkChildCompilerCache` result from cache
let isOutdated = hasOutdatedChildCompilerDependenciesMap.get(childCompiler);
if (isOutdated !== undefined) {
return isOutdated;
}
// If `checkChildCompilerCache` has never been called for the given
// `mainCompilation` and `childCompiler` combination call it:
isOutdated = isChildCompilerCacheOutdated(mainCompilation, childCompiler);
hasOutdatedChildCompilerDependenciesMap.set(childCompiler, isOutdated);
return isOutdated;
}
/**
* Returns `true` if the file dependencies of the given childCompiler are outdated.
*
* @param {WebpackCompilation} mainCompilation
* @param {HtmlWebpackChildCompiler} childCompiler
* @returns {boolean}
*/
function isChildCompilerCacheOutdated (mainCompilation, childCompiler) {
// If the compilation was never run there is no invalid cache
if (!childCompiler.compilationStartedTimestamp) {
return false;
}
// Check if any dependent file was changed after the last compilation
const fileTimestamps = mainCompilation.fileTimestamps;
const isCacheOutOfDate = childCompiler.fileDependencies.some((fileDependency) => {
const timestamp = fileTimestamps.get(fileDependency);
// If the timestamp is not known the file is new
// If the timestamp is larger then the file has changed
// Otherwise the file is still the same
return !timestamp || timestamp > childCompiler.compilationStartedTimestamp;
});
return isCacheOutOfDate;
}
module.exports = {
addTemplateToCompiler,
compileTemplate,
hasOutDatedTemplateCache,
clearCache,
getFileDependencies
};