child-compiler.js 8.1 KB

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