index.js 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568
  1. // @ts-check
  2. "use strict";
  3. const promisify = require("util").promisify;
  4. const vm = require("vm");
  5. const fs = require("fs");
  6. const path = require("path");
  7. const { CachedChildCompilation } = require("./lib/cached-child-compiler");
  8. const {
  9. createHtmlTagObject,
  10. htmlTagObjectToString,
  11. HtmlTagArray,
  12. } = require("./lib/html-tags");
  13. const prettyError = require("./lib/errors.js");
  14. const chunkSorter = require("./lib/chunksorter.js");
  15. const { AsyncSeriesWaterfallHook } = require("tapable");
  16. /** @typedef {import("./typings").HtmlTagObject} HtmlTagObject */
  17. /** @typedef {import("./typings").Options} HtmlWebpackOptions */
  18. /** @typedef {import("./typings").ProcessedOptions} ProcessedHtmlWebpackOptions */
  19. /** @typedef {import("./typings").TemplateParameter} TemplateParameter */
  20. /** @typedef {import("webpack").Compiler} Compiler */
  21. /** @typedef {import("webpack").Compilation} Compilation */
  22. /** @typedef {Required<Compilation["outputOptions"]["publicPath"]>} PublicPath */
  23. /** @typedef {ReturnType<Compiler["getInfrastructureLogger"]>} Logger */
  24. /** @typedef {Compilation["entrypoints"] extends Map<string, infer I> ? I : never} Entrypoint */
  25. /** @typedef {Array<{ name: string, source: import('webpack').sources.Source, info?: import('webpack').AssetInfo }>} PreviousEmittedAssets */
  26. /** @typedef {{ publicPath: string, js: Array<string>, css: Array<string>, manifest?: string, favicon?: string }} AssetsInformationByGroups */
  27. /** @typedef {import("./typings").Hooks} HtmlWebpackPluginHooks */
  28. /**
  29. * @type {WeakMap<Compilation, HtmlWebpackPluginHooks>}}
  30. */
  31. const compilationHooksMap = new WeakMap();
  32. class HtmlWebpackPlugin {
  33. // The following is the API definition for all available hooks
  34. // For the TypeScript definition, see the Hooks type in typings.d.ts
  35. /**
  36. beforeAssetTagGeneration:
  37. AsyncSeriesWaterfallHook<{
  38. assets: {
  39. publicPath: string,
  40. js: Array<string>,
  41. css: Array<string>,
  42. favicon?: string | undefined,
  43. manifest?: string | undefined
  44. },
  45. outputName: string,
  46. plugin: HtmlWebpackPlugin
  47. }>,
  48. alterAssetTags:
  49. AsyncSeriesWaterfallHook<{
  50. assetTags: {
  51. scripts: Array<HtmlTagObject>,
  52. styles: Array<HtmlTagObject>,
  53. meta: Array<HtmlTagObject>,
  54. },
  55. publicPath: string,
  56. outputName: string,
  57. plugin: HtmlWebpackPlugin
  58. }>,
  59. alterAssetTagGroups:
  60. AsyncSeriesWaterfallHook<{
  61. headTags: Array<HtmlTagObject | HtmlTagObject>,
  62. bodyTags: Array<HtmlTagObject | HtmlTagObject>,
  63. publicPath: string,
  64. outputName: string,
  65. plugin: HtmlWebpackPlugin
  66. }>,
  67. afterTemplateExecution:
  68. AsyncSeriesWaterfallHook<{
  69. html: string,
  70. headTags: Array<HtmlTagObject | HtmlTagObject>,
  71. bodyTags: Array<HtmlTagObject | HtmlTagObject>,
  72. outputName: string,
  73. plugin: HtmlWebpackPlugin,
  74. }>,
  75. beforeEmit:
  76. AsyncSeriesWaterfallHook<{
  77. html: string,
  78. outputName: string,
  79. plugin: HtmlWebpackPlugin,
  80. }>,
  81. afterEmit:
  82. AsyncSeriesWaterfallHook<{
  83. outputName: string,
  84. plugin: HtmlWebpackPlugin
  85. }>
  86. */
  87. /**
  88. * Returns all public hooks of the html webpack plugin for the given compilation
  89. *
  90. * @param {Compilation} compilation
  91. * @returns {HtmlWebpackPluginHooks}
  92. */
  93. static getCompilationHooks(compilation) {
  94. let hooks = compilationHooksMap.get(compilation);
  95. if (!hooks) {
  96. hooks = {
  97. beforeAssetTagGeneration: new AsyncSeriesWaterfallHook(["pluginArgs"]),
  98. alterAssetTags: new AsyncSeriesWaterfallHook(["pluginArgs"]),
  99. alterAssetTagGroups: new AsyncSeriesWaterfallHook(["pluginArgs"]),
  100. afterTemplateExecution: new AsyncSeriesWaterfallHook(["pluginArgs"]),
  101. beforeEmit: new AsyncSeriesWaterfallHook(["pluginArgs"]),
  102. afterEmit: new AsyncSeriesWaterfallHook(["pluginArgs"]),
  103. };
  104. compilationHooksMap.set(compilation, hooks);
  105. }
  106. return hooks;
  107. }
  108. /**
  109. * @param {HtmlWebpackOptions} [options]
  110. */
  111. constructor(options) {
  112. /** @type {HtmlWebpackOptions} */
  113. // TODO remove me in the next major release
  114. this.userOptions = options || {};
  115. this.version = HtmlWebpackPlugin.version;
  116. // Default options
  117. /** @type {ProcessedHtmlWebpackOptions} */
  118. const defaultOptions = {
  119. template: "auto",
  120. templateContent: false,
  121. templateParameters: templateParametersGenerator,
  122. filename: "index.html",
  123. publicPath:
  124. this.userOptions.publicPath === undefined
  125. ? "auto"
  126. : this.userOptions.publicPath,
  127. hash: false,
  128. inject: this.userOptions.scriptLoading === "blocking" ? "body" : "head",
  129. scriptLoading: "defer",
  130. compile: true,
  131. favicon: false,
  132. minify: "auto",
  133. cache: true,
  134. showErrors: true,
  135. chunks: "all",
  136. excludeChunks: [],
  137. chunksSortMode: "auto",
  138. meta: {},
  139. base: false,
  140. title: "Webpack App",
  141. xhtml: false,
  142. };
  143. /** @type {ProcessedHtmlWebpackOptions} */
  144. this.options = Object.assign(defaultOptions, this.userOptions);
  145. }
  146. /**
  147. *
  148. * @param {Compiler} compiler
  149. * @returns {void}
  150. */
  151. apply(compiler) {
  152. this.logger = compiler.getInfrastructureLogger("HtmlWebpackPlugin");
  153. const options = this.options;
  154. options.template = this.getTemplatePath(
  155. this.options.template,
  156. compiler.context,
  157. );
  158. // Assert correct option spelling
  159. if (
  160. options.scriptLoading !== "defer" &&
  161. options.scriptLoading !== "blocking" &&
  162. options.scriptLoading !== "module" &&
  163. options.scriptLoading !== "systemjs-module"
  164. ) {
  165. /** @type {Logger} */
  166. (this.logger).error(
  167. 'The "scriptLoading" option need to be set to "defer", "blocking" or "module" or "systemjs-module"',
  168. );
  169. }
  170. if (
  171. options.inject !== true &&
  172. options.inject !== false &&
  173. options.inject !== "head" &&
  174. options.inject !== "body"
  175. ) {
  176. /** @type {Logger} */
  177. (this.logger).error(
  178. 'The `inject` option needs to be set to true, false, "head" or "body',
  179. );
  180. }
  181. if (
  182. this.options.templateParameters !== false &&
  183. typeof this.options.templateParameters !== "function" &&
  184. typeof this.options.templateParameters !== "object"
  185. ) {
  186. /** @type {Logger} */
  187. (this.logger).error(
  188. "The `templateParameters` has to be either a function or an object or false",
  189. );
  190. }
  191. // Default metaOptions if no template is provided
  192. if (
  193. !this.userOptions.template &&
  194. options.templateContent === false &&
  195. options.meta
  196. ) {
  197. options.meta = Object.assign(
  198. {},
  199. options.meta,
  200. {
  201. // TODO remove in the next major release
  202. // From https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
  203. viewport: "width=device-width, initial-scale=1",
  204. },
  205. this.userOptions.meta,
  206. );
  207. }
  208. // entryName to fileName conversion function
  209. const userOptionFilename =
  210. this.userOptions.filename || this.options.filename;
  211. const filenameFunction =
  212. typeof userOptionFilename === "function"
  213. ? userOptionFilename
  214. : // Replace '[name]' with entry name
  215. (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
  216. /** output filenames for the given entry names */
  217. const entryNames = Object.keys(compiler.options.entry);
  218. const outputFileNames = new Set(
  219. (entryNames.length ? entryNames : ["main"]).map(filenameFunction),
  220. );
  221. // Hook all options into the webpack compiler
  222. outputFileNames.forEach((outputFileName) => {
  223. // Instance variables to keep caching information for multiple builds
  224. const assetJson = { value: undefined };
  225. /**
  226. * store the previous generated asset to emit them even if the content did not change
  227. * to support watch mode for third party plugins like the clean-webpack-plugin or the compression plugin
  228. * @type {PreviousEmittedAssets}
  229. */
  230. const previousEmittedAssets = [];
  231. // Inject child compiler plugin
  232. const childCompilerPlugin = new CachedChildCompilation(compiler);
  233. if (!this.options.templateContent) {
  234. childCompilerPlugin.addEntry(this.options.template);
  235. }
  236. // convert absolute filename into relative so that webpack can
  237. // generate it at correct location
  238. let filename = outputFileName;
  239. if (path.resolve(filename) === path.normalize(filename)) {
  240. const outputPath =
  241. /** @type {string} - Once initialized the path is always a string */ (
  242. compiler.options.output.path
  243. );
  244. filename = path.relative(outputPath, filename);
  245. }
  246. compiler.hooks.thisCompilation.tap(
  247. "HtmlWebpackPlugin",
  248. /**
  249. * Hook into the webpack compilation
  250. * @param {Compilation} compilation
  251. */
  252. (compilation) => {
  253. compilation.hooks.processAssets.tapAsync(
  254. {
  255. name: "HtmlWebpackPlugin",
  256. stage:
  257. /**
  258. * Generate the html after minification and dev tooling is done
  259. */
  260. compiler.webpack.Compilation
  261. .PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
  262. },
  263. /**
  264. * Hook into the process assets hook
  265. * @param {any} _
  266. * @param {(err?: Error) => void} callback
  267. */
  268. (_, callback) => {
  269. this.generateHTML(
  270. compiler,
  271. compilation,
  272. filename,
  273. childCompilerPlugin,
  274. previousEmittedAssets,
  275. assetJson,
  276. callback,
  277. );
  278. },
  279. );
  280. },
  281. );
  282. });
  283. }
  284. /**
  285. * Helper to return the absolute template path with a fallback loader
  286. *
  287. * @private
  288. * @param {string} template The path to the template e.g. './index.html'
  289. * @param {string} context The webpack base resolution path for relative paths e.g. process.cwd()
  290. */
  291. getTemplatePath(template, context) {
  292. if (template === "auto") {
  293. template = path.resolve(context, "src/index.ejs");
  294. if (!fs.existsSync(template)) {
  295. template = path.join(__dirname, "default_index.ejs");
  296. }
  297. }
  298. // If the template doesn't use a loader use the lodash template loader
  299. if (template.indexOf("!") === -1) {
  300. template =
  301. require.resolve("./lib/loader.js") +
  302. "!" +
  303. path.resolve(context, template);
  304. }
  305. // Resolve template path
  306. return template.replace(
  307. /([!])([^/\\][^!?]+|[^/\\!?])($|\?[^!?\n]+$)/,
  308. (match, prefix, filepath, postfix) =>
  309. prefix + path.resolve(filepath) + postfix,
  310. );
  311. }
  312. /**
  313. * Return all chunks from the compilation result which match the exclude and include filters
  314. *
  315. * @private
  316. * @param {any} chunks
  317. * @param {string[]|'all'} includedChunks
  318. * @param {string[]} excludedChunks
  319. */
  320. filterEntryChunks(chunks, includedChunks, excludedChunks) {
  321. return chunks.filter((chunkName) => {
  322. // Skip if the chunks should be filtered and the given chunk was not added explicity
  323. if (
  324. Array.isArray(includedChunks) &&
  325. includedChunks.indexOf(chunkName) === -1
  326. ) {
  327. return false;
  328. }
  329. // Skip if the chunks should be filtered and the given chunk was excluded explicity
  330. if (
  331. Array.isArray(excludedChunks) &&
  332. excludedChunks.indexOf(chunkName) !== -1
  333. ) {
  334. return false;
  335. }
  336. // Add otherwise
  337. return true;
  338. });
  339. }
  340. /**
  341. * Helper to sort chunks
  342. *
  343. * @private
  344. * @param {string[]} entryNames
  345. * @param {string|((entryNameA: string, entryNameB: string) => number)} sortMode
  346. * @param {Compilation} compilation
  347. */
  348. sortEntryChunks(entryNames, sortMode, compilation) {
  349. // Custom function
  350. if (typeof sortMode === "function") {
  351. return entryNames.sort(sortMode);
  352. }
  353. // Check if the given sort mode is a valid chunkSorter sort mode
  354. if (typeof chunkSorter[sortMode] !== "undefined") {
  355. return chunkSorter[sortMode](entryNames, compilation, this.options);
  356. }
  357. throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
  358. }
  359. /**
  360. * Encode each path component using `encodeURIComponent` as files can contain characters
  361. * which needs special encoding in URLs like `+ `.
  362. *
  363. * Valid filesystem characters which need to be encoded for urls:
  364. *
  365. * # pound, % percent, & ampersand, { left curly bracket, } right curly bracket,
  366. * \ back slash, < left angle bracket, > right angle bracket, * asterisk, ? question mark,
  367. * blank spaces, $ dollar sign, ! exclamation point, ' single quotes, " double quotes,
  368. * : colon, @ at sign, + plus sign, ` backtick, | pipe, = equal sign
  369. *
  370. * However the query string must not be encoded:
  371. *
  372. * fo:demonstration-path/very fancy+name.js?path=/home?value=abc&value=def#zzz
  373. * ^ ^ ^ ^ ^ ^ ^ ^^ ^ ^ ^ ^ ^
  374. * | | | | | | | || | | | | |
  375. * encoded | | encoded | | || | | | | |
  376. * ignored ignored ignored ignored ignored
  377. *
  378. * @private
  379. * @param {string} filePath
  380. */
  381. urlencodePath(filePath) {
  382. // People use the filepath in quite unexpected ways.
  383. // Try to extract the first querystring of the url:
  384. //
  385. // some+path/demo.html?value=abc?def
  386. //
  387. const queryStringStart = filePath.indexOf("?");
  388. const urlPath =
  389. queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart);
  390. const queryString = filePath.substr(urlPath.length);
  391. // Encode all parts except '/' which are not part of the querystring:
  392. const encodedUrlPath = urlPath.split("/").map(encodeURIComponent).join("/");
  393. return encodedUrlPath + queryString;
  394. }
  395. /**
  396. * Appends a cache busting hash to the query string of the url
  397. * E.g. http://localhost:8080/ -> http://localhost:8080/?50c9096ba6183fd728eeb065a26ec175
  398. *
  399. * @private
  400. * @param {string | undefined} url
  401. * @param {string} hash
  402. */
  403. appendHash(url, hash) {
  404. if (!url) {
  405. return url;
  406. }
  407. return url + (url.indexOf("?") === -1 ? "?" : "&") + hash;
  408. }
  409. /**
  410. * Generate the relative or absolute base url to reference images, css, and javascript files
  411. * from within the html file - the publicPath
  412. *
  413. * @private
  414. * @param {Compilation} compilation
  415. * @param {string} filename
  416. * @param {string | 'auto'} customPublicPath
  417. * @returns {string}
  418. */
  419. getPublicPath(compilation, filename, customPublicPath) {
  420. /**
  421. * @type {string} the configured public path to the asset root
  422. * if a path publicPath is set in the current webpack config use it otherwise
  423. * fallback to a relative path
  424. */
  425. const webpackPublicPath = compilation.getAssetPath(
  426. /** @type {NonNullable<Compilation["outputOptions"]["publicPath"]>} */ (
  427. compilation.outputOptions.publicPath
  428. ),
  429. { hash: compilation.hash },
  430. );
  431. // Webpack 5 introduced "auto" as default value
  432. const isPublicPathDefined = webpackPublicPath !== "auto";
  433. let publicPath =
  434. // If the html-webpack-plugin options contain a custom public path unset it
  435. customPublicPath !== "auto"
  436. ? customPublicPath
  437. : isPublicPathDefined
  438. ? // If a hard coded public path exists use it
  439. webpackPublicPath
  440. : // If no public path was set get a relative url path
  441. path
  442. .relative(
  443. path.resolve(
  444. /** @type {string} */ (compilation.options.output.path),
  445. path.dirname(filename),
  446. ),
  447. /** @type {string} */ (compilation.options.output.path),
  448. )
  449. .split(path.sep)
  450. .join("/");
  451. if (publicPath.length && publicPath.substr(-1, 1) !== "/") {
  452. publicPath += "/";
  453. }
  454. return publicPath;
  455. }
  456. /**
  457. * The getAssetsForHTML extracts the asset information of a webpack compilation for all given entry names.
  458. *
  459. * @private
  460. * @param {Compilation} compilation
  461. * @param {string} outputName
  462. * @param {string[]} entryNames
  463. * @returns {AssetsInformationByGroups}
  464. */
  465. getAssetsInformationByGroups(compilation, outputName, entryNames) {
  466. /** The public path used inside the html file */
  467. const publicPath = this.getPublicPath(
  468. compilation,
  469. outputName,
  470. this.options.publicPath,
  471. );
  472. /**
  473. * @type {AssetsInformationByGroups}
  474. */
  475. const assets = {
  476. // The public path
  477. publicPath,
  478. // Will contain all js and mjs files
  479. js: [],
  480. // Will contain all css files
  481. css: [],
  482. // Will contain the html5 appcache manifest files if it exists
  483. manifest: Object.keys(compilation.assets).find(
  484. (assetFile) => path.extname(assetFile) === ".appcache",
  485. ),
  486. // Favicon
  487. favicon: undefined,
  488. };
  489. // Append a hash for cache busting
  490. if (this.options.hash && assets.manifest) {
  491. assets.manifest = this.appendHash(
  492. assets.manifest,
  493. /** @type {string} */ (compilation.hash),
  494. );
  495. }
  496. // Extract paths to .js, .mjs and .css files from the current compilation
  497. const entryPointPublicPathMap = {};
  498. const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
  499. for (let i = 0; i < entryNames.length; i++) {
  500. const entryName = entryNames[i];
  501. /** entryPointUnfilteredFiles - also includes hot module update files */
  502. const entryPointUnfilteredFiles = /** @type {Entrypoint} */ (
  503. compilation.entrypoints.get(entryName)
  504. ).getFiles();
  505. const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
  506. const asset = compilation.getAsset(chunkFile);
  507. if (!asset) {
  508. return true;
  509. }
  510. // Prevent hot-module files from being included:
  511. const assetMetaInformation = asset.info || {};
  512. return !(
  513. assetMetaInformation.hotModuleReplacement ||
  514. assetMetaInformation.development
  515. );
  516. });
  517. // Prepend the publicPath and append the hash depending on the
  518. // webpack.output.publicPath and hashOptions
  519. // E.g. bundle.js -> /bundle.js?hash
  520. const entryPointPublicPaths = entryPointFiles.map((chunkFile) => {
  521. const entryPointPublicPath = publicPath + this.urlencodePath(chunkFile);
  522. return this.options.hash
  523. ? this.appendHash(
  524. entryPointPublicPath,
  525. /** @type {string} */ (compilation.hash),
  526. )
  527. : entryPointPublicPath;
  528. });
  529. entryPointPublicPaths.forEach((entryPointPublicPath) => {
  530. const extMatch = extensionRegexp.exec(
  531. /** @type {string} */ (entryPointPublicPath),
  532. );
  533. // Skip if the public path is not a .css, .mjs or .js file
  534. if (!extMatch) {
  535. return;
  536. }
  537. // Skip if this file is already known
  538. // (e.g. because of common chunk optimizations)
  539. if (entryPointPublicPathMap[entryPointPublicPath]) {
  540. return;
  541. }
  542. entryPointPublicPathMap[entryPointPublicPath] = true;
  543. // ext will contain .js or .css, because .mjs recognizes as .js
  544. const ext = extMatch[1] === "mjs" ? "js" : extMatch[1];
  545. assets[ext].push(entryPointPublicPath);
  546. });
  547. }
  548. return assets;
  549. }
  550. /**
  551. * Once webpack is done with compiling the template into a NodeJS code this function
  552. * evaluates it to generate the html result
  553. *
  554. * The evaluateCompilationResult is only a class function to allow spying during testing.
  555. * Please change that in a further refactoring
  556. *
  557. * @param {string} source
  558. * @param {string} publicPath
  559. * @param {string} templateFilename
  560. * @returns {Promise<string | (() => string | Promise<string>)>}
  561. */
  562. evaluateCompilationResult(source, publicPath, templateFilename) {
  563. if (!source) {
  564. return Promise.reject(
  565. new Error("The child compilation didn't provide a result"),
  566. );
  567. }
  568. // The LibraryTemplatePlugin stores the template result in a local variable.
  569. // By adding it to the end the value gets extracted during evaluation
  570. if (source.indexOf("HTML_WEBPACK_PLUGIN_RESULT") >= 0) {
  571. source += ";\nHTML_WEBPACK_PLUGIN_RESULT";
  572. }
  573. const templateWithoutLoaders = templateFilename
  574. .replace(/^.+!/, "")
  575. .replace(/\?.+$/, "");
  576. const globalClone = Object.create(
  577. Object.getPrototypeOf(global),
  578. Object.getOwnPropertyDescriptors(global),
  579. );
  580. // Presence of `eval` and `Function` breaks template's explicit `eval` call
  581. // Ref: https://github.com/nodejs/help/issues/2880
  582. delete globalClone.eval;
  583. delete globalClone.Function;
  584. // Not using `...global` as it throws when localStorage is not explicitly enabled in Node 25+
  585. // Provide a CommonJS-style `module`/`exports` pair so templates compiled as CommonJS
  586. // (e.g. Rspack's child compilation output, which wraps the result in `module.exports = ...`)
  587. // can assign to them instead of failing with `module is not defined`.
  588. const sandboxModule = { exports: {} };
  589. const vmContext = vm.createContext(
  590. Object.assign(globalClone, {
  591. HTML_WEBPACK_PLUGIN: true,
  592. // Copying nonstandard globals like `require` explicitly as they may be absent from `global`
  593. require: require,
  594. module: sandboxModule,
  595. exports: sandboxModule.exports,
  596. htmlWebpackPluginPublicPath: publicPath,
  597. __filename: templateWithoutLoaders,
  598. __dirname: path.dirname(templateWithoutLoaders),
  599. }),
  600. );
  601. const vmScript = new vm.Script(source, {
  602. filename: templateWithoutLoaders,
  603. });
  604. // Evaluate code and cast to string
  605. let newSource;
  606. try {
  607. newSource = vmScript.runInContext(vmContext);
  608. } catch (e) {
  609. return Promise.reject(e);
  610. }
  611. if (
  612. typeof newSource === "object" &&
  613. newSource.__esModule &&
  614. newSource.default !== undefined
  615. ) {
  616. newSource = newSource.default;
  617. }
  618. return typeof newSource === "string" || typeof newSource === "function"
  619. ? Promise.resolve(newSource)
  620. : Promise.reject(
  621. new Error(
  622. 'The loader "' + templateWithoutLoaders + "\" didn't return html.",
  623. ),
  624. );
  625. }
  626. /**
  627. * Add toString methods for easier rendering inside the template
  628. *
  629. * @private
  630. * @param {Array<HtmlTagObject>} assetTagGroup
  631. * @returns {Array<HtmlTagObject>}
  632. */
  633. prepareAssetTagGroupForRendering(assetTagGroup) {
  634. const xhtml = this.options.xhtml;
  635. return HtmlTagArray.from(
  636. assetTagGroup.map((assetTag) => {
  637. const copiedAssetTag = Object.assign({}, assetTag);
  638. copiedAssetTag.toString = function () {
  639. return htmlTagObjectToString(this, xhtml);
  640. };
  641. return copiedAssetTag;
  642. }),
  643. );
  644. }
  645. /**
  646. * Generate the template parameters for the template function
  647. *
  648. * @private
  649. * @param {Compilation} compilation
  650. * @param {AssetsInformationByGroups} assetsInformationByGroups
  651. * @param {{
  652. headTags: HtmlTagObject[],
  653. bodyTags: HtmlTagObject[]
  654. }} assetTags
  655. * @returns {Promise<{[key: any]: any}>}
  656. */
  657. getTemplateParameters(compilation, assetsInformationByGroups, assetTags) {
  658. const templateParameters = this.options.templateParameters;
  659. if (templateParameters === false) {
  660. return Promise.resolve({});
  661. }
  662. if (
  663. typeof templateParameters !== "function" &&
  664. typeof templateParameters !== "object"
  665. ) {
  666. throw new Error(
  667. "templateParameters has to be either a function or an object",
  668. );
  669. }
  670. const templateParameterFunction =
  671. typeof templateParameters === "function"
  672. ? // A custom function can overwrite the entire template parameter preparation
  673. templateParameters
  674. : // If the template parameters is an object merge it with the default values
  675. (compilation, assetsInformationByGroups, assetTags, options) =>
  676. Object.assign(
  677. {},
  678. templateParametersGenerator(
  679. compilation,
  680. assetsInformationByGroups,
  681. assetTags,
  682. options,
  683. ),
  684. templateParameters,
  685. );
  686. const preparedAssetTags = {
  687. headTags: this.prepareAssetTagGroupForRendering(assetTags.headTags),
  688. bodyTags: this.prepareAssetTagGroupForRendering(assetTags.bodyTags),
  689. };
  690. return Promise.resolve().then(() =>
  691. templateParameterFunction(
  692. compilation,
  693. assetsInformationByGroups,
  694. preparedAssetTags,
  695. this.options,
  696. ),
  697. );
  698. }
  699. /**
  700. * This function renders the actual html by executing the template function
  701. *
  702. * @private
  703. * @param {(templateParameters) => string | Promise<string>} templateFunction
  704. * @param {AssetsInformationByGroups} assetsInformationByGroups
  705. * @param {{
  706. headTags: HtmlTagObject[],
  707. bodyTags: HtmlTagObject[]
  708. }} assetTags
  709. * @param {Compilation} compilation
  710. * @returns Promise<string>
  711. */
  712. executeTemplate(
  713. templateFunction,
  714. assetsInformationByGroups,
  715. assetTags,
  716. compilation,
  717. ) {
  718. // Template processing
  719. const templateParamsPromise = this.getTemplateParameters(
  720. compilation,
  721. assetsInformationByGroups,
  722. assetTags,
  723. );
  724. return templateParamsPromise.then((templateParams) => {
  725. try {
  726. // If html is a promise return the promise
  727. // If html is a string turn it into a promise
  728. return templateFunction(templateParams);
  729. } catch (e) {
  730. // @ts-ignore
  731. compilation.errors.push(new Error("Template execution failed: " + e));
  732. return Promise.reject(e);
  733. }
  734. });
  735. }
  736. /**
  737. * Html Post processing
  738. *
  739. * @private
  740. * @param {Compiler} compiler The compiler instance
  741. * @param {any} originalHtml The input html
  742. * @param {AssetsInformationByGroups} assetsInformationByGroups
  743. * @param {{headTags: HtmlTagObject[], bodyTags: HtmlTagObject[]}} assetTags The asset tags to inject
  744. * @returns {Promise<string>}
  745. */
  746. postProcessHtml(
  747. compiler,
  748. originalHtml,
  749. assetsInformationByGroups,
  750. assetTags,
  751. ) {
  752. let html = originalHtml;
  753. if (typeof html !== "string") {
  754. return Promise.reject(
  755. new Error(
  756. "Expected html to be a string but got " + JSON.stringify(html),
  757. ),
  758. );
  759. }
  760. if (this.options.inject) {
  761. const htmlRegExp = /(<html[^>]*>)/i;
  762. const headRegExp = /(<\/head\s*>)/i;
  763. const bodyRegExp = /(<\/body\s*>)/i;
  764. const metaViewportRegExp = /<meta[^>]+name=["']viewport["'][^>]*>/i;
  765. const body = assetTags.bodyTags.map((assetTagObject) =>
  766. htmlTagObjectToString(assetTagObject, this.options.xhtml),
  767. );
  768. const head = assetTags.headTags
  769. .filter((item) => {
  770. if (
  771. item.tagName === "meta" &&
  772. item.attributes &&
  773. item.attributes.name === "viewport" &&
  774. metaViewportRegExp.test(html)
  775. ) {
  776. return false;
  777. }
  778. return true;
  779. })
  780. .map((assetTagObject) =>
  781. htmlTagObjectToString(assetTagObject, this.options.xhtml),
  782. );
  783. if (body.length) {
  784. if (bodyRegExp.test(html)) {
  785. // Append assets to body element
  786. html = html.replace(bodyRegExp, (match) => body.join("") + match);
  787. } else {
  788. // Append scripts to the end of the file if no <body> element exists:
  789. html += body.join("");
  790. }
  791. }
  792. if (head.length) {
  793. // Create a head tag if none exists
  794. if (!headRegExp.test(html)) {
  795. if (!htmlRegExp.test(html)) {
  796. html = "<head></head>" + html;
  797. } else {
  798. html = html.replace(htmlRegExp, (match) => match + "<head></head>");
  799. }
  800. }
  801. // Append assets to head element
  802. html = html.replace(headRegExp, (match) => head.join("") + match);
  803. }
  804. // Inject manifest into the opening html tag
  805. if (assetsInformationByGroups.manifest) {
  806. html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {
  807. // Append the manifest only if no manifest was specified
  808. if (/\smanifest\s*=/.test(match)) {
  809. return match;
  810. }
  811. return (
  812. start +
  813. ' manifest="' +
  814. assetsInformationByGroups.manifest +
  815. '"' +
  816. end
  817. );
  818. });
  819. }
  820. }
  821. // TODO avoid this logic and use https://github.com/webpack-contrib/html-minimizer-webpack-plugin under the hood in the next major version
  822. // Check if webpack is running in production mode
  823. // @see https://github.com/webpack/webpack/blob/3366421f1784c449f415cda5930a8e445086f688/lib/WebpackOptionsDefaulter.js#L12-L14
  824. const isProductionLikeMode =
  825. compiler.options.mode === "production" || !compiler.options.mode;
  826. const needMinify =
  827. this.options.minify === true ||
  828. typeof this.options.minify === "object" ||
  829. (this.options.minify === "auto" && isProductionLikeMode);
  830. if (!needMinify) {
  831. return Promise.resolve(html);
  832. }
  833. const minifyOptions =
  834. typeof this.options.minify === "object"
  835. ? this.options.minify
  836. : {
  837. // https://www.npmjs.com/package/html-minifier-terser#options-quick-reference
  838. collapseWhitespace: true,
  839. keepClosingSlash: true,
  840. removeComments: true,
  841. removeRedundantAttributes: true,
  842. removeScriptTypeAttributes: true,
  843. removeStyleLinkTypeAttributes: true,
  844. useShortDoctype: true,
  845. };
  846. try {
  847. html = require("html-minifier-terser").minify(html, minifyOptions);
  848. } catch (e) {
  849. const isParseError = String(e.message).indexOf("Parse Error") === 0;
  850. if (isParseError) {
  851. e.message =
  852. "html-webpack-plugin could not minify the generated output.\n" +
  853. "In production mode the html minification is enabled by default.\n" +
  854. "If you are not generating a valid html output please disable it manually.\n" +
  855. "You can do so by adding the following setting to your HtmlWebpackPlugin config:\n|\n|" +
  856. " minify: false\n|\n" +
  857. "See https://github.com/jantimon/html-webpack-plugin#options for details.\n\n" +
  858. "For parser dedicated bugs please create an issue here:\n" +
  859. "https://danielruf.github.io/html-minifier-terser/" +
  860. "\n" +
  861. e.message;
  862. }
  863. return Promise.reject(e);
  864. }
  865. return Promise.resolve(html);
  866. }
  867. /**
  868. * Helper to return a sorted unique array of all asset files out of the asset object
  869. * @private
  870. */
  871. getAssetFiles(assets) {
  872. const files = [
  873. ...new Set(
  874. Object.keys(assets)
  875. .filter((assetType) => assetType !== "chunks" && assets[assetType])
  876. .reduce((files, assetType) => files.concat(assets[assetType]), []),
  877. ),
  878. ];
  879. files.sort();
  880. return files;
  881. }
  882. /**
  883. * Converts a favicon file from disk to a webpack resource and returns the url to the resource
  884. *
  885. * @private
  886. * @param {Compiler} compiler
  887. * @param {string|false} favicon
  888. * @param {Compilation} compilation
  889. * @param {string} publicPath
  890. * @param {PreviousEmittedAssets} previousEmittedAssets
  891. * @returns {Promise<string|undefined>}
  892. */
  893. generateFavicon(
  894. compiler,
  895. favicon,
  896. compilation,
  897. publicPath,
  898. previousEmittedAssets,
  899. ) {
  900. if (!favicon) {
  901. return Promise.resolve(undefined);
  902. }
  903. const filename = path.resolve(compilation.compiler.context, favicon);
  904. return promisify(compilation.inputFileSystem.readFile)(filename)
  905. .then((buf) => {
  906. const source = new compiler.webpack.sources.RawSource(
  907. /** @type {string | Buffer} */ (buf),
  908. false,
  909. );
  910. const name = path.basename(filename);
  911. compilation.fileDependencies.add(filename);
  912. compilation.emitAsset(name, source);
  913. previousEmittedAssets.push({ name, source });
  914. const faviconPath = publicPath + name;
  915. if (this.options.hash) {
  916. return this.appendHash(
  917. faviconPath,
  918. /** @type {string} */ (compilation.hash),
  919. );
  920. }
  921. return faviconPath;
  922. })
  923. .catch(() =>
  924. Promise.reject(
  925. new Error("HtmlWebpackPlugin: could not load file " + filename),
  926. ),
  927. );
  928. }
  929. /**
  930. * Generate all tags script for the given file paths
  931. *
  932. * @private
  933. * @param {Array<string>} jsAssets
  934. * @returns {Array<HtmlTagObject>}
  935. */
  936. generatedScriptTags(jsAssets) {
  937. // @ts-ignore
  938. return jsAssets.map((src) => {
  939. const attributes = {};
  940. if (this.options.scriptLoading === "defer") {
  941. attributes.defer = true;
  942. } else if (this.options.scriptLoading === "module") {
  943. attributes.type = "module";
  944. } else if (this.options.scriptLoading === "systemjs-module") {
  945. attributes.type = "systemjs-module";
  946. }
  947. attributes.src = src;
  948. return {
  949. tagName: "script",
  950. voidTag: false,
  951. meta: { plugin: "html-webpack-plugin" },
  952. attributes,
  953. };
  954. });
  955. }
  956. /**
  957. * Generate all style tags for the given file paths
  958. *
  959. * @private
  960. * @param {Array<string>} cssAssets
  961. * @returns {Array<HtmlTagObject>}
  962. */
  963. generateStyleTags(cssAssets) {
  964. return cssAssets.map((styleAsset) => ({
  965. tagName: "link",
  966. voidTag: true,
  967. meta: { plugin: "html-webpack-plugin" },
  968. attributes: {
  969. href: styleAsset,
  970. rel: "stylesheet",
  971. },
  972. }));
  973. }
  974. /**
  975. * Generate an optional base tag
  976. *
  977. * @param {string | {[attributeName: string]: string}} base
  978. * @returns {Array<HtmlTagObject>}
  979. */
  980. generateBaseTag(base) {
  981. return [
  982. {
  983. tagName: "base",
  984. voidTag: true,
  985. meta: { plugin: "html-webpack-plugin" },
  986. // attributes e.g. { href:"http://example.com/page.html" target:"_blank" }
  987. attributes:
  988. typeof base === "string"
  989. ? {
  990. href: base,
  991. }
  992. : base,
  993. },
  994. ];
  995. }
  996. /**
  997. * Generate all meta tags for the given meta configuration
  998. *
  999. * @private
  1000. * @param {false | {[name: string]: false | string | {[attributeName: string]: string|boolean}}} metaOptions
  1001. * @returns {Array<HtmlTagObject>}
  1002. */
  1003. generatedMetaTags(metaOptions) {
  1004. if (metaOptions === false) {
  1005. return [];
  1006. }
  1007. // Make tags self-closing in case of xhtml
  1008. // Turn { "viewport" : "width=500, initial-scale=1" } into
  1009. // [{ name:"viewport" content:"width=500, initial-scale=1" }]
  1010. const metaTagAttributeObjects = Object.keys(metaOptions)
  1011. .map((metaName) => {
  1012. const metaTagContent = metaOptions[metaName];
  1013. return typeof metaTagContent === "string"
  1014. ? {
  1015. name: metaName,
  1016. content: metaTagContent,
  1017. }
  1018. : metaTagContent;
  1019. })
  1020. .filter((attribute) => attribute !== false);
  1021. // Turn [{ name:"viewport" content:"width=500, initial-scale=1" }] into
  1022. // the html-webpack-plugin tag structure
  1023. return metaTagAttributeObjects.map((metaTagAttributes) => {
  1024. if (metaTagAttributes === false) {
  1025. throw new Error("Invalid meta tag");
  1026. }
  1027. return {
  1028. tagName: "meta",
  1029. voidTag: true,
  1030. meta: { plugin: "html-webpack-plugin" },
  1031. attributes: metaTagAttributes,
  1032. };
  1033. });
  1034. }
  1035. /**
  1036. * Generate a favicon tag for the given file path
  1037. *
  1038. * @private
  1039. * @param {string} favicon
  1040. * @returns {Array<HtmlTagObject>}
  1041. */
  1042. generateFaviconTag(favicon) {
  1043. return [
  1044. {
  1045. tagName: "link",
  1046. voidTag: true,
  1047. meta: { plugin: "html-webpack-plugin" },
  1048. attributes: {
  1049. rel: "icon",
  1050. href: favicon,
  1051. },
  1052. },
  1053. ];
  1054. }
  1055. /**
  1056. * Group assets to head and body tags
  1057. *
  1058. * @param {{
  1059. scripts: Array<HtmlTagObject>;
  1060. styles: Array<HtmlTagObject>;
  1061. meta: Array<HtmlTagObject>;
  1062. }} assetTags
  1063. * @param {"body" | "head"} scriptTarget
  1064. * @returns {{
  1065. headTags: Array<HtmlTagObject>;
  1066. bodyTags: Array<HtmlTagObject>;
  1067. }}
  1068. */
  1069. groupAssetsByElements(assetTags, scriptTarget) {
  1070. /** @type {{ headTags: Array<HtmlTagObject>; bodyTags: Array<HtmlTagObject>; }} */
  1071. const result = {
  1072. headTags: [...assetTags.meta, ...assetTags.styles],
  1073. bodyTags: [],
  1074. };
  1075. // Add script tags to head or body depending on
  1076. // the htmlPluginOptions
  1077. if (scriptTarget === "body") {
  1078. result.bodyTags.push(...assetTags.scripts);
  1079. } else {
  1080. // If script loading is blocking add the scripts to the end of the head
  1081. // If script loading is non-blocking add the scripts in front of the css files
  1082. const insertPosition =
  1083. this.options.scriptLoading === "blocking"
  1084. ? result.headTags.length
  1085. : assetTags.meta.length;
  1086. result.headTags.splice(insertPosition, 0, ...assetTags.scripts);
  1087. }
  1088. return result;
  1089. }
  1090. /**
  1091. * Replace [contenthash] in filename
  1092. *
  1093. * @see https://survivejs.com/webpack/optimizing/adding-hashes-to-filenames/
  1094. *
  1095. * @private
  1096. * @param {Compiler} compiler
  1097. * @param {string} filename
  1098. * @param {string|Buffer} fileContent
  1099. * @param {Compilation} compilation
  1100. * @returns {{ path: string, info: {} }}
  1101. */
  1102. replacePlaceholdersInFilename(compiler, filename, fileContent, compilation) {
  1103. if (/\[\\*([\w:]+)\\*\]/i.test(filename) === false) {
  1104. return { path: filename, info: {} };
  1105. }
  1106. const hash = compiler.webpack.util.createHash(
  1107. compilation.outputOptions.hashFunction,
  1108. );
  1109. hash.update(fileContent);
  1110. if (compilation.outputOptions.hashSalt) {
  1111. hash.update(compilation.outputOptions.hashSalt);
  1112. }
  1113. const contentHash = /** @type {string} */ (
  1114. hash
  1115. .digest(compilation.outputOptions.hashDigest)
  1116. .slice(0, compilation.outputOptions.hashDigestLength)
  1117. );
  1118. return compilation.getPathWithInfo(filename, {
  1119. contentHash,
  1120. chunk: {
  1121. hash: contentHash,
  1122. // @ts-ignore
  1123. contentHash,
  1124. },
  1125. });
  1126. }
  1127. /**
  1128. * Function to generate HTML file.
  1129. *
  1130. * @private
  1131. * @param {Compiler} compiler
  1132. * @param {Compilation} compilation
  1133. * @param {string} outputName
  1134. * @param {CachedChildCompilation} childCompilerPlugin
  1135. * @param {PreviousEmittedAssets} previousEmittedAssets
  1136. * @param {{ value: string | undefined }} assetJson
  1137. * @param {(err?: Error) => void} callback
  1138. */
  1139. generateHTML(
  1140. compiler,
  1141. compilation,
  1142. outputName,
  1143. childCompilerPlugin,
  1144. previousEmittedAssets,
  1145. assetJson,
  1146. callback,
  1147. ) {
  1148. // Get all entry point names for this html file
  1149. const entryNames = Array.from(compilation.entrypoints.keys());
  1150. const filteredEntryNames = this.filterEntryChunks(
  1151. entryNames,
  1152. this.options.chunks,
  1153. this.options.excludeChunks,
  1154. );
  1155. const sortedEntryNames = this.sortEntryChunks(
  1156. filteredEntryNames,
  1157. this.options.chunksSortMode,
  1158. compilation,
  1159. );
  1160. const templateResult = this.options.templateContent
  1161. ? { mainCompilationHash: compilation.hash }
  1162. : childCompilerPlugin.getCompilationEntryResult(this.options.template);
  1163. if ("error" in templateResult) {
  1164. compilation.errors.push(
  1165. new Error(
  1166. prettyError(templateResult.error, compiler.context).toString(),
  1167. ),
  1168. );
  1169. }
  1170. // If the child compilation was not executed during a previous main compile run
  1171. // it is a cached result
  1172. const isCompilationCached =
  1173. templateResult.mainCompilationHash !== compilation.hash;
  1174. /** Generated file paths from the entry point names */
  1175. const assetsInformationByGroups = this.getAssetsInformationByGroups(
  1176. compilation,
  1177. outputName,
  1178. sortedEntryNames,
  1179. );
  1180. // If the template and the assets did not change we don't have to emit the html
  1181. const newAssetJson = JSON.stringify(
  1182. this.getAssetFiles(assetsInformationByGroups),
  1183. );
  1184. if (
  1185. isCompilationCached &&
  1186. this.options.cache &&
  1187. assetJson.value === newAssetJson
  1188. ) {
  1189. previousEmittedAssets.forEach(({ name, source, info }) => {
  1190. compilation.emitAsset(name, source, info);
  1191. });
  1192. return callback();
  1193. } else {
  1194. previousEmittedAssets.length = 0;
  1195. assetJson.value = newAssetJson;
  1196. }
  1197. // The html-webpack plugin uses a object representation for the html-tags which will be injected
  1198. // to allow altering them more easily
  1199. // Just before they are converted a third-party-plugin author might change the order and content
  1200. const assetsPromise = this.generateFavicon(
  1201. compiler,
  1202. this.options.favicon,
  1203. compilation,
  1204. assetsInformationByGroups.publicPath,
  1205. previousEmittedAssets,
  1206. ).then((faviconPath) => {
  1207. assetsInformationByGroups.favicon = faviconPath;
  1208. return HtmlWebpackPlugin.getCompilationHooks(
  1209. compilation,
  1210. ).beforeAssetTagGeneration.promise({
  1211. assets: assetsInformationByGroups,
  1212. outputName,
  1213. plugin: this,
  1214. });
  1215. });
  1216. // Turn the js and css paths into grouped HtmlTagObjects
  1217. const assetTagGroupsPromise = assetsPromise
  1218. // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
  1219. .then(({ assets }) =>
  1220. HtmlWebpackPlugin.getCompilationHooks(
  1221. compilation,
  1222. ).alterAssetTags.promise({
  1223. assetTags: {
  1224. scripts: this.generatedScriptTags(assets.js),
  1225. styles: this.generateStyleTags(assets.css),
  1226. meta: [
  1227. ...(this.options.base !== false
  1228. ? this.generateBaseTag(this.options.base)
  1229. : []),
  1230. ...this.generatedMetaTags(this.options.meta),
  1231. ...(assets.favicon
  1232. ? this.generateFaviconTag(assets.favicon)
  1233. : []),
  1234. ],
  1235. },
  1236. outputName,
  1237. publicPath: assetsInformationByGroups.publicPath,
  1238. plugin: this,
  1239. }),
  1240. )
  1241. .then(({ assetTags }) => {
  1242. // Inject scripts to body unless it set explicitly to head
  1243. const scriptTarget =
  1244. this.options.inject === "head" ||
  1245. (this.options.inject !== "body" &&
  1246. this.options.scriptLoading !== "blocking")
  1247. ? "head"
  1248. : "body";
  1249. // Group assets to `head` and `body` tag arrays
  1250. const assetGroups = this.groupAssetsByElements(assetTags, scriptTarget);
  1251. // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
  1252. return HtmlWebpackPlugin.getCompilationHooks(
  1253. compilation,
  1254. ).alterAssetTagGroups.promise({
  1255. headTags: assetGroups.headTags,
  1256. bodyTags: assetGroups.bodyTags,
  1257. outputName,
  1258. publicPath: assetsInformationByGroups.publicPath,
  1259. plugin: this,
  1260. });
  1261. });
  1262. // Turn the compiled template into a nodejs function or into a nodejs string
  1263. const templateEvaluationPromise = Promise.resolve().then(() => {
  1264. if ("error" in templateResult) {
  1265. return this.options.showErrors
  1266. ? prettyError(templateResult.error, compiler.context).toHtml()
  1267. : "ERROR";
  1268. }
  1269. // Allow to use a custom function / string instead
  1270. if (this.options.templateContent !== false) {
  1271. return this.options.templateContent;
  1272. }
  1273. // Once everything is compiled evaluate the html factory and replace it with its content
  1274. if ("compiledEntry" in templateResult) {
  1275. const compiledEntry = templateResult.compiledEntry;
  1276. const assets = compiledEntry.assets;
  1277. // Store assets from child compiler to re-emit them later
  1278. for (const name in assets) {
  1279. previousEmittedAssets.push({
  1280. name,
  1281. source: assets[name].source,
  1282. info: assets[name].info,
  1283. });
  1284. }
  1285. return this.evaluateCompilationResult(
  1286. compiledEntry.content,
  1287. assetsInformationByGroups.publicPath,
  1288. this.options.template,
  1289. );
  1290. }
  1291. return Promise.reject(
  1292. new Error("Child compilation contained no compiledEntry"),
  1293. );
  1294. });
  1295. const templateExecutionPromise = Promise.all([
  1296. assetsPromise,
  1297. assetTagGroupsPromise,
  1298. templateEvaluationPromise,
  1299. ])
  1300. // Execute the template
  1301. .then(([assetsHookResult, assetTags, compilationResult]) =>
  1302. typeof compilationResult !== "function"
  1303. ? compilationResult
  1304. : this.executeTemplate(
  1305. compilationResult,
  1306. assetsHookResult.assets,
  1307. { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags },
  1308. compilation,
  1309. ),
  1310. );
  1311. const injectedHtmlPromise = Promise.all([
  1312. assetTagGroupsPromise,
  1313. templateExecutionPromise,
  1314. ])
  1315. // Allow plugins to change the html before assets are injected
  1316. .then(([assetTags, html]) => {
  1317. const pluginArgs = {
  1318. html,
  1319. headTags: assetTags.headTags,
  1320. bodyTags: assetTags.bodyTags,
  1321. plugin: this,
  1322. outputName,
  1323. };
  1324. return HtmlWebpackPlugin.getCompilationHooks(
  1325. compilation,
  1326. ).afterTemplateExecution.promise(pluginArgs);
  1327. })
  1328. .then(({ html, headTags, bodyTags }) => {
  1329. return this.postProcessHtml(compiler, html, assetsInformationByGroups, {
  1330. headTags,
  1331. bodyTags,
  1332. });
  1333. });
  1334. const emitHtmlPromise = injectedHtmlPromise
  1335. // Allow plugins to change the html after assets are injected
  1336. .then((html) => {
  1337. const pluginArgs = { html, plugin: this, outputName };
  1338. return HtmlWebpackPlugin.getCompilationHooks(compilation)
  1339. .beforeEmit.promise(pluginArgs)
  1340. .then((result) => result.html);
  1341. })
  1342. .catch((err) => {
  1343. // In case anything went wrong the promise is resolved
  1344. // with the error message and an error is logged
  1345. compilation.errors.push(
  1346. new Error(prettyError(err, compiler.context).toString()),
  1347. );
  1348. return this.options.showErrors
  1349. ? prettyError(err, compiler.context).toHtml()
  1350. : "ERROR";
  1351. })
  1352. .then((html) => {
  1353. const filename = outputName.replace(
  1354. /\[templatehash([^\]]*)\]/g,
  1355. require("util").deprecate(
  1356. (match, options) => `[contenthash${options}]`,
  1357. "[templatehash] is now [contenthash]",
  1358. ),
  1359. );
  1360. const replacedFilename = this.replacePlaceholdersInFilename(
  1361. compiler,
  1362. filename,
  1363. html,
  1364. compilation,
  1365. );
  1366. const source = new compiler.webpack.sources.RawSource(html, false);
  1367. // Add the evaluated html code to the webpack assets
  1368. compilation.emitAsset(
  1369. replacedFilename.path,
  1370. source,
  1371. replacedFilename.info,
  1372. );
  1373. previousEmittedAssets.push({ name: replacedFilename.path, source });
  1374. return replacedFilename.path;
  1375. })
  1376. .then((finalOutputName) =>
  1377. HtmlWebpackPlugin.getCompilationHooks(compilation)
  1378. .afterEmit.promise({
  1379. outputName: finalOutputName,
  1380. plugin: this,
  1381. })
  1382. .catch((err) => {
  1383. /** @type {Logger} */
  1384. (this.logger).error(err);
  1385. return null;
  1386. })
  1387. .then(() => null),
  1388. );
  1389. // Once all files are added to the webpack compilation
  1390. // let the webpack compiler continue
  1391. emitHtmlPromise.then(() => {
  1392. callback();
  1393. });
  1394. }
  1395. }
  1396. /**
  1397. * The default for options.templateParameter
  1398. * Generate the template parameters
  1399. *
  1400. * Generate the template parameters for the template function
  1401. * @param {Compilation} compilation
  1402. * @param {AssetsInformationByGroups} assets
  1403. * @param {{
  1404. headTags: HtmlTagObject[],
  1405. bodyTags: HtmlTagObject[]
  1406. }} assetTags
  1407. * @param {ProcessedHtmlWebpackOptions} options
  1408. * @returns {TemplateParameter}
  1409. */
  1410. function templateParametersGenerator(compilation, assets, assetTags, options) {
  1411. return {
  1412. compilation: compilation,
  1413. webpackConfig: compilation.options,
  1414. htmlWebpackPlugin: {
  1415. tags: assetTags,
  1416. files: assets,
  1417. options: options,
  1418. },
  1419. };
  1420. }
  1421. // Statics:
  1422. /**
  1423. * The major version number of this plugin
  1424. */
  1425. HtmlWebpackPlugin.version = 5;
  1426. /**
  1427. * A static helper to get the hooks for this plugin
  1428. *
  1429. * Usage: HtmlWebpackPlugin.getHooks(compilation).HOOK_NAME.tapAsync('YourPluginName', () => { ... });
  1430. */
  1431. // TODO remove me in the next major release in favor getCompilationHooks
  1432. HtmlWebpackPlugin.getHooks = HtmlWebpackPlugin.getCompilationHooks;
  1433. HtmlWebpackPlugin.createHtmlTagObject = createHtmlTagObject;
  1434. module.exports = HtmlWebpackPlugin;