compiler.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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 resusing one childCompiler
  21. * for multile HtmlWebpackPlugin instances to improve the compilation performance.
  22. */
  23. class HtmlWebpackChildCompiler {
  24. constructor () {
  25. /**
  26. * @type {string[]} templateIds
  27. * The template array will allow us to keep track which input generated which output
  28. */
  29. this.templates = [];
  30. /**
  31. * @type {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
  32. */
  33. this.compilationPromise; // eslint-disable-line
  34. /**
  35. * @type {number}
  36. */
  37. this.compilationStartedTimestamp; // eslint-disable-line
  38. /**
  39. * @type {number}
  40. */
  41. this.compilationEndedTimestamp; // eslint-disable-line
  42. /**
  43. * All file dependencies of the child compiler
  44. * @type {string[]}
  45. */
  46. this.fileDependencies = [];
  47. }
  48. /**
  49. * Add a templatePath to the child compiler
  50. * The given template will be compiled by `compileTemplates`
  51. * @param {string} template - The webpack path to the template e.g. `'!!html-loader!index.html'`
  52. * @returns {boolean} true if the template is new
  53. */
  54. addTemplate (template) {
  55. const templateId = this.templates.indexOf(template);
  56. // Don't add the template to the compiler if a similar template was already added
  57. if (templateId !== -1) {
  58. return false;
  59. }
  60. // A child compiler can compile only once
  61. // throw an error if a new template is added after the compilation started
  62. if (this.isCompiling()) {
  63. throw new Error('New templates can only be added before `compileTemplates` was called.');
  64. }
  65. // Add the template to the childCompiler
  66. this.templates.push(template);
  67. // Mark the cache invalid
  68. return true;
  69. }
  70. /**
  71. * Returns true if the childCompiler is currently compiling
  72. * @retuns {boolean}
  73. */
  74. isCompiling () {
  75. return !this.didCompile() && this.compilationStartedTimestamp !== undefined;
  76. }
  77. /**
  78. * Returns true if the childCOmpiler is done compiling
  79. */
  80. didCompile () {
  81. return this.compilationEndedTimestamp !== undefined;
  82. }
  83. /**
  84. * This function will start the template compilation
  85. * once it is started no more templates can be added
  86. *
  87. * @param {WebpackCompilation} mainCompilation
  88. * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
  89. */
  90. compileTemplates (mainCompilation) {
  91. // To prevent multiple compilations for the same template
  92. // the compilation is cached in a promise.
  93. // If it already exists return
  94. if (this.compilationPromise) {
  95. return this.compilationPromise;
  96. }
  97. // The entry file is just an empty helper as the dynamic template
  98. // require is added in "loader.js"
  99. const outputOptions = {
  100. filename: '__child-[name]',
  101. publicPath: mainCompilation.outputOptions.publicPath
  102. };
  103. const compilerName = 'HtmlWebpackCompiler';
  104. // Create an additional child compiler which takes the template
  105. // and turns it into an Node.JS html factory.
  106. // This allows us to use loaders during the compilation
  107. const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
  108. // The file path context which webpack uses to resolve all relative files to
  109. childCompiler.context = mainCompilation.compiler.context;
  110. // Compile the template to nodejs javascript
  111. new NodeTemplatePlugin(outputOptions).apply(childCompiler);
  112. new NodeTargetPlugin().apply(childCompiler);
  113. new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
  114. new LoaderTargetPlugin('node').apply(childCompiler);
  115. // Add all templates
  116. this.templates.forEach((template, index) => {
  117. new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
  118. });
  119. this.compilationStartedTimestamp = new Date().getTime();
  120. this.compilationPromise = new Promise((resolve, reject) => {
  121. childCompiler.runAsChild((err, entries, childCompilation) => {
  122. // Extract templates
  123. const compiledTemplates = entries
  124. ? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
  125. : [];
  126. // Extract file dependencies
  127. if (entries) {
  128. this.fileDependencies = Array.from(childCompilation.fileDependencies);
  129. }
  130. // Reject the promise if the childCompilation contains error
  131. if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
  132. const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n');
  133. reject(new Error('Child compilation failed:\n' + errorDetails));
  134. return;
  135. }
  136. // Reject if the error object contains errors
  137. if (err) {
  138. reject(err);
  139. return;
  140. }
  141. /**
  142. * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
  143. */
  144. const result = {};
  145. compiledTemplates.forEach((templateSource, entryIndex) => {
  146. // The compiledTemplates are generated from the entries added in
  147. // the addTemplate function.
  148. // Therefore the array index of this.templates should be the as entryIndex.
  149. result[this.templates[entryIndex]] = {
  150. content: templateSource,
  151. hash: childCompilation.hash,
  152. entry: entries[entryIndex]
  153. };
  154. });
  155. this.compilationEndedTimestamp = new Date().getTime();
  156. resolve(result);
  157. });
  158. });
  159. return this.compilationPromise;
  160. }
  161. }
  162. /**
  163. * The webpack child compilation will create files as a side effect.
  164. * This function will extract them and clean them up so they won't be written to disk.
  165. *
  166. * Returns the source code of the compiled templates as string
  167. *
  168. * @returns Array<string>
  169. */
  170. function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
  171. const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
  172. return mainCompilation.mainTemplate.getAssetPath(filename, {
  173. hash: childCompilation.hash,
  174. chunk: entryChunk,
  175. name: `HtmlWebpackPlugin_${index}`
  176. });
  177. });
  178. helperAssetNames.forEach((helperFileName) => {
  179. delete mainCompilation.assets[helperFileName];
  180. });
  181. const helperContents = helperAssetNames.map((helperFileName) => {
  182. return childCompilation.assets[helperFileName].source();
  183. });
  184. return helperContents;
  185. }
  186. /**
  187. * @type {WeakMap<WebpackCompiler, HtmlWebpackChildCompiler>}}
  188. */
  189. const childCompilerCache = new WeakMap();
  190. /**
  191. * Get child compiler from cache or a new child compiler for the given mainCompilation
  192. *
  193. * @param {WebpackCompiler} mainCompiler
  194. */
  195. function getChildCompiler (mainCompiler) {
  196. const cachedChildCompiler = childCompilerCache.get(mainCompiler);
  197. if (cachedChildCompiler) {
  198. return cachedChildCompiler;
  199. }
  200. const newCompiler = new HtmlWebpackChildCompiler();
  201. childCompilerCache.set(mainCompiler, newCompiler);
  202. return newCompiler;
  203. }
  204. /**
  205. * Remove the childCompiler from the cache
  206. *
  207. * @param {WebpackCompiler} mainCompiler
  208. */
  209. function clearCache (mainCompiler) {
  210. const childCompiler = getChildCompiler(mainCompiler);
  211. // If this childCompiler was already used
  212. // remove the entire childCompiler from the cache
  213. if (childCompiler.isCompiling() || childCompiler.didCompile()) {
  214. childCompilerCache.delete(mainCompiler);
  215. }
  216. }
  217. /**
  218. * Register a template for the current main compiler
  219. * @param {WebpackCompiler} mainCompiler
  220. * @param {string} templatePath
  221. */
  222. function addTemplateToCompiler (mainCompiler, templatePath) {
  223. const childCompiler = getChildCompiler(mainCompiler);
  224. const isNew = childCompiler.addTemplate(templatePath);
  225. if (isNew) {
  226. clearCache(mainCompiler);
  227. }
  228. }
  229. /**
  230. * Starts the compilation for all templates.
  231. * This has to be called once all templates where added.
  232. *
  233. * If this function is called multiple times it will use a cache inside
  234. * the childCompiler
  235. *
  236. * @param {string} templatePath
  237. * @param {string} outputFilename
  238. * @param {WebpackCompilation} mainCompilation
  239. */
  240. function compileTemplate (templatePath, outputFilename, mainCompilation) {
  241. const childCompiler = getChildCompiler(mainCompilation.compiler);
  242. return childCompiler.compileTemplates(mainCompilation).then((compiledTemplates) => {
  243. if (!compiledTemplates[templatePath]) console.log(Object.keys(compiledTemplates), templatePath);
  244. const compiledTemplate = compiledTemplates[templatePath];
  245. // Replace [hash] placeholders in filename
  246. const outputName = mainCompilation.mainTemplate.getAssetPath(outputFilename, {
  247. hash: compiledTemplate.hash,
  248. chunk: compiledTemplate.entry
  249. });
  250. return {
  251. // Hash of the template entry point
  252. hash: compiledTemplate.hash,
  253. // Output name
  254. outputName: outputName,
  255. // Compiled code
  256. content: compiledTemplate.content
  257. };
  258. });
  259. }
  260. /**
  261. * Return all file dependencies of the last child compilation
  262. *
  263. * @param {WebpackCompiler} compiler
  264. * @returns {Array<string>}
  265. */
  266. function getFileDependencies (compiler) {
  267. const childCompiler = getChildCompiler(compiler);
  268. return childCompiler.fileDependencies;
  269. }
  270. /**
  271. * @type {WeakMap<WebpackCompilation, WeakMap<HtmlWebpackChildCompiler, boolean>>}}
  272. */
  273. const hasOutdatedCompilationDependenciesMap = new WeakMap();
  274. /**
  275. * Returns `true` if the file dependencies of the current childCompiler
  276. * for the given mainCompilation are outdated.
  277. *
  278. * Uses the `hasOutdatedCompilationDependenciesMap` cache if possible.
  279. *
  280. * @param {WebpackCompilation} mainCompilation
  281. * @returns {boolean}
  282. */
  283. function hasOutDatedTemplateCache (mainCompilation) {
  284. const childCompiler = getChildCompiler(mainCompilation.compiler);
  285. /**
  286. * @type {WeakMap<HtmlWebpackChildCompiler, boolean>|undefined}
  287. */
  288. let hasOutdatedChildCompilerDependenciesMap = hasOutdatedCompilationDependenciesMap.get(mainCompilation);
  289. // Create map for childCompiler if none exist
  290. if (!hasOutdatedChildCompilerDependenciesMap) {
  291. hasOutdatedChildCompilerDependenciesMap = new WeakMap();
  292. hasOutdatedCompilationDependenciesMap.set(mainCompilation, hasOutdatedChildCompilerDependenciesMap);
  293. }
  294. // Try to get the `checkChildCompilerCache` result from cache
  295. let isOutdated = hasOutdatedChildCompilerDependenciesMap.get(childCompiler);
  296. if (isOutdated !== undefined) {
  297. return isOutdated;
  298. }
  299. // If `checkChildCompilerCache` has never been called for the given
  300. // `mainCompilation` and `childCompiler` combination call it:
  301. isOutdated = isChildCompilerCacheOutdated(mainCompilation, childCompiler);
  302. hasOutdatedChildCompilerDependenciesMap.set(childCompiler, isOutdated);
  303. return isOutdated;
  304. }
  305. /**
  306. * Returns `true` if the file dependencies of the given childCompiler are outdated.
  307. *
  308. * @param {WebpackCompilation} mainCompilation
  309. * @param {HtmlWebpackChildCompiler} childCompiler
  310. * @returns {boolean}
  311. */
  312. function isChildCompilerCacheOutdated (mainCompilation, childCompiler) {
  313. // If the compilation was never run there is no invalid cache
  314. if (!childCompiler.compilationStartedTimestamp) {
  315. return false;
  316. }
  317. // Check if any dependent file was changed after the last compilation
  318. const fileTimestamps = mainCompilation.fileTimestamps;
  319. const isCacheOutOfDate = childCompiler.fileDependencies.some((fileDependency) => {
  320. const timestamp = fileTimestamps.get(fileDependency);
  321. // If the timestamp is not known the file is new
  322. // If the timestamp is larger then the file has changed
  323. // Otherwise the file is still the same
  324. return !timestamp || timestamp > childCompiler.compilationStartedTimestamp;
  325. });
  326. return isCacheOutOfDate;
  327. }
  328. module.exports = {
  329. addTemplateToCompiler,
  330. compileTemplate,
  331. hasOutDatedTemplateCache,
  332. clearCache,
  333. getFileDependencies
  334. };