// @ts-check /** * @file * Helper plugin manages the cached state of the child compilation * * To optimize performance the child compilation is running asyncronously. * Therefore it needs to be started in the compiler.make phase and ends after * the compilation.afterCompile phase. * * To prevent bugs from blocked hooks there is no promise or event based api * for this plugin. * * Example usage: * * ```js const childCompilerPlugin = new PersistentChildCompilerPlugin(); childCompilerPlugin.addEntry('./src/index.js'); compiler.hooks.afterCompile.tapAsync('MyPlugin', (compilation, callback) => { console.log(childCompilerPlugin.getCompilationResult()['./src/index.js'])); return true; }); * ``` */ // Import types /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */ /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */ /** @typedef {{hash: string, entry: any, content: string }} ChildCompilationResultEntry */ /** @typedef {import("./webpack4/file-watcher-api").Snapshot} Snapshot */ /** @typedef {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} FileDependencies */ /** @typedef {{ dependencies: FileDependencies, compiledEntries: {[entryName: string]: ChildCompilationResultEntry} } | { dependencies: FileDependencies, error: Error }} ChildCompilationResult */ 'use strict'; const { HtmlWebpackChildCompiler } = require('./child-compiler'); const fileWatcherApi = require('./file-watcher-api'); /** * This plugin is a singleton for performance reasons. * To keep track if a plugin does already exist for the compiler they are cached * in this map * @type {WeakMap}} */ const compilerMap = new WeakMap(); class CachedChildCompilation { /** * @param {WebpackCompiler} compiler */ constructor (compiler) { /** * @private * @type {WebpackCompiler} */ this.compiler = compiler; // Create a singleton instance for the compiler // if there is none if (compilerMap.has(compiler)) { return; } const persistentChildCompilerSingletonPlugin = new PersistentChildCompilerSingletonPlugin(); compilerMap.set(compiler, persistentChildCompilerSingletonPlugin); persistentChildCompilerSingletonPlugin.apply(compiler); } /** * apply is called by the webpack main compiler during the start phase * @param {string} entry */ addEntry (entry) { const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler); if (!persistentChildCompilerSingletonPlugin) { throw new Error( 'PersistentChildCompilerSingletonPlugin instance not found.' ); } persistentChildCompilerSingletonPlugin.addEntry(entry); } getCompilationResult () { const persistentChildCompilerSingletonPlugin = compilerMap.get(this.compiler); if (!persistentChildCompilerSingletonPlugin) { throw new Error( 'PersistentChildCompilerSingletonPlugin instance not found.' ); } return persistentChildCompilerSingletonPlugin.getLatestResult(); } /** * Returns the result for the given entry * @param {string} entry * @returns { | { mainCompilationHash: string, error: Error } | { mainCompilationHash: string, compiledEntry: ChildCompilationResultEntry } } */ getCompilationEntryResult (entry) { const latestResult = this.getCompilationResult(); const compilationResult = latestResult.compilationResult; return 'error' in compilationResult ? { mainCompilationHash: latestResult.mainCompilationHash, error: compilationResult.error } : { mainCompilationHash: latestResult.mainCompilationHash, compiledEntry: compilationResult.compiledEntries[entry] }; } } class PersistentChildCompilerSingletonPlugin { constructor () { /** * @private * @type { | { isCompiling: false, isVerifyingCache: false, entries: string[], compiledEntries: string[], mainCompilationHash: string, compilationResult: ChildCompilationResult } | Readonly<{ isCompiling: false, isVerifyingCache: true, entries: string[], previousEntries: string[], previousResult: ChildCompilationResult }> | Readonly <{ isVerifyingCache: false, isCompiling: true, entries: string[], }> } the internal compilation state */ this.compilationState = { isCompiling: false, isVerifyingCache: false, entries: [], compiledEntries: [], mainCompilationHash: 'initial', compilationResult: { dependencies: { fileDependencies: [], contextDependencies: [], missingDependencies: [] }, compiledEntries: {} } }; } /** * apply is called by the webpack main compiler during the start phase * @param {WebpackCompiler} compiler */ apply (compiler) { /** @type Promise */ let childCompilationResultPromise = Promise.resolve({ dependencies: { fileDependencies: [], contextDependencies: [], missingDependencies: [] }, compiledEntries: {} }); /** * The main compilation hash which will only be updated * if the childCompiler changes */ let mainCompilationHashOfLastChildRecompile = ''; /** @typedef{Snapshot|undefined} */ let previousFileSystemSnapshot; let compilationStartTime = new Date().getTime(); compiler.hooks.make.tapAsync( 'PersistentChildCompilerSingletonPlugin', (mainCompilation, callback) => { if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) { return callback(new Error('Child compilation has already started')); } // Update the time to the current compile start time compilationStartTime = new Date().getTime(); // The compilation starts - adding new templates is now not possible anymore this.compilationState = { isCompiling: false, isVerifyingCache: true, previousEntries: this.compilationState.compiledEntries, previousResult: this.compilationState.compilationResult, entries: this.compilationState.entries }; // Validate cache: const isCacheValidPromise = this.isCacheValid(previousFileSystemSnapshot, mainCompilation); let cachedResult = childCompilationResultPromise; childCompilationResultPromise = isCacheValidPromise.then((isCacheValid) => { // Reuse cache if (isCacheValid) { return cachedResult; } // Start the compilation const compiledEntriesPromise = this.compileEntries( mainCompilation, this.compilationState.entries ); // Update snapshot as soon as we know the filedependencies // this might possibly cause bugs if files were changed inbetween // compilation start and snapshot creation compiledEntriesPromise.then((childCompilationResult) => { return fileWatcherApi.createSnapshot(childCompilationResult.dependencies, mainCompilation, compilationStartTime); }).then((snapshot) => { previousFileSystemSnapshot = snapshot; }); return compiledEntriesPromise; }); // Add files to compilation which needs to be watched: mainCompilation.hooks.optimizeTree.tapAsync( 'PersistentChildCompilerSingletonPlugin', (chunks, modules, callback) => { const handleCompilationDonePromise = childCompilationResultPromise.then( childCompilationResult => { this.watchFiles( mainCompilation, childCompilationResult.dependencies ); }); handleCompilationDonePromise.then(() => callback(null, chunks, modules), callback); } ); // Store the final compilation once the main compilation hash is known mainCompilation.hooks.additionalAssets.tapAsync( 'PersistentChildCompilerSingletonPlugin', (callback) => { const didRecompilePromise = Promise.all([childCompilationResultPromise, cachedResult]).then( ([childCompilationResult, cachedResult]) => { // Update if childCompilation changed return (cachedResult !== childCompilationResult); } ); const handleCompilationDonePromise = Promise.all([childCompilationResultPromise, didRecompilePromise]).then( ([childCompilationResult, didRecompile]) => { // Update hash and snapshot if childCompilation changed if (didRecompile) { mainCompilationHashOfLastChildRecompile = mainCompilation.hash; } this.compilationState = { isCompiling: false, isVerifyingCache: false, entries: this.compilationState.entries, compiledEntries: this.compilationState.entries, compilationResult: childCompilationResult, mainCompilationHash: mainCompilationHashOfLastChildRecompile }; }); handleCompilationDonePromise.then(() => callback(null), callback); } ); // Continue compilation: callback(null); } ); } /** * Add a new entry to the next compile run * @param {string} entry */ addEntry (entry) { if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) { throw new Error( 'The child compiler has already started to compile. ' + "Please add entries before the main compiler 'make' phase has started or " + 'after the compilation is done.' ); } if (this.compilationState.entries.indexOf(entry) === -1) { this.compilationState.entries = [...this.compilationState.entries, entry]; } } getLatestResult () { if (this.compilationState.isCompiling || this.compilationState.isVerifyingCache) { throw new Error( 'The child compiler is not done compiling. ' + "Please access the result after the compiler 'make' phase has started or " + 'after the compilation is done.' ); } return { mainCompilationHash: this.compilationState.mainCompilationHash, compilationResult: this.compilationState.compilationResult }; } /** * Verify that the cache is still valid * @private * @param {Snapshot | undefined} snapshot * @param {WebpackCompilation} mainCompilation * @returns {Promise} */ isCacheValid (snapshot, mainCompilation) { if (!this.compilationState.isVerifyingCache) { return Promise.reject(new Error('Cache validation can only be done right before the compilation starts')); } // If there are no entries we don't need a new child compilation if (this.compilationState.entries.length === 0) { return Promise.resolve(true); } // If there are new entries the cache is invalid if (this.compilationState.entries !== this.compilationState.previousEntries) { return Promise.resolve(false); } // Mark the cache as invalid if there is no snapshot if (!snapshot) { return Promise.resolve(false); } return fileWatcherApi.isSnapShotValid(snapshot, mainCompilation); } /** * Start to compile all templates * * @private * @param {WebpackCompilation} mainCompilation * @param {string[]} entries * @returns {Promise} */ compileEntries (mainCompilation, entries) { const compiler = new HtmlWebpackChildCompiler(entries); return compiler.compileTemplates(mainCompilation).then((result) => { return { // The compiled sources to render the content compiledEntries: result, // The file dependencies to find out if a // recompilation is required dependencies: compiler.fileDependencies, // The main compilation hash can be used to find out // if this compilation was done during the current compilation mainCompilationHash: mainCompilation.hash }; }, error => ({ // The compiled sources to render the content error, // The file dependencies to find out if a // recompilation is required dependencies: compiler.fileDependencies, // The main compilation hash can be used to find out // if this compilation was done during the current compilation mainCompilationHash: mainCompilation.hash })); } /** * @private * @param {WebpackCompilation} mainCompilation * @param {FileDependencies} files */ watchFiles (mainCompilation, files) { fileWatcherApi.watchFiles(mainCompilation, files); } } module.exports = { CachedChildCompilation };