UNPKG

7.31 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';
14const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
15const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
16const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
17const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
18const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
19
20/**
21 * The HtmlWebpackChildCompiler is a helper to allow reusing one childCompiler
22 * for multiple HtmlWebpackPlugin instances to improve the compilation performance.
23 */
24class HtmlWebpackChildCompiler {
25 /**
26 *
27 * @param {string[]} templates
28 */
29 constructor (templates) {
30 /**
31 * @type {string[]} templateIds
32 * The template array will allow us to keep track which input generated which output
33 */
34 this.templates = templates;
35 /**
36 * @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
37 */
38 this.compilationPromise; // eslint-disable-line
39 /**
40 * @type {number}
41 */
42 this.compilationStartedTimestamp; // eslint-disable-line
43 /**
44 * @type {number}
45 */
46 this.compilationEndedTimestamp; // eslint-disable-line
47 /**
48 * All file dependencies of the child compiler
49 * @type {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}}
50 */
51 this.fileDependencies = { fileDependencies: [], contextDependencies: [], missingDependencies: [] };
52 }
53
54 /**
55 * Returns true if the childCompiler is currently compiling
56 * @returns {boolean}
57 */
58 isCompiling () {
59 return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
60 }
61
62 /**
63 * Returns true if the childCompiler is done compiling
64 */
65 didCompile () {
66 return this.compilationEndedTimestamp !== undefined;
67 }
68
69 /**
70 * This function will start the template compilation
71 * once it is started no more templates can be added
72 *
73 * @param {WebpackCompilation} mainCompilation
74 * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
75 */
76 compileTemplates (mainCompilation) {
77 // To prevent multiple compilations for the same template
78 // the compilation is cached in a promise.
79 // If it already exists return
80 if (this.compilationPromise) {
81 return this.compilationPromise;
82 }
83
84 // The entry file is just an empty helper as the dynamic template
85 // require is added in "loader.js"
86 const outputOptions = {
87 filename: '__child-[name]',
88 publicPath: mainCompilation.outputOptions.publicPath
89 };
90 const compilerName = 'HtmlWebpackCompiler';
91 // Create an additional child compiler which takes the template
92 // and turns it into an Node.JS html factory.
93 // This allows us to use loaders during the compilation
94 const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
95 // The file path context which webpack uses to resolve all relative files to
96 childCompiler.context = mainCompilation.compiler.context;
97 // Compile the template to nodejs javascript
98 new NodeTemplatePlugin(outputOptions).apply(childCompiler);
99 new NodeTargetPlugin().apply(childCompiler);
100 new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
101 new LoaderTargetPlugin('node').apply(childCompiler);
102
103 // Add all templates
104 this.templates.forEach((template, index) => {
105 new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
106 });
107
108 this.compilationStartedTimestamp = new Date().getTime();
109 this.compilationPromise = new Promise((resolve, reject) => {
110 childCompiler.runAsChild((err, entries, childCompilation) => {
111 // Extract templates
112 const compiledTemplates = entries
113 ? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
114 : [];
115 // Extract file dependencies
116 if (entries) {
117 this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Array.from(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) };
118 }
119 // Reject the promise if the childCompilation contains error
120 if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
121 const errorDetails = childCompilation.errors.map(error => {
122 let message = error.message;
123 if (error.error) {
124 message += ':\n' + error.error;
125 }
126 if (error.stack) {
127 message += '\n' + error.stack;
128 }
129 return message;
130 }).join('\n');
131 reject(new Error('Child compilation failed:\n' + errorDetails));
132 return;
133 }
134 // Reject if the error object contains errors
135 if (err) {
136 reject(err);
137 return;
138 }
139 /**
140 * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
141 */
142 const result = {};
143 compiledTemplates.forEach((templateSource, entryIndex) => {
144 // The compiledTemplates are generated from the entries added in
145 // the addTemplate function.
146 // Therefore the array index of this.templates should be the as entryIndex.
147 result[this.templates[entryIndex]] = {
148 content: templateSource,
149 hash: childCompilation.hash,
150 entry: entries[entryIndex]
151 };
152 });
153 this.compilationEndedTimestamp = new Date().getTime();
154 resolve(result);
155 });
156 });
157
158 return this.compilationPromise;
159 }
160}
161
162/**
163 * The webpack child compilation will create files as a side effect.
164 * This function will extract them and clean them up so they won't be written to disk.
165 *
166 * Returns the source code of the compiled templates as string
167 *
168 * @returns Array<string>
169 */
170function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
171 const webpackMajorVersion = Number(require('webpack/package.json').version.split('.')[0]);
172
173 const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
174 const entryConfig = {
175 hash: childCompilation.hash,
176 chunk: entryChunk,
177 name: `HtmlWebpackPlugin_${index}`
178 };
179
180 return webpackMajorVersion === 4
181 ? mainCompilation.mainTemplate.getAssetPath(filename, entryConfig)
182 : mainCompilation.getAssetPath(filename, entryConfig);
183 });
184
185 helperAssetNames.forEach((helperFileName) => {
186 delete mainCompilation.assets[helperFileName];
187 });
188
189 const helperContents = helperAssetNames.map((helperFileName) => {
190 return childCompilation.assets[helperFileName].source();
191 });
192
193 return helperContents;
194}
195
196module.exports = {
197 HtmlWebpackChildCompiler
198};