1 | // @ts-check
|
---|
2 | "use strict";
|
---|
3 |
|
---|
4 | /**
|
---|
5 | * @file
|
---|
6 | * This file uses webpack to compile a template with a child compiler.
|
---|
7 | *
|
---|
8 | * [TEMPLATE] -> [JAVASCRIPT]
|
---|
9 | *
|
---|
10 | */
|
---|
11 |
|
---|
12 | /** @typedef {import("webpack").Chunk} Chunk */
|
---|
13 | /** @typedef {import("webpack").sources.Source} Source */
|
---|
14 | /** @typedef {{hash: string, entry: Chunk, content: string, assets: {[name: string]: { source: Source, info: import("webpack").AssetInfo }}}} ChildCompilationTemplateResult */
|
---|
15 |
|
---|
16 | /**
|
---|
17 | * The HtmlWebpackChildCompiler is a helper to allow reusing one childCompiler
|
---|
18 | * for multiple HtmlWebpackPlugin instances to improve the compilation performance.
|
---|
19 | */
|
---|
20 | class HtmlWebpackChildCompiler {
|
---|
21 | /**
|
---|
22 | *
|
---|
23 | * @param {string[]} templates
|
---|
24 | */
|
---|
25 | constructor(templates) {
|
---|
26 | /**
|
---|
27 | * @type {string[]} templateIds
|
---|
28 | * The template array will allow us to keep track which input generated which output
|
---|
29 | */
|
---|
30 | this.templates = templates;
|
---|
31 | /** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */
|
---|
32 | this.compilationPromise; // eslint-disable-line
|
---|
33 | /** @type {number | undefined} */
|
---|
34 | this.compilationStartedTimestamp; // eslint-disable-line
|
---|
35 | /** @type {number | undefined} */
|
---|
36 | this.compilationEndedTimestamp; // eslint-disable-line
|
---|
37 | /**
|
---|
38 | * All file dependencies of the child compiler
|
---|
39 | * @type {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}}
|
---|
40 | */
|
---|
41 | this.fileDependencies = {
|
---|
42 | fileDependencies: [],
|
---|
43 | contextDependencies: [],
|
---|
44 | missingDependencies: [],
|
---|
45 | };
|
---|
46 | }
|
---|
47 |
|
---|
48 | /**
|
---|
49 | * Returns true if the childCompiler is currently compiling
|
---|
50 | *
|
---|
51 | * @returns {boolean}
|
---|
52 | */
|
---|
53 | isCompiling() {
|
---|
54 | return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
|
---|
55 | }
|
---|
56 |
|
---|
57 | /**
|
---|
58 | * Returns true if the childCompiler is done compiling
|
---|
59 | *
|
---|
60 | * @returns {boolean}
|
---|
61 | */
|
---|
62 | didCompile() {
|
---|
63 | return this.compilationEndedTimestamp !== undefined;
|
---|
64 | }
|
---|
65 |
|
---|
66 | /**
|
---|
67 | * This function will start the template compilation
|
---|
68 | * once it is started no more templates can be added
|
---|
69 | *
|
---|
70 | * @param {import('webpack').Compilation} mainCompilation
|
---|
71 | * @returns {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>}
|
---|
72 | */
|
---|
73 | compileTemplates(mainCompilation) {
|
---|
74 | const webpack = mainCompilation.compiler.webpack;
|
---|
75 | const Compilation = webpack.Compilation;
|
---|
76 |
|
---|
77 | const NodeTemplatePlugin = webpack.node.NodeTemplatePlugin;
|
---|
78 | const NodeTargetPlugin = webpack.node.NodeTargetPlugin;
|
---|
79 | const LoaderTargetPlugin = webpack.LoaderTargetPlugin;
|
---|
80 | const EntryPlugin = webpack.EntryPlugin;
|
---|
81 |
|
---|
82 | // To prevent multiple compilations for the same template
|
---|
83 | // the compilation is cached in a promise.
|
---|
84 | // If it already exists return
|
---|
85 | if (this.compilationPromise) {
|
---|
86 | return this.compilationPromise;
|
---|
87 | }
|
---|
88 |
|
---|
89 | const outputOptions = {
|
---|
90 | filename: "__child-[name]",
|
---|
91 | publicPath: "",
|
---|
92 | library: {
|
---|
93 | type: "var",
|
---|
94 | name: "HTML_WEBPACK_PLUGIN_RESULT",
|
---|
95 | },
|
---|
96 | scriptType: /** @type {'text/javascript'} */ ("text/javascript"),
|
---|
97 | iife: true,
|
---|
98 | };
|
---|
99 | const compilerName = "HtmlWebpackCompiler";
|
---|
100 | // Create an additional child compiler which takes the template
|
---|
101 | // and turns it into an Node.JS html factory.
|
---|
102 | // This allows us to use loaders during the compilation
|
---|
103 | const childCompiler = mainCompilation.createChildCompiler(
|
---|
104 | compilerName,
|
---|
105 | outputOptions,
|
---|
106 | [
|
---|
107 | // Compile the template to nodejs javascript
|
---|
108 | new NodeTargetPlugin(),
|
---|
109 | new NodeTemplatePlugin(),
|
---|
110 | new LoaderTargetPlugin("node"),
|
---|
111 | new webpack.library.EnableLibraryPlugin("var"),
|
---|
112 | ],
|
---|
113 | );
|
---|
114 | // The file path context which webpack uses to resolve all relative files to
|
---|
115 | childCompiler.context = mainCompilation.compiler.context;
|
---|
116 |
|
---|
117 | // Generate output file names
|
---|
118 | const temporaryTemplateNames = this.templates.map(
|
---|
119 | (template, index) => `__child-HtmlWebpackPlugin_${index}-${template}`,
|
---|
120 | );
|
---|
121 |
|
---|
122 | // Add all templates
|
---|
123 | this.templates.forEach((template, index) => {
|
---|
124 | new EntryPlugin(
|
---|
125 | childCompiler.context,
|
---|
126 | "data:text/javascript,__webpack_public_path__ = __webpack_base_uri__ = htmlWebpackPluginPublicPath;",
|
---|
127 | `HtmlWebpackPlugin_${index}-${template}`,
|
---|
128 | ).apply(childCompiler);
|
---|
129 | new EntryPlugin(
|
---|
130 | childCompiler.context,
|
---|
131 | template,
|
---|
132 | `HtmlWebpackPlugin_${index}-${template}`,
|
---|
133 | ).apply(childCompiler);
|
---|
134 | });
|
---|
135 |
|
---|
136 | // The templates are compiled and executed by NodeJS - similar to server side rendering
|
---|
137 | // Unfortunately this causes issues as some loaders require an absolute URL to support ES Modules
|
---|
138 | // The following config enables relative URL support for the child compiler
|
---|
139 | childCompiler.options.module = { ...childCompiler.options.module };
|
---|
140 | childCompiler.options.module.parser = {
|
---|
141 | ...childCompiler.options.module.parser,
|
---|
142 | };
|
---|
143 | childCompiler.options.module.parser.javascript = {
|
---|
144 | ...childCompiler.options.module.parser.javascript,
|
---|
145 | url: "relative",
|
---|
146 | };
|
---|
147 |
|
---|
148 | this.compilationStartedTimestamp = new Date().getTime();
|
---|
149 | /** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */
|
---|
150 | this.compilationPromise = new Promise((resolve, reject) => {
|
---|
151 | /** @type {Source[]} */
|
---|
152 | const extractedAssets = [];
|
---|
153 |
|
---|
154 | childCompiler.hooks.thisCompilation.tap(
|
---|
155 | "HtmlWebpackPlugin",
|
---|
156 | (compilation) => {
|
---|
157 | compilation.hooks.processAssets.tap(
|
---|
158 | {
|
---|
159 | name: "HtmlWebpackPlugin",
|
---|
160 | stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
|
---|
161 | },
|
---|
162 | (assets) => {
|
---|
163 | temporaryTemplateNames.forEach((temporaryTemplateName) => {
|
---|
164 | if (assets[temporaryTemplateName]) {
|
---|
165 | extractedAssets.push(assets[temporaryTemplateName]);
|
---|
166 |
|
---|
167 | compilation.deleteAsset(temporaryTemplateName);
|
---|
168 | }
|
---|
169 | });
|
---|
170 | },
|
---|
171 | );
|
---|
172 | },
|
---|
173 | );
|
---|
174 |
|
---|
175 | childCompiler.runAsChild((err, entries, childCompilation) => {
|
---|
176 | // Extract templates
|
---|
177 | // TODO fine a better way to store entries and results, to avoid duplicate chunks and assets
|
---|
178 | const compiledTemplates = entries
|
---|
179 | ? extractedAssets.map((asset) => asset.source())
|
---|
180 | : [];
|
---|
181 |
|
---|
182 | // Extract file dependencies
|
---|
183 | if (entries && childCompilation) {
|
---|
184 | this.fileDependencies = {
|
---|
185 | fileDependencies: Array.from(childCompilation.fileDependencies),
|
---|
186 | contextDependencies: Array.from(
|
---|
187 | childCompilation.contextDependencies,
|
---|
188 | ),
|
---|
189 | missingDependencies: Array.from(
|
---|
190 | childCompilation.missingDependencies,
|
---|
191 | ),
|
---|
192 | };
|
---|
193 | }
|
---|
194 |
|
---|
195 | // Reject the promise if the childCompilation contains error
|
---|
196 | if (
|
---|
197 | childCompilation &&
|
---|
198 | childCompilation.errors &&
|
---|
199 | childCompilation.errors.length
|
---|
200 | ) {
|
---|
201 | const errorDetailsArray = [];
|
---|
202 | for (const error of childCompilation.errors) {
|
---|
203 | let message = error.message;
|
---|
204 | if (error.stack) {
|
---|
205 | message += "\n" + error.stack;
|
---|
206 | }
|
---|
207 | errorDetailsArray.push(message);
|
---|
208 | }
|
---|
209 | const errorDetails = errorDetailsArray.join("\n");
|
---|
210 |
|
---|
211 | reject(new Error("Child compilation failed:\n" + errorDetails));
|
---|
212 |
|
---|
213 | return;
|
---|
214 | }
|
---|
215 |
|
---|
216 | // Reject if the error object contains errors
|
---|
217 | if (err) {
|
---|
218 | reject(err);
|
---|
219 | return;
|
---|
220 | }
|
---|
221 |
|
---|
222 | if (!childCompilation || !entries) {
|
---|
223 | reject(new Error("Empty child compilation"));
|
---|
224 | return;
|
---|
225 | }
|
---|
226 |
|
---|
227 | /**
|
---|
228 | * @type {{[templatePath: string]: ChildCompilationTemplateResult}}
|
---|
229 | */
|
---|
230 | const result = {};
|
---|
231 |
|
---|
232 | /** @type {{[name: string]: { source: Source, info: import("webpack").AssetInfo }}} */
|
---|
233 | const assets = {};
|
---|
234 |
|
---|
235 | for (const asset of childCompilation.getAssets()) {
|
---|
236 | assets[asset.name] = { source: asset.source, info: asset.info };
|
---|
237 | }
|
---|
238 |
|
---|
239 | compiledTemplates.forEach((templateSource, entryIndex) => {
|
---|
240 | // The compiledTemplates are generated from the entries added in
|
---|
241 | // the addTemplate function.
|
---|
242 | // Therefore, the array index of this.templates should be the as entryIndex.
|
---|
243 | result[this.templates[entryIndex]] = {
|
---|
244 | // TODO, can we have Buffer here?
|
---|
245 | content: /** @type {string} */ (templateSource),
|
---|
246 | hash: childCompilation.hash || "XXXX",
|
---|
247 | entry: entries[entryIndex],
|
---|
248 | assets,
|
---|
249 | };
|
---|
250 | });
|
---|
251 |
|
---|
252 | this.compilationEndedTimestamp = new Date().getTime();
|
---|
253 |
|
---|
254 | resolve(result);
|
---|
255 | });
|
---|
256 | });
|
---|
257 |
|
---|
258 | return this.compilationPromise;
|
---|
259 | }
|
---|
260 | }
|
---|
261 |
|
---|
262 | module.exports = {
|
---|
263 | HtmlWebpackChildCompiler,
|
---|
264 | };
|
---|