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/child-compiler.js

265 lines
8.6 KiB

// @ts-check
"use strict";
/**
* @file
* This file uses webpack to compile a template with a child compiler.
*
* [TEMPLATE] -> [JAVASCRIPT]
*
*/
/** @typedef {import("webpack").Chunk} Chunk */
/** @typedef {import("webpack").sources.Source} Source */
/** @typedef {{hash: string, entry: Chunk, content: string, assets: {[name: string]: { source: Source, info: import("webpack").AssetInfo }}}} ChildCompilationTemplateResult */
/**
* 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]: ChildCompilationTemplateResult}>} */
this.compilationPromise; // eslint-disable-line
/** @type {number | undefined} */
this.compilationStartedTimestamp; // eslint-disable-line
/** @type {number | undefined} */
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
*
* @returns {boolean}
*/
didCompile() {
return this.compilationEndedTimestamp !== undefined;
}
/**
* This function will start the template compilation
* once it is started no more templates can be added
*
* @param {import('webpack').Compilation} mainCompilation
* @returns {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>}
*/
compileTemplates(mainCompilation) {
const webpack = mainCompilation.compiler.webpack;
const Compilation = webpack.Compilation;
const NodeTemplatePlugin = webpack.node.NodeTemplatePlugin;
const NodeTargetPlugin = webpack.node.NodeTargetPlugin;
const LoaderTargetPlugin = webpack.LoaderTargetPlugin;
const EntryPlugin = webpack.EntryPlugin;
// 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;
}
const outputOptions = {
filename: "__child-[name]",
publicPath: "",
library: {
type: "var",
name: "HTML_WEBPACK_PLUGIN_RESULT",
},
scriptType: /** @type {'text/javascript'} */ ("text/javascript"),
iife: true,
};
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,
[
// Compile the template to nodejs javascript
new NodeTargetPlugin(),
new NodeTemplatePlugin(),
new LoaderTargetPlugin("node"),
new webpack.library.EnableLibraryPlugin("var"),
],
);
// The file path context which webpack uses to resolve all relative files to
childCompiler.context = mainCompilation.compiler.context;
// Generate output file names
const temporaryTemplateNames = this.templates.map(
(template, index) => `__child-HtmlWebpackPlugin_${index}-${template}`,
);
// Add all templates
this.templates.forEach((template, index) => {
new EntryPlugin(
childCompiler.context,
"data:text/javascript,__webpack_public_path__ = __webpack_base_uri__ = htmlWebpackPluginPublicPath;",
`HtmlWebpackPlugin_${index}-${template}`,
).apply(childCompiler);
new EntryPlugin(
childCompiler.context,
template,
`HtmlWebpackPlugin_${index}-${template}`,
).apply(childCompiler);
});
// The templates are compiled and executed by NodeJS - similar to server side rendering
// Unfortunately this causes issues as some loaders require an absolute URL to support ES Modules
// The following config enables relative URL support for the child compiler
childCompiler.options.module = { ...childCompiler.options.module };
childCompiler.options.module.parser = {
...childCompiler.options.module.parser,
};
childCompiler.options.module.parser.javascript = {
...childCompiler.options.module.parser.javascript,
url: "relative",
};
this.compilationStartedTimestamp = new Date().getTime();
/** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */
this.compilationPromise = new Promise((resolve, reject) => {
/** @type {Source[]} */
const extractedAssets = [];
childCompiler.hooks.thisCompilation.tap(
"HtmlWebpackPlugin",
(compilation) => {
compilation.hooks.processAssets.tap(
{
name: "HtmlWebpackPlugin",
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
(assets) => {
temporaryTemplateNames.forEach((temporaryTemplateName) => {
if (assets[temporaryTemplateName]) {
extractedAssets.push(assets[temporaryTemplateName]);
compilation.deleteAsset(temporaryTemplateName);
}
});
},
);
},
);
childCompiler.runAsChild((err, entries, childCompilation) => {
// Extract templates
// TODO fine a better way to store entries and results, to avoid duplicate chunks and assets
const compiledTemplates = entries
? extractedAssets.map((asset) => asset.source())
: [];
// Extract file dependencies
if (entries && childCompilation) {
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 errorDetailsArray = [];
for (const error of childCompilation.errors) {
let message = error.message;
if (error.stack) {
message += "\n" + error.stack;
}
errorDetailsArray.push(message);
}
const errorDetails = errorDetailsArray.join("\n");
reject(new Error("Child compilation failed:\n" + errorDetails));
return;
}
// Reject if the error object contains errors
if (err) {
reject(err);
return;
}
if (!childCompilation || !entries) {
reject(new Error("Empty child compilation"));
return;
}
/**
* @type {{[templatePath: string]: ChildCompilationTemplateResult}}
*/
const result = {};
/** @type {{[name: string]: { source: Source, info: import("webpack").AssetInfo }}} */
const assets = {};
for (const asset of childCompilation.getAssets()) {
assets[asset.name] = { source: asset.source, info: asset.info };
}
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]] = {
// TODO, can we have Buffer here?
content: /** @type {string} */ (templateSource),
hash: childCompilation.hash || "XXXX",
entry: entries[entryIndex],
assets,
};
});
this.compilationEndedTimestamp = new Date().getTime();
resolve(result);
});
});
return this.compilationPromise;
}
}
module.exports = {
HtmlWebpackChildCompiler,
};