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.
265 lines
8.6 KiB
265 lines
8.6 KiB
4 weeks ago
|
// @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,
|
||
|
};
|