child-compiler.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. const NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
  15. const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
  16. const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
  17. const LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
  18. const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
  19. /**
  20. * The HtmlWebpackChildCompiler is a helper to allow reusing one childCompiler
  21. * for multiple HtmlWebpackPlugin instances to improve the compilation performance.
  22. */
  23. class HtmlWebpackChildCompiler {
  24. /**
  25. *
  26. * @param {string[]} templates
  27. */
  28. constructor (templates) {
  29. /**
  30. * @type {string[]} templateIds
  31. * The template array will allow us to keep track which input generated which output
  32. */
  33. this.templates = templates;
  34. /**
  35. * @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
  36. */
  37. this.compilationPromise; // eslint-disable-line
  38. /**
  39. * @type {number}
  40. */
  41. this.compilationStartedTimestamp; // eslint-disable-line
  42. /**
  43. * @type {number}
  44. */
  45. this.compilationEndedTimestamp; // eslint-disable-line
  46. /**
  47. * All file dependencies of the child compiler
  48. * @type {{fileDependencies: string[], contextDependencies: string[], missingDependencies: string[]}}
  49. */
  50. this.fileDependencies = { fileDependencies: [], contextDependencies: [], missingDependencies: [] };
  51. }
  52. /**
  53. * Returns true if the childCompiler is currently compiling
  54. * @returns {boolean}
  55. */
  56. isCompiling () {
  57. return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
  58. }
  59. /**
  60. * Returns true if the childCompiler is done compiling
  61. */
  62. didCompile () {
  63. return this.compilationEndedTimestamp !== undefined;
  64. }
  65. /**
  66. * This function will start the template compilation
  67. * once it is started no more templates can be added
  68. *
  69. * @param {WebpackCompilation} mainCompilation
  70. * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
  71. */
  72. compileTemplates (mainCompilation) {
  73. // To prevent multiple compilations for the same template
  74. // the compilation is cached in a promise.
  75. // If it already exists return
  76. if (this.compilationPromise) {
  77. return this.compilationPromise;
  78. }
  79. // The entry file is just an empty helper as the dynamic template
  80. // require is added in "loader.js"
  81. const outputOptions = {
  82. filename: '__child-[name]',
  83. publicPath: mainCompilation.outputOptions.publicPath
  84. };
  85. const compilerName = 'HtmlWebpackCompiler';
  86. // Create an additional child compiler which takes the template
  87. // and turns it into an Node.JS html factory.
  88. // This allows us to use loaders during the compilation
  89. const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
  90. // The file path context which webpack uses to resolve all relative files to
  91. childCompiler.context = mainCompilation.compiler.context;
  92. // Compile the template to nodejs javascript
  93. new NodeTemplatePlugin(outputOptions).apply(childCompiler);
  94. new NodeTargetPlugin().apply(childCompiler);
  95. new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
  96. new LoaderTargetPlugin('node').apply(childCompiler);
  97. // Add all templates
  98. this.templates.forEach((template, index) => {
  99. new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
  100. });
  101. this.compilationStartedTimestamp = new Date().getTime();
  102. this.compilationPromise = new Promise((resolve, reject) => {
  103. childCompiler.runAsChild((err, entries, childCompilation) => {
  104. // Extract templates
  105. const compiledTemplates = entries
  106. ? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
  107. : [];
  108. // Extract file dependencies
  109. if (entries) {
  110. this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Array.from(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) };
  111. }
  112. // Reject the promise if the childCompilation contains error
  113. if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
  114. const errorDetails = childCompilation.errors.map(error => {
  115. let message = error.message;
  116. if (error.error) {
  117. message += ':\n' + error.error;
  118. }
  119. if (error.stack) {
  120. message += '\n' + error.stack;
  121. }
  122. return message;
  123. }).join('\n');
  124. reject(new Error('Child compilation failed:\n' + errorDetails));
  125. return;
  126. }
  127. // Reject if the error object contains errors
  128. if (err) {
  129. reject(err);
  130. return;
  131. }
  132. /**
  133. * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
  134. */
  135. const result = {};
  136. compiledTemplates.forEach((templateSource, entryIndex) => {
  137. // The compiledTemplates are generated from the entries added in
  138. // the addTemplate function.
  139. // Therefore the array index of this.templates should be the as entryIndex.
  140. result[this.templates[entryIndex]] = {
  141. content: templateSource,
  142. hash: childCompilation.hash,
  143. entry: entries[entryIndex]
  144. };
  145. });
  146. this.compilationEndedTimestamp = new Date().getTime();
  147. resolve(result);
  148. });
  149. });
  150. return this.compilationPromise;
  151. }
  152. }
  153. /**
  154. * The webpack child compilation will create files as a side effect.
  155. * This function will extract them and clean them up so they won't be written to disk.
  156. *
  157. * Returns the source code of the compiled templates as string
  158. *
  159. * @returns Array<string>
  160. */
  161. function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
  162. const webpackMajorVersion = Number(require('webpack/package.json').version.split('.')[0]);
  163. const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
  164. const entryConfig = {
  165. hash: childCompilation.hash,
  166. chunk: entryChunk,
  167. name: `HtmlWebpackPlugin_${index}`
  168. };
  169. return webpackMajorVersion === 4
  170. ? mainCompilation.mainTemplate.getAssetPath(filename, entryConfig)
  171. : mainCompilation.getAssetPath(filename, entryConfig);
  172. });
  173. helperAssetNames.forEach((helperFileName) => {
  174. delete mainCompilation.assets[helperFileName];
  175. });
  176. const helperContents = helperAssetNames.map((helperFileName) => {
  177. return childCompilation.assets[helperFileName].source();
  178. });
  179. return helperContents;
  180. }
  181. module.exports = {
  182. HtmlWebpackChildCompiler
  183. };