10
0

child-compiler.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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 = {
  39. fileDependencies: [],
  40. contextDependencies: [],
  41. missingDependencies: [],
  42. };
  43. }
  44. /**
  45. * Returns true if the childCompiler is currently compiling
  46. *
  47. * @returns {boolean}
  48. */
  49. isCompiling() {
  50. return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
  51. }
  52. /**
  53. * Returns true if the childCompiler is done compiling
  54. *
  55. * @returns {boolean}
  56. */
  57. didCompile() {
  58. return this.compilationEndedTimestamp !== undefined;
  59. }
  60. /**
  61. * This function will start the template compilation
  62. * once it is started no more templates can be added
  63. *
  64. * @param {import('webpack').Compilation} mainCompilation
  65. * @returns {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>}
  66. */
  67. compileTemplates(mainCompilation) {
  68. const webpack = mainCompilation.compiler.webpack;
  69. const Compilation = webpack.Compilation;
  70. const NodeTemplatePlugin = webpack.node.NodeTemplatePlugin;
  71. const NodeTargetPlugin = webpack.node.NodeTargetPlugin;
  72. const LoaderTargetPlugin = webpack.LoaderTargetPlugin;
  73. const EntryPlugin = webpack.EntryPlugin;
  74. // To prevent multiple compilations for the same template
  75. // the compilation is cached in a promise.
  76. // If it already exists return
  77. if (this.compilationPromise) {
  78. return this.compilationPromise;
  79. }
  80. const outputOptions = {
  81. filename: "__child-[name]",
  82. publicPath: "",
  83. library: {
  84. type: "var",
  85. name: "HTML_WEBPACK_PLUGIN_RESULT",
  86. },
  87. scriptType: /** @type {'text/javascript'} */ ("text/javascript"),
  88. iife: true,
  89. };
  90. const compilerName = "HtmlWebpackCompiler";
  91. // Create an additional child compiler which takes the template
  92. // and turns it into an Node.JS html factory.
  93. // This allows us to use loaders during the compilation
  94. const childCompiler = mainCompilation.createChildCompiler(
  95. compilerName,
  96. outputOptions,
  97. [
  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. );
  105. // The file path context which webpack uses to resolve all relative files to
  106. childCompiler.context = mainCompilation.compiler.context;
  107. // Generate output file names
  108. const temporaryTemplateNames = this.templates.map(
  109. (template, index) => `__child-HtmlWebpackPlugin_${index}-${template}`,
  110. );
  111. // Add all templates
  112. this.templates.forEach((template, index) => {
  113. new EntryPlugin(
  114. childCompiler.context,
  115. "data:text/javascript,__webpack_public_path__ = __webpack_base_uri__ = htmlWebpackPluginPublicPath;",
  116. `HtmlWebpackPlugin_${index}-${template}`,
  117. ).apply(childCompiler);
  118. new EntryPlugin(
  119. childCompiler.context,
  120. template,
  121. `HtmlWebpackPlugin_${index}-${template}`,
  122. ).apply(childCompiler);
  123. });
  124. // The templates are compiled and executed by NodeJS - similar to server side rendering
  125. // Unfortunately this causes issues as some loaders require an absolute URL to support ES Modules
  126. // The following config enables relative URL support for the child compiler
  127. childCompiler.options.module = { ...childCompiler.options.module };
  128. childCompiler.options.module.parser = {
  129. ...childCompiler.options.module.parser,
  130. };
  131. childCompiler.options.module.parser.javascript = {
  132. ...childCompiler.options.module.parser.javascript,
  133. url: "relative",
  134. };
  135. this.compilationStartedTimestamp = new Date().getTime();
  136. /** @type {Promise<{[templatePath: string]: ChildCompilationTemplateResult}>} */
  137. this.compilationPromise = new Promise((resolve, reject) => {
  138. /** @type {Source[]} */
  139. const extractedAssets = [];
  140. childCompiler.hooks.thisCompilation.tap(
  141. "HtmlWebpackPlugin",
  142. (compilation) => {
  143. compilation.hooks.processAssets.tap(
  144. {
  145. name: "HtmlWebpackPlugin",
  146. stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
  147. },
  148. (assets) => {
  149. temporaryTemplateNames.forEach((temporaryTemplateName) => {
  150. if (assets[temporaryTemplateName]) {
  151. extractedAssets.push(assets[temporaryTemplateName]);
  152. compilation.deleteAsset(temporaryTemplateName);
  153. }
  154. });
  155. },
  156. );
  157. },
  158. );
  159. childCompiler.runAsChild((err, entries, childCompilation) => {
  160. // Extract templates
  161. // TODO fine a better way to store entries and results, to avoid duplicate chunks and assets
  162. const compiledTemplates = entries
  163. ? extractedAssets.map((asset) => asset.source())
  164. : [];
  165. // Extract file dependencies
  166. if (entries && childCompilation) {
  167. this.fileDependencies = {
  168. fileDependencies: Array.from(childCompilation.fileDependencies),
  169. contextDependencies: Array.from(
  170. childCompilation.contextDependencies,
  171. ),
  172. missingDependencies: Array.from(
  173. childCompilation.missingDependencies,
  174. ),
  175. };
  176. }
  177. // Reject the promise if the childCompilation contains error
  178. if (
  179. childCompilation &&
  180. childCompilation.errors &&
  181. childCompilation.errors.length
  182. ) {
  183. const errorDetailsArray = [];
  184. for (const error of childCompilation.errors) {
  185. let message = error.message;
  186. if (error.stack) {
  187. message += "\n" + error.stack;
  188. }
  189. errorDetailsArray.push(message);
  190. }
  191. const errorDetails = errorDetailsArray.join("\n");
  192. reject(new Error("Child compilation failed:\n" + errorDetails));
  193. return;
  194. }
  195. // Reject if the error object contains errors
  196. if (err) {
  197. reject(err);
  198. return;
  199. }
  200. if (!childCompilation || !entries) {
  201. reject(new Error("Empty child compilation"));
  202. return;
  203. }
  204. /**
  205. * @type {{[templatePath: string]: ChildCompilationTemplateResult}}
  206. */
  207. const result = {};
  208. /** @type {{[name: string]: { source: Source, info: import("webpack").AssetInfo }}} */
  209. const assets = {};
  210. for (const asset of childCompilation.getAssets()) {
  211. assets[asset.name] = { source: asset.source, info: asset.info };
  212. }
  213. compiledTemplates.forEach((templateSource, entryIndex) => {
  214. // The compiledTemplates are generated from the entries added in
  215. // the addTemplate function.
  216. // Therefore, the array index of this.templates should be the as entryIndex.
  217. result[this.templates[entryIndex]] = {
  218. // TODO, can we have Buffer here?
  219. content: /** @type {string} */ (templateSource),
  220. hash: childCompilation.hash || "XXXX",
  221. entry: entries[entryIndex],
  222. assets,
  223. };
  224. });
  225. this.compilationEndedTimestamp = new Date().getTime();
  226. resolve(result);
  227. });
  228. });
  229. return this.compilationPromise;
  230. }
  231. }
  232. module.exports = {
  233. HtmlWebpackChildCompiler,
  234. };