|
|
// @ts-check
/** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */ /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */ /** @typedef {import("webpack/lib/Chunk.js")} WebpackChunk */ 'use strict'; /** * @file * This file uses webpack to compile a template with a child compiler. * * [TEMPLATE] -> [JAVASCRIPT] * */ 'use strict'; const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin'); const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin'); const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
/** * The HtmlWebpackChildCompiler is a helper to allow reusing one childCompiler * for multiple HtmlWebpackPlugin instances to improve the compilation performance. */ class HtmlWebpackChildCompiler { /** * * @param {string[]} templates */ constructor (templates) { /** * @type {string[]} templateIds * The template array will allow us to keep track which input generated which output */ this.templates = templates; /** * @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>} */ this.compilationPromise; // eslint-disable-line
/** * @type {number} */ this.compilationStartedTimestamp; // eslint-disable-line
/** * @type {number} */ this.compilationEndedTimestamp; // eslint-disable-line
/** * All file dependencies of the child compiler * @type {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}} */ this.fileDependencies = { fileDependencies: [], contextDependencies: [], missingDependencies: [] }; }
/** * Returns true if the childCompiler is currently compiling * @returns {boolean} */ isCompiling () { return !this.didCompile() && this.compilationStartedTimestamp !== undefined; }
/** * Returns true if the childCompiler is done compiling */ didCompile () { return this.compilationEndedTimestamp !== undefined; }
/** * This function will start the template compilation * once it is started no more templates can be added * * @param {WebpackCompilation} mainCompilation * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>} */ compileTemplates (mainCompilation) { // To prevent multiple compilations for the same template
// the compilation is cached in a promise.
// If it already exists return
if (this.compilationPromise) { return this.compilationPromise; }
// The entry file is just an empty helper as the dynamic template
// require is added in "loader.js"
const outputOptions = { filename: '__child-[name]', publicPath: mainCompilation.outputOptions.publicPath }; const compilerName = 'HtmlWebpackCompiler'; // Create an additional child compiler which takes the template
// and turns it into an Node.JS html factory.
// This allows us to use loaders during the compilation
const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions); // The file path context which webpack uses to resolve all relative files to
childCompiler.context = mainCompilation.compiler.context; // Compile the template to nodejs javascript
new NodeTemplatePlugin(outputOptions).apply(childCompiler); new NodeTargetPlugin().apply(childCompiler); new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler); new LoaderTargetPlugin('node').apply(childCompiler);
// Add all templates
this.templates.forEach((template, index) => { new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler); });
this.compilationStartedTimestamp = new Date().getTime(); this.compilationPromise = new Promise((resolve, reject) => { childCompiler.runAsChild((err, entries, childCompilation) => { // Extract templates
const compiledTemplates = entries ? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries) : []; // Extract file dependencies
if (entries) { this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Array.from(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) }; } // Reject the promise if the childCompilation contains error
if (childCompilation && childCompilation.errors && childCompilation.errors.length) { const errorDetails = childCompilation.errors.map(error => { let message = error.message; if (error.error) { message += ':\n' + error.error; } if (error.stack) { message += '\n' + error.stack; } return message; }).join('\n'); reject(new Error('Child compilation failed:\n' + errorDetails)); return; } // Reject if the error object contains errors
if (err) { reject(err); return; } /** * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}} */ const result = {}; compiledTemplates.forEach((templateSource, entryIndex) => { // The compiledTemplates are generated from the entries added in
// the addTemplate function.
// Therefore the array index of this.templates should be the as entryIndex.
result[this.templates[entryIndex]] = { content: templateSource, hash: childCompilation.hash, entry: entries[entryIndex] }; }); this.compilationEndedTimestamp = new Date().getTime(); resolve(result); }); });
return this.compilationPromise; } }
/** * The webpack child compilation will create files as a side effect. * This function will extract them and clean them up so they won't be written to disk. * * Returns the source code of the compiled templates as string * * @returns Array<string> */ function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) { const webpackMajorVersion = Number(require('webpack/package.json').version.split('.')[0]);
const helperAssetNames = childEntryChunks.map((entryChunk, index) => { const entryConfig = { hash: childCompilation.hash, chunk: entryChunk, name: `HtmlWebpackPlugin_${index}` };
return webpackMajorVersion === 4 ? mainCompilation.mainTemplate.getAssetPath(filename, entryConfig) : mainCompilation.getAssetPath(filename, entryConfig); });
helperAssetNames.forEach((helperFileName) => { delete mainCompilation.assets[helperFileName]; });
const helperContents = helperAssetNames.map((helperFileName) => { return childCompilation.assets[helperFileName].source(); });
return helperContents; }
module.exports = { HtmlWebpackChildCompiler };
|