child-compiler.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // @ts-check
  2. /** @typedef {import("webpack/lib/Compilation.js")} WebpackCompilation */
  3. /** @typedef {import("webpack/lib/Compiler.js")} WebpackCompiler */
  4. /** @typedef {import("webpack/lib/Chunk.js")} WebpackChunk */
  5. 'use strict';
  6. /**
  7. * @file
  8. * This file uses webpack to compile a template with a child compiler.
  9. *
  10. * [TEMPLATE] -> [JAVASCRIPT]
  11. *
  12. */
  13. 'use strict';
  14. let instanceId = 0;
  15. /**
  16. * The HtmlWebpackChildCompiler is a helper to allow reusing one childCompiler
  17. * for multiple HtmlWebpackPlugin instances to improve the compilation performance.
  18. */
  19. class HtmlWebpackChildCompiler {
  20. /**
  21. *
  22. * @param {string[]} templates
  23. */
  24. constructor (templates) {
  25. /** Id for this ChildCompiler */
  26. this.id = instanceId++;
  27. /**
  28. * @type {string[]} templateIds
  29. * The template array will allow us to keep track which input generated which output
  30. */
  31. this.templates = templates;
  32. /**
  33. * @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
  34. */
  35. this.compilationPromise; // eslint-disable-line
  36. /**
  37. * @type {number}
  38. */
  39. this.compilationStartedTimestamp; // eslint-disable-line
  40. /**
  41. * @type {number}
  42. */
  43. this.compilationEndedTimestamp; // eslint-disable-line
  44. /**
  45. * All file dependencies of the child compiler
  46. * @type {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}}
  47. */
  48. this.fileDependencies = { fileDependencies: [], contextDependencies: [], missingDependencies: [] };
  49. }
  50. /**
  51. * Returns true if the childCompiler is currently compiling
  52. * @returns {boolean}
  53. */
  54. isCompiling () {
  55. return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
  56. }
  57. /**
  58. * Returns true if the childCompiler is done compiling
  59. */
  60. didCompile () {
  61. return this.compilationEndedTimestamp !== undefined;
  62. }
  63. /**
  64. * This function will start the template compilation
  65. * once it is started no more templates can be added
  66. *
  67. * @param {import('webpack').Compilation} mainCompilation
  68. * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
  69. */
  70. compileTemplates (mainCompilation) {
  71. const webpack = mainCompilation.compiler.webpack;
  72. const Compilation = webpack.Compilation;
  73. const NodeTemplatePlugin = webpack.node.NodeTemplatePlugin;
  74. const NodeTargetPlugin = webpack.node.NodeTargetPlugin;
  75. const LoaderTargetPlugin = webpack.LoaderTargetPlugin;
  76. const EntryPlugin = webpack.EntryPlugin;
  77. // To prevent multiple compilations for the same template
  78. // the compilation is cached in a promise.
  79. // If it already exists return
  80. if (this.compilationPromise) {
  81. return this.compilationPromise;
  82. }
  83. const outputOptions = {
  84. filename: '__child-[name]',
  85. publicPath: '',
  86. library: {
  87. type: 'var',
  88. name: 'HTML_WEBPACK_PLUGIN_RESULT'
  89. },
  90. scriptType: /** @type {'text/javascript'} */('text/javascript'),
  91. iife: true
  92. };
  93. const compilerName = 'HtmlWebpackCompiler';
  94. // Create an additional child compiler which takes the template
  95. // and turns it into an Node.JS html factory.
  96. // This allows us to use loaders during the compilation
  97. const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions, [
  98. // Compile the template to nodejs javascript
  99. new NodeTargetPlugin(),
  100. new NodeTemplatePlugin(),
  101. new LoaderTargetPlugin('node'),
  102. new webpack.library.EnableLibraryPlugin('var')
  103. ]);
  104. // The file path context which webpack uses to resolve all relative files to
  105. childCompiler.context = mainCompilation.compiler.context;
  106. // Generate output file names
  107. const temporaryTemplateNames = this.templates.map((template, index) => `__child-HtmlWebpackPlugin_${index}-${this.id}`);
  108. // Add all templates
  109. this.templates.forEach((template, index) => {
  110. new EntryPlugin(childCompiler.context, 'data:text/javascript,__webpack_public_path__ = __webpack_base_uri__ = htmlWebpackPluginPublicPath;', `HtmlWebpackPlugin_${index}-${this.id}`).apply(childCompiler);
  111. new EntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}-${this.id}`).apply(childCompiler);
  112. });
  113. // The templates are compiled and executed by NodeJS - similar to server side rendering
  114. // Unfortunately this causes issues as some loaders require an absolute URL to support ES Modules
  115. // The following config enables relative URL support for the child compiler
  116. childCompiler.options.module = { ...childCompiler.options.module };
  117. childCompiler.options.module.parser = { ...childCompiler.options.module.parser };
  118. childCompiler.options.module.parser.javascript = { ...childCompiler.options.module.parser.javascript,
  119. url: 'relative' };
  120. this.compilationStartedTimestamp = new Date().getTime();
  121. this.compilationPromise = new Promise((resolve, reject) => {
  122. const extractedAssets = [];
  123. childCompiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => {
  124. compilation.hooks.processAssets.tap(
  125. {
  126. name: 'HtmlWebpackPlugin',
  127. stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
  128. },
  129. (assets) => {
  130. temporaryTemplateNames.forEach((temporaryTemplateName) => {
  131. if (assets[temporaryTemplateName]) {
  132. extractedAssets.push(assets[temporaryTemplateName]);
  133. compilation.deleteAsset(temporaryTemplateName);
  134. }
  135. });
  136. }
  137. );
  138. });
  139. childCompiler.runAsChild((err, entries, childCompilation) => {
  140. // Extract templates
  141. const compiledTemplates = entries
  142. ? extractedAssets.map((asset) => asset.source())
  143. : [];
  144. // Extract file dependencies
  145. if (entries && childCompilation) {
  146. this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Array.from(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) };
  147. }
  148. // Reject the promise if the childCompilation contains error
  149. if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
  150. const errorDetails = childCompilation.errors.map(error => {
  151. let message = error.message;
  152. if (error.stack) {
  153. message += '\n' + error.stack;
  154. }
  155. return message;
  156. }).join('\n');
  157. reject(new Error('Child compilation failed:\n' + errorDetails));
  158. return;
  159. }
  160. // Reject if the error object contains errors
  161. if (err) {
  162. reject(err);
  163. return;
  164. }
  165. if (!childCompilation || !entries) {
  166. reject(new Error('Empty child compilation'));
  167. return;
  168. }
  169. /**
  170. * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
  171. */
  172. const result = {};
  173. compiledTemplates.forEach((templateSource, entryIndex) => {
  174. // The compiledTemplates are generated from the entries added in
  175. // the addTemplate function.
  176. // Therefore the array index of this.templates should be the as entryIndex.
  177. result[this.templates[entryIndex]] = {
  178. content: templateSource,
  179. hash: childCompilation.hash || 'XXXX',
  180. entry: entries[entryIndex]
  181. };
  182. });
  183. this.compilationEndedTimestamp = new Date().getTime();
  184. resolve(result);
  185. });
  186. });
  187. return this.compilationPromise;
  188. }
  189. }
  190. module.exports = {
  191. HtmlWebpackChildCompiler
  192. };