HtmlGenerator.js 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const { RawSource, ReplaceSource } = require("webpack-sources");
  6. const ConcatenationScope = require("../ConcatenationScope");
  7. const Generator = require("../Generator");
  8. const {
  9. HTML_TYPE,
  10. JAVASCRIPT_TYPE,
  11. JAVASCRIPT_TYPES
  12. } = require("../ModuleSourceTypeConstants");
  13. const RuntimeGlobals = require("../RuntimeGlobals");
  14. const Template = require("../Template");
  15. const CssModulesPlugin = require("../css/CssModulesPlugin");
  16. const WebpackError = require("../errors/WebpackError");
  17. const { getUndoPath } = require("../util/identifier");
  18. const memoize = require("../util/memoize");
  19. const { PUBLIC_PATH_AUTO } = require("../util/publicPathPlaceholder");
  20. const { VOID } = require("./data");
  21. const getCreateNodeHash = memoize(() => require("crypto").createHash);
  22. const getCssSyntax = memoize(() => require("../css/syntax"));
  23. const getHtmlSyntax = memoize(() => require("./syntax"));
  24. // Lazy: the dependency templates need this module's sentinel helpers on every
  25. // build, but the HTML tokenizer is only reached once HTML is actually emitted.
  26. // Lazy so loading the HTML generator doesn't pull in the whole CSS pipeline.
  27. // The CSS tokenizer, used to locate `url(...)` when rebasing inlined stylesheets.
  28. // node's crypto — real SHA hashes for Content-Security-Policy source lists
  29. // (the wasm hashes are xxhash, unusable for CSP), loaded only when CSP is on.
  30. // HTML void elements — serialized without a closing tag when a tag descriptor
  31. // (`output.html` `injectTags`) doesn't say otherwise.
  32. /**
  33. * Serializes an attribute map: `true` → bare attribute, `false`/`undefined`/
  34. * `null` → omitted, else a quoted (escaped) value.
  35. * @param {Record<string, string | boolean | undefined> | undefined} attrs attributes
  36. * @returns {string} the leading-space-prefixed attribute string
  37. */
  38. const serializeAttrs = (attrs) => {
  39. let out = "";
  40. if (attrs) {
  41. const { escapeAttribute } = getHtmlSyntax();
  42. for (const name of Object.keys(attrs)) {
  43. const value = attrs[name];
  44. if (value === false || value === undefined || value === null) continue;
  45. out +=
  46. value === true
  47. ? ` ${name}`
  48. : ` ${name}="${escapeAttribute(`${value}`)}"`;
  49. }
  50. }
  51. return out;
  52. };
  53. /**
  54. * Serializes one tag descriptor. `attrs` values: `true` → bare attribute,
  55. * `false`/`undefined` → omitted, else a quoted (escaped) value.
  56. * @param {HtmlTagDescriptor} tag the descriptor
  57. * @returns {string} the tag's HTML
  58. */
  59. const serializeTag = (tag) => {
  60. const attrs = serializeAttrs(tag.attrs);
  61. const isVoid =
  62. tag.voidTag !== undefined ? tag.voidTag : VOID.has(tag.tag.toLowerCase());
  63. return isVoid
  64. ? `<${tag.tag}${attrs}>`
  65. : `<${tag.tag}${attrs}>${tag.children || ""}</${tag.tag}>`;
  66. };
  67. /**
  68. * Canonical string of an attribute map (present keys, sorted) — used to detect
  69. * whether an `transformTags` plugin changed a tag, so unchanged tags stay untouched.
  70. * `false`/`undefined`/`null` count as absent.
  71. * @param {Record<string, string | boolean | undefined>} attrs attributes
  72. * @returns {string} a comparable snapshot
  73. */
  74. const snapshotAttrs = (attrs) =>
  75. JSON.stringify(
  76. Object.keys(attrs)
  77. .filter((name) => {
  78. const value = attrs[name];
  79. return value !== false && value !== undefined && value !== null;
  80. })
  81. .sort()
  82. .map((name) => [name, attrs[name]])
  83. );
  84. /**
  85. * Whether an attribute renders (present and not `false`/`undefined`/`null`).
  86. * @param {Record<string, string | boolean | undefined>} attrs attributes
  87. * @param {string} name attribute name
  88. * @returns {boolean} true if it renders
  89. */
  90. const attrPresent = (attrs, name) => {
  91. const value = attrs[name];
  92. return value !== undefined && value !== false && value !== null;
  93. };
  94. /**
  95. * Whether a `<meta>`'s attributes declare a Content-Security-Policy (case-
  96. * insensitive `http-equiv`), used to detect an author-declared CSP.
  97. * @param {Record<string, string | boolean | undefined>} attrs attributes
  98. * @returns {boolean} true if it is a CSP meta
  99. */
  100. const metaIsCsp = (attrs) => {
  101. for (const name of Object.keys(attrs)) {
  102. const value = attrs[name];
  103. if (
  104. name.toLowerCase() === "http-equiv" &&
  105. typeof value === "string" &&
  106. value.trim().toLowerCase() === "content-security-policy"
  107. ) {
  108. return true;
  109. }
  110. }
  111. return false;
  112. };
  113. /** @import { Source } from "webpack-sources" */
  114. /** @import { HtmlGeneratorOptions } from "../../declarations/WebpackOptions" */
  115. /** @typedef {import("../../declarations/WebpackOptions").OutputHtmlOptions["csp"]} CspOption */
  116. /** @import { HtmlTagDescriptor, HtmlMutableTag } from "./HtmlModulesPlugin" */
  117. /** @typedef {"head" | "body"} HtmlTagLocation */
  118. /**
  119. * @typedef {object} HtmlTagSpan
  120. * @property {number} start opening `<` offset
  121. * @property {number} openEnd offset just after the opening `>`
  122. * @property {number} end offset just after the whole element
  123. * @property {HtmlTagLocation} location the `<head>`/`<body>` region it sits in
  124. * @property {number} nameEnd offset after the tag name (nonce insertion point), `-1` for non-`<script>`/`<style>`
  125. * @property {number} contentEnd raw-text body end for `<script>`/`<style>`, `-1` otherwise
  126. * @property {boolean} selfClosing whether the element is self-closing
  127. */
  128. /** @typedef {{ headOpen: number, headEnd: number, bodyOpen: number, bodyEnd: number }} HtmlAnchors */
  129. /** @typedef {{ tags: HtmlMutableTag[], spans: (HtmlTagSpan | null)[], originals: (string | null)[], descriptors: (HtmlTagDescriptor | null)[], anchors: HtmlAnchors }} HtmlModel */
  130. /** @import Chunk from "../Chunk" */
  131. /** @import Compilation, { DependencyConstructor } from "../Compilation" */
  132. /** @typedef {boolean | string[] | ((asset: { chunk: Chunk, filename: string }) => string[] | false)} HtmlIntegrity */
  133. /** @import CodeGenerationResults from "../CodeGenerationResults" */
  134. /** @import Dependency from "../Dependency" */
  135. /** @import { DependencyTemplateContext } from "../DependencyTemplate" */
  136. /** @import { GenerateContext, UpdateHashContext } from "../Generator" */
  137. /**
  138. * @import Module, {
  139. * SourceType,
  140. * SourceTypes,
  141. * ConcatenationBailoutReasonContext,
  142. * RuntimeRequirements
  143. * } from "../Module"
  144. */
  145. /** @import ModuleGraph from "../ModuleGraph" */
  146. /** @import NormalModule from "../NormalModule" */
  147. /** @import { HtmlModuleBuildInfo } from "./HtmlModule" */
  148. /** @import RuntimeTemplate from "../RuntimeTemplate" */
  149. /** @import Hash from "../util/Hash" */
  150. /**
  151. * @template T
  152. * @typedef {import("../InitFragment")<T>} InitFragment
  153. */
  154. /**
  155. * @type {ReadonlySet<"javascript" | "html">}
  156. */
  157. const JAVASCRIPT_AND_HTML_TYPES = new Set([JAVASCRIPT_TYPE, HTML_TYPE]);
  158. /** @type {WeakMap<Compilation, Map<string, Chunk>>} */
  159. const chunksByIdCache = new WeakMap();
  160. /**
  161. * `String(chunk.id)` → chunk lookup, memoized per compilation and shared by the
  162. * URL and integrity sentinel resolvers.
  163. * @param {Compilation} compilation compilation
  164. * @returns {Map<string, Chunk>} chunks keyed by stringified id
  165. */
  166. const getChunksById = (compilation) => {
  167. let chunksById = chunksByIdCache.get(compilation);
  168. if (chunksById === undefined) {
  169. chunksById = new Map();
  170. for (const chunk of compilation.chunks) {
  171. chunksById.set(String(chunk.id), chunk);
  172. }
  173. chunksByIdCache.set(compilation, chunksById);
  174. }
  175. return chunksById;
  176. };
  177. /** @type {WeakMap<Compilation, Map<string, Module>>} */
  178. const modulesByIdentifierCache = new WeakMap();
  179. /**
  180. * `module.identifier()` → module lookup, memoized per compilation so the asset
  181. * URL sentinel resolver doesn't re-scan the whole module graph per HTML module.
  182. * @param {Compilation} compilation compilation
  183. * @returns {Map<string, Module>} modules keyed by identifier
  184. */
  185. const getModulesByIdentifier = (compilation) => {
  186. let modulesByIdentifier = modulesByIdentifierCache.get(compilation);
  187. if (modulesByIdentifier === undefined) {
  188. modulesByIdentifier = new Map();
  189. for (const module of compilation.modules) {
  190. modulesByIdentifier.set(module.identifier(), module);
  191. }
  192. modulesByIdentifierCache.set(compilation, modulesByIdentifier);
  193. }
  194. return modulesByIdentifier;
  195. };
  196. // Hoisted so `resolveChunkUrlSentinels` (per chunk × module) doesn't allocate a
  197. // fresh `RegExp` each call. `String#replace` resets `lastIndex`, so sharing the
  198. // global-flag instance is safe under these synchronous, non-reentrant calls.
  199. const CHUNK_URL_SENTINEL_REGEXP =
  200. /__WEBPACK_HTML_CHUNK_URL__([0-9a-f]+)__([a-z]+)__END__/g;
  201. // Sentinel for a linked HTML page (a tag mapped to the `html` source type);
  202. // resolved to the page's emitted filename once it has been built. Keyed by the
  203. // linked module's id so it shares the `<iframe srcdoc>` nested-module mechanism.
  204. const HTML_PAGE_URL_SENTINEL_REGEXP =
  205. /__WEBPACK_HTML_PAGE_URL__([0-9a-f]+)__END__/g;
  206. // Sentinel for a URL-referenced asset (font/image/…) emitted into a resource-hint
  207. // `<link>` tag by `HtmlEntryDependency`; resolved to the asset's real emitted
  208. // URL from its codegen data during `HtmlModulesPlugin` render. Deferred because
  209. // HtmlEntry deps run *while* the HTML module is being codegen'd — the asset
  210. // module's own codegen might not have completed yet. Keyed by the asset
  211. // module's identifier, hex-encoded so any character in it is safe in a URL.
  212. const ASSET_URL_SENTINEL_REGEXP =
  213. /__WEBPACK_HTML_ASSET_URL__([0-9a-f]+)__END__/g;
  214. // Compilations whose HTML modules must emit SRI sentinels (see
  215. // `enableIntegritySentinels`).
  216. /** @type {WeakSet<Compilation>} */
  217. const integritySentinelCompilations = new WeakSet();
  218. // Matches the whole ` integrity="<sentinel>"` so the attribute can be dropped
  219. // entirely when the per-asset `integrity` function returns `false`. Minification
  220. // runs first and may have unquoted the value (§13.1.2.3), so the quote is
  221. // optional and back-referenced to keep the pair matched; the replacement always
  222. // re-quotes, since an SRI list carries spaces and `=` padding.
  223. const INTEGRITY_SENTINEL_REGEXP =
  224. / integrity=(["']?)__WEBPACK_HTML_INTEGRITY__([0-9a-f]+)__([a-z]+)__END__\1/g;
  225. // Same attribute, but tolerant of the `\"` escaping the surrounding quotes get
  226. // when the HTML lives inside a JS string literal — used only to strip it there.
  227. const INTEGRITY_SENTINEL_STRIP_REGEXP =
  228. / integrity=(\\?["']|)__WEBPACK_HTML_INTEGRITY__[0-9a-f]+__[a-z]+__END__\1/g;
  229. // The trailing `(?:__<hash>)?` is the inlined chunk's content hash, embedded by
  230. // `embedInlineChunkHashes` so the host HTML's bytes (and its `[contenthash]`,
  231. // incl. RealContentHashPlugin's recompute) change with the inlined content; the
  232. // resolver ignores it.
  233. const INLINE_SENTINEL_REGEXP =
  234. /__WEBPACK_HTML_INLINE__([0-9a-f]+)__([a-z]+)(?:__[0-9a-f]+)?__END__/g;
  235. /**
  236. * Rewrite relative `url(...)` references in a CSS chunk so they still resolve
  237. * after the styles are inlined into the HTML document (which may sit in a
  238. * different directory). webpack emits every url as `undoPath(cssFile) + <asset
  239. * path from the output root>`, so swapping the CSS file's undo prefix for the
  240. * HTML file's re-bases them — no path math. Uses the CSS tokenizer to find url
  241. * tokens exactly (never a `url(` inside a comment or string); webpack emits
  242. * unquoted url()s (special characters `\`-escaped), so only that shape occurs.
  243. * @param {string} css the CSS content
  244. * @param {string} fromFile the CSS file's output name
  245. * @param {string} toFile the HTML file's output name
  246. * @param {string} outputPath the compilation output path
  247. * @returns {string} the CSS with rebased urls
  248. */
  249. const rebaseCssUrls = (css, fromFile, toFile, outputPath) => {
  250. const fromUndo = getUndoPath(fromFile, outputPath, false);
  251. const toUndo = getUndoPath(toFile, outputPath, false);
  252. if (fromUndo === toUndo) return css;
  253. const { readToken, TT_URL } = getCssSyntax();
  254. const token = {
  255. type: 0,
  256. start: 0,
  257. end: 0,
  258. isId: false,
  259. contentStart: 0,
  260. contentEnd: 0,
  261. unitStart: 0
  262. };
  263. let pos = 0;
  264. let last = 0;
  265. let out = "";
  266. while (readToken(css, pos, token) !== undefined) {
  267. pos = token.end;
  268. if (token.type !== TT_URL) continue;
  269. const url = css.slice(token.contentStart, token.contentEnd);
  270. // Rebase only the undo-path-relative urls; a root-relative (`/`), fragment
  271. // (`#`) or scheme (`data:`, …, a `:` before any `/`) url is absolute.
  272. const colon = url.indexOf(":");
  273. const slash = url.indexOf("/");
  274. if (
  275. !url.startsWith(fromUndo) ||
  276. url[0] === "/" ||
  277. url[0] === "#" ||
  278. (colon !== -1 && (slash === -1 || colon < slash))
  279. ) {
  280. continue;
  281. }
  282. out += `${css.slice(last, token.contentStart)}${toUndo}${url.slice(
  283. fromUndo.length
  284. )}`;
  285. last = token.contentEnd;
  286. }
  287. return last === 0 ? css : out + css.slice(last);
  288. };
  289. class HtmlGenerator extends Generator {
  290. /**
  291. * Emit a sentinel for a chunk URL that can't be resolved at code-gen time
  292. * (chunk hashes aren't computed yet); `resolveChunkUrlSentinels` swaps it
  293. * for `${PUBLIC_PATH_AUTO}<chunkFilename>` once they are.
  294. * @param {Chunk} chunk chunk
  295. * @param {"javascript" | "css"} contentHashType which chunk content hash slot the resolved URL should reference
  296. * @returns {string} sentinel
  297. */
  298. static makeChunkUrlSentinel(chunk, contentHashType) {
  299. const hexId = Buffer.from(String(chunk.id), "utf8").toString("hex");
  300. return `__WEBPACK_HTML_CHUNK_URL__${hexId}__${contentHashType}__END__`;
  301. }
  302. /**
  303. * Replace every `makeChunkUrlSentinel` sentinel in `content` with
  304. * `${PUBLIC_PATH_AUTO}<chunkFilename>`. Must run after
  305. * `Compilation#createHash()` so `[contenthash]` resolves.
  306. * @param {string} content content
  307. * @param {Compilation} compilation compilation
  308. * @returns {string} resolved content
  309. */
  310. static resolveChunkUrlSentinels(content, compilation) {
  311. if (!content.includes("__WEBPACK_HTML_CHUNK_URL__")) return content;
  312. const outputOptions = compilation.outputOptions;
  313. const chunksById = getChunksById(compilation);
  314. return content.replace(
  315. CHUNK_URL_SENTINEL_REGEXP,
  316. (_, hexId, contentHashType) => {
  317. const chunkId = Buffer.from(hexId, "hex").toString("utf8");
  318. const chunk = chunksById.get(chunkId);
  319. if (!chunk) return "data:,";
  320. // Reuse each pipeline's own filename-template resolver so the URL
  321. // matches exactly what that plugin emits (worker/hot-update chunk
  322. // handling included), mirroring the CSS branch.
  323. let filenameTemplate;
  324. if (contentHashType === "css") {
  325. filenameTemplate = CssModulesPlugin.getChunkFilenameTemplate(
  326. chunk,
  327. outputOptions
  328. );
  329. } else {
  330. const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin");
  331. filenameTemplate = JavascriptModulesPlugin.getChunkFilenameTemplate(
  332. chunk,
  333. outputOptions
  334. );
  335. }
  336. const filename = compilation.getPath(
  337. /** @type {import("../TemplatedPathPlugin").TemplatePath} */
  338. (filenameTemplate),
  339. {
  340. chunk,
  341. contentHashType
  342. }
  343. );
  344. return `${PUBLIC_PATH_AUTO}${filename}`;
  345. }
  346. );
  347. }
  348. /**
  349. * Emit a sentinel for a linked HTML page's URL (a tag the user mapped to
  350. * the `html` source `type`). The page's emitted filename isn't known at
  351. * code-gen time; `resolveHtmlPageUrlSentinels` swaps it for
  352. * `${PUBLIC_PATH_AUTO}<htmlFilename>` once the page has been built.
  353. * @param {string | number} moduleId id of the linked HTML module
  354. * @returns {string} sentinel
  355. */
  356. static makeHtmlPageUrlSentinel(moduleId) {
  357. const hexId = Buffer.from(String(moduleId), "utf8").toString("hex");
  358. return `__WEBPACK_HTML_PAGE_URL__${hexId}__END__`;
  359. }
  360. /**
  361. * Replace every `makeHtmlPageUrlSentinel` sentinel in `content` with
  362. * `${PUBLIC_PATH_AUTO}<htmlFilename>`, resolving each linked page's emitted
  363. * filename through `getFilename`.
  364. * @param {string} content content
  365. * @param {(moduleId: string) => string} getFilename maps a linked module id to its emitted html filename
  366. * @returns {string} resolved content
  367. */
  368. static resolveHtmlPageUrlSentinels(content, getFilename) {
  369. if (!content.includes("__WEBPACK_HTML_PAGE_URL__")) return content;
  370. return content.replace(
  371. HTML_PAGE_URL_SENTINEL_REGEXP,
  372. (_, hexId) =>
  373. `${PUBLIC_PATH_AUTO}${getFilename(
  374. Buffer.from(hexId, "hex").toString("utf8")
  375. )}`
  376. );
  377. }
  378. /**
  379. * Emit a sentinel for a URL-referenced asset module. Resolved by
  380. * `resolveAssetUrlSentinels` from the asset module's own codegen data
  381. * (`data.get("url")`), which isn't populated when `HtmlEntryDependency`
  382. * runs — the asset module's own codegen may still be pending.
  383. * @param {string} moduleIdentifier the referenced asset module's `identifier()`
  384. * @returns {string} sentinel
  385. */
  386. static makeAssetUrlSentinel(moduleIdentifier) {
  387. const hex = Buffer.from(moduleIdentifier, "utf8").toString("hex");
  388. return `__WEBPACK_HTML_ASSET_URL__${hex}__END__`;
  389. }
  390. /**
  391. * Replace every `makeAssetUrlSentinel` in `content` with the asset module's
  392. * emitted URL, read from its codegen data. Unresolved sentinels (module
  393. * absent, no codegen result yet, or no URL channel — e.g. an inlined
  394. * data-URI) map to `data:,` so the surrounding `<link>` degrades cleanly
  395. * instead of shipping the raw placeholder.
  396. * @param {string} content content
  397. * @param {Compilation} compilation compilation
  398. * @returns {string} resolved content
  399. */
  400. static resolveAssetUrlSentinels(content, compilation) {
  401. if (!content.includes("__WEBPACK_HTML_ASSET_URL__")) return content;
  402. const byIdentifier = getModulesByIdentifier(compilation);
  403. const codeGenerationResults =
  404. /** @type {import("../CodeGenerationResults")} */
  405. (compilation.codeGenerationResults);
  406. return content.replace(ASSET_URL_SENTINEL_REGEXP, (_, hex) => {
  407. const id = Buffer.from(hex, "hex").toString("utf8");
  408. const module = byIdentifier.get(id);
  409. if (!module) return "data:,";
  410. if (!codeGenerationResults.has(module, undefined)) return "data:,";
  411. const codeGen = codeGenerationResults.get(module, undefined);
  412. const data = codeGen.data;
  413. if (!data) return "data:,";
  414. const url =
  415. /** @type {Record<string, string | undefined> | undefined} */
  416. (data.get("url"));
  417. // Prefer the `asset-url` channel — it's the actual URL string
  418. // (publicPath + filename). Also handled: assets reached only from JS
  419. // have no `asset-url` channel (their only URL is the JS expression
  420. // `__webpack_require__.p + "…"`); for those we reconstruct the URL
  421. // from the emitted `filename` + `PUBLIC_PATH_AUTO` so the same
  422. // undo-path substitution runs at asset render.
  423. if (url && url["asset-url"]) return url["asset-url"];
  424. const filename = /** @type {string | undefined} */ (data.get("filename"));
  425. if (filename) return `${PUBLIC_PATH_AUTO}${filename}`;
  426. return "data:,";
  427. });
  428. }
  429. /**
  430. * Turns SRI sentinel emission on for this compilation. Tags are generated
  431. * once per HTML module, but one module can back several pages with different
  432. * `integrity` options, so HtmlModulesPlugin flags the compilation when any
  433. * page wants SRI and each page resolves or strips the sentinels on emit.
  434. * @param {Compilation} compilation compilation
  435. * @returns {void}
  436. */
  437. static enableIntegritySentinels(compilation) {
  438. integritySentinelCompilations.add(compilation);
  439. }
  440. /**
  441. * @param {Compilation} compilation compilation
  442. * @returns {boolean} true when SRI sentinels must be emitted
  443. */
  444. static hasIntegritySentinels(compilation) {
  445. return integritySentinelCompilations.has(compilation);
  446. }
  447. /**
  448. * Emit a sentinel for a chunk's SRI hash. Resolved late (after
  449. * `RealContentHashPlugin`) by `resolveChunkIntegritySentinels`, since the
  450. * hash must cover the chunk's final emitted bytes.
  451. * @param {Chunk} chunk chunk
  452. * @param {"javascript" | "css"} contentHashType which chunk asset to hash
  453. * @returns {string} sentinel
  454. */
  455. static makeChunkIntegritySentinel(chunk, contentHashType) {
  456. const hexId = Buffer.from(String(chunk.id), "utf8").toString("hex");
  457. return `__WEBPACK_HTML_INTEGRITY__${hexId}__${contentHashType}__END__`;
  458. }
  459. /**
  460. * Drop every ` integrity="<sentinel>"` from `content`. Used on the HTML
  461. * string embedded in a JS chunk (e.g. `<iframe srcdoc>`), where a real SRI
  462. * hash can't be produced — the chunk's own bytes aren't final at render time
  463. * — and resolving it later would mutate the chunk after its content hash was
  464. * computed. Only real HTML output assets keep the sentinel for late resolution.
  465. * @param {string} content content
  466. * @returns {string} content with integrity sentinels removed
  467. */
  468. static stripChunkIntegritySentinels(content) {
  469. if (!content.includes("__WEBPACK_HTML_INTEGRITY__")) return content;
  470. return content.replace(INTEGRITY_SENTINEL_STRIP_REGEXP, "");
  471. }
  472. /**
  473. * Replace every ` integrity="<sentinel>"` with the chunk's real SRI hashes,
  474. * or drop the attribute when `integrity` (a function) returns `false` for it.
  475. * @param {string} content content
  476. * @param {Compilation} compilation compilation
  477. * @param {HtmlIntegrity} integrity the `output.html.integrity` option
  478. * @returns {string} resolved content
  479. */
  480. static resolveChunkIntegritySentinels(content, compilation, integrity) {
  481. if (!content.includes("__WEBPACK_HTML_INTEGRITY__")) return content;
  482. const crypto = require("crypto");
  483. // Report each bad hash algorithm once, not once per referencing tag.
  484. const reportedBadAlgorithms = new Set();
  485. const chunksById = getChunksById(compilation);
  486. return content.replace(
  487. INTEGRITY_SENTINEL_REGEXP,
  488. (_, _quote, hexId, contentHashType) => {
  489. const chunkId = Buffer.from(hexId, "hex").toString("utf8");
  490. const chunk = chunksById.get(chunkId);
  491. if (!chunk) return "";
  492. // `chunk.files` holds the real emitted names (after
  493. // `RealContentHashPlugin`), so hash those bytes — not a name
  494. // re-derived from the template, which still has the pre-hash value.
  495. let filename;
  496. for (const file of chunk.files) {
  497. if ((contentHashType === "css") === file.endsWith(".css")) {
  498. filename = file;
  499. break;
  500. }
  501. }
  502. if (!filename) return "";
  503. const asset = compilation.getAsset(filename);
  504. if (!asset) return "";
  505. const algorithms =
  506. typeof integrity === "function"
  507. ? integrity({ chunk, filename })
  508. : integrity === true
  509. ? ["sha384"]
  510. : integrity;
  511. if (!algorithms || algorithms.length === 0) return "";
  512. const buffer = asset.source.buffer();
  513. const parts = [];
  514. for (const algorithm of algorithms) {
  515. // `crypto.createHash` throws on an unknown algorithm name — turn
  516. // that into a webpack error and skip the algorithm instead of
  517. // crashing the whole compilation out of `processAssets`.
  518. try {
  519. parts.push(
  520. `${algorithm}-${crypto
  521. .createHash(algorithm)
  522. .update(buffer)
  523. .digest("base64")}`
  524. );
  525. } catch (_err) {
  526. if (!reportedBadAlgorithms.has(algorithm)) {
  527. reportedBadAlgorithms.add(algorithm);
  528. compilation.errors.push(
  529. new WebpackError(
  530. `output.html.integrity: unsupported hash algorithm ${JSON.stringify(
  531. algorithm
  532. )}`
  533. )
  534. );
  535. }
  536. }
  537. }
  538. if (parts.length === 0) return "";
  539. return ` integrity="${parts.join(" ")}"`;
  540. }
  541. );
  542. }
  543. /**
  544. * @param {Chunk} chunk chunk
  545. * @param {"javascript" | "css"} contentHashType which chunk asset to inline
  546. * @returns {string} sentinel
  547. */
  548. static makeChunkInlineSentinel(chunk, contentHashType) {
  549. const hexId = Buffer.from(String(chunk.id), "utf8").toString("hex");
  550. return `__WEBPACK_HTML_INLINE__${hexId}__${contentHashType}__END__`;
  551. }
  552. /**
  553. * Replace every inline sentinel with the chunk's actual source content.
  554. * Must run after `RealContentHashPlugin` so the final emitted filenames are
  555. * known (we look up the asset by filename from `chunk.files`).
  556. * @param {string} content content
  557. * @param {Compilation} compilation compilation
  558. * @param {string} htmlFilename output name of the HTML file being resolved (to rebase CSS urls)
  559. * @param {Set<string>=} inlinedFiles collects every chunk file inlined here, so the caller can drop now-unreferenced assets
  560. * @returns {string} resolved content
  561. */
  562. static resolveChunkInlineSentinels(
  563. content,
  564. compilation,
  565. htmlFilename,
  566. inlinedFiles
  567. ) {
  568. if (!content.includes("__WEBPACK_HTML_INLINE__")) return content;
  569. const chunksById = getChunksById(compilation);
  570. const outputPath = /** @type {string} */ (compilation.outputOptions.path);
  571. return content.replace(
  572. INLINE_SENTINEL_REGEXP,
  573. (_, hexId, contentHashType) => {
  574. const chunkId = Buffer.from(hexId, "hex").toString("utf8");
  575. const chunk = chunksById.get(chunkId);
  576. if (!chunk) return "";
  577. const isCss = contentHashType === "css";
  578. let out = "";
  579. for (const file of chunk.files) {
  580. if (isCss !== file.endsWith(".css")) continue;
  581. const asset = compilation.getAsset(file);
  582. if (!asset) continue;
  583. let raw = String(asset.source.source());
  584. if (isCss) {
  585. // Styles move into the document, so relative `url(...)` must be
  586. // rebased; then escape `</style` so it can't close the block early.
  587. raw = rebaseCssUrls(raw, file, htmlFilename, outputPath).replace(
  588. /<\/(style)/gi,
  589. "<\\/$1"
  590. );
  591. } else {
  592. // Escape `</script` (early close) and `<!--` (enters the script-data
  593. // escaped state, where a later `<script` hides the real close); both
  594. // only occur inside a string/comment/regex, where `<\` is inert.
  595. raw = raw
  596. .replace(/<\/(script)/gi, "<\\/$1")
  597. .replace(/<!--/g, "<\\!--");
  598. }
  599. out += raw;
  600. if (inlinedFiles) inlinedFiles.add(file);
  601. }
  602. return out;
  603. }
  604. );
  605. }
  606. /**
  607. * Parses the final HTML once and collects everything the `output.html`
  608. * post-processing stages need: every `<script>`/`<link>`/`<style>`/`<meta>` as
  609. * a mutable descriptor (for `transformTags`) paired with its source span, an
  610. * attribute snapshot (to detect changes), and — for `<script>`/`<style>` — the
  611. * raw-text body range and nonce insertion point (for CSP); plus the
  612. * `<head>`/`<body>` open/content-end anchors (for `injectTags`). One parse feeds
  613. * `injectTags` + `transformTags` + CSP, which `renderHtml` then applies in a
  614. * single pass. Uses the pipeline's own parser (no regex).
  615. * @param {string} content the final HTML (inline content already resolved)
  616. * @returns {HtmlModel} the collected model
  617. */
  618. static collectHtml(content) {
  619. /** @type {HtmlMutableTag[]} */
  620. const tags = [];
  621. /** @type {(HtmlTagSpan | null)[]} */
  622. const spans = [];
  623. /** @type {(string | null)[]} */
  624. const originals = [];
  625. /** @type {(HtmlTagDescriptor | null)[]} */
  626. const descriptors = [];
  627. let headOpen = -1;
  628. let headEnd = -1;
  629. let bodyOpen = -1;
  630. let bodyEnd = -1;
  631. let headDepth = 0;
  632. let bodyDepth = 0;
  633. // `skip.text` populates `contentEnd` (raw-text body end) without
  634. // materializing text nodes; `</script` stays escaped, so CSP hashes match
  635. // the exact bytes the browser sees.
  636. const { NodeType, SourceProcessor } = getHtmlSyntax();
  637. new SourceProcessor()
  638. .use({
  639. [NodeType.Element]: {
  640. enter: (path) => {
  641. const name = path.tagName();
  642. if (name === "head") {
  643. if (headOpen < 0) headOpen = headEnd = path.tagEnd();
  644. headDepth++;
  645. } else if (name === "body") {
  646. if (bodyOpen < 0) bodyOpen = bodyEnd = path.tagEnd();
  647. bodyDepth++;
  648. }
  649. },
  650. // Collected on exit: an element's `end` is only final once it
  651. // closes, and the streamed walk enters it while it is still open.
  652. // `script` / `style` / `link` / `meta` never nest in each other, so
  653. // exiting keeps them in source order.
  654. exit: (path) => {
  655. const name = path.tagName();
  656. if (name === "head") {
  657. if (headDepth > 0) headDepth--;
  658. return;
  659. }
  660. if (name === "body") {
  661. if (bodyDepth > 0) bodyDepth--;
  662. return;
  663. }
  664. const end = path.end();
  665. if (headDepth > 0 && end > headEnd) headEnd = end;
  666. if (bodyDepth > 0 && end > bodyEnd) bodyEnd = end;
  667. if (
  668. name !== "script" &&
  669. name !== "link" &&
  670. name !== "style" &&
  671. name !== "meta"
  672. ) {
  673. return;
  674. }
  675. /** @type {Record<string, string | boolean | undefined>} */
  676. const attrs = {};
  677. const count = path.attributeCount();
  678. for (let i = 0; i < count; i++) {
  679. const attr = path.attributeAt(i);
  680. attrs[path.attributeName(attr)] =
  681. path.attributeValueStart(attr) === -1
  682. ? true
  683. : path.attributeValue(attr);
  684. }
  685. const isRawText = name === "script" || name === "style";
  686. tags.push({
  687. tag: name,
  688. attrs,
  689. injectTo: bodyDepth > 0 ? "body" : "head"
  690. });
  691. spans.push({
  692. start: path.start(),
  693. openEnd: path.tagEnd(),
  694. end,
  695. location: bodyDepth > 0 ? "body" : "head",
  696. nameEnd: isRawText ? path.nameEnd() : -1,
  697. contentEnd: isRawText ? path.contentEnd() : -1,
  698. selfClosing: path.selfClosing()
  699. });
  700. originals.push(snapshotAttrs(attrs));
  701. descriptors.push(null);
  702. }
  703. }
  704. })
  705. .process(content, { skip: { text: true, doctype: true } });
  706. return {
  707. tags,
  708. spans,
  709. originals,
  710. descriptors,
  711. anchors: { headOpen, headEnd, bodyOpen, bodyEnd }
  712. };
  713. }
  714. /**
  715. * Appends `injectTags` descriptors to the model as spanless tags (pure inserts
  716. * placed by `injectTo`), so `transformTags` sees them alongside the page's own
  717. * tags and CSP hashes any injected inline `<script>`/`<style>`. `attrs` is
  718. * copied so a `transformTags` mutation never corrupts the caller's descriptor.
  719. * @param {HtmlModel} model the value from `collectHtml`
  720. * @param {HtmlTagDescriptor[]} injected the `injectTags` descriptors
  721. * @returns {void}
  722. */
  723. static addInjectedTags(model, injected) {
  724. for (const descriptor of injected) {
  725. model.tags.push({
  726. tag: descriptor.tag,
  727. attrs: descriptor.attrs ? { ...descriptor.attrs } : {},
  728. injectTo: descriptor.injectTo || "head"
  729. });
  730. model.spans.push(null);
  731. model.originals.push(null);
  732. model.descriptors.push(descriptor);
  733. }
  734. }
  735. /**
  736. * Renders the collected (and hook-mutated) model back into HTML in a single
  737. * pass: injected tags are placed by `injectTo`; an existing tag is left
  738. * byte-for-byte unless it was removed, moved (`<head>`↔`<body>`, honoring
  739. * `*-prepend`), or had its `attrs` changed; and — when `csp` is set and the
  740. * page declares no CSP — a strict `<meta http-equiv="Content-Security-Policy">`
  741. * is injected with a hash of every inline `<script>`/`<style>` (page's own and
  742. * injected) plus an optional placeholder `nonce` on each. Edits are applied
  743. * in one ascending pass; at a shared offset an insert is emitted before a
  744. * delete/replace's text.
  745. * @param {string} content the collected HTML (inline content already resolved)
  746. * @param {HtmlModel} model the value from `collectHtml` after the hooks
  747. * @param {CspOption} csp the `output.html.csp` option
  748. * @returns {string} the rendered HTML
  749. */
  750. static renderHtml(content, model, csp) {
  751. const { escapeAttribute } = getHtmlSyntax();
  752. const { tags, spans, originals, descriptors, anchors } = model;
  753. // CSP setup: an author-declared policy (page's own or injected) wins.
  754. const options = csp ? (csp === true ? {} : csp) : undefined;
  755. const nonce = options && options.nonce;
  756. let hasCsp = false;
  757. if (options) {
  758. for (let i = 0; i < tags.length; i++) {
  759. const tag = tags[i];
  760. if (!tag.remove && tag.tag === "meta" && metaIsCsp(tag.attrs)) {
  761. hasCsp = true;
  762. break;
  763. }
  764. }
  765. }
  766. const addCsp = Boolean(options) && !hasCsp;
  767. /** @type {Set<string>} */
  768. const scriptSrc = new Set();
  769. /** @type {Set<string>} */
  770. const styleSrc = new Set();
  771. let hashOf;
  772. if (addCsp) {
  773. const hashFunction =
  774. /** @type {Exclude<CspOption, boolean | undefined>} */ (options)
  775. .hashFunction || "sha256";
  776. const createHash = getCreateNodeHash();
  777. hashOf = (/** @type {string} */ body) =>
  778. `'${hashFunction}-${createHash(hashFunction)
  779. .update(body, "utf8")
  780. .digest("base64")}'`;
  781. }
  782. // Insertions accumulated per offset (placement order preserved); range edits
  783. // replace/delete an existing tag's span; region groups collect placements so
  784. // `*-prepend` precedes the plain name at a shared (empty-region) anchor.
  785. /** @type {Map<number, string>} */
  786. const inserts = new Map();
  787. /** @type {(pos: number, text: string) => void} */
  788. const at = (pos, text) => {
  789. if (text) inserts.set(pos, (inserts.get(pos) || "") + text);
  790. };
  791. // Placements accumulate per region so `*-prepend` always precedes the plain
  792. // name at a shared offset (an empty `<head>`/`<body>`); emitted at the anchors
  793. // after the loop.
  794. /** @type {Record<string, string>} */
  795. const groups = {
  796. "head-prepend": "",
  797. head: "",
  798. "body-prepend": "",
  799. body: ""
  800. };
  801. /** @type {(region: string | undefined, text: string) => void} */
  802. const place = (region, text) => {
  803. if (text) {
  804. groups[region !== undefined && region in groups ? region : "head"] +=
  805. text;
  806. }
  807. };
  808. /** @type {{ start: number, end: number, text: string, insert: boolean }[]} */
  809. const edits = [];
  810. for (let i = 0; i < tags.length; i++) {
  811. const tag = tags[i];
  812. const span = spans[i];
  813. const isRawText = tag.tag === "script" || tag.tag === "style";
  814. const needsNonce =
  815. addCsp &&
  816. Boolean(nonce) &&
  817. isRawText &&
  818. !attrPresent(tag.attrs, "nonce");
  819. // Hash an inline `<script>`/`<style>` (page's own or injected). External
  820. // `<script src>` is covered by `'self'`; a self-closing tag has no body.
  821. if (addCsp && isRawText && !tag.remove) {
  822. const isScript = tag.tag === "script";
  823. let body = "";
  824. if (span) {
  825. if (
  826. !span.selfClosing &&
  827. !(isScript && attrPresent(tag.attrs, "src"))
  828. ) {
  829. body = content.slice(span.openEnd, span.contentEnd);
  830. }
  831. } else if (!(isScript && attrPresent(tag.attrs, "src"))) {
  832. body =
  833. /** @type {HtmlTagDescriptor} */ (descriptors[i]).children || "";
  834. }
  835. if (body !== "") {
  836. (isScript ? scriptSrc : styleSrc).add(
  837. /** @type {(body: string) => string} */ (hashOf)(body)
  838. );
  839. }
  840. }
  841. if (!span) {
  842. // Injected tag: serialize (with a nonce when CSP asks) and place it.
  843. if (tag.remove) continue;
  844. const descriptor = /** @type {HtmlTagDescriptor} */ (descriptors[i]);
  845. const attrs = needsNonce
  846. ? { nonce: /** @type {string} */ (nonce), ...tag.attrs }
  847. : tag.attrs;
  848. place(
  849. tag.injectTo || "head",
  850. serializeTag({
  851. tag: tag.tag,
  852. attrs,
  853. children: descriptor.children,
  854. voidTag: descriptor.voidTag
  855. })
  856. );
  857. continue;
  858. }
  859. // Existing tag.
  860. if (tag.remove) {
  861. edits.push({
  862. start: span.start,
  863. end: span.end,
  864. text: "",
  865. insert: false
  866. });
  867. continue;
  868. }
  869. const attrsChanged = snapshotAttrs(tag.attrs) !== originals[i];
  870. const moved =
  871. tag.injectTo !== undefined && tag.injectTo !== span.location;
  872. const nonceStr = needsNonce
  873. ? ` nonce="${escapeAttribute(/** @type {string} */ (nonce))}"`
  874. : "";
  875. if (moved) {
  876. const opening = `<${tag.tag}${nonceStr}${serializeAttrs(tag.attrs)}>`;
  877. edits.push({
  878. start: span.start,
  879. end: span.end,
  880. text: "",
  881. insert: false
  882. });
  883. place(tag.injectTo, opening + content.slice(span.openEnd, span.end));
  884. } else if (attrsChanged) {
  885. // Rewrite the opening tag (a nonce, if any, lands first).
  886. edits.push({
  887. start: span.start,
  888. end: span.openEnd,
  889. text: `<${tag.tag}${nonceStr}${serializeAttrs(tag.attrs)}>`,
  890. insert: false
  891. });
  892. } else if (nonceStr) {
  893. // Nonce-only change: insert right after the tag name, rest verbatim.
  894. edits.push({
  895. start: span.nameEnd,
  896. end: span.nameEnd,
  897. text: nonceStr,
  898. insert: true
  899. });
  900. }
  901. }
  902. if (addCsp) {
  903. if (nonce) {
  904. scriptSrc.add(`'nonce-${nonce}'`);
  905. styleSrc.add(`'nonce-${nonce}'`);
  906. }
  907. // Strict baseline; a user `policy` directive replaces one, hashes/nonce append.
  908. /** @type {Map<string, Set<string>>} */
  909. const directives = new Map([
  910. ["script-src", new Set(["'self'"])],
  911. ["style-src", new Set(["'self'"])],
  912. ["object-src", new Set(["'none'"])],
  913. ["base-uri", new Set(["'self'"])]
  914. ]);
  915. const policyOption =
  916. /** @type {Exclude<CspOption, boolean | undefined>} */ (options).policy;
  917. if (policyOption) {
  918. for (const [name, value] of Object.entries(policyOption)) {
  919. directives.set(name, new Set(Array.isArray(value) ? value : [value]));
  920. }
  921. }
  922. /** @type {(name: string, extra: Set<string>) => void} */
  923. const append = (name, extra) => {
  924. let set = directives.get(name);
  925. if (!set) directives.set(name, (set = new Set()));
  926. for (const value of extra) set.add(value);
  927. };
  928. append("script-src", scriptSrc);
  929. append("style-src", styleSrc);
  930. const policy = [...directives]
  931. .filter(([, values]) => values.size > 0)
  932. .map(([name, values]) => `${name} ${[...values].join(" ")}`)
  933. .join("; ");
  934. const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(
  935. policy
  936. )}">`;
  937. // First at the head anchor, so it precedes any head-prepend tag.
  938. at(anchors.headOpen, meta);
  939. }
  940. // Emit region placements at the `<head>`/`<body>` anchors (after any CSP
  941. // meta, prepend before the plain name). The parser always synthesizes an
  942. // implied `<head>`/`<body>`, so both anchors are always present.
  943. at(anchors.headOpen, groups["head-prepend"]);
  944. at(anchors.headEnd, groups.head);
  945. at(anchors.bodyOpen, groups["body-prepend"]);
  946. at(anchors.bodyEnd, groups.body);
  947. for (const [pos, text] of inserts) {
  948. edits.push({ start: pos, end: pos, text, insert: true });
  949. }
  950. if (edits.length === 0) return content;
  951. // Ascending single pass over the (non-overlapping) edits — O(page + edits)
  952. // instead of two full-page copies per edit; the tie-break reproduces the
  953. // former right-to-left order (insert text lands before a replacement's).
  954. edits.sort(
  955. (a, b) => a.start - b.start || Number(b.insert) - Number(a.insert)
  956. );
  957. let result = "";
  958. let cursor = 0;
  959. for (const edit of edits) {
  960. result += content.slice(cursor, edit.start);
  961. result += edit.text;
  962. if (edit.end > cursor) cursor = edit.end;
  963. }
  964. return result + content.slice(cursor);
  965. }
  966. /**
  967. * Embeds each inlined chunk's content hash into its inline sentinel
  968. * (`…__<type>__END__` → `…__<type>__<hash>__END__`). The sentinel otherwise
  969. * carries only the stable chunk id, so the host HTML's bytes wouldn't change
  970. * when the inlined content changes — the resolved chunk URL that normally
  971. * busts the page `[contenthash]` is gone once inlined. Embedding the hash
  972. * makes the emitted bytes content-dependent, so both the initial hash and
  973. * RealContentHashPlugin's recompute pick up inlined-content changes. The
  974. * resolver (`resolveChunkInlineSentinels`) ignores this segment.
  975. * @param {string} content content with chunk-url sentinels already resolved
  976. * @param {Compilation} compilation compilation
  977. * @returns {string} content with content-hash-tagged inline sentinels
  978. */
  979. static embedInlineChunkHashes(content, compilation) {
  980. if (!content.includes("__WEBPACK_HTML_INLINE__")) return content;
  981. const chunksById = getChunksById(compilation);
  982. return content.replace(
  983. INLINE_SENTINEL_REGEXP,
  984. (match, hexId, contentHashType) => {
  985. const chunkId = Buffer.from(hexId, "hex").toString("utf8");
  986. const chunk = chunksById.get(chunkId);
  987. if (!chunk) return match;
  988. const hash =
  989. (chunk.contentHash && chunk.contentHash[contentHashType]) ||
  990. chunk.renderedHash ||
  991. chunk.hash;
  992. if (!hash) return match;
  993. return `__WEBPACK_HTML_INLINE__${hexId}__${contentHashType}__${hash}__END__`;
  994. }
  995. );
  996. }
  997. /**
  998. * Creates an instance of HtmlGenerator.
  999. * @param {HtmlGeneratorOptions=} options generator options
  1000. * @param {ModuleGraph=} moduleGraph the module graph; used to detect when an HTML module is reached as a compilation entry so `extract` can default to `true` for it
  1001. */
  1002. constructor(options, moduleGraph) {
  1003. super();
  1004. /** @type {HtmlGeneratorOptions} */
  1005. this.options = options || {};
  1006. /** @type {ModuleGraph | undefined} */
  1007. this._moduleGraph = moduleGraph;
  1008. // Raw dependency-template render (before `[webpack/auto]` resolution),
  1009. // shared between the JS and HTML type passes of a single
  1010. // `codeGeneration()` call. Keyed by that call's `runtimeRequirements`
  1011. // Set — created once per call and passed to every type's `generate()`,
  1012. // so it uniquely scopes the cache to one module's one code generation
  1013. // and releases with it. The two passes differ only in `undoPath`.
  1014. /** @type {WeakMap<RuntimeRequirements, string>} */
  1015. this._rawRenderCache = new WeakMap();
  1016. }
  1017. /**
  1018. * Returns the reason this module cannot be concatenated, when one exists.
  1019. * @param {NormalModule} module module for which the bailout reason should be determined
  1020. * @param {ConcatenationBailoutReasonContext} context context
  1021. * @returns {string | undefined} reason why this module can't be concatenated, undefined when it can be concatenated
  1022. */
  1023. getConcatenationBailoutReason(module, context) {
  1024. // The HMR shim references the per-module `module.hot` object — when an
  1025. // HTML module is concatenated, that scope is gone (the merged file
  1026. // shares a single `module`), so the self-accept / DOM-patch wiring
  1027. // would target the wrong module id. Bail out of concatenation so the
  1028. // HMR-aware HTML module keeps its own module scope.
  1029. if (module.hot) {
  1030. return "HTML module needs its own module scope for HMR";
  1031. }
  1032. return undefined;
  1033. }
  1034. /**
  1035. * Whether this HTML module is reached as a compilation entry. Entry
  1036. * modules have at least one incoming connection without an
  1037. * `originModule` (the EntryDependency added by `compilation.addEntry`) —
  1038. * this covers both HTML-as-entry-point and a linked `type: "html"` page
  1039. * (added as its own entry by HtmlModulesPlugin).
  1040. * @param {NormalModule} module module
  1041. * @returns {boolean} true when the module is an entry
  1042. */
  1043. _isEntryModule(module) {
  1044. if (!this._moduleGraph) return false;
  1045. for (const connection of this._moduleGraph.getIncomingConnections(module)) {
  1046. if (!connection.originModule) return true;
  1047. }
  1048. return false;
  1049. }
  1050. /**
  1051. * Whether to emit the extracted `.html` file for this module.
  1052. * `options.extract === true` always extracts; `false` and `"inline"` never;
  1053. * when the option is left unspecified, extraction defaults to on for HTML
  1054. * modules used as compilation entries — that's the HTML-as-entry-point case
  1055. * and a linked `type: "html"` page (which is added as its own entry).
  1056. * @param {NormalModule} module module
  1057. * @returns {boolean} true when the `.html` file should be emitted
  1058. */
  1059. _shouldExtract(module) {
  1060. const { extract } = this.options;
  1061. if (extract === true) return true;
  1062. if (extract === false || extract === "inline") return false;
  1063. return this._isEntryModule(module);
  1064. }
  1065. /**
  1066. * Whether this module exposes the `html` source type. Like `_shouldExtract`,
  1067. * but `"inline"` also exposes it — the processed HTML is read back into the
  1068. * host attribute (e.g. `<iframe srcdoc>`) instead of emitted as a file.
  1069. * @param {NormalModule} module module
  1070. * @returns {boolean} true when the `html` source type is available
  1071. */
  1072. _shouldExposeHtmlType(module) {
  1073. if (this.options.extract === "inline") return true;
  1074. return this._shouldExtract(module);
  1075. }
  1076. /**
  1077. * Returns the source types available for this module.
  1078. * @param {NormalModule} module fresh module
  1079. * @returns {SourceTypes} available types (do not mutate)
  1080. */
  1081. getTypes(module) {
  1082. if (this._shouldExposeHtmlType(module)) {
  1083. return JAVASCRIPT_AND_HTML_TYPES;
  1084. }
  1085. return JAVASCRIPT_TYPES;
  1086. }
  1087. /**
  1088. * @returns {boolean} whether getTypes() depends on the module's incoming connections
  1089. */
  1090. getTypesDependOnIncomingConnections() {
  1091. // A fixed extract (`true` / `false` / `"inline"`) gives a stable type set;
  1092. // only the unset default reads `_isEntryModule` (incoming connections).
  1093. const { extract } = this.options;
  1094. return extract !== true && extract !== false && extract !== "inline";
  1095. }
  1096. /**
  1097. * Returns the estimated size for the requested source type.
  1098. * @param {NormalModule} module the module
  1099. * @param {SourceType=} type source type
  1100. * @returns {number} estimate size of the module
  1101. */
  1102. getSize(module, type) {
  1103. const originalSource = module.originalSource();
  1104. if (!originalSource) return 0;
  1105. if (type === HTML_TYPE) return originalSource.size();
  1106. return originalSource.size() + 10;
  1107. }
  1108. /**
  1109. * Processes the provided module.
  1110. * @param {Dependency} dependency the dependency to generate
  1111. * @param {ReplaceSource} source the current replace source which can be modified
  1112. * @param {DependencyTemplateContext} templateContext the template context (shared across all dependencies of the module)
  1113. * @returns {void}
  1114. */
  1115. sourceDependency(dependency, source, templateContext) {
  1116. const constructor =
  1117. /** @type {DependencyConstructor} */
  1118. (dependency.constructor);
  1119. const template = templateContext.dependencyTemplates.get(constructor);
  1120. if (!template) {
  1121. throw new Error(
  1122. `No template for dependency: ${dependency.constructor.name}`
  1123. );
  1124. }
  1125. template.apply(dependency, source, templateContext);
  1126. }
  1127. /**
  1128. * Processes the provided dependencies block.
  1129. * @param {import("../DependenciesBlock")} block the dependencies block which will be processed
  1130. * @param {ReplaceSource} source the current replace source which can be modified
  1131. * @param {DependencyTemplateContext} templateContext the template context
  1132. * @returns {void}
  1133. */
  1134. sourceBlock(block, source, templateContext) {
  1135. for (const dependency of block.dependencies) {
  1136. this.sourceDependency(dependency, source, templateContext);
  1137. }
  1138. for (const childBlock of block.blocks) {
  1139. this.sourceBlock(childBlock, source, templateContext);
  1140. }
  1141. }
  1142. /**
  1143. * Processes the provided module.
  1144. * @param {NormalModule} module the module to generate
  1145. * @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
  1146. * @param {ReplaceSource} source the current replace source which can be modified
  1147. * @param {GenerateContext} generateContext the generateContext
  1148. * @returns {void}
  1149. */
  1150. sourceModule(module, initFragments, source, generateContext) {
  1151. // Only `dependency` varies across `template.apply` calls, so one context
  1152. // (and one lazy `chunkInitFragments` getter) serves the whole module.
  1153. /** @type {InitFragment<GenerateContext>[] | undefined} */
  1154. let chunkInitFragments;
  1155. /** @type {DependencyTemplateContext} */
  1156. const templateContext = {
  1157. runtimeTemplate: generateContext.runtimeTemplate,
  1158. dependencyTemplates: generateContext.dependencyTemplates,
  1159. moduleGraph: generateContext.moduleGraph,
  1160. chunkGraph: generateContext.chunkGraph,
  1161. module,
  1162. runtime: generateContext.runtime,
  1163. runtimeRequirements: generateContext.runtimeRequirements,
  1164. concatenationScope: generateContext.concatenationScope,
  1165. codeGenerationResults:
  1166. /** @type {CodeGenerationResults} */
  1167. (generateContext.codeGenerationResults),
  1168. initFragments,
  1169. get chunkInitFragments() {
  1170. if (!chunkInitFragments) {
  1171. const data =
  1172. /** @type {NonNullable<GenerateContext["getData"]>} */
  1173. (generateContext.getData)();
  1174. chunkInitFragments = data.get("chunkInitFragments");
  1175. if (!chunkInitFragments) {
  1176. chunkInitFragments = [];
  1177. data.set("chunkInitFragments", chunkInitFragments);
  1178. }
  1179. }
  1180. return chunkInitFragments;
  1181. }
  1182. };
  1183. for (const dependency of module.dependencies) {
  1184. this.sourceDependency(dependency, source, templateContext);
  1185. }
  1186. if (module.presentationalDependencies !== undefined) {
  1187. for (const dependency of module.presentationalDependencies) {
  1188. this.sourceDependency(dependency, source, templateContext);
  1189. }
  1190. }
  1191. for (const childBlock of module.blocks) {
  1192. this.sourceBlock(childBlock, source, templateContext);
  1193. }
  1194. }
  1195. /**
  1196. * Run all HTML dependency templates against the original module source and
  1197. * return the rewritten HTML. When `undoPath` is a string, `[webpack/auto]`
  1198. * placeholders left in by asset/url dependencies are resolved to that
  1199. * undo path (use `""` to make URLs root-relative). When `undoPath` is
  1200. * `undefined`, the placeholders are preserved so the caller (typically
  1201. * `HtmlModulesPlugin#renderManifest`, which only knows the final
  1202. * `.html` filename after code generation) can resolve them itself.
  1203. * @param {NormalModule} module the module to render
  1204. * @param {GenerateContext} generateContext the generate context
  1205. * @param {string=} undoPath value to substitute for `[webpack/auto]` placeholders
  1206. * @returns {string} the rewritten HTML
  1207. */
  1208. _renderHtml(module, generateContext, undoPath) {
  1209. // The dependency-template pass is identical for both type passes (no
  1210. // template branches on `type` or writes `runtimeRequirements`), so
  1211. // compute the raw render once per `codeGeneration()` and reuse it; the
  1212. // passes diverge only in the `[webpack/auto]` substitution below.
  1213. const cacheKey = generateContext.runtimeRequirements;
  1214. let rawContent = this._rawRenderCache.get(cacheKey);
  1215. if (rawContent === undefined) {
  1216. const originalSource = /** @type {Source} */ (module.originalSource());
  1217. const source = new ReplaceSource(originalSource);
  1218. /** @type {InitFragment<GenerateContext>[]} */
  1219. const initFragments = [];
  1220. this.sourceModule(module, initFragments, source, generateContext);
  1221. rawContent = /** @type {string} */ (source.source());
  1222. this._rawRenderCache.set(cacheKey, rawContent);
  1223. }
  1224. if (undoPath === undefined) {
  1225. // HTML output — leave sentinels and `[webpack/auto]` for renderManifest.
  1226. return rawContent;
  1227. }
  1228. // JS-export path — resolve `[webpack/auto]` inline; chunk-URL sentinels
  1229. // stay for `HtmlModulesPlugin`'s `JavascriptModulesPlugin.render` tap.
  1230. if (!rawContent.includes(PUBLIC_PATH_AUTO)) return rawContent;
  1231. // A relative `<base href>` prepends `../`s so the base can't misdirect
  1232. // the rewritten URLs (see `HtmlParser`).
  1233. const basePrefix =
  1234. /** @type {HtmlModuleBuildInfo} */ (module.buildInfo).baseUrlPrefix || "";
  1235. return rawContent.split(PUBLIC_PATH_AUTO).join(basePrefix + undoPath);
  1236. }
  1237. /**
  1238. * Generates generated code for this runtime module.
  1239. * @param {NormalModule} module module for which the code should be generated
  1240. * @param {GenerateContext} generateContext context for generate
  1241. * @returns {Source | null} generated code
  1242. */
  1243. generate(module, generateContext) {
  1244. const originalSource = module.originalSource();
  1245. if (!originalSource) {
  1246. return new RawSource("");
  1247. }
  1248. if (generateContext.type === HTML_TYPE) {
  1249. // Preserve `[webpack/auto]`; renderManifest resolves it once `.html` filename is known.
  1250. return new RawSource(
  1251. this._renderHtml(module, generateContext, undefined)
  1252. );
  1253. }
  1254. // JS export: resolve `[webpack/auto]` to root-relative URLs. No `.html`
  1255. // asset carries this markup, so an asset-level minimizer never sees it.
  1256. const embedded =
  1257. generateContext.runtimeTemplate.compilation._resolveEmbeddedSource(
  1258. new RawSource(this._renderHtml(module, generateContext, "")),
  1259. { type: HTML_TYPE, hostType: JAVASCRIPT_TYPE, module }
  1260. );
  1261. const embeddedSource = embedded.source();
  1262. const generated =
  1263. typeof embeddedSource === "string"
  1264. ? embeddedSource
  1265. : embeddedSource.toString("utf8");
  1266. /** @type {string} */
  1267. let sourceContent;
  1268. // `module.hot` is set by `HotModuleReplacementPlugin` on every module
  1269. // when HMR is enabled. When set, we cannot use the concatenation path
  1270. // (it merges into a parent's module scope, where `module.hot.accept`
  1271. // would target the wrong id) — `getConcatenationBailoutReason` keeps
  1272. // us out of concatenation in that case.
  1273. if (generateContext.concatenationScope && !module.hot) {
  1274. generateContext.concatenationScope.registerNamespaceExport(
  1275. ConcatenationScope.NAMESPACE_OBJECT_EXPORT
  1276. );
  1277. sourceContent = `${generateContext.runtimeTemplate.renderConst()} ${
  1278. ConcatenationScope.NAMESPACE_OBJECT_EXPORT
  1279. } = ${JSON.stringify(generated)};`;
  1280. } else {
  1281. generateContext.runtimeRequirements.add(RuntimeGlobals.module);
  1282. if (module.hot) {
  1283. sourceContent = this._renderHmrShim(
  1284. module,
  1285. generated,
  1286. this._shouldExtract(module),
  1287. generateContext.runtimeTemplate
  1288. );
  1289. } else {
  1290. sourceContent = `${module.moduleArgument}.exports = ${JSON.stringify(
  1291. generated
  1292. )};`;
  1293. }
  1294. }
  1295. return new RawSource(sourceContent);
  1296. }
  1297. /**
  1298. * Emits the JS shim with HMR self-acceptance. When `extracting` is true the
  1299. * HTML module is also the document the browser is viewing, so the shim
  1300. * additionally patches `document.body` and `document.title` on every hot
  1301. * update so the rendered page reflects the new HTML without a full
  1302. * reload. The patching guard `module.hot.data` ensures the body is only
  1303. * replaced on a re-evaluation after a hot update — never on the initial
  1304. * page load, where `document.body` already matches the extracted HTML.
  1305. *
  1306. * The runtime extracts the body / title / head-sans-title sections from
  1307. * the HTML with regexes. HTML comments are *masked* first — their
  1308. * characters replaced with spaces, preserving length — via a `<!--…-->`
  1309. * regex, so a `<body>` (or `</body>` / `<title>` / etc.) that appears
  1310. * inside a comment can't fool the tag regexes — that was the one
  1311. * well-known sharp edge of HTML+regex. Because masking keeps offsets
  1312. * stable, the body / title content is sliced back out of the *original*
  1313. * HTML, so real comments inside `<body>` survive into the patched DOM
  1314. * exactly as a full page reload would render them.
  1315. *
  1316. * `<head>` changes beyond `<title>` (a new `<meta>`, a swapped
  1317. * `<link rel=icon>`, an inline `<style>` block, …) are applied as a
  1318. * delta against the previous evaluation's authored head. Replacing
  1319. * `document.head.innerHTML` wholesale would tear down the runtime
  1320. * scripts and stylesheet links webpack injects there on initial load;
  1321. * a delta cannot, since those appear in neither authored head. An
  1322. * added `<script>` is re-created before it is inserted, so it runs the
  1323. * way the reload this replaces would have run it, and every addition
  1324. * lands at its authored position rather than at the end. Two edits are
  1325. * still worth reloading for: a removed `<script>` the browser *executed*,
  1326. * since one that has run cannot be un-run (a removed data block such as
  1327. * `application/ld+json` never ran, so it is patched like any other
  1328. * element), and a reordering of elements the delta cannot
  1329. * express — fine in a dev-server context because the regular
  1330. * (non-hot-update) `page.html` chunk is re-emitted on every rebuild,
  1331. * so the reloaded page picks up the new head.
  1332. * @param {NormalModule} module module
  1333. * @param {string} html the rewritten HTML content (with placeholders resolved)
  1334. * @param {boolean} extracting whether the HTML module is being extracted as a real `.html` file
  1335. * @param {RuntimeTemplate} runtimeTemplate runtime template (drives `const`/arrow emission based on environment support)
  1336. * @returns {string} JS shim source
  1337. */
  1338. _renderHmrShim(module, html, extracting, runtimeTemplate) {
  1339. const declare = runtimeTemplate.renderConst();
  1340. const mutable = runtimeTemplate.renderLet();
  1341. const acceptBlock = extracting
  1342. ? [
  1343. "module.hot.accept();",
  1344. // Mask `<!-- … -->` comments — replace their characters
  1345. // with spaces, preserving length — before any tag regex
  1346. // runs. Masking (rather than deleting) keeps string offsets
  1347. // stable, so the body/title content can be sliced back out
  1348. // of the original HTML with comments intact, while a
  1349. // `<body>`/`</body>`/`<title>` inside a comment can no
  1350. // longer fool the tag regexes (the non-greedy `*?` would
  1351. // otherwise stop at the FIRST `</body>` it sees, even one
  1352. // inside a comment).
  1353. `${declare} __webpack_mask_comments__ = ${runtimeTemplate.returningFunction(
  1354. `h.replace(/<!--[\\s\\S]*?-->/g, ${runtimeTemplate.returningFunction(
  1355. 'c.replace(/[^\\n]/g, " ")',
  1356. "c"
  1357. )})`,
  1358. "h"
  1359. )};`,
  1360. // Mask comments once per evaluation; reused for every
  1361. // tag-boundary search (head/body/title and the dispose-time
  1362. // head diff) so a large page isn't re-scanned per section.
  1363. `${declare} __webpack_masked_html__ = __webpack_mask_comments__(__webpack_html__);`,
  1364. // Slice the inner content of `<tag>…</tag>` out of the original
  1365. // HTML, using the pre-masked copy to locate the tag boundaries.
  1366. `${declare} __webpack_extract__ = ${runtimeTemplate.basicFunction(
  1367. "tag",
  1368. [
  1369. `${declare} open = new RegExp("<" + tag + "[^>]*>", "i").exec(__webpack_masked_html__);`,
  1370. "if (!open) return null;",
  1371. `${declare} start = open.index + open[0].length;`,
  1372. `${declare} close = new RegExp("</" + tag + ">", "i").exec(__webpack_masked_html__.slice(start));`,
  1373. "if (!close) return null;",
  1374. "return __webpack_html__.slice(start, start + close.index);"
  1375. ]
  1376. )};`,
  1377. `${declare} __webpack_extract_head__ = ${runtimeTemplate.basicFunction(
  1378. "",
  1379. [
  1380. // Mask only the small extracted head so a comment-only head
  1381. // edit doesn't force a full reload.
  1382. `${declare} head = __webpack_extract__("head");`,
  1383. 'return head === null ? "" : __webpack_mask_comments__(head).replace(/<title[^>]*>[\\s\\S]*?<\\/title>/i, "").trim();'
  1384. ]
  1385. )};`,
  1386. // The authored head's elements, parsed but detached.
  1387. `${declare} __webpack_head_children__ = ${runtimeTemplate.basicFunction(
  1388. "html",
  1389. [
  1390. `${declare} container = document.createElement("head");`,
  1391. "container.innerHTML = html;",
  1392. "return Array.prototype.slice.call(container.children);"
  1393. ]
  1394. )};`,
  1395. // A `<script>` parsed out of a string carries the already-started flag,
  1396. // so appending that element would never run it. A fresh one with the
  1397. // same attributes and body does — which is what the reload this
  1398. // replaces would have done.
  1399. `${declare} __webpack_revive_script__ = ${runtimeTemplate.basicFunction(
  1400. "node",
  1401. [
  1402. 'if (node.nodeName !== "SCRIPT") return node;',
  1403. `${declare} script = document.createElement("script");`,
  1404. `${declare} attributes = node.attributes;`,
  1405. `for (${mutable} i = 0; i < attributes.length; i++) {`,
  1406. Template.indent(
  1407. "script.setAttribute(attributes[i].name, attributes[i].value);"
  1408. ),
  1409. "}",
  1410. "script.textContent = node.textContent;",
  1411. "return script;"
  1412. ]
  1413. )};`,
  1414. // Apply only the difference between the two authored heads, keyed on
  1415. // serialized markup: the runtime's own injected `<script>` / `<link>`
  1416. // are in neither string, so replacing `document.head` wholesale would
  1417. // tear them down while a delta never touches them. Reports whether it
  1418. // could — the caller reloads when it could not.
  1419. `${declare} __webpack_patch_head__ = ${runtimeTemplate.basicFunction(
  1420. "oldHtml, newHtml",
  1421. [
  1422. "if (!document.head) return false;",
  1423. `${declare} oldChildren = __webpack_head_children__(oldHtml);`,
  1424. `${declare} newChildren = __webpack_head_children__(newHtml);`,
  1425. `${declare} count = ${runtimeTemplate.basicFunction("nodes", [
  1426. `${declare} counts = {};`,
  1427. `for (${mutable} i = 0; i < nodes.length; i++) {`,
  1428. Template.indent([
  1429. `${declare} key = "$" + nodes[i].outerHTML;`,
  1430. "counts[key] = (counts[key] || 0) + 1;"
  1431. ]),
  1432. "}",
  1433. "return counts;"
  1434. ])};`,
  1435. // An element present the same number of times on both sides is
  1436. // untouched; whatever is left over on one side is the delta.
  1437. `${declare} surplus = ${runtimeTemplate.basicFunction(
  1438. "nodes, others",
  1439. [
  1440. `${declare} counts = count(others);`,
  1441. `${declare} out = [];`,
  1442. `for (${mutable} i = 0; i < nodes.length; i++) {`,
  1443. Template.indent([
  1444. `${declare} key = "$" + nodes[i].outerHTML;`,
  1445. "if (counts[key] > 0) counts[key]--; else out.push(nodes[i]);"
  1446. ]),
  1447. "}",
  1448. "return out;"
  1449. ]
  1450. )};`,
  1451. `${declare} removed = surplus(oldChildren, newChildren);`,
  1452. `${declare} added = surplus(newChildren, oldChildren);`,
  1453. "if (removed.length === 0 && added.length === 0) return true;",
  1454. // A script that has run cannot be un-run, so dropping one is the
  1455. // one edit still worth a reload — but only a script that ran. A
  1456. // `type` that is neither a JavaScript MIME type nor `module` /
  1457. // `importmap` / `speculationrules` makes the element a data block
  1458. // (`application/ld+json`, `text/template`, …) the browser never
  1459. // executed, so removing it is as safe as removing a `<meta>`.
  1460. // Every JavaScript MIME essence ends in `script`, and a type
  1461. // nothing here recognizes falls back to the reload.
  1462. `if (removed.some(${runtimeTemplate.basicFunction("node", [
  1463. 'if (node.nodeName !== "SCRIPT") return false;',
  1464. `${declare} type = (node.getAttribute("type") || "").trim().toLowerCase();`,
  1465. 'return type === "" || type.indexOf("script") !== -1 || type === "module" || type === "importmap" || type === "speculationrules";'
  1466. ])})) return false;`,
  1467. // Resolve every removal before mutating, so a node the page
  1468. // dropped on its own leaves the head as it was.
  1469. `${declare} live = [];`,
  1470. `for (${mutable} i = 0; i < removed.length; i++) {`,
  1471. Template.indent([
  1472. `${declare} key = removed[i].outerHTML;`,
  1473. `${mutable} match = null;`,
  1474. `for (${mutable} j = 0; j < document.head.children.length; j++) {`,
  1475. Template.indent([
  1476. `${declare} child = document.head.children[j];`,
  1477. "if (child.outerHTML === key && live.indexOf(child) === -1) { match = child; break; }"
  1478. ]),
  1479. "}",
  1480. "if (match === null) return false;",
  1481. "live.push(match);"
  1482. ]),
  1483. "}",
  1484. `for (${mutable} i = 0; i < live.length; i++) document.head.removeChild(live[i]);`,
  1485. // Pair each retained element with its live node, scanning only
  1486. // forward: an authored element that has moved relative to the
  1487. // others finds no match from there on, and a reorder the delta
  1488. // cannot express falls back to the reload. Injected nodes simply
  1489. // never match, so they neither anchor nor block anything.
  1490. `${declare} liveByNew = [];`,
  1491. `${mutable} searchFrom = 0;`,
  1492. `for (${mutable} i = 0; i < newChildren.length; i++) {`,
  1493. Template.indent([
  1494. "if (added.indexOf(newChildren[i]) !== -1) { liveByNew.push(null); continue; }",
  1495. `${declare} key = newChildren[i].outerHTML;`,
  1496. `${mutable} match = null;`,
  1497. `for (${mutable} j = searchFrom; j < document.head.children.length; j++) {`,
  1498. Template.indent([
  1499. `${declare} child = document.head.children[j];`,
  1500. "if (child.outerHTML === key) { match = child; searchFrom = j + 1; break; }"
  1501. ]),
  1502. "}",
  1503. "if (match === null) return false;",
  1504. "liveByNew.push(match);"
  1505. ]),
  1506. "}",
  1507. // Back to front, so the element an addition must precede is
  1508. // already placed by the time it is needed.
  1509. `for (${mutable} i = newChildren.length - 1; i >= 0; i--) {`,
  1510. Template.indent([
  1511. "if (liveByNew[i] !== null) continue;",
  1512. `${mutable} anchor = null;`,
  1513. `for (${mutable} j = i + 1; j < newChildren.length; j++) {`,
  1514. Template.indent(
  1515. "if (liveByNew[j] !== null) { anchor = liveByNew[j]; break; }"
  1516. ),
  1517. "}",
  1518. `${declare} node = __webpack_revive_script__(newChildren[i]);`,
  1519. "if (anchor === null) document.head.appendChild(node);",
  1520. "else document.head.insertBefore(node, anchor);",
  1521. "liveByNew[i] = node;"
  1522. ]),
  1523. "}",
  1524. "return true;"
  1525. ]
  1526. )};`,
  1527. "if (module.hot.data && typeof document !== 'undefined') {",
  1528. Template.indent([
  1529. `${declare} __webpack_new_head__ = __webpack_extract_head__();`,
  1530. "if (module.hot.data.__webpack_head__ !== undefined && __webpack_new_head__ !== module.hot.data.__webpack_head__ && !__webpack_patch_head__(module.hot.data.__webpack_head__, __webpack_new_head__) && typeof window !== 'undefined' && window.location && typeof window.location.reload === 'function') {",
  1531. Template.indent("window.location.reload();"),
  1532. "} else {",
  1533. Template.indent([
  1534. `${declare} __webpack_body__ = __webpack_extract__("body");`,
  1535. "if (__webpack_body__ !== null && document.body) document.body.innerHTML = __webpack_body__;",
  1536. `${declare} __webpack_title__ = __webpack_extract__("title");`,
  1537. "if (__webpack_title__ !== null) document.title = __webpack_title__;"
  1538. ]),
  1539. "}"
  1540. ]),
  1541. "}",
  1542. // Capture this evaluation's head (sans title, comments
  1543. // masked) on dispose so the next module instance can diff
  1544. // against it.
  1545. `module.hot.dispose(${runtimeTemplate.basicFunction(
  1546. "data",
  1547. "data.__webpack_head__ = __webpack_extract_head__();"
  1548. )});`
  1549. ]
  1550. : ["module.hot.accept();"];
  1551. return Template.asString([
  1552. `${declare} __webpack_html__ = ${JSON.stringify(html)};`,
  1553. `${module.moduleArgument}.exports = __webpack_html__;`,
  1554. "if (module.hot) {",
  1555. Template.indent(acceptBlock),
  1556. "}"
  1557. ]);
  1558. }
  1559. /**
  1560. * Generates fallback output for the provided error condition.
  1561. * @param {Error} error the error
  1562. * @param {NormalModule} module module for which the code should be generated
  1563. * @param {GenerateContext} generateContext context for generate
  1564. * @returns {Source | null} generated code
  1565. */
  1566. generateError(error, module, generateContext) {
  1567. if (generateContext.type === HTML_TYPE) {
  1568. // Strip `<`, `>`, `--` runs from `error.message` so it can't escape the comment.
  1569. const safe = String(error.message)
  1570. .replace(/[<>]/g, "")
  1571. .replace(/-{2,}/g, (m) => `${"-".repeat(m.length - 1)} `);
  1572. return new RawSource(`<!-- webpack error: ${safe} -->`);
  1573. }
  1574. return new RawSource(Generator.throwBuildErrorCode(error));
  1575. }
  1576. /**
  1577. * Updates the hash with the data contributed by this instance.
  1578. * @param {Hash} hash hash that will be modified
  1579. * @param {UpdateHashContext} updateHashContext context for updating hash
  1580. */
  1581. updateHash(hash, updateHashContext) {
  1582. hash.update("html");
  1583. // Source-type set changes when html-type exposure flips; the HMR shim's
  1584. // `extracting` branch changes when the emit decision flips. They differ
  1585. // only for `"inline"` (exposes html, but does not emit), so hash both.
  1586. // One `_shouldExtract` walk covers both checks (expose = extract ∪ inline).
  1587. const extract = this._shouldExtract(updateHashContext.module);
  1588. if (extract || this.options.extract === "inline") {
  1589. hash.update("html-type");
  1590. }
  1591. if (extract) {
  1592. hash.update("extract");
  1593. }
  1594. // The HMR shim emits additional self-accept / DOM-patch code that
  1595. // isn't emitted in non-HMR builds, so the cached codegen must
  1596. // invalidate when `module.hot` toggles.
  1597. if (updateHashContext.module.hot) {
  1598. hash.update("hot");
  1599. }
  1600. // A `renderEmbeddedSource` tap has to reach the codegen cache key, or a
  1601. // changed option replays the output from before it.
  1602. const { runtimeTemplate } = updateHashContext;
  1603. if (runtimeTemplate) {
  1604. runtimeTemplate.compilation.hooks.embeddedSourceHash.call(
  1605. updateHashContext.module,
  1606. hash
  1607. );
  1608. }
  1609. }
  1610. }
  1611. module.exports = HtmlGenerator;