|
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 |
2 | 8 | * This file uses webpack to compile a template with a child compiler.
|
3 | 9 | *
|
4 | 10 | * [TEMPLATE] -> [JAVASCRIPT]
|
5 | 11 | *
|
6 | 12 | */
|
7 | 13 | 'use strict';
|
8 |
| -const path = require('path'); |
9 | 14 | const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
|
10 | 15 | const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
|
11 | 16 | const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
|
12 | 17 | const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
|
13 | 18 | const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
|
14 | 19 |
|
15 | 20 | /**
|
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. |
30 | 23 | */
|
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; |
68 | 53 | }
|
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 | + } |
70 | 64 |
|
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); |
86 | 98 |
|
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] = {}; |
93 | 106 | }
|
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]; |
102 | 108 | }
|
103 | 109 | });
|
| 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 | + }); |
104 | 171 | });
|
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 | +} |
106 | 200 |
|
107 | 201 | /**
|
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 |
109 | 214 | */
|
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); |
114 | 217 | }
|
| 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