Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

384 строки
13 KiB

  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;
  34. /**
  35. * @type {number}
  36. */
  37. this.compilationStartedTimestamp;
  38. /**
  39. * @type {number}
  40. */
  41. this.compilationEndedTimestamp;
  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. // Fix for "Uncaught TypeError: __webpack_require__(...) is not a function"
  116. // Hot module replacement requires that every child compiler has its own
  117. // cache. @see https://github.com/ampedandwired/html-webpack-plugin/pull/179
  118. childCompiler.hooks.compilation.tap('HtmlWebpackPlugin', compilation => {
  119. if (compilation.cache) {
  120. if (!compilation.cache[compilerName]) {
  121. compilation.cache[compilerName] = {};
  122. }
  123. compilation.cache = compilation.cache[compilerName];
  124. }
  125. });
  126. // Add all templates
  127. this.templates.forEach((template, index) => {
  128. new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
  129. });
  130. this.compilationStartedTimestamp = new Date().getTime();
  131. this.compilationPromise = new Promise((resolve, reject) => {
  132. childCompiler.runAsChild((err, entries, childCompilation) => {
  133. // Extract templates
  134. const compiledTemplates = entries
  135. ? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
  136. : [];
  137. // Extract file dependencies
  138. if (entries) {
  139. this.fileDependencies = extractFileDependenciesFilesFromCompilation(entries);
  140. }
  141. // Reject the promise if the childCompilation contains error
  142. if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
  143. const errorDetails = childCompilation.errors.map(error => error.message + (error.error ? ':\n' + error.error : '')).join('\n');
  144. reject(new Error('Child compilation failed:\n' + errorDetails));
  145. return;
  146. }
  147. // Reject if the error object contains errors
  148. if (err) {
  149. reject(err);
  150. return;
  151. }
  152. /**
  153. * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
  154. */
  155. const result = {};
  156. compiledTemplates.forEach((templateSource, entryIndex) => {
  157. // The compiledTemplates are generated from the entries added in
  158. // the addTemplate function.
  159. // Therefore the array index of this.templates should be the as entryIndex.
  160. result[this.templates[entryIndex]] = {
  161. content: templateSource,
  162. hash: childCompilation.hash,
  163. entry: entries[entryIndex]
  164. };
  165. });
  166. this.compilationEndedTimestamp = new Date().getTime();
  167. resolve(result);
  168. });
  169. });
  170. return this.compilationPromise;
  171. }
  172. }
  173. /**
  174. * The webpack child compilation will create files as a side effect.
  175. * This function will extract them and clean them up so they won't be written to disk.
  176. *
  177. * Returns the source code of the compiled templates as string
  178. *
  179. * @returns Array<string>
  180. */
  181. function extractHelperFilesFromCompilation (mainCompilation, childCompilation, filename, childEntryChunks) {
  182. const helperAssetNames = childEntryChunks.map((entryChunk, index) => {
  183. return mainCompilation.mainTemplate.hooks.assetPath.call(filename, {
  184. hash: childCompilation.hash,
  185. chunk: entryChunk,
  186. name: `HtmlWebpackPlugin_${index}`
  187. });
  188. });
  189. helperAssetNames.forEach((helperFileName) => {
  190. delete mainCompilation.assets[helperFileName];
  191. });
  192. const helperContents = helperAssetNames.map((helperFileName) => {
  193. return childCompilation.assets[helperFileName].source();
  194. });
  195. return helperContents;
  196. }
  197. /**
  198. * Return all file dependencies from the given set of entries.
  199. * @param {WebpackChunk[]} entries
  200. * @returns {string[]}
  201. */
  202. function extractFileDependenciesFilesFromCompilation (entries) {
  203. const fileDependencies = new Map();
  204. entries.forEach((entry) => {
  205. entry.entryModule.buildInfo.fileDependencies.forEach((fileDependency) => {
  206. fileDependencies.set(fileDependency, true);
  207. });
  208. });
  209. return Array.from(fileDependencies.keys());
  210. }
  211. /**
  212. * @type {WeakMap<WebpackCompiler, HtmlWebpackChildCompiler>}}
  213. */
  214. const childCompilerCache = new WeakMap();
  215. /**
  216. * Get child compiler from cache or a new child compiler for the given mainCompilation
  217. *
  218. * @param {WebpackCompiler} mainCompiler
  219. */
  220. function getChildCompiler (mainCompiler) {
  221. const cachedChildCompiler = childCompilerCache.get(mainCompiler);
  222. if (cachedChildCompiler) {
  223. return cachedChildCompiler;
  224. }
  225. const newCompiler = new HtmlWebpackChildCompiler();
  226. childCompilerCache.set(mainCompiler, newCompiler);
  227. return newCompiler;
  228. }
  229. /**
  230. * Remove the childCompiler from the cache
  231. *
  232. * @param {WebpackCompiler} mainCompiler
  233. */
  234. function clearCache (mainCompiler) {
  235. const childCompiler = getChildCompiler(mainCompiler);
  236. // If this childCompiler was already used
  237. // remove the entire childCompiler from the cache
  238. if (childCompiler.isCompiling() || childCompiler.didCompile()) {
  239. childCompilerCache.delete(mainCompiler);
  240. }
  241. }
  242. /**
  243. * Register a template for the current main compiler
  244. * @param {WebpackCompiler} mainCompiler
  245. * @param {string} templatePath
  246. */
  247. function addTemplateToCompiler (mainCompiler, templatePath) {
  248. const childCompiler = getChildCompiler(mainCompiler);
  249. const isNew = childCompiler.addTemplate(templatePath);
  250. if (isNew) {
  251. clearCache(mainCompiler);
  252. }
  253. }
  254. /**
  255. * Starts the compilation for all templates.
  256. * This has to be called once all templates where added.
  257. *
  258. * If this function is called multiple times it will use a cache inside
  259. * the childCompiler
  260. *
  261. * @param {string} templatePath
  262. * @param {string} outputFilename
  263. * @param {WebpackCompilation} mainCompilation
  264. */
  265. function compileTemplate (templatePath, outputFilename, mainCompilation) {
  266. const childCompiler = getChildCompiler(mainCompilation.compiler);
  267. return childCompiler.compileTemplates(mainCompilation).then((compiledTemplates) => {
  268. if (!compiledTemplates[templatePath]) console.log(Object.keys(compiledTemplates), templatePath);
  269. const compiledTemplate = compiledTemplates[templatePath];
  270. // Replace [hash] placeholders in filename
  271. const outputName = mainCompilation.mainTemplate.hooks.assetPath.call(outputFilename, {
  272. hash: compiledTemplate.hash,
  273. chunk: compiledTemplate.entry
  274. });
  275. return {
  276. // Hash of the template entry point
  277. hash: compiledTemplate.hash,
  278. // Output name
  279. outputName: outputName,
  280. // Compiled code
  281. content: compiledTemplate.content
  282. };
  283. });
  284. }
  285. /**
  286. * Return all file dependencies of the last child compilation
  287. *
  288. * @param {WebpackCompiler} compiler
  289. * @returns {Array<string>}
  290. */
  291. function getFileDependencies (compiler) {
  292. const childCompiler = getChildCompiler(compiler);
  293. return childCompiler.fileDependencies;
  294. }
  295. /**
  296. * @type {WeakMap<WebpackCompilation, WeakMap<HtmlWebpackChildCompiler, boolean>>}}
  297. */
  298. const hasOutdatedCompilationDependenciesMap = new WeakMap();
  299. /**
  300. * Returns `true` if the file dependencies of the current childCompiler
  301. * for the given mainCompilation are outdated.
  302. *
  303. * Uses the `hasOutdatedCompilationDependenciesMap` cache if possible.
  304. *
  305. * @param {WebpackCompilation} mainCompilation
  306. * @returns {boolean}
  307. */
  308. function hasOutDatedTemplateCache (mainCompilation) {
  309. const childCompiler = getChildCompiler(mainCompilation.compiler);
  310. /**
  311. * @type {WeakMap<HtmlWebpackChildCompiler, boolean>|undefined}
  312. */
  313. let hasOutdatedChildCompilerDependenciesMap = hasOutdatedCompilationDependenciesMap.get(mainCompilation);
  314. // Create map for childCompiler if none exist
  315. if (!hasOutdatedChildCompilerDependenciesMap) {
  316. hasOutdatedChildCompilerDependenciesMap = new WeakMap();
  317. hasOutdatedCompilationDependenciesMap.set(mainCompilation, hasOutdatedChildCompilerDependenciesMap);
  318. }
  319. // Try to get the `checkChildCompilerCache` result from cache
  320. let isOutdated = hasOutdatedChildCompilerDependenciesMap.get(childCompiler);
  321. if (isOutdated !== undefined) {
  322. return isOutdated;
  323. }
  324. // If `checkChildCompilerCache` has never been called for the given
  325. // `mainCompilation` and `childCompiler` combination call it:
  326. isOutdated = isChildCompilerCacheOutdated(mainCompilation, childCompiler);
  327. hasOutdatedChildCompilerDependenciesMap.set(childCompiler, isOutdated);
  328. return isOutdated;
  329. }
  330. /**
  331. * Returns `true` if the file dependencies of the given childCompiler are outdated.
  332. *
  333. * @param {WebpackCompilation} mainCompilation
  334. * @param {HtmlWebpackChildCompiler} childCompiler
  335. * @returns {boolean}
  336. */
  337. function isChildCompilerCacheOutdated (mainCompilation, childCompiler) {
  338. // If the compilation was never run there is no invalid cache
  339. if (!childCompiler.compilationStartedTimestamp) {
  340. return false;
  341. }
  342. // Check if any dependent file was changed after the last compilation
  343. const fileTimestamps = mainCompilation.fileTimestamps;
  344. const isCacheOutOfDate = childCompiler.fileDependencies.some((fileDependency) => {
  345. const timestamp = fileTimestamps.get(fileDependency);
  346. // If the timestamp is not known the file is new
  347. // If the timestamp is larger then the file has changed
  348. // Otherwise the file is still the same
  349. return !timestamp || timestamp > childCompiler.compilationStartedTimestamp;
  350. });
  351. return isCacheOutOfDate;
  352. }
  353. module.exports = {
  354. addTemplateToCompiler,
  355. compileTemplate,
  356. hasOutDatedTemplateCache,
  357. clearCache,
  358. getFileDependencies
  359. };