Skip to content

Commit 66ae631

Browse files
committed
feat(compiler): Use a single compiler for multiple plugin instances
1 parent c6f0f5a commit 66ae631

File tree

2 files changed

+240
-90
lines changed

2 files changed

+240
-90
lines changed

index.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,19 @@ class HtmlWebpackPlugin {
8484
this.options.filename = path.relative(compiler.options.output.path, filename);
8585
}
8686

87+
// Clear the cache once a new HtmlWebpackPlugin is added
88+
childCompiler.clearCache(compiler);
89+
90+
compiler.hooks.compile.tap('HtmlWebpackPlugin', () => {
91+
childCompiler.addTemplateToCompiler(compiler, this.options.template);
92+
});
93+
8794
// setup hooks for third party plugins
8895
compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', getHtmlWebpackPluginHooks);
8996

9097
compiler.hooks.make.tapAsync('HtmlWebpackPlugin', (compilation, callback) => {
9198
// Compile the template (queued)
92-
compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
99+
compilationPromise = childCompiler.compileTemplate(self.options.template, self.options.filename, compilation)
93100
.catch(err => {
94101
compilation.errors.push(prettyError(err, compiler.context).toString());
95102
return {
@@ -114,6 +121,9 @@ class HtmlWebpackPlugin {
114121
* @param {() => void} callback
115122
*/
116123
(compilation, callback) => {
124+
// Clear the childCompilerCache
125+
childCompiler.clearCache(compiler);
126+
117127
// Get all entry point names for this html file
118128
const entryNames = Array.from(compilation.entrypoints.keys());
119129
const filteredEntryNames = self.filterChunks(entryNames, self.options.chunks, self.options.excludeChunks);

lib/compiler.js

+229-89
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,254 @@
1-
/*
1+
// @ts-check
2+
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
3+
/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
4+
/** @typedef {import("webpack/lib/Chunk.js")} WebpackChunk */
5+
'use strict';
6+
/**
7+
* @file
28
* This file uses webpack to compile a template with a child compiler.
39
*
410
* [TEMPLATE] -> [JAVASCRIPT]
511
*
612
*/
713
'use strict';
8-
const path = require('path');
914
const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
1015
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
1116
const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
1217
const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
1318
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
1419

1520
/**
16-
* Compiles the template into a nodejs factory, adds its to the compilation.assets
17-
* and returns a promise of the result asset object.
18-
*
19-
* @param template relative path to the template file
20-
* @param context path context
21-
* @param outputFilename the file name
22-
* @param compilation The webpack compilation object
23-
*
24-
* Returns an object:
25-
* {
26-
* hash: {String} - Base64 hash of the file
27-
* content: {String} - Javascript executable code of the template
28-
* }
29-
*
21+
* The HtmlWebpackChildCompiler is a helper to allow resusing one childCompiler
22+
* for multile HtmlWebpackPlugin instances to improve the compilation performance.
3023
*/
31-
module.exports.compileTemplate = function compileTemplate (template, context, outputFilename, compilation) {
32-
// The entry file is just an empty helper as the dynamic template
33-
// require is added in "loader.js"
34-
const outputOptions = {
35-
filename: outputFilename,
36-
publicPath: compilation.outputOptions.publicPath
37-
};
38-
// Store the result of the parent compilation before we start the child compilation
39-
const assetsBeforeCompilation = Object.assign({}, compilation.assets[outputOptions.filename]);
40-
// Create an additional child compiler which takes the template
41-
// and turns it into an Node.JS html factory.
42-
// This allows us to use loaders during the compilation
43-
const compilerName = getCompilerName(context, outputFilename);
44-
const childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
45-
childCompiler.context = context;
46-
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
47-
new NodeTargetPlugin().apply(childCompiler);
48-
new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
49-
50-
// Using undefined as name for the SingleEntryPlugin causes a unexpected output as described in
51-
// https://github.com/jantimon/html-webpack-plugin/issues/895
52-
// Using a string as a name for the SingleEntryPlugin causes problems with HMR as described in
53-
// https://github.com/jantimon/html-webpack-plugin/issues/900
54-
// Until the HMR issue is fixed we keep the ugly output:
55-
new SingleEntryPlugin(this.context, template, undefined).apply(childCompiler);
56-
57-
new LoaderTargetPlugin('node').apply(childCompiler);
58-
59-
// Fix for "Uncaught TypeError: __webpack_require__(...) is not a function"
60-
// Hot module replacement requires that every child compiler has its own
61-
// cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179
62-
childCompiler.hooks.compilation.tap('HtmlWebpackPlugin', compilation => {
63-
if (compilation.cache) {
64-
if (!compilation.cache[compilerName]) {
65-
compilation.cache[compilerName] = {};
66-
}
67-
compilation.cache = compilation.cache[compilerName];
24+
class HtmlWebpackChildCompiler {
25+
26+
constructor () {
27+
/**
28+
* @type {string[]} templateIds
29+
* The template array will allow us to keep track which input generated which output
30+
*/
31+
this.templates = [];
32+
/**
33+
* @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
34+
*/
35+
this.compilationPromise;
36+
37+
/**
38+
* @type {Date}
39+
*/
40+
this.compilationStarted;
41+
}
42+
43+
/**
44+
* Add a templatePath to the child compiler
45+
* The given template will be compiled by `compileTemplates`
46+
* @param {string} template - The webpack path to the template e.g. `'!!html-loader!index.html'`
47+
*/
48+
addTemplate (template) {
49+
const templateId = this.templates.indexOf(template);
50+
// Don't add the template to the compiler if a similar template was already added
51+
if (templateId !== -1) {
52+
return templateId;
6853
}
69-
});
54+
// A child compiler can compile only once
55+
// throw an error if a new template is added after the compilation started
56+
if (this.compilationPromise) {
57+
throw new Error('New templates can only be added before `compileTemplates` was called.');
58+
}
59+
// Add the template to the childCompiler
60+
const newTemplateId = this.templates.length;
61+
this.templates.push(template);
62+
return newTemplateId;
63+
}
7064

71-
// Compile and return a promise
72-
return new Promise((resolve, reject) => {
73-
childCompiler.runAsChild((err, entries, childCompilation) => {
74-
// Resolve / reject the promise
75-
if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
76-
const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n');
77-
reject(new Error('Child compilation failed:\n' + errorDetails));
78-
} else if (err) {
79-
reject(err);
80-
} else {
81-
// Replace [hash] placeholders in filename
82-
const outputName = compilation.mainTemplate.hooks.assetPath.call(outputOptions.filename, {
83-
hash: childCompilation.hash,
84-
chunk: entries[0]
85-
});
65+
/**
66+
* This function will start the template compilation
67+
* once it is started no more templates can be added
68+
*
69+
* @param {WebpackCompilation} mainCompilation
70+
* @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
71+
*/
72+
compileTemplates (mainCompilation) {
73+
// To prevent multiple compilations for the same template
74+
// the compilation is cached in a promise.
75+
// If it already exists return
76+
if (this.compilationPromise) {
77+
return this.compilationPromise;
78+
}
79+
80+
// The entry file is just an empty helper as the dynamic template
81+
// require is added in "loader.js"
82+
const outputOptions = {
83+
filename: '__child-[name]',
84+
publicPath: mainCompilation.outputOptions.publicPath
85+
};
86+
const compilerName = 'HtmlWebpackCompiler';
87+
// Create an additional child compiler which takes the template
88+
// and turns it into an Node.JS html factory.
89+
// This allows us to use loaders during the compilation
90+
const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
91+
// The file path context which webpack uses to resolve all relative files to
92+
childCompiler.context = mainCompilation.compiler.context;
93+
// Compile the template to nodejs javascript
94+
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
95+
new NodeTargetPlugin().apply(childCompiler);
96+
new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
97+
new LoaderTargetPlugin('node').apply(childCompiler);
8698

87-
// Restore the parent compilation to the state like it
88-
// was before the child compilation
89-
compilation.assets[outputName] = assetsBeforeCompilation[outputName];
90-
if (assetsBeforeCompilation[outputName] === undefined) {
91-
// If it wasn't there - delete it
92-
delete compilation.assets[outputName];
99+
// Fix for "Uncaught TypeError: __webpack_require__(...) is not a function"
100+
// Hot module replacement requires that every child compiler has its own
101+
// cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179
102+
childCompiler.hooks.compilation.tap('HtmlWebpackPlugin', compilation => {
103+
if (compilation.cache) {
104+
if (!compilation.cache[compilerName]) {
105+
compilation.cache[compilerName] = {};
93106
}
94-
resolve({
95-
// Hash of the template entry point
96-
hash: entries[0].hash,
97-
// Output name
98-
outputName: outputName,
99-
// Compiled code
100-
content: childCompilation.assets[outputName].source()
101-
});
107+
compilation.cache = compilation.cache[compilerName];
102108
}
103109
});
110+
111+
// Add all templates
112+
this.templates.forEach((template, index) => {
113+
new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
114+
});
115+
116+
this.compilationStarted = new Date();
117+
this.compilationPromise = new Promise((resolve, reject) => {
118+
childCompiler.runAsChild((err, entries, childCompilation) => {
119+
// Extract templates
120+
const compiledTemplates = entries
121+
? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
122+
: [];
123+
// Reject the promise if the childCompilation contains error
124+
if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
125+
const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n');
126+
reject(new Error('Child compilation failed:\n' + errorDetails));
127+
return;
128+
}
129+
// Reject if the error object contains errors
130+
if (err) {
131+
reject(err);
132+
return;
133+
}
134+
/**
135+
* @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
136+
*/
137+
const result = {};
138+
compiledTemplates.forEach((templateSource, entryIndex) => {
139+
// The compiledTemplates are generated from the entries added in
140+
// the addTemplate function.
141+
// Therefore the array index of this.templates should be the as entryIndex.
142+
result[this.templates[entryIndex]] = {
143+
content: templateSource,
144+
hash: childCompilation.hash,
145+
entry: entries[entryIndex]
146+
};
147+
});
148+
resolve(result);
149+
});
150+
});
151+
152+
return this.compilationPromise;
153+
}
154+
}
155+
156+
/**
157+
* The webpack child compilation will create files as a side effect.
158+
* This function will extract them and clean them up so they won't be written to disk.
159+
*
160+
* Returns the source code of the compiled templates as string
161+
*
162+
* @returns Array<string>
163+
*/
164+
function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
165+
const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
166+
return mainCompilation.mainTemplate.hooks.assetPath.call(filename, {
167+
hash: childCompilation.hash,
168+
chunk: entryChunk,
169+
name: `HtmlWebpackPlugin_${index}`
170+
});
104171
});
105-
};
172+
173+
helperAssetNames.forEach((helperFileName) => {
174+
delete mainCompilation.assets[helperFileName];
175+
});
176+
177+
const helperContents = helperAssetNames.map((helperFileName) => {
178+
return childCompilation.assets[helperFileName].source();
179+
});
180+
181+
return helperContents;
182+
}
183+
184+
/**
185+
* @type {WeakMap<WebpackCompiler, HtmlWebpackChildCompiler>}}
186+
*/
187+
const childCompilerCache = new WeakMap();
188+
189+
/**
190+
* Get child compiler from cache or a new child compiler for the given mainCompilation
191+
*
192+
* @param {WebpackCompiler} mainCompiler
193+
*/
194+
function getChildCompiler (mainCompiler) {
195+
if (!childCompilerCache[mainCompiler]) {
196+
childCompilerCache[mainCompiler] = new HtmlWebpackChildCompiler();
197+
}
198+
return childCompilerCache[mainCompiler];
199+
}
106200

107201
/**
108-
* Returns the child compiler name e.g. 'html-webpack-plugin for "index.html"'
202+
* Remove the childCompiler from the cache
203+
*
204+
* @param {WebpackCompiler} mainCompiler
205+
*/
206+
function clearCache (mainCompiler) {
207+
delete (childCompilerCache[mainCompiler]);
208+
}
209+
210+
/**
211+
* Register a template for the current main compiler
212+
* @param {WebpackCompiler} mainCompiler
213+
* @param {string} templatePath
109214
*/
110-
function getCompilerName (context, filename) {
111-
const absolutePath = path.resolve(context, filename);
112-
const relativePath = path.relative(context, absolutePath);
113-
return 'html-webpack-plugin for "' + (absolutePath.length < relativePath.length ? absolutePath : relativePath) + '"';
215+
function addTemplateToCompiler (mainCompiler, templatePath) {
216+
getChildCompiler(mainCompiler).addTemplate(templatePath);
114217
}
218+
219+
/**
220+
* Starts the compilation for all templates.
221+
* This has to be called once all templates where added.
222+
*
223+
* If this function is called multiple times it will use a cache inside
224+
* the childCompiler
225+
*
226+
* @param {string} templatePath
227+
* @param {string} outputFilename
228+
* @param {WebpackCompilation} mainCompilation
229+
*/
230+
function compileTemplate (templatePath, outputFilename, mainCompilation) {
231+
const childCompiler = getChildCompiler(mainCompilation.compiler);
232+
return childCompiler.compileTemplates(mainCompilation).then((compiledTemplates) => {
233+
const compiledTemplate = compiledTemplates[templatePath];
234+
// Replace [hash] placeholders in filename
235+
const outputName = mainCompilation.mainTemplate.hooks.assetPath.call(outputFilename, {
236+
hash: compiledTemplate.hash,
237+
chunk: compiledTemplate.entry
238+
});
239+
return {
240+
// Hash of the template entry point
241+
hash: compiledTemplate.hash,
242+
// Output name
243+
outputName: outputName,
244+
// Compiled code
245+
content: compiledTemplate.content
246+
};
247+
});
248+
}
249+
250+
module.exports = {
251+
addTemplateToCompiler,
252+
compileTemplate,
253+
clearCache
254+
};

0 commit comments

Comments
 (0)