265 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			265 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
// @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,
 | 
						|
};
 |