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,
 | |
| };
 |