HtmlEntryDependency.js 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const {
  6. CSS_IMPORT_TYPE,
  7. CSS_TYPE,
  8. JAVASCRIPT_TYPE
  9. } = require("../ModuleSourceTypeConstants");
  10. const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin");
  11. const ResourceHintRuntimeModule = require("../prefetch/ResourceHintRuntimeModule");
  12. const makeSerializable = require("../util/makeSerializable");
  13. const memoize = require("../util/memoize");
  14. const ModuleDependency = require("./ModuleDependency");
  15. // only the template needs it, and that runs only for a real html module
  16. const getHtmlGenerator = memoize(() => require("../html/HtmlGenerator"));
  17. /** @import { ReplaceSource } from "webpack-sources" */
  18. /** @import Chunk from "../Chunk" */
  19. /** @import ChunkGraph from "../ChunkGraph" */
  20. /** @import Dependency from "../Dependency" */
  21. /** @import { DependencyTemplateContext } from "../DependencyTemplate" */
  22. /** @import Entrypoint from "../Entrypoint" */
  23. /** @import { Range } from "../javascript/JavascriptParser" */
  24. /** @typedef {"script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch"} HtmlEntryElementKind */
  25. /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext<[string, string, HtmlEntryElementKind, number, number, boolean, string, number, boolean, number, (Range | null), (Range | null), boolean, number, number]>} ObjectDeserializerContext */
  26. /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext<[string, string, HtmlEntryElementKind, number, number, boolean, string, number, boolean, number, (Range | null), (Range | null), boolean, number, number]>} ObjectSerializerContext */
  27. class HtmlEntryDependency extends ModuleDependency {
  28. /**
  29. * Creates an instance of HtmlEntryDependency.
  30. * @param {string} request request
  31. * @param {Range} range range of the attribute value in the source
  32. * @param {string} entryName name of the entry this script src is bundled into
  33. * @param {string=} category dependency category used for resolving and grouping
  34. * @param {HtmlEntryElementKind=} elementKind shape of the originating HTML element; used when expanding sibling tags for split/runtime chunks
  35. * @param {number=} tagStart position of the opening `<` of the originating tag in the source; sibling tags emitted for additional entry chunks are inserted right before this
  36. * @param {number=} tagOpenEnd position of the character immediately after the opening tag's `>` in the source; combined with `tagStart` this lets the template clone the original opening tag verbatim (preserving attributes like `nonce`, `crossorigin`, `referrerpolicy`, `defer`, `async`) when generating sibling tags
  37. * @param {boolean=} tagIsNative whether the originating element is the native tag for `elementKind` (`<script>` / `<link>`); decided at parse time from the tag name so the template needn't re-parse the source text. A custom element mapped to a `script`/`stylesheet` source `type` is non-native and gets a freshly synthesized sibling tag instead of a verbatim clone
  38. * @param {string=} copyableAttrsText the originating tag's `nonce`/`crossorigin`/`referrerpolicy` attribute source spans (leading-space-prefixed, in that fixed order), captured at parse time so synthesized sibling `<link>`/`<script>` tags carry the same CSP/fetch policy without the template re-parsing the tag text; empty when none are present
  39. * @param {number=} tagNameEnd position right after the originating tag's name (e.g. after `<script`), captured at parse time so the template can insert a `crossorigin` attribute without re-scanning the tag text for the name boundary
  40. * @param {boolean=} hasOwnCrossOrigin whether the originating tag already carries a `crossorigin` attribute; when true the author's value wins and `output.crossOriginLoading` is not applied
  41. * @param {number=} cssAnchor source offset of the first classic blocking `<script>` tag (not `defer`/`async`/module — those execute after parsing and after pending stylesheets); injected stylesheet `<link>`s are kept ahead of it. -1 when the document has no blocking script, letting CSS follow `defer`/module script tags (the order Vite emits)
  42. * @param {Range | null=} integrityRange source span of the originating tag's `integrity` attribute (offsets relative to `tagStart`, leading whitespace and quotes included), captured at parse time so cloned sibling tags can drop it without re-parsing; null when absent
  43. * @param {Range | null=} typeValueRange source span of the originating tag's `type` attribute value (offsets relative to `tagStart`), captured at parse time so `script-module` sibling clones can force `type="module"` in place; null when there is no value to rewrite (a `type="module"` is then inserted instead)
  44. * @param {boolean=} forceInline `<!-- webpackInline: true -->` was set before this tag; takes precedence over `output.html.inline`
  45. * @param {number=} headOpenEnd position just inside the (possibly implicit) `<head>` — after its opening tag, else after `<html>` or the doctype — captured from the parse so the template needn't re-scan the source (a text scan would match markup inside comments); resource hints are inserted here. -1 when unknown
  46. * @param {number=} headAnchor position right after the last node inside `<head>` (the same anchor `output.html` title/meta injection uses); `inject: "head"` sibling tags are inserted here. -1 when the document has no usable head (insertion then falls back to the entry tag)
  47. */
  48. constructor(
  49. request,
  50. range,
  51. entryName,
  52. category,
  53. elementKind,
  54. tagStart,
  55. tagOpenEnd,
  56. tagIsNative,
  57. copyableAttrsText,
  58. tagNameEnd,
  59. hasOwnCrossOrigin,
  60. cssAnchor,
  61. integrityRange,
  62. typeValueRange,
  63. forceInline,
  64. headOpenEnd,
  65. headAnchor
  66. ) {
  67. super(request);
  68. this.range = range;
  69. /** @type {string} */
  70. this.entryName = entryName;
  71. /** @type {string} */
  72. this._category = category || "commonjs";
  73. /** @type {HtmlEntryElementKind} */
  74. this.elementKind = elementKind || "script";
  75. /** @type {number} */
  76. this.tagStart = tagStart === undefined ? -1 : tagStart;
  77. /** @type {number} */
  78. this.tagOpenEnd = tagOpenEnd === undefined ? -1 : tagOpenEnd;
  79. /** @type {boolean} */
  80. this.tagIsNative = tagIsNative !== false;
  81. /** @type {string} */
  82. this.copyableAttrsText = copyableAttrsText || "";
  83. /** @type {number} */
  84. this.tagNameEnd = tagNameEnd === undefined ? -1 : tagNameEnd;
  85. /** @type {boolean} */
  86. this.hasOwnCrossOrigin = hasOwnCrossOrigin === true;
  87. /** @type {number} */
  88. this.cssAnchor = cssAnchor === undefined ? -1 : cssAnchor;
  89. /** @type {Range | null} */
  90. this.integrityRange = integrityRange || null;
  91. /** @type {Range | null} */
  92. this.typeValueRange = typeValueRange || null;
  93. /** @type {boolean} */
  94. this.forceInline = forceInline === true;
  95. /** @type {number} */
  96. this.headOpenEnd = headOpenEnd === undefined ? -1 : headOpenEnd;
  97. /** @type {number} */
  98. this.headAnchor = headAnchor === undefined ? -1 : headAnchor;
  99. }
  100. get type() {
  101. return "html entry";
  102. }
  103. get category() {
  104. return this._category;
  105. }
  106. /**
  107. * Serializes this instance into the provided serializer context.
  108. * @param {ObjectSerializerContext} context context
  109. */
  110. serialize(context) {
  111. context
  112. .write(this.entryName)
  113. .write(this._category)
  114. .write(this.elementKind)
  115. .write(this.tagStart)
  116. .write(this.tagOpenEnd)
  117. .write(this.tagIsNative)
  118. .write(this.copyableAttrsText)
  119. .write(this.tagNameEnd)
  120. .write(this.hasOwnCrossOrigin)
  121. .write(this.cssAnchor)
  122. .write(this.integrityRange)
  123. .write(this.typeValueRange)
  124. .write(this.forceInline)
  125. .write(this.headOpenEnd)
  126. .write(this.headAnchor);
  127. super.serialize(context);
  128. }
  129. /**
  130. * Restores this instance from the provided deserializer context.
  131. * @param {ObjectDeserializerContext} context context
  132. */
  133. deserialize(context) {
  134. this.entryName = context.read();
  135. const c1 = context.rest;
  136. this._category = c1.read();
  137. const c2 = c1.rest;
  138. this.elementKind = c2.read();
  139. const c3 = c2.rest;
  140. this.tagStart = c3.read();
  141. const c4 = c3.rest;
  142. this.tagOpenEnd = c4.read();
  143. const c5 = c4.rest;
  144. this.tagIsNative = c5.read();
  145. const c6 = c5.rest;
  146. this.copyableAttrsText = c6.read();
  147. const c7 = c6.rest;
  148. this.tagNameEnd = c7.read();
  149. const c8 = c7.rest;
  150. this.hasOwnCrossOrigin = c8.read();
  151. const c9 = c8.rest;
  152. this.cssAnchor = c9.read();
  153. const c10 = c9.rest;
  154. this.integrityRange = c10.read();
  155. const c11 = c10.rest;
  156. this.typeValueRange = c11.read();
  157. const c12 = c11.rest;
  158. this.forceInline = c12.read();
  159. const c13 = c12.rest;
  160. this.headOpenEnd = c13.read();
  161. const c14 = c13.rest;
  162. this.headAnchor = c14.read();
  163. super.deserialize(c14.rest);
  164. }
  165. }
  166. /**
  167. * @param {Entrypoint} entrypoint entrypoint
  168. * @returns {Chunk[]} every chunk this entrypoint needs in load order: the
  169. * runtime chunk first (when `optimization.runtimeChunk` splits it off), then
  170. * any intermediate chunks (e.g. from `optimization.splitChunks`), and finally
  171. * the entry chunk itself. The entry chunk is always returned last so callers
  172. * can identify it as the tag whose `src`/`href` attribute is being rewritten
  173. * in place. Chunks that are already loaded by an ancestor (`dependOn`) entry's
  174. * own script tag — i.e. the parent entrypoint's entry chunk *and* its runtime
  175. * chunk — are skipped, otherwise they would be loaded twice when the same
  176. * HTML contains tags for both the leader and the dependant entries.
  177. */
  178. const getEntrypointChunksInLoadOrder = (entrypoint) => {
  179. const entryChunk = /** @type {Chunk} */ (entrypoint.getEntrypointChunk());
  180. const runtimeChunk = entrypoint.getRuntimeChunk();
  181. /** @type {Set<Chunk>} */
  182. const chunksLoadedByAncestorTags = new Set();
  183. /** @type {Set<import("../ChunkGroup")>} */
  184. const visitedGroups = new Set();
  185. const walk = (/** @type {import("../ChunkGroup")} */ group) => {
  186. if (visitedGroups.has(group)) return;
  187. visitedGroups.add(group);
  188. for (const parent of group.parentsIterable) {
  189. if (
  190. typeof (/** @type {Entrypoint} */ (parent).getEntrypointChunk) ===
  191. "function"
  192. ) {
  193. const parentEntry =
  194. /** @type {Entrypoint} */
  195. (parent).getEntrypointChunk();
  196. if (parentEntry) chunksLoadedByAncestorTags.add(parentEntry);
  197. const parentRuntime =
  198. /** @type {Entrypoint} */
  199. (parent).getRuntimeChunk();
  200. if (parentRuntime) chunksLoadedByAncestorTags.add(parentRuntime);
  201. }
  202. walk(parent);
  203. }
  204. };
  205. walk(entrypoint);
  206. /** @type {Chunk[]} */
  207. const ordered = [];
  208. /** @type {Set<Chunk>} */
  209. const seen = new Set();
  210. const push = (/** @type {Chunk | null | undefined} */ chunk) => {
  211. if (!chunk || seen.has(chunk) || chunk === entryChunk) return;
  212. if (chunksLoadedByAncestorTags.has(chunk)) return;
  213. seen.add(chunk);
  214. ordered.push(chunk);
  215. };
  216. if (runtimeChunk !== entryChunk) {
  217. push(runtimeChunk);
  218. }
  219. for (const chunk of entrypoint.chunks) {
  220. push(chunk);
  221. }
  222. ordered.push(entryChunk);
  223. return ordered;
  224. };
  225. /**
  226. * Whether webpack will emit a `.js` file for this chunk that must be
  227. * loaded with a `<script>` tag. Covers three independent reasons a
  228. * chunk needs JS output: it owns one or more JS-source-type modules;
  229. * it has entry modules whose source types include JavaScript (entry
  230. * modules don't show up in `getChunkModulesIterableBySourceType` until
  231. * they're connected as regular modules — this is why
  232. * `JavascriptModulesPlugin#_chunkHasJs` checks them separately); or it
  233. * is a runtime chunk — `chunk.hasRuntime()` — which produces a `.js`
  234. * file holding the webpack runtime, but its `RuntimeModule`s live in
  235. * a separate `runtimeModules` set and are *not* surfaced via
  236. * `getChunkModulesIterableBySourceType`. Missing the runtime case
  237. * would cause a `runtimeChunk`-split chunk to fall out of the
  238. * `<script>` list and re-emerge after the chunks that depend on it,
  239. * producing `__webpack_require__ is not defined` at load time.
  240. * @param {Chunk} chunk chunk
  241. * @param {ChunkGraph} chunkGraph chunk graph
  242. * @returns {boolean} true if the chunk emits a `.js` file
  243. */
  244. const chunkHasJs = (chunk, chunkGraph) => {
  245. if (chunk.hasRuntime()) return true;
  246. if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
  247. for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
  248. if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
  249. return true;
  250. }
  251. }
  252. }
  253. return Boolean(
  254. chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
  255. );
  256. };
  257. /**
  258. * Whether webpack will emit a `.css` file for this chunk that must be
  259. * loaded with a `<link rel="stylesheet">` tag. Matches
  260. * `CssModulesPlugin.chunkHasCss` exactly — both regular CSS modules
  261. * and pure `@import` placeholder modules count, since the latter
  262. * still contribute a `.css` asset to the chunk.
  263. * @param {Chunk} chunk chunk
  264. * @param {ChunkGraph} chunkGraph chunk graph
  265. * @returns {boolean} true if the chunk emits a `.css` file
  266. */
  267. const chunkHasCss = (chunk, chunkGraph) =>
  268. Boolean(chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_TYPE)) ||
  269. Boolean(
  270. chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_IMPORT_TYPE)
  271. );
  272. /**
  273. * Deterministic tie-break key for CSS link ordering. `chunk.name` and
  274. * `chunk.id` are both stable strings (when present); one of them is set for
  275. * every chunk webpack emits. We can't rely on `Array.prototype.sort` being
  276. * stable — webpack still supports Node 10.13 where V8's sort is not guaranteed
  277. * stable for arrays larger than ten elements — so any time
  278. * `firstCssModulePostOrderIndex` returns the same value for two chunks (most
  279. * commonly when several chunks have no reachable CSS module in the entrypoint's
  280. * dependency walk and all map to `Infinity`) this key picks the canonical order.
  281. * Computed once per chunk when building the sort array rather than per
  282. * comparison.
  283. * @param {Chunk} chunk chunk
  284. * @returns {string} the tie-break key
  285. */
  286. const cssChunkSortKey = (chunk) =>
  287. `${chunk.name || ""} ${
  288. chunk.id === null || chunk.id === undefined ? "" : chunk.id
  289. }`;
  290. /**
  291. * Sort comparator for the CSS sibling emission order. Direct subtraction
  292. * would yield `NaN` when both indices are `Infinity` (the documented
  293. * fallback for chunks whose CSS modules the entrypoint's walk never
  294. * reaches), and `Array#sort` doesn't promise stable ordering on the legacy
  295. * Node 10 targets this repo still supports — so the tie-breaker must always
  296. * run when the indices match, including the `Infinity === Infinity` case.
  297. * @param {{ index: number, key: string }} a first entry
  298. * @param {{ index: number, key: string }} b second entry
  299. * @returns {-1 | 0 | 1} sort order
  300. */
  301. const compareCssChunkOrder = (a, b) => {
  302. if (a.index < b.index) return -1;
  303. if (a.index > b.index) return 1;
  304. if (a.key < b.key) return -1;
  305. if (a.key > b.key) return 1;
  306. return 0;
  307. };
  308. const CSS_SOURCE_TYPES = [CSS_TYPE, CSS_IMPORT_TYPE];
  309. /**
  310. * Smallest post-order index among the CSS modules of a chunk, taken
  311. * from the entrypoint's view of the dependency graph. Used to sort
  312. * sibling CSS chunks so they appear in source import order in the
  313. * extracted HTML — `entrypoint.chunks` itself does not give that
  314. * ordering for arbitrary splitChunks layouts. Considers both
  315. * `CSS_TYPE` and `CSS_IMPORT_TYPE` modules so a chunk made up
  316. * exclusively of `@import` placeholder modules (e.g. when splitChunks
  317. * separates them from their target CSS) still sorts by its true
  318. * source position rather than collapsing to `Infinity` and relying on
  319. * the chunk-name tie-breaker.
  320. * @param {Chunk} chunk chunk
  321. * @param {Entrypoint} entrypoint entrypoint the chunk belongs to
  322. * @param {ChunkGraph} chunkGraph chunk graph
  323. * @returns {number} the lowest post-order index of any CSS or
  324. * CSS-import module in the chunk, or `Number.POSITIVE_INFINITY` when
  325. * no such module has a defined index (e.g. for a module the
  326. * entrypoint never reached on its own dependency walk — runtime-only
  327. * modules, modules reached via `dependOn`, etc.) so such chunks sort
  328. * last among CSS chunks
  329. */
  330. const firstCssModulePostOrderIndex = (chunk, entrypoint, chunkGraph) => {
  331. let min = Number.POSITIVE_INFINITY;
  332. for (const sourceType of CSS_SOURCE_TYPES) {
  333. const modules = chunkGraph.getChunkModulesIterableBySourceType(
  334. chunk,
  335. sourceType
  336. );
  337. if (!modules) continue;
  338. for (const module of modules) {
  339. const idx = entrypoint.getModulePostOrderIndex(module);
  340. if (idx !== undefined && idx < min) min = idx;
  341. }
  342. }
  343. return min;
  344. };
  345. /**
  346. * Build a fresh `<link rel="stylesheet" href="…">` for a CSS chunk that
  347. * was pulled in by a `<script src>` entry — the originating tag was a
  348. * `<script>`, but the chunk is CSS so cloning the script tag verbatim
  349. * would produce nonsense (`<script src="…\.css">`). `extra` carries the
  350. * `nonce`/`crossorigin`/`referrerpolicy` source spans captured from the
  351. * original element at parse time so the same CSP and fetch policy applies;
  352. * `defer`/`async`/`type` have no meaning on `<link>` and are dropped.
  353. * @param {string} extra the copyable CSP/fetch attribute source text (leading-space-prefixed, or empty)
  354. * @param {string} href URL for the stylesheet
  355. * @param {string} crossOrigin ` crossorigin="…"` to append from `output.crossOriginLoading`, or `""`
  356. * @param {string} integrity ` integrity="<sentinel>"` to append when `output.html.integrity` is on, or `""`
  357. * @returns {string} the sibling `<link>` tag's HTML
  358. */
  359. const buildStylesheetLink = (extra, href, crossOrigin, integrity) => {
  360. const safeHref = href.replace(/"/g, "&quot;");
  361. return `<link rel="stylesheet" href="${safeHref}"${extra}${crossOrigin}${integrity}>`;
  362. };
  363. /**
  364. * Build a fresh `<script src="…">` for a JS chunk pulled in by a custom
  365. * (non-`<script>`) element that the user mapped to `type: "script"` /
  366. * `"script-module"`. Cloning the custom element verbatim and appending
  367. * `</script>` (as `cloneTagWithUrl` does for a real `<script>`) would emit
  368. * mismatched, non-executing markup such as `<my-script src="…"></script>`,
  369. * so synthesize a real script element instead — copying only the CSP/fetch
  370. * attributes (`extra`, captured at parse time), since the custom element's
  371. * other attributes have no defined meaning on a `<script>`.
  372. * @param {string} extra the copyable CSP/fetch attribute source text (leading-space-prefixed, or empty)
  373. * @param {string} src URL for the script
  374. * @param {boolean} isModule whether to emit `type="module"`
  375. * @param {string} crossOrigin ` crossorigin="…"` to append from `output.crossOriginLoading`, or `""`
  376. * @param {string} integrity ` integrity="<sentinel>"` to append when `output.html.integrity` is on, or `""`
  377. * @returns {string} the sibling `<script>` tag's HTML
  378. */
  379. const buildScriptTag = (extra, src, isModule, crossOrigin, integrity) => {
  380. const safeSrc = src.replace(/"/g, "&quot;");
  381. return `<script${isModule ? ' type="module"' : ""} src="${safeSrc}"${extra}${crossOrigin}${integrity}></script>`;
  382. };
  383. /**
  384. * Clone the original `<script>`/`<link>` opening tag with its `src`/`href`
  385. * value swapped for a different chunk URL. Reusing the source text verbatim
  386. * preserves attributes such as `nonce`, `crossorigin`, `referrerpolicy`,
  387. * `defer`, and `async` so the sibling tags load with the same semantics as
  388. * the entry tag that's already there. `integrity` is dropped because it's
  389. * content-specific. When the original tag was upgraded to a module script
  390. * (either by the author or by the `output.module` auto-upgrade in
  391. * `HtmlParser`), the sibling is forced to `type="module"` regardless of what
  392. * the source originally said. The `integrity`/`type` spans come from the
  393. * parser (`HtmlParser` captured them off the AST); edits are applied
  394. * right-to-left so earlier offsets stay valid — no re-parsing of the tag.
  395. * @param {string} originalTag the opening tag's source text including `>`
  396. * @param {number} srcStartInTag offset of the src/href value start within `originalTag`
  397. * @param {number} srcEndInTag offset of the src/href value end within `originalTag`
  398. * @param {string} newUrl URL to put into the cloned tag's src/href slot
  399. * @param {HtmlEntryElementKind} elementKind shape of the originating tag
  400. * @param {string} crossOrigin ` crossorigin="…"` to insert from `output.crossOriginLoading`, or `""`
  401. * @param {number} tagNameEndInTag offset right after the tag name within `originalTag`, where `crossOrigin`/`integrity` are inserted
  402. * @param {Range | null} integrityRange `integrity` attribute span within `originalTag` to drop, or null
  403. * @param {Range | null} typeValueRange `type` value span within `originalTag` to overwrite with `module`, or null to insert `type="module"`
  404. * @param {string} integrity ` integrity="<sentinel>"` to insert when `output.html.integrity` is on, or `""`
  405. * @returns {string} the sibling tag's HTML (including a closing `</script>` for script tags)
  406. */
  407. const cloneTagWithUrl = (
  408. originalTag,
  409. srcStartInTag,
  410. srcEndInTag,
  411. newUrl,
  412. elementKind,
  413. crossOrigin,
  414. tagNameEndInTag,
  415. integrityRange,
  416. typeValueRange,
  417. integrity
  418. ) => {
  419. const isLink =
  420. elementKind === "modulepreload" || elementKind === "stylesheet";
  421. /** @type {{ start: number, end: number, text: string }[]} */
  422. const edits = [{ start: srcStartInTag, end: srcEndInTag, text: newUrl }];
  423. // Drop the author's `integrity` — content-specific, wrong for another
  424. // chunk's file. A correct per-chunk one is re-inserted below from
  425. // `output.html.integrity`.
  426. if (integrityRange) {
  427. edits.push({ start: integrityRange[0], end: integrityRange[1], text: "" });
  428. }
  429. // Inserted right after the tag name: `crossorigin` from
  430. // `output.crossOriginLoading`, then a forced `type="module"` when the tag
  431. // has no `type` value to rewrite in place.
  432. let afterName = crossOrigin;
  433. if (elementKind === "script-module") {
  434. if (typeValueRange) {
  435. edits.push({
  436. start: typeValueRange[0],
  437. end: typeValueRange[1],
  438. text: "module"
  439. });
  440. } else {
  441. afterName += ' type="module"';
  442. }
  443. }
  444. // Per-chunk `integrity` sentinel goes in alongside `crossorigin`/`type`.
  445. afterName += integrity;
  446. if (afterName) {
  447. edits.push({
  448. start: tagNameEndInTag,
  449. end: tagNameEndInTag,
  450. text: afterName
  451. });
  452. }
  453. edits.sort((a, b) => b.start - a.start);
  454. let body = originalTag;
  455. for (const edit of edits) {
  456. body = body.slice(0, edit.start) + edit.text + body.slice(edit.end);
  457. }
  458. // `<link>` is a void element — no closing tag. `<script>` needs `</script>`.
  459. return isLink ? body : `${body}</script>`;
  460. };
  461. // ES5 `<link rel="modulepreload">` polyfill (Safari <17, Firefox <115, …):
  462. // on browsers without native support, fetch each modulepreload target with the
  463. // tag's own credentials/integrity so the pending module import reuses it. The
  464. // `.ep` guard makes it idempotent, and the observer catches any later-inserted
  465. // links. Emitted only when the target environment lacks native support.
  466. const MODULEPRELOAD_POLYFILL = [
  467. "(function(){",
  468. 'var s=document.createElement("link").relList;',
  469. 'if(s&&s.supports&&s.supports("modulepreload"))return;',
  470. "function f(e){",
  471. "if(e.ep)return;e.ep=1;",
  472. "var o={};",
  473. "if(e.integrity)o.integrity=e.integrity;",
  474. "if(e.referrerPolicy)o.referrerPolicy=e.referrerPolicy;",
  475. 'o.credentials=e.crossOrigin==="use-credentials"?"include":e.crossOrigin==="anonymous"?"omit":"same-origin";',
  476. "fetch(e.href,o)}",
  477. "var l=document.querySelectorAll('link[rel=\"modulepreload\"]');",
  478. "for(var i=0;i<l.length;i++)f(l[i]);",
  479. "new MutationObserver(function(m){",
  480. "for(var j=0;j<m.length;j++)",
  481. "for(var a=m[j].addedNodes,k=0;k<a.length;k++)",
  482. 'if(a[k].tagName==="LINK"&&a[k].rel==="modulepreload")f(a[k])',
  483. "}).observe(document,{childList:true,subtree:true})",
  484. "})();"
  485. ].join("");
  486. // One polyfill `<script>` per rendered page. Keyed by the `ReplaceSource` of a
  487. // single HTML code-generation pass (shared by all of a page's entry tags, fresh
  488. // per pass), so multi-`<script>` pages get exactly one and the cache stays correct.
  489. /** @type {WeakSet<ReplaceSource>} */
  490. const polyfilledSources = new WeakSet();
  491. /** @typedef {"preload" | "prefetch" | "modulepreload" | "preconnect" | "dns-prefetch"} HtmlResourceHintRel */
  492. /**
  493. * A custom resource-hint `<link>` (`output.html.resourceHints`). Exactly one of
  494. * `href` / `chunk` / `entry` names the target; `chunk`/`entry` URLs (hash and
  495. * public path) are resolved by webpack, `href` is used verbatim.
  496. * @typedef {object} HtmlResourceHint
  497. * @property {HtmlResourceHintRel} rel hint relationship
  498. * @property {string=} href literal URL (external, `preconnect`, or an already-hashed asset)
  499. * @property {string=} chunk chunk name to hint
  500. * @property {string=} entry entrypoint name to hint (expands to one hint per initial chunk)
  501. * @property {string=} as the `as` attribute; defaults to `script` for chunk/entry refs
  502. * @property {string=} type the `type` attribute (MIME)
  503. * @property {(boolean | "anonymous" | "use-credentials")=} crossorigin CORS mode (`true` → `anonymous`)
  504. * @property {string=} media the `media` attribute
  505. * @property {boolean=} integrity SRI for a chunk/entry ref; follows `output.html.integrity` by default, `false` opts out
  506. * @property {("low" | "high" | "auto")=} fetchPriority the `fetchpriority` attribute (allowed on `preload`/`modulepreload`/`prefetch`)
  507. */
  508. /**
  509. * @typedef {object} HtmlResourceHintContext
  510. * @property {string} entryName name of the entry this page loads
  511. * @property {Entrypoint} entrypoint the entrypoint
  512. * @property {"html" | "js"} hostType `"html"` when the entry has an extracted HTML page; `"js"` when a JS-only entry is being consulted (SSR framework case)
  513. * @property {import("../Compilation")} compilation the compilation
  514. * @property {{ rel: "modulepreload" | "preload" | "prefetch", chunk: string, hostChunks: string[] }[]} defaultHints the auto initial-graph hints (`chunk` / `hostChunks` name the referencing chunk — Vite's `hostId`)
  515. */
  516. /**
  517. * @param {string} value attribute value
  518. * @returns {string} value with `"` escaped so it can't break out of the attribute
  519. */
  520. const escapeAttr = (value) => String(value).replace(/"/g, "&quot;");
  521. /**
  522. * @param {boolean | "anonymous" | "use-credentials" | undefined} value crossorigin option
  523. * @returns {string | undefined} ` crossorigin="…"`, or undefined when unset/false
  524. */
  525. const crossOriginAttr = (value) => {
  526. if (value === undefined || value === false) return undefined;
  527. return ` crossorigin="${value === true ? "anonymous" : value}"`;
  528. };
  529. /**
  530. * Build one resource-hint `<link>` tag from a resolved href and attributes.
  531. * @param {HtmlResourceHintRel} rel the `rel`
  532. * @param {{ href: string, as?: string, type?: string, crossorigin?: string, integrity?: string, media?: string, fetchpriority?: string }} attrs attributes (already href-resolved; `crossorigin`/`integrity` are full ` name="…"` spans)
  533. * @returns {string} the `<link>` tag
  534. */
  535. const buildLinkTag = (rel, attrs) =>
  536. `<link rel="${rel}"${attrs.as ? ` as="${escapeAttr(attrs.as)}"` : ""}${
  537. attrs.type ? ` type="${escapeAttr(attrs.type)}"` : ""
  538. } href="${escapeAttr(attrs.href)}"${attrs.crossorigin || ""}${
  539. attrs.integrity || ""
  540. }${attrs.media ? ` media="${escapeAttr(attrs.media)}"` : ""}${
  541. attrs.fetchpriority
  542. ? ` fetchpriority="${escapeAttr(attrs.fetchpriority)}"`
  543. : ""
  544. }>`;
  545. /**
  546. * Resolve one `output.html.resourceHints` descriptor to zero or more `<link>`
  547. * tags. A `chunk`/`entry` reference becomes the emitted chunk URL (a sentinel
  548. * resolved with the final hash and public path), an `href` is used verbatim, and
  549. * `preconnect`/`dns-prefetch` use only `href`. Unknown chunk/entry names are skipped.
  550. * @param {HtmlResourceHint} desc the descriptor
  551. * @param {{ compilation: import("../Compilation"), chunkGraph: ChunkGraph, chunkByName: Map<string, Chunk>, outputCrossOrigin: string, integrityOn: boolean }} ctx resolution context
  552. * @returns {string[]} the resolved tags
  553. */
  554. const resolveResourceHint = (desc, ctx) => {
  555. const rel = desc.rel;
  556. if (rel === "preconnect" || rel === "dns-prefetch") {
  557. if (!desc.href) return [];
  558. return [
  559. buildLinkTag(rel, {
  560. href: desc.href,
  561. crossorigin: crossOriginAttr(desc.crossorigin),
  562. media: desc.media
  563. })
  564. ];
  565. }
  566. if (desc.href) {
  567. return [
  568. buildLinkTag(rel, {
  569. href: desc.href,
  570. as: desc.as,
  571. type: desc.type,
  572. crossorigin: crossOriginAttr(desc.crossorigin),
  573. media: desc.media,
  574. fetchpriority: desc.fetchPriority
  575. })
  576. ];
  577. }
  578. /** @type {Chunk[]} */
  579. let chunks;
  580. if (desc.entry) {
  581. const entrypoint = ctx.compilation.entrypoints.get(desc.entry);
  582. if (!entrypoint) return [];
  583. chunks = getEntrypointChunksInLoadOrder(entrypoint);
  584. } else if (desc.chunk) {
  585. const chunk = ctx.chunkByName.get(desc.chunk);
  586. if (!chunk) return [];
  587. chunks = [chunk];
  588. } else {
  589. return [];
  590. }
  591. const kind = desc.as === "style" ? "css" : "javascript";
  592. const as = desc.as || (rel === "modulepreload" ? undefined : "script");
  593. // A hint targeting another chunk must match how that chunk is fetched, so
  594. // inherit `output.crossOriginLoading` unless the descriptor overrides it.
  595. const crossorigin =
  596. desc.crossorigin !== undefined
  597. ? crossOriginAttr(desc.crossorigin)
  598. : ctx.outputCrossOrigin || undefined;
  599. /** @type {string[]} */
  600. const tags = [];
  601. for (const chunk of chunks) {
  602. const has =
  603. kind === "css"
  604. ? chunkHasCss(chunk, ctx.chunkGraph)
  605. : chunkHasJs(chunk, ctx.chunkGraph);
  606. if (!has) continue;
  607. tags.push(
  608. buildLinkTag(rel, {
  609. href: getHtmlGenerator().makeChunkUrlSentinel(chunk, kind),
  610. as,
  611. type: desc.type,
  612. crossorigin,
  613. // SRI follows the global `output.html.integrity` policy (like the
  614. // auto hints); `integrity: false` opts a single hint out. Only
  615. // `preload`/`modulepreload` are SRI-eligible — `prefetch` is not.
  616. integrity:
  617. (rel === "preload" || rel === "modulepreload") &&
  618. desc.integrity !== false &&
  619. ctx.integrityOn
  620. ? ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(chunk, kind)}"`
  621. : undefined,
  622. media: desc.media,
  623. fetchpriority: desc.fetchPriority
  624. })
  625. );
  626. }
  627. return tags;
  628. };
  629. HtmlEntryDependency.Template = class HtmlEntryDependencyTemplate extends (
  630. ModuleDependency.Template
  631. ) {
  632. /**
  633. * Applies the plugin by registering its hooks on the compiler.
  634. * @param {Dependency} dependency the dependency for which the template should be applied
  635. * @param {ReplaceSource} source the current replace source which can be modified
  636. * @param {DependencyTemplateContext} templateContext the context object
  637. * @returns {void}
  638. */
  639. apply(dependency, source, templateContext) {
  640. const { runtimeTemplate } = templateContext;
  641. const dep = /** @type {HtmlEntryDependency} */ (dependency);
  642. const compilation = runtimeTemplate.compilation;
  643. // A `type: "html"` link points at another emitted page, not a JS/CSS
  644. // chunk: rewrite the attribute to that page's filename (resolved by
  645. // HtmlModulesPlugin once it's built) and emit no sibling tags — a page
  646. // link loads only itself.
  647. if (dep.elementKind === "html") {
  648. source.replace(
  649. dep.range[0],
  650. dep.range[1] - 1,
  651. getHtmlGenerator().makeHtmlPageUrlSentinel(dep.entryName)
  652. );
  653. return;
  654. }
  655. const { chunkGraph } = compilation;
  656. const { crossOriginLoading } = compilation.outputOptions;
  657. const entrypoint = /** @type {Entrypoint | undefined} */ (
  658. compilation.entrypoints.get(dep.entryName)
  659. );
  660. if (!entrypoint) {
  661. source.replace(dep.range[0], dep.range[1] - 1, "data:,");
  662. return;
  663. }
  664. const orderedChunks = getEntrypointChunksInLoadOrder(entrypoint);
  665. const entryChunk = orderedChunks[orderedChunks.length - 1];
  666. // `crossorigin` mirrors `output.crossOriginLoading` onto every injected
  667. // tag. Empty when the option is off or the originating tag already set
  668. // `crossorigin` (author value wins, flagged at parse time). Mirrors
  669. // webpack's runtime, which sets `crossOrigin` on chunk-loading scripts,
  670. // and matches Vite, which emits it on every injected script/stylesheet.
  671. const crossOrigin =
  672. crossOriginLoading && !dep.hasOwnCrossOrigin
  673. ? ` crossorigin="${crossOriginLoading}"`
  674. : "";
  675. // Per-chunk SRI: emit an `integrity` sentinel resolved late (after the
  676. // chunks' final bytes exist) by `resolveChunkIntegritySentinels`. Tags are
  677. // generated per HTML module, but one module can back pages with different
  678. // `integrity` options, so sentinels are emitted whenever any page wants SRI
  679. // and each page then resolves or strips them on emit.
  680. const htmlOption = compilation.outputOptions.html;
  681. const integrityOn = getHtmlGenerator().hasIntegritySentinels(compilation);
  682. const inject =
  683. typeof htmlOption === "object" ? htmlOption.inject : undefined;
  684. /**
  685. * Mirror `crossorigin`/`integrity` onto a native entry tag, inserted right
  686. * after the tag name so they sit alongside any `type="module"` the parser
  687. * injected. The author's content-specific `integrity` is dropped first (as
  688. * clones do) so the tag never carries two.
  689. * @param {Chunk} chunk chunk whose SRI hash the tag references
  690. * @param {"javascript" | "css"} contentHashType which chunk asset to hash
  691. * @returns {void}
  692. */
  693. const applyEntryTagAttrs = (chunk, contentHashType) => {
  694. if (!dep.tagIsNative || dep.tagNameEnd < 0) return;
  695. if (crossOrigin) source.insert(dep.tagNameEnd, crossOrigin);
  696. if (integrityOn) {
  697. if (dep.integrityRange) {
  698. source.replace(
  699. dep.tagStart + dep.integrityRange[0],
  700. dep.tagStart + dep.integrityRange[1] - 1,
  701. ""
  702. );
  703. }
  704. source.insert(
  705. dep.tagNameEnd,
  706. ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(
  707. chunk,
  708. contentHashType
  709. )}"`
  710. );
  711. }
  712. };
  713. // `rel="preload"/"prefetch"` is a resource hint: rewrite the `href` to the
  714. // built chunk's URL (CSS for `as="style"`, JS otherwise) and stop — no
  715. // sibling tags and no execution.
  716. if (dep.elementKind === "preload" || dep.elementKind === "prefetch") {
  717. const contentHashType =
  718. dep.category === "css-import" ? "css" : "javascript";
  719. source.replace(
  720. dep.range[0],
  721. dep.range[1] - 1,
  722. getHtmlGenerator().makeChunkUrlSentinel(entryChunk, contentHashType)
  723. );
  724. // `preload` is integrity-eligible per the SRI spec, so mirror
  725. // `crossorigin`/`integrity` onto it (SRI requires CORS). `prefetch` is a
  726. // navigation cache hint the spec doesn't cover — leave it a bare href.
  727. if (dep.elementKind === "preload") {
  728. applyEntryTagAttrs(entryChunk, contentHashType);
  729. }
  730. return;
  731. }
  732. const isStylesheet = dep.elementKind === "stylesheet";
  733. // Inlined scripts are `type="module"` only for ESM output; classic-IIFE
  734. // chunks (the default) must stay a plain `<script>`.
  735. const scriptTypeAttr =
  736. compilation.outputOptions.module === true ? ' type="module"' : "";
  737. const entryContentHashType = isStylesheet ? "css" : "javascript";
  738. const inlineOption =
  739. typeof htmlOption === "object" ? htmlOption.inline : undefined;
  740. /** @type {RegExp[] | null} */
  741. const inlinePatterns = Array.isArray(inlineOption) ? inlineOption : null;
  742. /**
  743. * @param {Chunk} chunk chunk to test
  744. * @param {"javascript" | "css"} contentType which asset of the chunk is being emitted
  745. * @returns {boolean} true when this chunk's content should be inlined
  746. */
  747. const isChunkInlined = (chunk, contentType) => {
  748. // A `<link rel="modulepreload">` is a void fetch hint — inlining it would
  749. // emit an unclosed, executing `<script>`.
  750. if (dep.elementKind === "modulepreload") return false;
  751. if (dep.forceInline) return true;
  752. if (!inlineOption) return false;
  753. if (inlineOption === true) return true;
  754. // `"script"`/`"style"` inline by asset type, not chunk name — a chunk's
  755. // JS and CSS share a name, so only the emitted `contentType` tells them
  756. // apart.
  757. if (inlineOption === "script") return contentType === "javascript";
  758. if (inlineOption === "style") return contentType === "css";
  759. const name = chunk.name || String(chunk.id);
  760. return /** @type {RegExp[]} */ (inlinePatterns).some((re) =>
  761. re.test(name)
  762. );
  763. };
  764. // Only inline when the HTML module is emitted as a standalone page (extract mode).
  765. // If it's exported as a JS string, sentinels would end up embedded in the JS chunk.
  766. const isExtractedHtml = chunkGraph
  767. .getModuleSourceTypes(
  768. /** @type {import("../Module")} */ (templateContext.module)
  769. )
  770. .has("html");
  771. const entryInlined =
  772. isExtractedHtml &&
  773. dep.tagStart >= 0 &&
  774. dep.tagOpenEnd > dep.tagStart &&
  775. isChunkInlined(entryChunk, entryContentHashType);
  776. if (entryInlined) {
  777. const inlineSentinel = getHtmlGenerator().makeChunkInlineSentinel(
  778. entryChunk,
  779. entryContentHashType
  780. );
  781. if (isStylesheet) {
  782. source.replace(
  783. dep.tagStart,
  784. dep.tagOpenEnd - 1,
  785. `<style>${inlineSentinel}</style>`
  786. );
  787. } else {
  788. source.replace(
  789. dep.tagStart,
  790. dep.tagOpenEnd - 1,
  791. `<script${scriptTypeAttr}>${inlineSentinel}`
  792. );
  793. }
  794. } else {
  795. source.replace(
  796. dep.range[0],
  797. dep.range[1] - 1,
  798. getHtmlGenerator().makeChunkUrlSentinel(
  799. entryChunk,
  800. entryContentHashType
  801. )
  802. );
  803. }
  804. if (dep.tagStart < 0 || dep.tagOpenEnd <= dep.tagStart) {
  805. return;
  806. }
  807. const originalContent = /** @type {string} */ (source.original().source());
  808. const originalTag = originalContent.slice(dep.tagStart, dep.tagOpenEnd);
  809. const srcStartInTag = dep.range[0] - dep.tagStart;
  810. const srcEndInTag = dep.range[1] - dep.tagStart;
  811. const tagIsNative = dep.tagIsNative;
  812. const tagNameEndInTag = dep.tagNameEnd - dep.tagStart;
  813. // Mirror `crossorigin`/`integrity` onto the non-inlined entry tag
  814. // (siblings get them via the builders below).
  815. if (!entryInlined) applyEntryTagAttrs(entryChunk, entryContentHashType);
  816. // `inject: false` suppresses sibling-chunk tags only; resource hints
  817. // below still apply (the way to reference the suppressed chunks).
  818. if (inject !== false) {
  819. /**
  820. * @param {Chunk} chunk chunk to emit a sibling tag for
  821. * @param {"javascript" | "css"} kind content type slice of the chunk to emit
  822. * @returns {string} a single sibling tag's HTML
  823. */
  824. const buildSibling = (chunk, kind) => {
  825. if (isChunkInlined(chunk, kind)) {
  826. const sentinel = getHtmlGenerator().makeChunkInlineSentinel(
  827. chunk,
  828. kind
  829. );
  830. return kind === "css"
  831. ? `<style>${sentinel}</style>`
  832. : `<script${scriptTypeAttr}>${sentinel}</script>`;
  833. }
  834. const url = getHtmlGenerator().makeChunkUrlSentinel(chunk, kind);
  835. const integrityAttr = integrityOn
  836. ? ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(chunk, kind)}"`
  837. : "";
  838. if (kind === "css") {
  839. // A CSS chunk is always loaded via `<link rel="stylesheet">`.
  840. // Clone only when the entry tag is itself a native `<link
  841. // rel="stylesheet">` (so attributes like `media` carry over);
  842. // a `<script>` entry that imported CSS, or a custom element,
  843. // gets a freshly synthesized `<link>`.
  844. if (isStylesheet && tagIsNative) {
  845. return cloneTagWithUrl(
  846. originalTag,
  847. srcStartInTag,
  848. srcEndInTag,
  849. url,
  850. dep.elementKind,
  851. crossOrigin,
  852. tagNameEndInTag,
  853. dep.integrityRange,
  854. dep.typeValueRange,
  855. integrityAttr
  856. );
  857. }
  858. return buildStylesheetLink(
  859. dep.copyableAttrsText,
  860. url,
  861. crossOrigin,
  862. integrityAttr
  863. );
  864. }
  865. // A JS chunk is loaded via `<script>`. Clone a native `<script>`
  866. // verbatim; synthesize a real `<script>` for custom elements that
  867. // were mapped to `type: script`/`script-module`.
  868. if (tagIsNative) {
  869. return cloneTagWithUrl(
  870. originalTag,
  871. srcStartInTag,
  872. srcEndInTag,
  873. url,
  874. dep.elementKind,
  875. crossOrigin,
  876. tagNameEndInTag,
  877. dep.integrityRange,
  878. dep.typeValueRange,
  879. integrityAttr
  880. );
  881. }
  882. return buildScriptTag(
  883. dep.copyableAttrsText,
  884. url,
  885. dep.elementKind === "script-module",
  886. crossOrigin,
  887. integrityAttr
  888. );
  889. };
  890. const siblings = [];
  891. // Parser-derived anchor (a source text scan would match `</head>` inside
  892. // comments or raw text).
  893. const headClose = inject === "head" ? dep.headAnchor : -1;
  894. // When the entry tag is already inside <head> (tagStart < headClose),
  895. // inserting siblings at headClose would place them after the entry,
  896. // reversing runtime-before-entry execution order for deferred scripts.
  897. // Insert before the entry tag instead so the load order is preserved.
  898. const headAt =
  899. headClose < 0
  900. ? -1
  901. : dep.tagStart < headClose
  902. ? dep.tagStart
  903. : headClose;
  904. let cssHtml = "";
  905. let cssAt = -1;
  906. if (isStylesheet) {
  907. // `<link rel="stylesheet">` entries are CSS, but a shared/runtime
  908. // JS-only chunk (e.g. `optimization.runtimeChunk`) can still sit in
  909. // the entrypoint — only clone the `<link>` for siblings that actually
  910. // produce a `.css` asset (mirrors the JS branch below), otherwise the
  911. // link points at a non-existent `.css` file (a 404, or an empty
  912. // `<style>` when inlined). Attributes like `media` still carry over.
  913. for (let i = 0; i < orderedChunks.length - 1; i++) {
  914. if (chunkHasCss(orderedChunks[i], chunkGraph)) {
  915. siblings.push(buildSibling(orderedChunks[i], "css"));
  916. }
  917. }
  918. } else {
  919. // CSS chunks are emitted before JS chunks so the cascade is set
  920. // up before any script runs. Within CSS the order needs to match
  921. // the source's import order — `entrypoint.chunks` alone doesn't
  922. // give us that for arbitrary splitChunks layouts (splitChunks
  923. // inserts each new chunk before the entry chunk via
  924. // `insertChunk(_, before)`, so split CSS siblings end up in
  925. // *reverse* of the order they were processed — exactly the
  926. // html-webpack-plugin#1838 / mini-css-extract#959 symptom). We
  927. // re-derive the order from the entrypoint's module post-order
  928. // index, which mirrors the dependency walk and so reflects the
  929. // import order.
  930. /** @type {{ chunk: Chunk, index: number, key: string }[]} */
  931. const cssChunkOrder = [];
  932. /** @type {Chunk[]} */
  933. const jsChunks = [];
  934. for (let i = 0; i < orderedChunks.length - 1; i++) {
  935. const chunk = orderedChunks[i];
  936. const hasCss = chunkHasCss(chunk, chunkGraph);
  937. const hasJs = chunkHasJs(chunk, chunkGraph);
  938. if (hasCss) {
  939. cssChunkOrder.push({
  940. chunk,
  941. index: firstCssModulePostOrderIndex(
  942. chunk,
  943. entrypoint,
  944. chunkGraph
  945. ),
  946. key: cssChunkSortKey(chunk)
  947. });
  948. }
  949. // Anything that isn't CSS-only stays on the JS lane, in the
  950. // `orderedChunks` order — that preserves the runtime-first /
  951. // vendor-before-entry invariant of `getEntrypointChunksInLoadOrder`.
  952. // Chunks that produce no `.js` and no `.css` (e.g. wasm-only
  953. // or asset-only) still get a `<script>` clone here so we
  954. // keep prior behavior for users who relied on it.
  955. if (hasJs || !hasCss) jsChunks.push(chunk);
  956. }
  957. // If the entry chunk itself contains CSS (entry JS imports CSS
  958. // without splitChunks separating it), fold it into the same CSS
  959. // ordering so the entry-chunk `<link>` lands in the correct
  960. // cascade position relative to sibling CSS chunks.
  961. if (chunkHasCss(entryChunk, chunkGraph)) {
  962. cssChunkOrder.push({
  963. chunk: entryChunk,
  964. index: firstCssModulePostOrderIndex(
  965. entryChunk,
  966. entrypoint,
  967. chunkGraph
  968. ),
  969. key: cssChunkSortKey(entryChunk)
  970. });
  971. }
  972. cssChunkOrder.sort(compareCssChunkOrder);
  973. // Stylesheets always land in <head> when the page has one. A
  974. // classic blocking script pins them ahead of itself (`cssAnchor`)
  975. // so its execution never awaits later CSS; when every script is
  976. // `defer`/module they go to the *end* of head, after the script
  977. // tags — Vite's and html-webpack-plugin's order: the scripts
  978. // start fetching earlier yet still execute after parsing and
  979. // after pending stylesheets.
  980. cssAt =
  981. dep.headAnchor >= 0
  982. ? dep.cssAnchor >= 0 && dep.cssAnchor < dep.headAnchor
  983. ? dep.cssAnchor
  984. : dep.headAnchor
  985. : dep.cssAnchor >= 0
  986. ? dep.cssAnchor
  987. : dep.tagStart;
  988. cssHtml = cssChunkOrder
  989. .map(({ chunk }) => buildSibling(chunk, "css"))
  990. .join("");
  991. for (const chunk of jsChunks) {
  992. siblings.push(buildSibling(chunk, "javascript"));
  993. }
  994. }
  995. // Same-offset inserts keep call order: CSS goes in before the
  996. // sibling scripts only when a blocking script demands CSS-first.
  997. if (cssHtml !== "" && dep.cssAnchor >= 0) source.insert(cssAt, cssHtml);
  998. if (siblings.length > 0) {
  999. source.insert(headAt >= 0 ? headAt : dep.tagStart, siblings.join(""));
  1000. }
  1001. if (cssHtml !== "" && dep.cssAnchor < 0) source.insert(cssAt, cssHtml);
  1002. }
  1003. // Resource hints are opt-in — webpack already loads the initial chunks
  1004. // via parallel `<script>` tags, so there is no waterfall to prevent by
  1005. // default. `output.resourceHints` requests them: `true` preloads the
  1006. // entry's initial dependency chunks (runtime, vendor, split); an array
  1007. // or function adds custom hints (a function may spread the auto
  1008. // `defaultHints`). Only for extracted pages — a JS-string HTML export
  1009. // would embed the sentinels in the JS chunk. URL-referenced assets
  1010. // (fonts, images) carrying `webpackPrefetch` / `webpackPreload`
  1011. // (magic-comment or `parser.<type>.urlHints` rule) also emit here.
  1012. const resourceHintResolver =
  1013. ResourceHintPlugin.getCompilationResolver(compilation);
  1014. const resourceHints = resourceHintResolver.hints;
  1015. if (
  1016. isExtractedHtml &&
  1017. (dep.elementKind === "script" || dep.elementKind === "script-module")
  1018. ) {
  1019. const isModuleOutput = compilation.outputOptions.module === true;
  1020. /** @type {string[]} */
  1021. const hints = [];
  1022. // === preconnect channel — cross-origin publicPath (resourceHints.preconnect) ===
  1023. const rhOptions = compilation.outputOptions.resourceHints;
  1024. if (rhOptions && rhOptions.preconnect) {
  1025. const pp = compilation.outputOptions.publicPath;
  1026. const originMatch =
  1027. typeof pp === "string" ? /^(https?:)?\/\/[^/?#]+/i.exec(pp) : null;
  1028. if (originMatch) {
  1029. hints.push(
  1030. `<link rel="preconnect" href="${escapeAttr(originMatch[0])}"${crossOrigin}>`
  1031. );
  1032. }
  1033. }
  1034. // === chunks channel — the entry's own initial dependency graph ===
  1035. if (resourceHints !== undefined && resourceHints !== false) {
  1036. // The auto initial-graph preload as descriptors carrying their exact
  1037. // tag, built only when needed (`true`/`"prefetch"` emits them, a
  1038. // function may spread them). The entry chunk is skipped — it's
  1039. // already the `<script src>`. ESM uses `modulepreload` (fetch +
  1040. // parse), classic output `preload as="script"`, and `"prefetch"`
  1041. // opts into `<link rel="prefetch">` (idle-time fetch, no `as` / no
  1042. // `integrity` — the spec doesn't list them). Async `import()` chunks
  1043. // aren't covered here; use `module.parser.javascript.dynamicImport*`
  1044. // which routes through webpack's on-demand chunk-load runtime.
  1045. const prefetchMode = resourceHints === "prefetch";
  1046. const preloadMode = resourceHints === true;
  1047. /** @type {"modulepreload" | "preload" | "prefetch"} */
  1048. const rel = prefetchMode
  1049. ? "prefetch"
  1050. : isModuleOutput
  1051. ? "modulepreload"
  1052. : "preload";
  1053. /** @type {{ rel: "modulepreload" | "preload" | "prefetch", chunk: string, hostChunks: string[], _tag: string }[]} */
  1054. const defaultHints = [];
  1055. if (
  1056. preloadMode ||
  1057. prefetchMode ||
  1058. typeof resourceHints === "function"
  1059. ) {
  1060. for (const chunk of orderedChunks) {
  1061. if (
  1062. chunk === entryChunk ||
  1063. isChunkInlined(chunk, "javascript") ||
  1064. !chunkHasJs(chunk, chunkGraph)
  1065. ) {
  1066. continue;
  1067. }
  1068. const href = getHtmlGenerator().makeChunkUrlSentinel(
  1069. chunk,
  1070. "javascript"
  1071. );
  1072. const integrityAttr =
  1073. integrityOn && !prefetchMode
  1074. ? ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(
  1075. chunk,
  1076. "javascript"
  1077. )}"`
  1078. : "";
  1079. // Mirror the entry tag's `nonce`/`crossorigin`/`referrerpolicy`
  1080. // (and `output.crossOriginLoading`) so the request matches the
  1081. // sibling `<script>`'s and the browser reuses it instead of
  1082. // fetching twice.
  1083. const attrs = `${dep.copyableAttrsText}${crossOrigin}${integrityAttr}`;
  1084. const tag = prefetchMode
  1085. ? `<link rel="prefetch" href="${href}"${attrs}>`
  1086. : isModuleOutput
  1087. ? `<link rel="modulepreload" href="${href}"${attrs}>`
  1088. : `<link rel="preload" as="script" href="${href}"${attrs}>`;
  1089. const chunkName = chunk.name || String(chunk.id);
  1090. defaultHints.push({
  1091. rel,
  1092. chunk: chunkName,
  1093. hostChunks: [chunkName],
  1094. _tag: tag
  1095. });
  1096. }
  1097. }
  1098. if (preloadMode || prefetchMode) {
  1099. for (const d of defaultHints) {
  1100. hints.push(d._tag);
  1101. }
  1102. } else {
  1103. // Array (custom only) or function (returns the final list, may spread
  1104. // `defaultHints`). A returned default hint keeps its prebuilt tag; any
  1105. // other descriptor is resolved through the URL/SRI pipeline.
  1106. let descriptors = resourceHints;
  1107. if (typeof resourceHints === "function") {
  1108. // The page's own entry name (its named entry chunk), not the synthetic
  1109. // `<script>` entry — that's what users key on.
  1110. let entryName = dep.entryName;
  1111. for (const c of chunkGraph.getModuleChunks(
  1112. /** @type {import("../Module")} */ (templateContext.module)
  1113. )) {
  1114. if (c.name) {
  1115. entryName = c.name;
  1116. break;
  1117. }
  1118. }
  1119. descriptors = resourceHints({
  1120. entryName,
  1121. entrypoint,
  1122. hostType: "html",
  1123. compilation,
  1124. defaultHints
  1125. });
  1126. }
  1127. if (Array.isArray(descriptors) && descriptors.length > 0) {
  1128. /** @type {Map<string, Chunk>} */
  1129. const chunkByName = new Map();
  1130. for (const c of compilation.chunks) {
  1131. if (c.name) chunkByName.set(c.name, c);
  1132. }
  1133. const outputCrossOrigin = crossOriginLoading
  1134. ? ` crossorigin="${crossOriginLoading}"`
  1135. : "";
  1136. const ctx = {
  1137. compilation,
  1138. chunkGraph,
  1139. chunkByName,
  1140. outputCrossOrigin,
  1141. integrityOn
  1142. };
  1143. for (const desc of descriptors) {
  1144. if (!desc || typeof desc !== "object") continue;
  1145. const prebuilt = /** @type {{ _tag?: string }} */ (desc)._tag;
  1146. if (prebuilt) {
  1147. hints.push(prebuilt);
  1148. } else if (desc.rel) {
  1149. for (const tag of resolveResourceHint(desc, ctx)) {
  1150. hints.push(tag);
  1151. }
  1152. }
  1153. }
  1154. }
  1155. }
  1156. }
  1157. // === assets channel — URL-referenced assets carrying `prefetch`/`preload` ===
  1158. // The plugin has already scanned the entrypoint's chunks × modules
  1159. // once and cached the URL/CSS/HTML asset deps for this entry name;
  1160. // consume that list here so this template doesn't repeat the walk.
  1161. // Each unique asset emits one `<link rel="prefetch|preload">` into
  1162. // the HTML `<head>`; the JS chunk-startup runtime skips it (no
  1163. // double `<link>` in DOM). URLs are sentinels resolved from asset
  1164. // codegen data during `HtmlModulesPlugin` render — the asset
  1165. // module might not have been codegen'd yet at this point.
  1166. const outputCrossOriginAttr = crossOriginLoading
  1167. ? ` crossorigin="${crossOriginLoading}"`
  1168. : "";
  1169. for (const { module, dep: md } of resourceHintResolver.getHtmlHinted(
  1170. dep.entryName
  1171. )) {
  1172. const rel = md.preload ? "preload" : "prefetch";
  1173. const asAttr =
  1174. md.asAttribute ||
  1175. ResourceHintRuntimeModule.guessAsAttribute(
  1176. /** @type {import("./URLDependency")} */ (md).request || ""
  1177. );
  1178. const typeAttr = md.typeAttribute;
  1179. const mediaAttr = md.mediaAttribute;
  1180. const fetchPriority = md.fetchPriority;
  1181. const href = getHtmlGenerator().makeAssetUrlSentinel(
  1182. module.identifier()
  1183. );
  1184. const attrs =
  1185. `${asAttr ? ` as="${escapeAttr(asAttr)}"` : ""}` +
  1186. `${typeAttr ? ` type="${escapeAttr(typeAttr)}"` : ""}` +
  1187. ` href="${escapeAttr(href)}"${
  1188. outputCrossOriginAttr
  1189. }${mediaAttr ? ` media="${escapeAttr(mediaAttr)}"` : ""}` +
  1190. `${fetchPriority ? ` fetchpriority="${escapeAttr(fetchPriority)}"` : ""}`;
  1191. hints.push(`<link rel="${rel}"${attrs}>`);
  1192. }
  1193. if (hints.length > 0) {
  1194. // De-duplicate (a custom hint may repeat an auto one), preserving order.
  1195. /** @type {Set<string>} */
  1196. const seen = new Set();
  1197. /** @type {string[]} */
  1198. const finalHints = [];
  1199. for (const h of hints) {
  1200. if (!seen.has(h)) {
  1201. seen.add(h);
  1202. finalHints.push(h);
  1203. }
  1204. }
  1205. // When the target environment lacks native `modulepreload` (an
  1206. // `output.environment` capability, like the JS-feature checks), append
  1207. // a tiny polyfill once per page so the hints still warm the cache there.
  1208. // `resourceHints.modulePreloadPolyfill` defaults from the target's
  1209. // modulepreload support (`output.environment.modulePreload`); `false`
  1210. // opts out (strict CSP).
  1211. if (
  1212. rhOptions &&
  1213. rhOptions.modulePreloadPolyfill &&
  1214. !polyfilledSources.has(source) &&
  1215. finalHints.some((h) => h.startsWith('<link rel="modulepreload"'))
  1216. ) {
  1217. polyfilledSources.add(source);
  1218. finalHints.push(`<script>${MODULEPRELOAD_POLYFILL}</script>`);
  1219. }
  1220. // Insert right after the `<head>` open tag (parser-derived) so the
  1221. // hints are the document's first fetches; fall back to the first
  1222. // blocking script (else the entry tag) when the page has no
  1223. // written `<head>` (e.g. an HTML fragment).
  1224. const hintAt =
  1225. dep.headOpenEnd >= 0
  1226. ? dep.headOpenEnd
  1227. : dep.cssAnchor === -1
  1228. ? dep.tagStart
  1229. : dep.cssAnchor;
  1230. source.insert(hintAt, finalHints.join(""));
  1231. }
  1232. }
  1233. }
  1234. };
  1235. makeSerializable(
  1236. HtmlEntryDependency,
  1237. "webpack/lib/dependencies/HtmlEntryDependency"
  1238. );
  1239. module.exports = HtmlEntryDependency;