You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
379 lines
13 KiB
379 lines
13 KiB
// @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("./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<WebpackCompiler, PersistentChildCompilerSingletonPlugin>}} |
|
*/ |
|
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<ChildCompilationResult> */ |
|
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<boolean>} |
|
*/ |
|
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<ChildCompilationResult>} |
|
*/ |
|
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 |
|
};
|
|
|