123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- // @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,
- };
|