UNPKG

13.1 kBJavaScriptView Raw
1// @ts-check
2/**
3 * @file
4 * Helper plugin manages the cached state of the child compilation
5 *
6 * To optimize performance the child compilation is running asyncronously.
7 * Therefore it needs to be started in the compiler.make phase and ends after
8 * the compilation.afterCompile phase.
9 *
10 * To prevent bugs from blocked hooks there is no promise or event based api
11 * for this plugin.
12 *
13 * Example usage:
14 *
15 * ```js
16 const childCompilerPlugin = new PersistentChildCompilerPlugin();
17 childCompilerPlugin.addEntry('./src/index.js');
18 compiler.hooks.afterCompile.tapAsync('MyPlugin', (compilation, callback) => {
19 console.log(childCompilerPlugin.getCompilationResult()['./src/index.js']));
20 return true;
21 });
22 * ```
23 */
24
25// Import types
26/** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
27/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
28/** @typedef {{hash: string, entry: any, content: string }} ChildCompilationResultEntry */
29/** @typedef {import("./file-watcher-api").Snapshot} Snapshot */
30/** @typedef {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} FileDependencies */
31/** @typedef {{
32 dependencies: FileDependencies,
33 compiledEntries: {[entryName: string]: ChildCompilationResultEntry}
34} | {
35 dependencies: FileDependencies,
36 error: Error
37}} ChildCompilationResult */
38'use strict';
39
40const { HtmlWebpackChildCompiler } = require('./child-compiler');
41const fileWatcherApi = require('./file-watcher-api');
42
43/**
44 * This plugin is a singleton for performance reasons.
45 * To keep track if a plugin does already exist for the compiler they are cached
46 * in this map
47 * @type {WeakMap<WebpackCompiler, PersistentChildCompilerSingletonPlugin>}}
48 */
49const compilerMap = new WeakMap();
50
51class CachedChildCompilation {
52 /**
53 * @param {WebpackCompiler} compiler
54 */
55 constructor (compiler) {
56 /**
57 * @private
58 * @type {WebpackCompiler}
59 */
60 this.compiler = compiler;
61 // Create a singleton instance for the compiler
62 // if there is none
63 if (compilerMap.has(compiler)) {
64 return;
65 }
66 const persistentChildCompilerSingletonPlugin = new PersistentChildCompilerSingletonPlugin();
67 compilerMap.set(compiler, persistentChildCompilerSingletonPlugin);
68 persistentChildCompilerSingletonPlugin.apply(compiler);
69 }
70
71 /**
72 * apply is called by the webpack main compiler during the start phase
73 * @param {string} entry
74 */
75 addEntry (entry) {
76 const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler);
77 if (!persistentChildCompilerSingletonPlugin) {
78 throw new Error(
79 'PersistentChildCompilerSingletonPlugin instance not found.'
80 );
81 }
82 persistentChildCompilerSingletonPlugin.addEntry(entry);
83 }
84
85 getCompilationResult () {
86 const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler);
87 if (!persistentChildCompilerSingletonPlugin) {
88 throw new Error(
89 'PersistentChildCompilerSingletonPlugin instance not found.'
90 );
91 }
92 return persistentChildCompilerSingletonPlugin.getLatestResult();
93 }
94
95 /**
96 * Returns the result for the given entry
97 * @param {string} entry
98 * @returns {
99 | { mainCompilationHash: string, error: Error }
100 | { mainCompilationHash: string, compiledEntry: ChildCompilationResultEntry }
101 }
102 */
103 getCompilationEntryResult (entry) {
104 const latestResult = this.getCompilationResult();
105 const compilationResult = latestResult.compilationResult;
106 return 'error' in compilationResult ? {
107 mainCompilationHash: latestResult.mainCompilationHash,
108 error: compilationResult.error
109 } : {
110 mainCompilationHash: latestResult.mainCompilationHash,
111 compiledEntry: compilationResult.compiledEntries[entry]
112 };
113 }
114}
115
116class PersistentChildCompilerSingletonPlugin {
117 constructor () {
118 /**
119 * @private
120 * @type {
121 | {
122 isCompiling: false,
123 isVerifyingCache: false,
124 entries: string[],
125 compiledEntries: string[],
126 mainCompilationHash: string,
127 compilationResult: ChildCompilationResult
128 }
129 | Readonly<{
130 isCompiling: false,
131 isVerifyingCache: true,
132 entries: string[],
133 previousEntries: string[],
134 previousResult: ChildCompilationResult
135 }>
136 | Readonly <{
137 isVerifyingCache: false,
138 isCompiling: true,
139 entries: string[],
140 }>
141 } the internal compilation state */
142 this.compilationState = {
143 isCompiling: false,
144 isVerifyingCache: false,
145 entries: [],
146 compiledEntries: [],
147 mainCompilationHash: 'initial',
148 compilationResult: {
149 dependencies: {
150 fileDependencies: [],
151 contextDependencies: [],
152 missingDependencies: []
153 },
154 compiledEntries: {}
155 }
156 };
157 }
158
159 /**
160 * apply is called by the webpack main compiler during the start phase
161 * @param {WebpackCompiler} compiler
162 */
163 apply (compiler) {
164 /** @type Promise<ChildCompilationResult> */
165 let childCompilationResultPromise = Promise.resolve({
166 dependencies: {
167 fileDependencies: [],
168 contextDependencies: [],
169 missingDependencies: []
170 },
171 compiledEntries: {}
172 });
173 /**
174 * The main compilation hash which will only be updated
175 * if the childCompiler changes
176 */
177 let mainCompilationHashOfLastChildRecompile = '';
178 /** @typedef{Snapshot|undefined} */
179 let previousFileSystemSnapshot;
180 let compilationStartTime = new Date().getTime();
181
182 compiler.hooks.make.tapAsync(
183 'PersistentChildCompilerSingletonPlugin',
184 (mainCompilation, callback) => {
185 if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
186 return callback(new Error('Child compilation has already started'));
187 }
188
189 // Update the time to the current compile start time
190 compilationStartTime = new Date().getTime();
191
192 // The compilation starts - adding new templates is now not possible anymore
193 this.compilationState = {
194 isCompiling: false,
195 isVerifyingCache: true,
196 previousEntries: this.compilationState.compiledEntries,
197 previousResult: this.compilationState.compilationResult,
198 entries: this.compilationState.entries
199 };
200
201 // Validate cache:
202 const isCacheValidPromise = this.isCacheValid(previousFileSystemSnapshot, mainCompilation);
203
204 let cachedResult = childCompilationResultPromise;
205 childCompilationResultPromise = isCacheValidPromise.then((isCacheValid) => {
206 // Reuse cache
207 if (isCacheValid) {
208 return cachedResult;
209 }
210 // Start the compilation
211 const compiledEntriesPromise = this.compileEntries(
212 mainCompilation,
213 this.compilationState.entries
214 );
215 // Update snapshot as soon as we know the filedependencies
216 // this might possibly cause bugs if files were changed inbetween
217 // compilation start and snapshot creation
218 compiledEntriesPromise.then((childCompilationResult) => {
219 return fileWatcherApi.createSnapshot(childCompilationResult.dependencies, mainCompilation, compilationStartTime);
220 }).then((snapshot) => {
221 previousFileSystemSnapshot = snapshot;
222 });
223 return compiledEntriesPromise;
224 });
225
226 // Add files to compilation which needs to be watched:
227 mainCompilation.hooks.optimizeTree.tapAsync(
228 'PersistentChildCompilerSingletonPlugin',
229 (chunks, modules, callback) => {
230 const handleCompilationDonePromise = childCompilationResultPromise.then(
231 childCompilationResult => {
232 this.watchFiles(
233 mainCompilation,
234 childCompilationResult.dependencies
235 );
236 });
237 handleCompilationDonePromise.then(() => callback(null, chunks, modules), callback);
238 }
239 );
240
241 // Store the final compilation once the main compilation hash is known
242 mainCompilation.hooks.additionalAssets.tapAsync(
243 'PersistentChildCompilerSingletonPlugin',
244 (callback) => {
245 const didRecompilePromise = Promise.all([childCompilationResultPromise, cachedResult]).then(
246 ([childCompilationResult, cachedResult]) => {
247 // Update if childCompilation changed
248 return (cachedResult !== childCompilationResult);
249 }
250 );
251
252 const handleCompilationDonePromise = Promise.all([childCompilationResultPromise, didRecompilePromise]).then(
253 ([childCompilationResult, didRecompile]) => {
254 // Update hash and snapshot if childCompilation changed
255 if (didRecompile) {
256 mainCompilationHashOfLastChildRecompile = mainCompilation.hash;
257 }
258 this.compilationState = {
259 isCompiling: false,
260 isVerifyingCache: false,
261 entries: this.compilationState.entries,
262 compiledEntries: this.compilationState.entries,
263 compilationResult: childCompilationResult,
264 mainCompilationHash: mainCompilationHashOfLastChildRecompile
265 };
266 });
267 handleCompilationDonePromise.then(() => callback(null), callback);
268 }
269 );
270
271 // Continue compilation:
272 callback(null);
273 }
274 );
275 }
276
277 /**
278 * Add a new entry to the next compile run
279 * @param {string} entry
280 */
281 addEntry (entry) {
282 if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
283 throw new Error(
284 'The child compiler has already started to compile. ' +
285 "Please add entries before the main compiler 'make' phase has started or " +
286 'after the compilation is done.'
287 );
288 }
289 if (this.compilationState.entries.indexOf(entry) === -1) {
290 this.compilationState.entries = [...this.compilationState.entries, entry];
291 }
292 }
293
294 getLatestResult () {
295 if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) {
296 throw new Error(
297 'The child compiler is not done compiling. ' +
298 "Please access the result after the compiler 'make' phase has started or " +
299 'after the compilation is done.'
300 );
301 }
302 return {
303 mainCompilationHash: this.compilationState.mainCompilationHash,
304 compilationResult: this.compilationState.compilationResult
305 };
306 }
307
308 /**
309 * Verify that the cache is still valid
310 * @private
311 * @param {Snapshot | undefined} snapshot
312 * @param {WebpackCompilation} mainCompilation
313 * @returns {Promise<boolean>}
314 */
315 isCacheValid (snapshot, mainCompilation) {
316 if (!this.compilationState.isVerifyingCache) {
317 return Promise.reject(new Error('Cache validation can only be done right before the compilation starts'));
318 }
319 // If there are no entries we don't need a new child compilation
320 if (this.compilationState.entries.length === 0) {
321 return Promise.resolve(true);
322 }
323 // If there are new entries the cache is invalid
324 if (this.compilationState.entries !== this.compilationState.previousEntries) {
325 return Promise.resolve(false);
326 }
327 // Mark the cache as invalid if there is no snapshot
328 if (!snapshot) {
329 return Promise.resolve(false);
330 }
331 return fileWatcherApi.isSnapShotValid(snapshot, mainCompilation);
332 }
333
334 /**
335 * Start to compile all templates
336 *
337 * @private
338 * @param {WebpackCompilation} mainCompilation
339 * @param {string[]} entries
340 * @returns {Promise<ChildCompilationResult>}
341 */
342 compileEntries (mainCompilation, entries) {
343 const compiler = new HtmlWebpackChildCompiler(entries);
344 return compiler.compileTemplates(mainCompilation).then((result) => {
345 return {
346 // The compiled sources to render the content
347 compiledEntries: result,
348 // The file dependencies to find out if a
349 // recompilation is required
350 dependencies: compiler.fileDependencies,
351 // The main compilation hash can be used to find out
352 // if this compilation was done during the current compilation
353 mainCompilationHash: mainCompilation.hash
354 };
355 }, error => ({
356 // The compiled sources to render the content
357 error,
358 // The file dependencies to find out if a
359 // recompilation is required
360 dependencies: compiler.fileDependencies,
361 // The main compilation hash can be used to find out
362 // if this compilation was done during the current compilation
363 mainCompilationHash: mainCompilation.hash
364 }));
365 }
366
367 /**
368 * @private
369 * @param {WebpackCompilation} mainCompilation
370 * @param {FileDependencies} files
371 */
372 watchFiles (mainCompilation, files) {
373 fileWatcherApi.watchFiles(mainCompilation, files);
374 }
375}
376
377module.exports = {
378 CachedChildCompilation
379};