UNPKG

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