HtmlModulesPlugin.js 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const { pathToFileURL } = require("url");
  6. const { AsyncSeriesHook, AsyncSeriesWaterfallHook } = require("tapable");
  7. const { RawSource } = require("webpack-sources");
  8. const Compilation = require("../Compilation");
  9. const EntryOptionPlugin = require("../EntryOptionPlugin");
  10. const EntryPlugin = require("../EntryPlugin");
  11. const HotUpdateChunk = require("../HotUpdateChunk");
  12. const { HTML_TYPE } = require("../ModuleSourceTypeConstants");
  13. const { HTML_MODULE_TYPE } = require("../ModuleTypeConstants");
  14. const NormalModule = require("../NormalModule");
  15. const { toTemplateSourceFileName } = require("../TemplatedPathPlugin");
  16. const ConstDependency = require("../dependencies/ConstDependency");
  17. const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
  18. const WebpackError = require("../errors/WebpackError");
  19. const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
  20. const { compareModulesByFullName } = require("../util/comparators");
  21. const createHash = require("../util/createHash");
  22. const createHooksRegistry = require("../util/createHooksRegistry");
  23. const { getUndoPath, makePathsRelative } = require("../util/identifier");
  24. const implicitTypeLoaderFallback = require("../util/implicitTypeLoaderFallback");
  25. const lazyModule = require("../util/lazyModule");
  26. const memoize = require("../util/memoize");
  27. const { digestNonNumericOnly } = require("../util/nonNumericOnlyHash");
  28. const preloadModuleType = require("../util/preloadModuleType");
  29. const {
  30. PUBLIC_PATH_AUTO: autoPlaceholder
  31. } = require("../util/publicPathPlaceholder");
  32. const removeBOM = require("../util/removeBOM");
  33. const HtmlModule = require("./HtmlModule");
  34. const getMimeTypes = memoize(() => require("../util/mimeTypes"));
  35. const getHtmlSyntax = memoize(() => require("./syntax"));
  36. // Lazy: this plugin is applied on every build (`experiments.html` defaults on),
  37. // but the HTML parser and tokenizer are only reached once HTML is in the graph.
  38. const getHtmlParser = lazyModule(() => require("./HtmlParser"));
  39. // html generation only happens once a build actually has an html module
  40. const getHtmlGenerator = memoize(() => require("./HtmlGenerator"));
  41. /**
  42. * @import {
  43. * EntryDescriptionNormalized,
  44. * OutputHtmlOptions,
  45. * HtmlFaviconIcon
  46. * } from "../../declarations/WebpackOptions"
  47. */
  48. /** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["favicon"]} FaviconOption */
  49. /** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["manifest"]} ManifestOption */
  50. /** @typedef {Exclude<HtmlFaviconIcon, string>} FaviconIcon */
  51. /** @import Compiler from "../Compiler" */
  52. /** @import { HtmlModuleBuildInfo } from "./HtmlModule" */
  53. /** @import HtmlParser from "./HtmlParser" */
  54. /** @typedef {{ request: string, entryName: string, type: "script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch", css?: boolean }} HtmlEntryInfo */
  55. /** @typedef {{ outputName: string }} HtmlTransformHtmlContext */
  56. /** @typedef {{ outputName: string }} HtmlEmittedContext */
  57. /**
  58. * A tag to inject into an emitted page as a structured descriptor. Injected
  59. * verbatim (not re-bundled) at the `injectTo` position.
  60. * @typedef {object} HtmlTagDescriptor
  61. * @property {string} tag tag name, e.g. `"script"` / `"link"` / `"meta"`
  62. * @property {Record<string, string | boolean | undefined>=} attrs attributes; `true` renders a bare boolean attribute, `false`/`undefined` is omitted
  63. * @property {string=} children inner content (ignored for void elements like `<link>`/`<meta>`)
  64. * @property {("head" | "body" | "head-prepend" | "body-prepend")=} injectTo placement; defaults to `"head"`
  65. * @property {boolean=} voidTag force a void element (no closing tag); inferred from the tag name when omitted
  66. */
  67. /** @typedef {{ outputName: string, html: string }} HtmlInjectTagsContext */
  68. /**
  69. * A `<script>`/`<link>`/`<style>`/`<meta>` tag already present in an emitted
  70. * page, exposed for in-place mutation by `transformTags`. Mutate `attrs`, set
  71. * `remove`, or change `injectTo` to move it; don't reorder the array itself.
  72. * @typedef {object} HtmlMutableTag
  73. * @property {string} tag the (lowercased) tag name
  74. * @property {Record<string, string | boolean | undefined>} attrs mutable attributes; a string value renders `name="value"`, `true` a bare attribute, `false`/`undefined`/deleting the key drops it
  75. * @property {("head" | "body" | "head-prepend" | "body-prepend")=} injectTo the tag's current region (`"head"`/`"body"`); set a different value to move it there (`*-prepend` to the region's start)
  76. * @property {boolean=} remove set true to delete the whole element
  77. */
  78. /** @typedef {{ outputName: string, html: string }} HtmlTransformTagsContext */
  79. const createCompilationHooks = () => ({
  80. /**
  81. * Called with the list of extra tags to inject into each page (initially empty) plus the current HTML; push `HtmlTagDescriptor`s and return the list — webpack serializes and places them by `injectTo`. A structured alternative to the string-level `transformHtml` for adding tags; runs before CSP so injected inline tags are hashed.
  82. * @type {AsyncSeriesWaterfallHook<[HtmlTagDescriptor[], HtmlInjectTagsContext]>}
  83. * @since 5.109.0
  84. */
  85. injectTags: new AsyncSeriesWaterfallHook(["tags", "context"]),
  86. /**
  87. * Called with the page's `<script>`/`<link>`/`<style>`/`<meta>` tags (webpack's own and any injected) as mutable descriptors; mutate `attrs` (add a `nonce`/`data-*`, switch `defer`↔`async`, …), set `remove: true`, or change `injectTo` to move a tag between `<head>` and `<body>`, and webpack rewrites the changed tags. Add new tags with `injectTags` instead.
  88. * @type {AsyncSeriesHook<[HtmlMutableTag[], HtmlTransformTagsContext]>}
  89. * @since 5.109.0
  90. */
  91. transformTags: new AsyncSeriesHook(["tags", "context"]),
  92. /**
  93. * Called with each emitted page's final HTML (all sentinels resolved) just before it is written; return the (possibly transformed) HTML — e.g. to minify, inject a CSP meta, or rewrite tags.
  94. * @type {AsyncSeriesWaterfallHook<[string, HtmlTransformHtmlContext]>}
  95. * @since 5.109.0
  96. */
  97. transformHtml: new AsyncSeriesWaterfallHook(["html", "context"]),
  98. /**
  99. * Called once each page's HTML asset has been finalized — a post-emit notification (nothing to return).
  100. * @type {AsyncSeriesHook<[HtmlEmittedContext]>}
  101. * @since 5.109.0
  102. */
  103. htmlEmitted: new AsyncSeriesHook(["context"])
  104. });
  105. /**
  106. * @typedef {ReturnType<typeof createCompilationHooks>} HtmlCompilationHooks
  107. */
  108. const PLUGIN_NAME = "HtmlModulesPlugin";
  109. // Built-in default favicon (the webpack logo). Referenced as a `file:` request
  110. // so `<link rel="icon">` in the synthetic wrapper flows through the normal
  111. // asset pipeline — emitted hashed, with the correct publicPath, no direct IO.
  112. // Resolved from `__filename` (a sibling of this module) — no `path` needed.
  113. const DEFAULT_FAVICON = new URL("./favicon.svg", pathToFileURL(__filename))
  114. .href;
  115. // mime-db is heavy — only load it when a favicon actually needs a `type`.
  116. /**
  117. * Resolves `output.html.favicon` for one page to `[rel, icon]` pairs.
  118. * `false`/absent → none; `true` → the webpack logo; a string → one `icon`
  119. * link; an object maps each `rel` to an icon (a path string, an attributes
  120. * object, or an array of those for several icons under one `rel`); a function
  121. * receives the page name and returns any of those. Each icon is normalized to
  122. * an object so `faviconLinkTag` reads one shape.
  123. * @param {FaviconOption} favicon favicon option
  124. * @param {string} name page/entry name
  125. * @returns {[string, FaviconIcon][]} rel/icon pairs, in order
  126. */
  127. const resolveFaviconLinks = (favicon, name) => {
  128. const value = typeof favicon === "function" ? favicon(name) : favicon;
  129. if (value === true) return [["icon", { href: DEFAULT_FAVICON }]];
  130. if (typeof value === "string") return [["icon", { href: value }]];
  131. if (value && typeof value === "object") {
  132. /** @type {[string, FaviconIcon][]} */
  133. const pairs = [];
  134. for (const [rel, icon] of Object.entries(value)) {
  135. for (const one of Array.isArray(icon) ? icon : [icon]) {
  136. pairs.push([rel, typeof one === "string" ? { href: one } : one]);
  137. }
  138. }
  139. return pairs;
  140. }
  141. return [];
  142. };
  143. /**
  144. * @param {string} rel link relation
  145. * @param {FaviconIcon} icon icon `href` plus optional link attributes
  146. * @returns {string} a `<link rel=… [attrs…] href=…>` tag; `type` defaults to the file format
  147. */
  148. const faviconLinkTag = (rel, icon) => {
  149. const { escapeAttribute } = getHtmlSyntax();
  150. const type = icon.type || getMimeTypes().lookup(icon.href) || "";
  151. const attrs = [
  152. `rel="${escapeAttribute(rel)}"`,
  153. icon.sizes && `sizes="${escapeAttribute(icon.sizes)}"`,
  154. type && `type="${escapeAttribute(type)}"`,
  155. icon.media && `media="${escapeAttribute(icon.media)}"`,
  156. icon.color && `color="${escapeAttribute(icon.color)}"`,
  157. icon.crossorigin && `crossorigin="${escapeAttribute(icon.crossorigin)}"`,
  158. `href="${escapeAttribute(icon.href)}"`
  159. ];
  160. return `<link ${attrs.filter(Boolean).join(" ")}>`;
  161. };
  162. /**
  163. * Resolves `output.html.manifest` for one page to a `<link rel="manifest">`
  164. * tag, or `""` when unset. A string is a path to an existing `.webmanifest`
  165. * file; an object is serialized to a base64 `data:application/manifest+json`
  166. * URL (routed to `asset/webmanifest` by a default rule, so its relative icon
  167. * `src`s are hashed like any request); a function receives the page name.
  168. * Base64 (not percent-encoding) is required because this tag is embedded in
  169. * the outer `data:text/html` wrapper, which is itself percent-decoded — a
  170. * percent-encoded inner URL would be corrupted by that decode.
  171. * @param {ManifestOption} manifest manifest option
  172. * @param {string} name page/entry name
  173. * @returns {string} the `<link>` tag, or ""
  174. */
  175. const manifestLinkTag = (manifest, name) => {
  176. const value = typeof manifest === "function" ? manifest(name) : manifest;
  177. if (!value) return "";
  178. const href =
  179. typeof value === "string"
  180. ? value
  181. : `data:application/manifest+json;base64,${Buffer.from(
  182. JSON.stringify(value),
  183. "utf8"
  184. ).toString("base64")}`;
  185. return `<link rel="manifest" href="${getHtmlSyntax().escapeAttribute(
  186. href
  187. )}">`;
  188. };
  189. // Decided while the page's HTML is generated, which happens once per module —
  190. // and one synthetic page module can back several entries — so an entry's `html`
  191. // object can't override it.
  192. /** @type {"inline"[]} */
  193. const GLOBAL_ONLY_HTML_OPTIONS = ["inline"];
  194. /**
  195. * SRI is on when `integrity` names at least one algorithm.
  196. * @param {OutputHtmlOptions["integrity"]} integrity integrity option
  197. * @returns {boolean} true when integrity attributes must be emitted
  198. */
  199. const isIntegrityEnabled = (integrity) =>
  200. integrity === true ||
  201. typeof integrity === "function" ||
  202. (Array.isArray(integrity) && integrity.length > 0);
  203. // `\.html`/`\.css` request matchers for the synthetic `output.html` wrapper.
  204. const HTML_REQUEST_RE = /\.html(\?|$)/i;
  205. const CSS_REQUEST_RE = /\.css(\?|$)/i;
  206. /**
  207. * Requests an `output.html` page must load for an entry: its `dependOn`
  208. * ancestors first (transitive, deduped — so a diamond loads each once), then
  209. * the entry's own imports. `.html` imports are dropped (own HTML entries).
  210. * @param {string} name entry name
  211. * @param {Record<string, EntryDescriptionNormalized>} entries normalized static entries
  212. * @returns {string[]} deduped requests in load order
  213. */
  214. const collectHtmlEntryRequests = (name, entries) => {
  215. /** @type {string[]} */
  216. const requests = [];
  217. /** @type {Set<string>} */
  218. const seenRequests = new Set();
  219. /** @type {Set<string>} */
  220. const visited = new Set();
  221. const walk = (/** @type {string} */ entryName) => {
  222. if (visited.has(entryName)) return;
  223. visited.add(entryName);
  224. const desc = entries[entryName];
  225. if (!desc) return;
  226. if (desc.dependOn) {
  227. for (const dep of desc.dependOn) walk(dep);
  228. }
  229. if (!desc.import) return;
  230. for (const request of desc.import) {
  231. if (HTML_REQUEST_RE.test(request) || seenRequests.has(request)) continue;
  232. seenRequests.add(request);
  233. requests.push(request);
  234. }
  235. };
  236. walk(name);
  237. return requests;
  238. };
  239. /**
  240. * @param {string} name definition name in `schemas/WebpackOptions.json`
  241. * @returns {EXPECTED_OBJECT} a schema referencing `#/definitions/<name>`
  242. */
  243. const getSchema = (name) => {
  244. const { definitions } = require("../../schemas/WebpackOptions.json");
  245. return {
  246. definitions,
  247. oneOf: [{ $ref: `#/definitions/${name}` }]
  248. };
  249. };
  250. const generatorValidationOptions = {
  251. name: "Html Modules Plugin",
  252. baseDataPath: "generator"
  253. };
  254. const parserValidationOptions = {
  255. name: "Html Modules Plugin",
  256. baseDataPath: "parser"
  257. };
  258. class HtmlModulesPlugin {
  259. /**
  260. * `output.hashFunction`/`hashSalt`/`hashDigest`/`hashDigestLength`
  261. * digest of `content`, with `nonNumericOnlyHash` applied — webpack's
  262. * standard `[contenthash]` recipe.
  263. * @param {string | Buffer} content content to hash
  264. * @param {import("../../declarations/WebpackOptions").Output} outputOptions output options
  265. * @returns {string} content hash
  266. */
  267. static computeContentHash(content, outputOptions) {
  268. const hash = createHash(
  269. /** @type {import("../../declarations/WebpackOptions").HashFunction} */
  270. (outputOptions.hashFunction)
  271. );
  272. if (outputOptions.hashSalt) hash.update(outputOptions.hashSalt);
  273. hash.update(content);
  274. return digestNonNumericOnly(
  275. hash,
  276. /** @type {string} */ (outputOptions.hashDigest),
  277. /** @type {number} */ (outputOptions.hashDigestLength)
  278. );
  279. }
  280. /**
  281. * Filename template for an extracted HTML page: `output.htmlFilename` for
  282. * initial chunks, `output.htmlChunkFilename` otherwise — the HTML counterpart
  283. * of `CssModulesPlugin.getChunkFilenameTemplate`.
  284. * @param {import("../Chunk")} chunk chunk
  285. * @param {import("../../declarations/WebpackOptions").Output} outputOptions output options
  286. * @returns {import("../Chunk").ChunkFilenameTemplate} used filename template
  287. */
  288. static getChunkFilenameTemplate(chunk, outputOptions) {
  289. return chunk.canBeInitial()
  290. ? /** @type {import("../Chunk").ChunkFilenameTemplate} */ (
  291. outputOptions.htmlFilename
  292. )
  293. : /** @type {import("../Chunk").ChunkFilenameTemplate} */ (
  294. outputOptions.htmlChunkFilename
  295. );
  296. }
  297. /**
  298. * Applies the plugin by registering its hooks on the compiler.
  299. * @param {Compiler} compiler the compiler instance
  300. * @returns {void}
  301. */
  302. apply(compiler) {
  303. const { output } = compiler.options;
  304. const htmlOption = output.html;
  305. const globalHtmlOptions =
  306. typeof htmlOption === "object" ? htmlOption : undefined;
  307. // Collected while entries are resolved (`entryOption`, before the first
  308. // compilation) and reported on every compilation; a Set dedupes entries
  309. // that hit the same problem.
  310. /** @type {Set<string>} */
  311. const optionWarnings = new Set();
  312. compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
  313. for (const message of optionWarnings) {
  314. compilation.warnings.push(new WebpackError(message));
  315. }
  316. });
  317. // Resolved options per entry that owns a generated page, read back when the
  318. // page is emitted (`csp`, `integrity`) — one synthetic page module can back
  319. // several entries, so those can only be applied per emitted asset.
  320. /** @type {Map<string, OutputHtmlOptions>} */
  321. const htmlOptionsByEntry = new Map();
  322. // SRI sentinels are emitted while the page's HTML is generated, before the
  323. // owning entry is known, so any page asking for integrity turns them on for
  324. // all of them; pages that don't want SRI drop them again on emit. Authored
  325. // `.html` pages never reach the entry hook, hence the global seed.
  326. let anyIntegrity = isIntegrityEnabled(
  327. globalHtmlOptions && globalHtmlOptions.integrity
  328. );
  329. /**
  330. * Options for one page: an entry's `html` object overrides `output.html`
  331. * option by option, `true` inherits it as-is.
  332. * @param {EntryDescriptionNormalized["html"]} html the entry's `html` value
  333. * @param {string} name entry name
  334. * @returns {OutputHtmlOptions} resolved options
  335. */
  336. const resolveHtmlOptions = (html, name) => {
  337. const options =
  338. typeof html !== "object"
  339. ? globalHtmlOptions || {}
  340. : globalHtmlOptions
  341. ? { ...globalHtmlOptions, ...html }
  342. : html;
  343. if (typeof html === "object") {
  344. for (const option of GLOBAL_ONLY_HTML_OPTIONS) {
  345. if (html[option] !== undefined) {
  346. optionWarnings.add(
  347. `entry "${name}" html.${option} is ignored — ${option} is resolved once per generated page and can only be set on output.html.`
  348. );
  349. }
  350. }
  351. }
  352. htmlOptionsByEntry.set(name, options);
  353. if (isIntegrityEnabled(options.integrity)) anyIntegrity = true;
  354. return options;
  355. };
  356. // `output.html` (or an entry's `html`) wraps a non-HTML entry in a
  357. // synthetic HTML module so the existing pipeline injects its JS/CSS
  358. // chunks and applies the `template` option. `dependOn` ancestors are
  359. // injected first so the page loads the shared/runtime chunks before
  360. // the entry's own (see `collectHtmlEntryRequests`). `crossOriginLoading`
  361. // and SRI are applied to the injected tags centrally in
  362. // HtmlEntryDependency.
  363. EntryOptionPlugin.getHooks(compiler).entry.tap(
  364. PLUGIN_NAME,
  365. (context, name, desc) => {
  366. // Resolved for every entry, including the authored `.html` ones that
  367. // need no wrapper below — their page reads back `csp`/`integrity` too.
  368. const htmlObj = resolveHtmlOptions(desc.html, name);
  369. const html = desc.html !== undefined ? desc.html : htmlOption;
  370. const imports = desc.import;
  371. if (
  372. !html ||
  373. !imports ||
  374. imports.every((r) => HTML_REQUEST_RE.test(r))
  375. ) {
  376. return;
  377. }
  378. const scriptLoading = htmlObj.scriptLoading || "auto";
  379. // ESM output emits `type="module"` (already deferred), so
  380. // scriptLoading is ignored under output.module — warn on an
  381. // explicit defer/blocking.
  382. let scriptAttr = " defer";
  383. if (output.module) {
  384. scriptAttr = "";
  385. if (scriptLoading === "defer" || scriptLoading === "blocking") {
  386. const source =
  387. typeof desc.html === "object" &&
  388. desc.html.scriptLoading !== undefined
  389. ? `entry "${name}" html`
  390. : "output.html";
  391. optionWarnings.add(
  392. `${source}.scriptLoading: "${scriptLoading}" is ignored with output.module — ES module scripts are always deferred.`
  393. );
  394. }
  395. } else if (scriptLoading === "blocking") {
  396. scriptAttr = "";
  397. }
  398. const entries = compiler.options.entry;
  399. const requests =
  400. typeof entries === "object" && entries[name]
  401. ? collectHtmlEntryRequests(name, entries)
  402. : imports.filter((r) => !HTML_REQUEST_RE.test(r));
  403. const links = [];
  404. const scripts = [];
  405. for (const r of requests) {
  406. if (CSS_REQUEST_RE.test(r)) {
  407. links.push(`<link rel="stylesheet" href="${r}">`);
  408. } else {
  409. scripts.push(`<script${scriptAttr} src="${r}"></script>`);
  410. }
  411. }
  412. const headTags = getHtmlSyntax().buildHeadTags(htmlObj);
  413. // Injected into webpack-generated pages only; each icon's href is a
  414. // normal request the parser turns into a hashed asset.
  415. const faviconTag = resolveFaviconLinks(htmlObj.favicon, name)
  416. .map(([rel, icon]) => faviconLinkTag(rel, icon))
  417. .join("");
  418. const manifestTag = manifestLinkTag(htmlObj.manifest, name);
  419. const inject = htmlObj.inject;
  420. const scriptsInHead =
  421. inject === "head" || (inject !== "body" && output.module);
  422. const scriptsFirst = output.module || scriptAttr === " defer";
  423. const headScripts = scriptsInHead ? scripts.join("") : "";
  424. return `data:text/html,<!doctype html><html><head>${faviconTag}${manifestTag}${headTags}${
  425. scriptsFirst
  426. ? headScripts + links.join("")
  427. : links.join("") + headScripts
  428. }</head><body>${scriptsInHead ? "" : scripts.join("")}</body></html>`;
  429. }
  430. );
  431. // Per-chunk `RawSource` reused across builds when bytes are unchanged:
  432. // keeping identity stable avoids invalidating `RealContentHashPlugin|analyse`.
  433. /** @type {Map<string, { content: string, source: import("webpack-sources").RawSource }>} */
  434. const sentinelResolvedSourceCache = new Map();
  435. // `<script src>` and `<link rel="modulepreload">` references collected
  436. // by HtmlParser become real compilation entries here. The `script`
  437. // and `script-module` groups are chained via a leader-only dependOn so
  438. // they share a runtime — the first entry of the group owns it and
  439. // every subsequent entry sets `dependOn: [leader]`. Modulepreload
  440. // entries are emitted as independent entries (no dependOn) so they
  441. // can never be imported as a runtime leader by a later script —
  442. // that's what keeps the "preload but don't execute" contract of
  443. // `<link rel="modulepreload">` intact.
  444. // Per-compilation state: the HTML modules seen during make (so
  445. // `finishMake` creates their entries without scanning the whole module
  446. // graph), the stylesheet entry names (read in `afterChunks`) and the html
  447. // options of every emitted page, keyed by its asset name.
  448. /** @type {WeakMap<import("../Compilation"), { htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetOptions: Map<string, OutputHtmlOptions> }>} */
  449. const compilationState = new WeakMap();
  450. /**
  451. * @param {import("../Compilation")} compilation compilation
  452. * @returns {{ htmlModules: Set<import("../Module")>, stylesheetEntries: Set<string>, htmlAssetOptions: Map<string, OutputHtmlOptions> }} per-compilation state
  453. */
  454. const getState = (compilation) => {
  455. let state = compilationState.get(compilation);
  456. if (state === undefined) {
  457. state = {
  458. htmlModules: new Set(),
  459. stylesheetEntries: new Set(),
  460. htmlAssetOptions: new Map()
  461. };
  462. compilationState.set(compilation, state);
  463. }
  464. return state;
  465. };
  466. compiler.hooks.finishMake.tapAsync(PLUGIN_NAME, (compilation, callback) => {
  467. const { htmlModules, stylesheetEntries } = getState(compilation);
  468. // Collect the entries an HTML module asks for. Only the script chains
  469. // (`script`, `script-module`) share a runtime via a leader-only
  470. // `dependOn`; `modulepreload`, `stylesheet` and `html` links are all
  471. // independent entries (a CSS or page entry must not chain into a JS
  472. // leader, or the chunk would mix unrelated outputs).
  473. /** @type {(module: import("../Module")) => { context: string, request: string, name: string, dependOn: string[] | undefined }[]} */
  474. const collectEntrySpecs = (module) => {
  475. const { htmlEntries } = /** @type {HtmlModuleBuildInfo} */ (
  476. module.buildInfo
  477. );
  478. if (!htmlEntries) return [];
  479. const context = /** @type {string} */ (module.context);
  480. const specs = [];
  481. for (const [groupKind, group] of Object.entries(htmlEntries)) {
  482. const isChainGroup =
  483. groupKind === "script" || groupKind === "script-module";
  484. /** @type {string | undefined} */
  485. let leaderName;
  486. for (const entry of group) {
  487. const dependOn =
  488. isChainGroup && leaderName !== undefined
  489. ? [leaderName]
  490. : undefined;
  491. if (isChainGroup && leaderName === undefined) {
  492. leaderName = entry.entryName;
  493. }
  494. // Stylesheet entries and `as="style"` preload/prefetch entries emit
  495. // a CSS chunk, so they need the CSS filename template below.
  496. if (groupKind === "stylesheet" || entry.css) {
  497. stylesheetEntries.add(entry.entryName);
  498. }
  499. specs.push({
  500. context,
  501. request: entry.request,
  502. name: entry.entryName,
  503. dependOn
  504. });
  505. }
  506. }
  507. return specs;
  508. };
  509. // Push one collected reference as a compilation entry; resolves with
  510. // the built entry module.
  511. /** @type {(spec: { context: string, request: string, name: string, dependOn: string[] | undefined }) => Promise<import("../Module") | null>} */
  512. const addEntry = (spec) =>
  513. new Promise((resolve, reject) => {
  514. compilation.addEntry(
  515. spec.context,
  516. EntryPlugin.createDependency(spec.request, { name: spec.name }),
  517. {
  518. name: spec.name,
  519. // Each entry gets its own filename from the synthetic entry name so
  520. // it doesn't collide with `output.filename`. CSS entries set their
  521. // `.css` name via `cssFilenameTemplate` below; `html` page entries
  522. // emit their file via `renderManifest`.
  523. filename: compilation.outputOptions.chunkFilename || "[name].js",
  524. dependOn: spec.dependOn
  525. },
  526. (err, entryModule) =>
  527. err ? reject(err) : resolve(entryModule || null)
  528. );
  529. });
  530. // Create one HTML module's entries. A `type: "html"` link is itself an
  531. // HTML entry, so after it builds we recurse into it — the same handling
  532. // every HTML entry already gets. `processed` guards diamonds and cycles.
  533. /** @type {WeakSet<import("../Module")>} */
  534. const processed = new WeakSet();
  535. /** @type {(module: import("../Module")) => Promise<void>} */
  536. const processEntry = async (module) => {
  537. if (processed.has(module)) return;
  538. processed.add(module);
  539. await Promise.all(
  540. collectEntrySpecs(module).map(async (spec) => {
  541. const entryModule = await addEntry(spec);
  542. if (entryModule && entryModule.type === HTML_MODULE_TYPE) {
  543. await processEntry(entryModule);
  544. }
  545. })
  546. );
  547. };
  548. // Seed with the HTML modules seen during make (config entries /
  549. // imports, fresh or cache-restored); linked pages are reached by
  550. // recursion above.
  551. Promise.all([...htmlModules].map(processEntry)).then(
  552. () => callback(),
  553. callback
  554. );
  555. });
  556. // `csp` injects a `<meta http-equiv="Content-Security-Policy">` and
  557. // `integrity` resolves the SRI sentinels — both once the page's inline
  558. // content is final (in `processAssets` below), per page, so an entry's
  559. // `html` can override them.
  560. // `"auto"` marks implicit enablement (no user rule for HTML files) — see
  561. // `applyExperimentsDefaults`. Only then loaders win over the built-in type
  562. // (e.g. html-webpack-plugin's template loader in its child compiler).
  563. // TODO webpack 6: html defaults to `true`, drop this implicit-only fallback.
  564. const implicitlyEnabled = compiler.options.experiments.html === "auto";
  565. compiler.hooks.compilation.tap(
  566. PLUGIN_NAME,
  567. (compilation, { normalModuleFactory }) => {
  568. if (implicitlyEnabled) {
  569. implicitTypeLoaderFallback(
  570. normalModuleFactory,
  571. PLUGIN_NAME,
  572. /\.html$/i,
  573. HTML_MODULE_TYPE
  574. );
  575. }
  576. const { htmlModules } = getState(compilation);
  577. // Record HTML modules as they appear — freshly built
  578. // (`succeedModule`) or restored from cache (`stillValidModule`) —
  579. // so `finishMake` creates their entries without scanning every
  580. // module in the graph. Tracking only records; entries are still
  581. // added (and awaited) in `finishMake`.
  582. const trackHtmlModule = (/** @type {import("../Module")} */ module) => {
  583. if (module.type !== HTML_MODULE_TYPE) return;
  584. // enabled here, not on the compilation hook: an integrity build
  585. // with no html module must not load the generator
  586. if (anyIntegrity) {
  587. getHtmlGenerator().enableIntegritySentinels(compilation);
  588. }
  589. htmlModules.add(module);
  590. };
  591. compilation.hooks.succeedModule.tap(PLUGIN_NAME, trackHtmlModule);
  592. compilation.hooks.stillValidModule.tap(PLUGIN_NAME, trackHtmlModule);
  593. // Resolve integrity and inline sentinels after `RealContentHashPlugin`
  594. // so final bytes are in place. Only HTML pages carry these sentinels —
  595. // never JS chunks that embed an HTML string.
  596. // SRI only takes effect on a cross-origin subresource fetched
  597. // with CORS; without `output.crossOriginLoading` the browser
  598. // silently ignores `integrity` on cross-origin loads. Warn once
  599. // so a CDN deployment doesn't ship no-op integrity attributes.
  600. if (anyIntegrity && !compilation.outputOptions.crossOriginLoading) {
  601. compilation.warnings.push(
  602. new WebpackError(
  603. 'output.html.integrity is set but output.crossOriginLoading is not. Browsers ignore Subresource Integrity on cross-origin requests made without CORS; set output.crossOriginLoading (e.g. "anonymous") if any asset is served from a different origin.'
  604. )
  605. );
  606. }
  607. compilation.hooks.processAssets.tapPromise(
  608. {
  609. name: PLUGIN_NAME,
  610. stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH + 1
  611. },
  612. async (assets) => {
  613. const { htmlAssetOptions } = getState(compilation);
  614. const hooks = HtmlModulesPlugin.getCompilationHooks(compilation);
  615. /** @type {Set<string>} */
  616. const inlinedFiles = new Set();
  617. for (const name of Object.keys(assets)) {
  618. const pageOptions = htmlAssetOptions.get(name);
  619. if (pageOptions === undefined) continue;
  620. const content = assets[name].source();
  621. if (typeof content !== "string") continue;
  622. const { csp, integrity } = pageOptions;
  623. let resolved = content;
  624. // Sentinels are emitted whenever any page wants SRI, so a page
  625. // that doesn't drops them again here.
  626. if (resolved.includes("__WEBPACK_HTML_INTEGRITY__")) {
  627. resolved = isIntegrityEnabled(integrity)
  628. ? getHtmlGenerator().resolveChunkIntegritySentinels(
  629. resolved,
  630. compilation,
  631. /** @type {import("./HtmlGenerator").HtmlIntegrity} */ (
  632. integrity
  633. )
  634. )
  635. : getHtmlGenerator().stripChunkIntegritySentinels(resolved);
  636. }
  637. if (resolved.includes("__WEBPACK_HTML_INLINE__")) {
  638. resolved = getHtmlGenerator().resolveChunkInlineSentinels(
  639. resolved,
  640. compilation,
  641. name,
  642. inlinedFiles
  643. );
  644. }
  645. // `injectTags` (add tags), `transformTags` (mutate/move/remove the
  646. // page's own and injected tags), and CSP all run over a single
  647. // parse: collect the tags/anchors once, let the hooks act, then
  648. // render every edit — placement, attribute rewrites, moves,
  649. // removals, CSP hashes/nonce/meta — in one pass. Injected inline
  650. // `<script>`/`<style>` are hashed like the page's own.
  651. const injected = await hooks.injectTags.promise([], {
  652. outputName: name,
  653. html: resolved
  654. });
  655. const transformTags = hooks.transformTags.taps.length > 0;
  656. if (injected.length > 0 || transformTags || csp) {
  657. const model = getHtmlGenerator().collectHtml(resolved);
  658. getHtmlGenerator().addInjectedTags(model, injected);
  659. if (transformTags) {
  660. await hooks.transformTags.promise(model.tags, {
  661. outputName: name,
  662. html: resolved
  663. });
  664. }
  665. resolved = getHtmlGenerator().renderHtml(resolved, model, csp);
  666. }
  667. // Final, fully-resolved HTML — let plugins transform it (minify,
  668. // inject a CSP meta, rewrite tags) before it is written.
  669. resolved = await hooks.transformHtml.promise(resolved, {
  670. outputName: name
  671. });
  672. if (resolved !== content) {
  673. compilation.updateAsset(name, new RawSource(resolved));
  674. }
  675. await hooks.htmlEmitted.promise({ outputName: name });
  676. }
  677. // An inlined chunk file is dead weight once nothing else references
  678. // it by URL (another page's tag, or the runtime's async chunk map);
  679. // a content-hashed filename makes a substring hit a real reference.
  680. // Materialize each other asset's source once (it can be expensive to
  681. // render) and test all inlined files against it, rather than
  682. // re-materializing every asset per inlined file.
  683. if (inlinedFiles.size > 0) {
  684. /** @type {Set<string>} */
  685. const stillReferenced = new Set();
  686. for (const name of Object.keys(compilation.assets)) {
  687. if (
  688. inlinedFiles.has(name) ||
  689. stillReferenced.size === inlinedFiles.size
  690. ) {
  691. continue;
  692. }
  693. const source = compilation.assets[name].source();
  694. if (typeof source !== "string") continue;
  695. for (const file of inlinedFiles) {
  696. if (!stillReferenced.has(file) && source.includes(file)) {
  697. stillReferenced.add(file);
  698. }
  699. }
  700. }
  701. for (const file of inlinedFiles) {
  702. if (!stillReferenced.has(file)) compilation.deleteAsset(file);
  703. }
  704. }
  705. }
  706. );
  707. // CSS entries created by `<link rel="stylesheet">` in HTML need
  708. // their `.css` filename set via `chunk.cssFilenameTemplate`
  709. // (the field `CssModulesPlugin.getChunkFilenameTemplate` reads).
  710. // Compilation only flows `options.filename` to `chunk.filenameTemplate`,
  711. // which controls JS emit — there's no entry-level `cssFilename`.
  712. // Set it ourselves after chunks are created so each stylesheet
  713. // entry emits to a distinct file derived from `output.cssFilename`
  714. // (or `output.cssChunkFilename` for non-initial CSS chunks).
  715. compilation.hooks.afterChunks.tap(PLUGIN_NAME, () => {
  716. const { stylesheetEntries } = getState(compilation);
  717. if (stylesheetEntries.size === 0) return;
  718. for (const entryName of stylesheetEntries) {
  719. const entrypoint = compilation.entrypoints.get(entryName);
  720. if (!entrypoint) continue;
  721. const chunk = entrypoint.getEntrypointChunk();
  722. if (!chunk) continue;
  723. // Each html-derived stylesheet entry uses the
  724. // `cssChunkFilename` template — even though the entry
  725. // chunk technically `canBeInitial()`, we deliberately
  726. // avoid `cssFilename` here because that template often
  727. // has no per-entry placeholder (it's derived from
  728. // `output.filename`, which can be a literal like
  729. // `bundle0.js`), and multiple `<link rel="stylesheet">`
  730. // tags would then collide on the same emitted `.css`
  731. // file. `cssChunkFilename` is derived from
  732. // `output.chunkFilename` which webpack auto-extends
  733. // with `[id].` when needed, guaranteeing uniqueness.
  734. chunk.cssFilenameTemplate =
  735. compilation.outputOptions.cssChunkFilename;
  736. }
  737. });
  738. compilation.dependencyTemplates.set(
  739. StaticExportsDependency,
  740. new StaticExportsDependency.Template()
  741. );
  742. // `ConstDependency` is used by HtmlParser to insert
  743. // ` type="module"` into the rewritten <script> tag when
  744. // `output.module` is on. Register its template so the HTML
  745. // generator runs the insertion.
  746. compilation.dependencyTemplates.set(
  747. ConstDependency,
  748. new ConstDependency.Template()
  749. );
  750. // registered when the first html module is prepared, so a build
  751. // without html never loads these dependency classes
  752. normalModuleFactory.hooks.prepareModuleType
  753. .for(HTML_MODULE_TYPE)
  754. .tap(PLUGIN_NAME, () => {
  755. const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
  756. const HtmlEntryDependency = require("../dependencies/HtmlEntryDependency");
  757. const HtmlInlineScriptDependency = require("../dependencies/HtmlInlineScriptDependency");
  758. const HtmlInlineStyleDependency = require("../dependencies/HtmlInlineStyleDependency");
  759. const HtmlInlineHtmlDependency = require("../dependencies/HtmlInlineHtmlDependency");
  760. compilation.dependencyFactories.set(
  761. HtmlSourceDependency,
  762. normalModuleFactory
  763. );
  764. compilation.dependencyTemplates.set(
  765. HtmlSourceDependency,
  766. new HtmlSourceDependency.Template()
  767. );
  768. compilation.dependencyFactories.set(
  769. HtmlEntryDependency,
  770. normalModuleFactory
  771. );
  772. compilation.dependencyTemplates.set(
  773. HtmlEntryDependency,
  774. new HtmlEntryDependency.Template()
  775. );
  776. // Inline `<script>` content is bundled as its own entry — the
  777. // same pipeline that handles `<script src>` — via a
  778. // `data:text/javascript,...` request. The dependency
  779. // template rewrites the original tag to `<script src=…>`.
  780. compilation.dependencyFactories.set(
  781. HtmlInlineScriptDependency,
  782. normalModuleFactory
  783. );
  784. compilation.dependencyTemplates.set(
  785. HtmlInlineScriptDependency,
  786. new HtmlInlineScriptDependency.Template()
  787. );
  788. // Inline `<style>` content is routed through the CSS pipeline
  789. // as a `data:text/css` module. The dependency template reads
  790. // the processed CSS text from the CSS module's code
  791. // generation data (`css-text` channel set by CssGenerator
  792. // when `exportType` is `"text"`).
  793. compilation.dependencyFactories.set(
  794. HtmlInlineStyleDependency,
  795. normalModuleFactory
  796. );
  797. compilation.dependencyTemplates.set(
  798. HtmlInlineStyleDependency,
  799. new HtmlInlineStyleDependency.Template()
  800. );
  801. // `<iframe srcdoc>` content is routed back through the HTML
  802. // pipeline as a `data:text/html` module; the template reads the
  803. // processed HTML from the nested module's `html` channel.
  804. compilation.dependencyFactories.set(
  805. HtmlInlineHtmlDependency,
  806. normalModuleFactory
  807. );
  808. compilation.dependencyTemplates.set(
  809. HtmlInlineHtmlDependency,
  810. new HtmlInlineHtmlDependency.Template()
  811. );
  812. });
  813. preloadModuleType(normalModuleFactory, PLUGIN_NAME, [
  814. [HTML_MODULE_TYPE, [getHtmlParser]]
  815. ]);
  816. normalModuleFactory.hooks.createModuleClass
  817. .for(HTML_MODULE_TYPE)
  818. .tap(
  819. PLUGIN_NAME,
  820. (createData, _resolveData) => new HtmlModule(createData)
  821. );
  822. normalModuleFactory.hooks.createParser
  823. .for(HTML_MODULE_TYPE)
  824. .tap(PLUGIN_NAME, (parserOptions) => {
  825. compiler.validate(
  826. () => getSchema("HtmlParserOptions"),
  827. parserOptions,
  828. parserValidationOptions,
  829. (options) =>
  830. require("../../schemas/plugins/HtmlParserOptions.check")(
  831. options
  832. )
  833. );
  834. return new (getHtmlParser.loaded())(parserOptions);
  835. });
  836. normalModuleFactory.hooks.createGenerator
  837. .for(HTML_MODULE_TYPE)
  838. .tap(PLUGIN_NAME, (generatorOptions) => {
  839. compiler.validate(
  840. () => getSchema("HtmlGeneratorOptions"),
  841. generatorOptions,
  842. generatorValidationOptions,
  843. (options) =>
  844. require("../../schemas/plugins/HtmlGeneratorOptions.check")(
  845. options
  846. )
  847. );
  848. return new (getHtmlGenerator())(
  849. generatorOptions,
  850. compilation.moduleGraph
  851. );
  852. });
  853. NormalModule.getCompilationHooks(compilation).processResult.tap(
  854. PLUGIN_NAME,
  855. (result, module) => {
  856. if (module.type === HTML_MODULE_TYPE) {
  857. const [source, ...rest] = result;
  858. // `applyTemplate` is a no-op unless `module.parser.html.template`
  859. // is set. Running it here (where the returned source becomes the
  860. // module's stored source) keeps the parser's dependency offsets
  861. // and the generator's render base in sync.
  862. const parser = /** @type {HtmlParser} */ (module.parser);
  863. return [parser.applyTemplate(removeBOM(source), module), ...rest];
  864. }
  865. return result;
  866. }
  867. );
  868. // Emit extracted `.html` files for any HTML module that opted
  869. // into extraction. The opt-in is computed by
  870. // `HtmlGenerator#_shouldExtract`: `module.generator.html.extract:
  871. // true` always extracts, `false` never extracts, and when
  872. // `extract` is unset the generator extracts iff the HTML module
  873. // is a compilation entry — the iteration below picks up only
  874. // modules whose generator reported the `html` source type, so
  875. // that decision is honored implicitly. The HTML content is read
  876. // from the generator's secondary `"html"` source type (see
  877. // HtmlGenerator#generate). The filename template comes from
  878. // `output.htmlFilename` (initial chunks) or
  879. // `output.htmlChunkFilename` (non-initial chunks), mirroring
  880. // the CSS pipeline. Path data follows the asset-module pattern —
  881. // `module` + a relative source `filename`, with `chunk`
  882. // intentionally omitted so `[name]` resolves to the HTML
  883. // source's basename (e.g. `page` for `./page.html`) rather
  884. // than the importing chunk's name (e.g. `main`). A per-module
  885. // content hash is computed from the rewritten HTML so the
  886. // template's `[contenthash]` placeholder works; the
  887. // compilation hash is also forwarded so `[fullhash]` /
  888. // `[hash]` work in user-supplied templates.
  889. // Sentinel-resolved content and its content hash depend only on
  890. // the HTML-type module source — not on the chunk or output
  891. // filename — yet `renderManifest` runs per `(chunk, module)`.
  892. // Memoize so a module landing in multiple chunks resolves and
  893. // hashes its sentinels once. Scoped to this compilation because
  894. // sentinel resolution embeds chunk filenames, which change
  895. // across rebuilds; weakly keyed by source so entries release.
  896. /** @type {WeakMap<import("webpack-sources").Source, { resolvedContent: string, contentHash: string }>} */
  897. const resolvedSentinelHashCache = new WeakMap();
  898. // Compute a linked/entry HTML page's emitted filename + chunk-URL-resolved
  899. // content + content hash for one (module, chunk). Shared by the emit loop
  900. // below and the page-link resolver. The content hash covers only the
  901. // chunk-URL-resolved source — links to *other* pages stay sentinels here,
  902. // so a page's filename doesn't depend on the filenames of pages it links to.
  903. const computePageEmit = (
  904. /** @type {NormalModule} */ module,
  905. /** @type {import("../Chunk")} */ chunk
  906. ) => {
  907. const { chunkGraph, outputOptions } = compilation;
  908. const codeGenResult =
  909. /** @type {import("../CodeGenerationResults")} */ (
  910. compilation.codeGenerationResults
  911. ).get(module, chunk.runtime);
  912. const placeholderSource = codeGenResult.sources.get(HTML_TYPE);
  913. if (!placeholderSource) return undefined;
  914. let cached = resolvedSentinelHashCache.get(placeholderSource);
  915. if (cached === undefined) {
  916. // Resolve chunk-URL sentinels *before* hashing so the HTML's
  917. // `[contenthash]` invalidates with the referenced chunks'
  918. // filenames. Inlined chunks have no URL, so tag their inline
  919. // sentinels with the chunk content hash — that keeps the emitted
  920. // bytes (and RealContentHashPlugin's later recompute)
  921. // content-dependent, so the page hash tracks inlined content too.
  922. // Asset URL sentinels (from `HtmlEntryDependency` resource-hint
  923. // tags) resolve here too — deferred from template.apply so it
  924. // doesn't race with asset-module codegen.
  925. const resolvedContent = getHtmlGenerator().resolveAssetUrlSentinels(
  926. getHtmlGenerator().embedInlineChunkHashes(
  927. getHtmlGenerator().resolveChunkUrlSentinels(
  928. /** @type {string} */ (placeholderSource.source()),
  929. compilation
  930. ),
  931. compilation
  932. ),
  933. compilation
  934. );
  935. cached = {
  936. resolvedContent,
  937. contentHash: HtmlModulesPlugin.computeContentHash(
  938. resolvedContent,
  939. outputOptions
  940. )
  941. };
  942. resolvedSentinelHashCache.set(placeholderSource, cached);
  943. }
  944. const resource = module.getResource() || module.resource;
  945. // Synthetic `output.html` entries are `data:text/html` modules with no
  946. // real basename — name the file after the entry instead.
  947. const sourceFilename =
  948. resource.startsWith("data:text/html") && chunk.name
  949. ? chunk.name
  950. : toTemplateSourceFileName(
  951. makePathsRelative(
  952. compiler.context,
  953. /** @type {string} */ (resource),
  954. compiler.root
  955. ).replace(/^\.\//, ""),
  956. compilation
  957. );
  958. const filenameTemplate = HtmlModulesPlugin.getChunkFilenameTemplate(
  959. chunk,
  960. outputOptions
  961. );
  962. const { path: filename, info } = compilation.getPathWithInfo(
  963. /** @type {import("../TemplatedPathPlugin").TemplatePath} */
  964. (filenameTemplate),
  965. {
  966. module,
  967. runtime: chunk.runtime,
  968. chunkGraph,
  969. contentHash: cached.contentHash,
  970. contentHashType: HTML_TYPE,
  971. filename: sourceFilename,
  972. hash: compilation.hash
  973. }
  974. );
  975. return {
  976. resolvedContent: cached.resolvedContent,
  977. contentHash: cached.contentHash,
  978. filename,
  979. info
  980. };
  981. };
  982. // Emitted filename of a linked `type: "html"` page, keyed by its entry
  983. // name. Looked up from the page's own entry chunk so it's independent of
  984. // chunk render order; cached for the compilation.
  985. /** @type {Map<string, string>} */
  986. const htmlPageFilenameCache = new Map();
  987. const computePageFilenameByEntry = (
  988. /** @type {string} */ entryName
  989. ) => {
  990. const cached = htmlPageFilenameCache.get(entryName);
  991. if (cached !== undefined) return cached;
  992. let filename = "data:,";
  993. const entrypoint = compilation.entrypoints.get(entryName);
  994. const chunk = entrypoint && entrypoint.getEntrypointChunk();
  995. if (chunk) {
  996. const modules =
  997. compilation.chunkGraph.getOrderedChunkModulesIterableBySourceType(
  998. chunk,
  999. HTML_TYPE,
  1000. compareModulesByFullName(compilation.compiler)
  1001. );
  1002. const module = modules && modules[Symbol.iterator]().next().value;
  1003. if (module) {
  1004. const emit = computePageEmit(
  1005. /** @type {NormalModule} */ (module),
  1006. chunk
  1007. );
  1008. if (emit) filename = emit.filename;
  1009. }
  1010. }
  1011. htmlPageFilenameCache.set(entryName, filename);
  1012. return filename;
  1013. };
  1014. compilation.hooks.renderManifest.tap(
  1015. PLUGIN_NAME,
  1016. (result, { chunk }) => {
  1017. // HMR's `HotUpdateChunk`s flow through the same hook
  1018. // but aren't real output chunks — extracting `.html`
  1019. // for them would create stray hot-update HTML files.
  1020. // `CssModulesPlugin` early-returns for the same reason.
  1021. if (chunk instanceof HotUpdateChunk) return result;
  1022. const { chunkGraph } = compilation;
  1023. const modules =
  1024. chunkGraph.getOrderedChunkModulesIterableBySourceType(
  1025. chunk,
  1026. HTML_TYPE,
  1027. compareModulesByFullName(compilation.compiler)
  1028. );
  1029. if (!modules) return result;
  1030. const outputOptions = compilation.outputOptions;
  1031. for (const module of modules) {
  1032. const normalModule = /** @type {NormalModule} */ (module);
  1033. // `<iframe srcdoc>` modules expose `html` only so HtmlInlineHtmlDependency
  1034. // can write the processed markup back into the host attribute — they are
  1035. // never standalone pages. `generator.extract: "inline"` marks them so no
  1036. // `.html` file is emitted; without this they'd collide on the
  1037. // `data:text/html` → chunk-name path (every srcdoc module in a chunk would
  1038. // emit `<chunk>.html`). An O(1) flag check, not a scan.
  1039. const generatorOptions = normalModule.generatorOptions;
  1040. if (generatorOptions && generatorOptions.extract === "inline") {
  1041. continue;
  1042. }
  1043. const emit = computePageEmit(normalModule, chunk);
  1044. if (!emit) continue;
  1045. const { resolvedContent, contentHash, filename, info } = emit;
  1046. // Resolve any remaining `[webpack/auto]` placeholders to an undo
  1047. // path computed from the emitted HTML's location, so asset/chunk
  1048. // URLs stay relative to `output.path` even when the page emits into
  1049. // a subdirectory. A relative `<base href>` prepends `../`s so the
  1050. // base can't misdirect the rewritten URLs (see `HtmlParser`).
  1051. const basePrefix =
  1052. /** @type {HtmlModuleBuildInfo} */ (normalModule.buildInfo)
  1053. .baseUrlPrefix || "";
  1054. const undoPath =
  1055. basePrefix +
  1056. getUndoPath(
  1057. filename,
  1058. /** @type {string} */ (outputOptions.path),
  1059. false
  1060. );
  1061. // Resolve linked-page (`type: "html"`) sentinels to each page's
  1062. // emitted filename before the undo path, so the href is relative to
  1063. // this page.
  1064. const finalContent = getHtmlGenerator()
  1065. .resolveHtmlPageUrlSentinels(
  1066. resolvedContent,
  1067. computePageFilenameByEntry
  1068. )
  1069. .split(autoPlaceholder)
  1070. .join(undoPath);
  1071. const finalSource = new RawSource(finalContent);
  1072. // The same HTML module can land in multiple chunks with different
  1073. // `htmlFilename`/`htmlChunkFilename` shapes → different `undoPath`s and
  1074. // final content for the same module id. Include the emitted filename in
  1075. // the asset cache key and the post-undo-path content in the hash so the
  1076. // asset cache can't reuse one variant's bytes at another variant's URL.
  1077. // Unchanged content reuses the memoized hash instead of re-digesting.
  1078. const finalContentHash =
  1079. finalContent === resolvedContent
  1080. ? contentHash
  1081. : HtmlModulesPlugin.computeContentHash(
  1082. finalContent,
  1083. outputOptions
  1084. );
  1085. // Track HTML pages (never JS chunks) with the html options of the
  1086. // entry that owns them, read back by the emit passes below.
  1087. getState(compilation).htmlAssetOptions.set(
  1088. filename,
  1089. (chunk.name && htmlOptionsByEntry.get(chunk.name)) ||
  1090. globalHtmlOptions ||
  1091. {}
  1092. );
  1093. result.push({
  1094. render: () => finalSource,
  1095. filename,
  1096. info,
  1097. auxiliary: true,
  1098. identifier: `htmlModule${chunkGraph.getModuleId(
  1099. module
  1100. )}|${filename}`,
  1101. hash: finalContentHash
  1102. });
  1103. }
  1104. return result;
  1105. }
  1106. );
  1107. // Resolve sentinels at JS chunk render time so later passes
  1108. // (SourceMapDevToolPlugin, size optimizers, RealContentHash) see resolved bytes.
  1109. const jsHooks =
  1110. JavascriptModulesPlugin.getCompilationHooks(compilation);
  1111. jsHooks.render.tap(PLUGIN_NAME, (source, renderContext) => {
  1112. // No HTML modules ⇒ no sentinels; skip materializing the JS source.
  1113. if (htmlModules.size === 0) return source;
  1114. const raw = source.source();
  1115. if (typeof raw !== "string") return source;
  1116. // Every sentinel shares this prefix — one scan answers the common
  1117. // sentinel-free case instead of five.
  1118. if (!raw.includes("__WEBPACK_HTML_")) {
  1119. return source;
  1120. }
  1121. // Exact re-check on a prefix hit: the autoPlaceholder strip below is
  1122. // not sentinel-guarded, so bare-prefix false positives must bail here.
  1123. if (
  1124. !raw.includes("__WEBPACK_HTML_CHUNK_URL__") &&
  1125. !raw.includes("__WEBPACK_HTML_PAGE_URL__") &&
  1126. !raw.includes("__WEBPACK_HTML_INTEGRITY__") &&
  1127. !raw.includes("__WEBPACK_HTML_INLINE__") &&
  1128. !raw.includes("__WEBPACK_HTML_ASSET_URL__")
  1129. ) {
  1130. return source;
  1131. }
  1132. // Strip integrity and inline sentinels (not resolve): a JS chunk
  1133. // can't hold real SRI hashes for its own not-yet-final bytes, and
  1134. // inline content belongs only in the final .html, never in JS bundles.
  1135. const resolved = getHtmlGenerator()
  1136. .stripChunkIntegritySentinels(
  1137. getHtmlGenerator()
  1138. .resolveAssetUrlSentinels(
  1139. getHtmlGenerator().resolveHtmlPageUrlSentinels(
  1140. getHtmlGenerator().resolveChunkUrlSentinels(
  1141. raw,
  1142. compilation
  1143. ),
  1144. computePageFilenameByEntry
  1145. ),
  1146. compilation
  1147. )
  1148. .split(autoPlaceholder)
  1149. .join("")
  1150. )
  1151. .replace(
  1152. /__WEBPACK_HTML_INLINE__[0-9a-f]+__[a-z]+(?:__[0-9a-f]+)?__END__/g,
  1153. ""
  1154. );
  1155. if (resolved === raw) return source;
  1156. const chunkId = String(renderContext.chunk.id);
  1157. const prior = sentinelResolvedSourceCache.get(chunkId);
  1158. if (prior !== undefined && prior.content === resolved) {
  1159. return prior.source;
  1160. }
  1161. const newSource = new RawSource(resolved);
  1162. sentinelResolvedSourceCache.set(chunkId, {
  1163. content: resolved,
  1164. source: newSource
  1165. });
  1166. return newSource;
  1167. });
  1168. // Prune cache entries for chunks no longer in the graph so a
  1169. // long watch session can't accumulate stale entries.
  1170. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  1171. if (sentinelResolvedSourceCache.size === 0) return;
  1172. const live = new Set();
  1173. for (const chunk of compilation.chunks) {
  1174. live.add(String(chunk.id));
  1175. }
  1176. for (const id of sentinelResolvedSourceCache.keys()) {
  1177. if (!live.has(id)) sentinelResolvedSourceCache.delete(id);
  1178. }
  1179. });
  1180. }
  1181. );
  1182. }
  1183. }
  1184. /**
  1185. * Per-compilation hooks for the experimental HTML support.
  1186. * @param {Compilation} compilation the compilation
  1187. * @returns {HtmlCompilationHooks} the hooks
  1188. */
  1189. HtmlModulesPlugin.getCompilationHooks = createHooksRegistry(
  1190. createCompilationHooks
  1191. );
  1192. module.exports = HtmlModulesPlugin;