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.
parttimejob/node_modules/html-webpack-plugin/lib/cached-child-compiler.js

483 lines
15 KiB

4 weeks ago
// @ts-check
/**
* @file
* Helper plugin manages the cached state of the child compilation
*
* To optimize performance the child compilation is running asynchronously.
* 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;
});
* ```
*/
"use strict";
// Import types
/** @typedef {import("webpack").Compiler} Compiler */
/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("webpack/lib/FileSystemInfo").Snapshot} Snapshot */
/** @typedef {import("./child-compiler").ChildCompilationTemplateResult} ChildCompilationTemplateResult */
/** @typedef {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} FileDependencies */
/** @typedef {{
dependencies: FileDependencies,
compiledEntries: {[entryName: string]: ChildCompilationTemplateResult}
} | {
dependencies: FileDependencies,
error: Error
}} ChildCompilationResult */
const { HtmlWebpackChildCompiler } = require("./child-compiler");
/**
* 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<Compiler, PersistentChildCompilerSingletonPlugin>}}
*/
const compilerMap = new WeakMap();
class CachedChildCompilation {
/**
* @param {Compiler} compiler
*/
constructor(compiler) {
/**
* @private
* @type {Compiler}
*/
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: ChildCompilationTemplateResult }
}
*/
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 {
/**
*
* @param {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} fileDependencies
* @param {Compilation} mainCompilation
* @param {number} startTime
*/
static createSnapshot(fileDependencies, mainCompilation, startTime) {
return new Promise((resolve, reject) => {
mainCompilation.fileSystemInfo.createSnapshot(
startTime,
fileDependencies.fileDependencies,
fileDependencies.contextDependencies,
fileDependencies.missingDependencies,
// @ts-ignore
null,
(err, snapshot) => {
if (err) {
return reject(err);
}
resolve(snapshot);
},
);
});
}
/**
* Returns true if the files inside this snapshot
* have not been changed
*
* @param {Snapshot} snapshot
* @param {Compilation} mainCompilation
* @returns {Promise<boolean | undefined>}
*/
static isSnapshotValid(snapshot, mainCompilation) {
return new Promise((resolve, reject) => {
mainCompilation.fileSystemInfo.checkSnapshotValid(
snapshot,
(err, isValid) => {
if (err) {
reject(err);
}
resolve(isValid);
},
);
});
}
static watchFiles(mainCompilation, fileDependencies) {
Object.keys(fileDependencies).forEach((dependencyType) => {
fileDependencies[dependencyType].forEach((fileDependency) => {
mainCompilation[dependencyType].add(fileDependency);
});
});
}
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 {Compiler} 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
*/
/** @type {string} */
let mainCompilationHashOfLastChildRecompile = "";
/** @type {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 between
// compilation start and snapshot creation
compiledEntriesPromise
.then((childCompilationResult) => {
return PersistentChildCompilerSingletonPlugin.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(
// @ts-ignore
() => 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 =
/** @type {string} */ (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 {Compilation} mainCompilation
* @returns {Promise<boolean | undefined>}
*/
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 PersistentChildCompilerSingletonPlugin.isSnapshotValid(
snapshot,
mainCompilation,
);
}
/**
* Start to compile all templates
*
* @private
* @param {Compilation} 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 {Compilation} mainCompilation
* @param {FileDependencies} files
*/
watchFiles(mainCompilation, files) {
PersistentChildCompilerSingletonPlugin.watchFiles(mainCompilation, files);
}
}
module.exports = {
CachedChildCompilation,
};