| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- */
- "use strict";
- const {
- CSS_IMPORT_TYPE,
- CSS_TYPE,
- JAVASCRIPT_TYPE
- } = require("../ModuleSourceTypeConstants");
- const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin");
- const ResourceHintRuntimeModule = require("../prefetch/ResourceHintRuntimeModule");
- const makeSerializable = require("../util/makeSerializable");
- const memoize = require("../util/memoize");
- const ModuleDependency = require("./ModuleDependency");
- // only the template needs it, and that runs only for a real html module
- const getHtmlGenerator = memoize(() => require("../html/HtmlGenerator"));
- /** @import { ReplaceSource } from "webpack-sources" */
- /** @import Chunk from "../Chunk" */
- /** @import ChunkGraph from "../ChunkGraph" */
- /** @import Dependency from "../Dependency" */
- /** @import { DependencyTemplateContext } from "../DependencyTemplate" */
- /** @import Entrypoint from "../Entrypoint" */
- /** @import { Range } from "../javascript/JavascriptParser" */
- /** @typedef {"script" | "script-module" | "modulepreload" | "stylesheet" | "html" | "preload" | "prefetch"} HtmlEntryElementKind */
- /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext<[string, string, HtmlEntryElementKind, number, number, boolean, string, number, boolean, number, (Range | null), (Range | null), boolean, number, number]>} ObjectDeserializerContext */
- /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext<[string, string, HtmlEntryElementKind, number, number, boolean, string, number, boolean, number, (Range | null), (Range | null), boolean, number, number]>} ObjectSerializerContext */
- class HtmlEntryDependency extends ModuleDependency {
- /**
- * Creates an instance of HtmlEntryDependency.
- * @param {string} request request
- * @param {Range} range range of the attribute value in the source
- * @param {string} entryName name of the entry this script src is bundled into
- * @param {string=} category dependency category used for resolving and grouping
- * @param {HtmlEntryElementKind=} elementKind shape of the originating HTML element; used when expanding sibling tags for split/runtime chunks
- * @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
- * @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
- * @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
- * @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
- * @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
- * @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
- * @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)
- * @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
- * @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)
- * @param {boolean=} forceInline `<!-- webpackInline: true -->` was set before this tag; takes precedence over `output.html.inline`
- * @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
- * @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)
- */
- constructor(
- request,
- range,
- entryName,
- category,
- elementKind,
- tagStart,
- tagOpenEnd,
- tagIsNative,
- copyableAttrsText,
- tagNameEnd,
- hasOwnCrossOrigin,
- cssAnchor,
- integrityRange,
- typeValueRange,
- forceInline,
- headOpenEnd,
- headAnchor
- ) {
- super(request);
- this.range = range;
- /** @type {string} */
- this.entryName = entryName;
- /** @type {string} */
- this._category = category || "commonjs";
- /** @type {HtmlEntryElementKind} */
- this.elementKind = elementKind || "script";
- /** @type {number} */
- this.tagStart = tagStart === undefined ? -1 : tagStart;
- /** @type {number} */
- this.tagOpenEnd = tagOpenEnd === undefined ? -1 : tagOpenEnd;
- /** @type {boolean} */
- this.tagIsNative = tagIsNative !== false;
- /** @type {string} */
- this.copyableAttrsText = copyableAttrsText || "";
- /** @type {number} */
- this.tagNameEnd = tagNameEnd === undefined ? -1 : tagNameEnd;
- /** @type {boolean} */
- this.hasOwnCrossOrigin = hasOwnCrossOrigin === true;
- /** @type {number} */
- this.cssAnchor = cssAnchor === undefined ? -1 : cssAnchor;
- /** @type {Range | null} */
- this.integrityRange = integrityRange || null;
- /** @type {Range | null} */
- this.typeValueRange = typeValueRange || null;
- /** @type {boolean} */
- this.forceInline = forceInline === true;
- /** @type {number} */
- this.headOpenEnd = headOpenEnd === undefined ? -1 : headOpenEnd;
- /** @type {number} */
- this.headAnchor = headAnchor === undefined ? -1 : headAnchor;
- }
- get type() {
- return "html entry";
- }
- get category() {
- return this._category;
- }
- /**
- * Serializes this instance into the provided serializer context.
- * @param {ObjectSerializerContext} context context
- */
- serialize(context) {
- context
- .write(this.entryName)
- .write(this._category)
- .write(this.elementKind)
- .write(this.tagStart)
- .write(this.tagOpenEnd)
- .write(this.tagIsNative)
- .write(this.copyableAttrsText)
- .write(this.tagNameEnd)
- .write(this.hasOwnCrossOrigin)
- .write(this.cssAnchor)
- .write(this.integrityRange)
- .write(this.typeValueRange)
- .write(this.forceInline)
- .write(this.headOpenEnd)
- .write(this.headAnchor);
- super.serialize(context);
- }
- /**
- * Restores this instance from the provided deserializer context.
- * @param {ObjectDeserializerContext} context context
- */
- deserialize(context) {
- this.entryName = context.read();
- const c1 = context.rest;
- this._category = c1.read();
- const c2 = c1.rest;
- this.elementKind = c2.read();
- const c3 = c2.rest;
- this.tagStart = c3.read();
- const c4 = c3.rest;
- this.tagOpenEnd = c4.read();
- const c5 = c4.rest;
- this.tagIsNative = c5.read();
- const c6 = c5.rest;
- this.copyableAttrsText = c6.read();
- const c7 = c6.rest;
- this.tagNameEnd = c7.read();
- const c8 = c7.rest;
- this.hasOwnCrossOrigin = c8.read();
- const c9 = c8.rest;
- this.cssAnchor = c9.read();
- const c10 = c9.rest;
- this.integrityRange = c10.read();
- const c11 = c10.rest;
- this.typeValueRange = c11.read();
- const c12 = c11.rest;
- this.forceInline = c12.read();
- const c13 = c12.rest;
- this.headOpenEnd = c13.read();
- const c14 = c13.rest;
- this.headAnchor = c14.read();
- super.deserialize(c14.rest);
- }
- }
- /**
- * @param {Entrypoint} entrypoint entrypoint
- * @returns {Chunk[]} every chunk this entrypoint needs in load order: the
- * runtime chunk first (when `optimization.runtimeChunk` splits it off), then
- * any intermediate chunks (e.g. from `optimization.splitChunks`), and finally
- * the entry chunk itself. The entry chunk is always returned last so callers
- * can identify it as the tag whose `src`/`href` attribute is being rewritten
- * in place. Chunks that are already loaded by an ancestor (`dependOn`) entry's
- * own script tag — i.e. the parent entrypoint's entry chunk *and* its runtime
- * chunk — are skipped, otherwise they would be loaded twice when the same
- * HTML contains tags for both the leader and the dependant entries.
- */
- const getEntrypointChunksInLoadOrder = (entrypoint) => {
- const entryChunk = /** @type {Chunk} */ (entrypoint.getEntrypointChunk());
- const runtimeChunk = entrypoint.getRuntimeChunk();
- /** @type {Set<Chunk>} */
- const chunksLoadedByAncestorTags = new Set();
- /** @type {Set<import("../ChunkGroup")>} */
- const visitedGroups = new Set();
- const walk = (/** @type {import("../ChunkGroup")} */ group) => {
- if (visitedGroups.has(group)) return;
- visitedGroups.add(group);
- for (const parent of group.parentsIterable) {
- if (
- typeof (/** @type {Entrypoint} */ (parent).getEntrypointChunk) ===
- "function"
- ) {
- const parentEntry =
- /** @type {Entrypoint} */
- (parent).getEntrypointChunk();
- if (parentEntry) chunksLoadedByAncestorTags.add(parentEntry);
- const parentRuntime =
- /** @type {Entrypoint} */
- (parent).getRuntimeChunk();
- if (parentRuntime) chunksLoadedByAncestorTags.add(parentRuntime);
- }
- walk(parent);
- }
- };
- walk(entrypoint);
- /** @type {Chunk[]} */
- const ordered = [];
- /** @type {Set<Chunk>} */
- const seen = new Set();
- const push = (/** @type {Chunk | null | undefined} */ chunk) => {
- if (!chunk || seen.has(chunk) || chunk === entryChunk) return;
- if (chunksLoadedByAncestorTags.has(chunk)) return;
- seen.add(chunk);
- ordered.push(chunk);
- };
- if (runtimeChunk !== entryChunk) {
- push(runtimeChunk);
- }
- for (const chunk of entrypoint.chunks) {
- push(chunk);
- }
- ordered.push(entryChunk);
- return ordered;
- };
- /**
- * Whether webpack will emit a `.js` file for this chunk that must be
- * loaded with a `<script>` tag. Covers three independent reasons a
- * chunk needs JS output: it owns one or more JS-source-type modules;
- * it has entry modules whose source types include JavaScript (entry
- * modules don't show up in `getChunkModulesIterableBySourceType` until
- * they're connected as regular modules — this is why
- * `JavascriptModulesPlugin#_chunkHasJs` checks them separately); or it
- * is a runtime chunk — `chunk.hasRuntime()` — which produces a `.js`
- * file holding the webpack runtime, but its `RuntimeModule`s live in
- * a separate `runtimeModules` set and are *not* surfaced via
- * `getChunkModulesIterableBySourceType`. Missing the runtime case
- * would cause a `runtimeChunk`-split chunk to fall out of the
- * `<script>` list and re-emerge after the chunks that depend on it,
- * producing `__webpack_require__ is not defined` at load time.
- * @param {Chunk} chunk chunk
- * @param {ChunkGraph} chunkGraph chunk graph
- * @returns {boolean} true if the chunk emits a `.js` file
- */
- const chunkHasJs = (chunk, chunkGraph) => {
- if (chunk.hasRuntime()) return true;
- if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
- for (const module of chunkGraph.getChunkEntryModulesIterable(chunk)) {
- if (chunkGraph.getModuleSourceTypes(module).has(JAVASCRIPT_TYPE)) {
- return true;
- }
- }
- }
- return Boolean(
- chunkGraph.getChunkModulesIterableBySourceType(chunk, JAVASCRIPT_TYPE)
- );
- };
- /**
- * Whether webpack will emit a `.css` file for this chunk that must be
- * loaded with a `<link rel="stylesheet">` tag. Matches
- * `CssModulesPlugin.chunkHasCss` exactly — both regular CSS modules
- * and pure `@import` placeholder modules count, since the latter
- * still contribute a `.css` asset to the chunk.
- * @param {Chunk} chunk chunk
- * @param {ChunkGraph} chunkGraph chunk graph
- * @returns {boolean} true if the chunk emits a `.css` file
- */
- const chunkHasCss = (chunk, chunkGraph) =>
- Boolean(chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_TYPE)) ||
- Boolean(
- chunkGraph.getChunkModulesIterableBySourceType(chunk, CSS_IMPORT_TYPE)
- );
- /**
- * Deterministic tie-break key for CSS link ordering. `chunk.name` and
- * `chunk.id` are both stable strings (when present); one of them is set for
- * every chunk webpack emits. We can't rely on `Array.prototype.sort` being
- * stable — webpack still supports Node 10.13 where V8's sort is not guaranteed
- * stable for arrays larger than ten elements — so any time
- * `firstCssModulePostOrderIndex` returns the same value for two chunks (most
- * commonly when several chunks have no reachable CSS module in the entrypoint's
- * dependency walk and all map to `Infinity`) this key picks the canonical order.
- * Computed once per chunk when building the sort array rather than per
- * comparison.
- * @param {Chunk} chunk chunk
- * @returns {string} the tie-break key
- */
- const cssChunkSortKey = (chunk) =>
- `${chunk.name || ""} ${
- chunk.id === null || chunk.id === undefined ? "" : chunk.id
- }`;
- /**
- * Sort comparator for the CSS sibling emission order. Direct subtraction
- * would yield `NaN` when both indices are `Infinity` (the documented
- * fallback for chunks whose CSS modules the entrypoint's walk never
- * reaches), and `Array#sort` doesn't promise stable ordering on the legacy
- * Node 10 targets this repo still supports — so the tie-breaker must always
- * run when the indices match, including the `Infinity === Infinity` case.
- * @param {{ index: number, key: string }} a first entry
- * @param {{ index: number, key: string }} b second entry
- * @returns {-1 | 0 | 1} sort order
- */
- const compareCssChunkOrder = (a, b) => {
- if (a.index < b.index) return -1;
- if (a.index > b.index) return 1;
- if (a.key < b.key) return -1;
- if (a.key > b.key) return 1;
- return 0;
- };
- const CSS_SOURCE_TYPES = [CSS_TYPE, CSS_IMPORT_TYPE];
- /**
- * Smallest post-order index among the CSS modules of a chunk, taken
- * from the entrypoint's view of the dependency graph. Used to sort
- * sibling CSS chunks so they appear in source import order in the
- * extracted HTML — `entrypoint.chunks` itself does not give that
- * ordering for arbitrary splitChunks layouts. Considers both
- * `CSS_TYPE` and `CSS_IMPORT_TYPE` modules so a chunk made up
- * exclusively of `@import` placeholder modules (e.g. when splitChunks
- * separates them from their target CSS) still sorts by its true
- * source position rather than collapsing to `Infinity` and relying on
- * the chunk-name tie-breaker.
- * @param {Chunk} chunk chunk
- * @param {Entrypoint} entrypoint entrypoint the chunk belongs to
- * @param {ChunkGraph} chunkGraph chunk graph
- * @returns {number} the lowest post-order index of any CSS or
- * CSS-import module in the chunk, or `Number.POSITIVE_INFINITY` when
- * no such module has a defined index (e.g. for a module the
- * entrypoint never reached on its own dependency walk — runtime-only
- * modules, modules reached via `dependOn`, etc.) so such chunks sort
- * last among CSS chunks
- */
- const firstCssModulePostOrderIndex = (chunk, entrypoint, chunkGraph) => {
- let min = Number.POSITIVE_INFINITY;
- for (const sourceType of CSS_SOURCE_TYPES) {
- const modules = chunkGraph.getChunkModulesIterableBySourceType(
- chunk,
- sourceType
- );
- if (!modules) continue;
- for (const module of modules) {
- const idx = entrypoint.getModulePostOrderIndex(module);
- if (idx !== undefined && idx < min) min = idx;
- }
- }
- return min;
- };
- /**
- * Build a fresh `<link rel="stylesheet" href="…">` for a CSS chunk that
- * was pulled in by a `<script src>` entry — the originating tag was a
- * `<script>`, but the chunk is CSS so cloning the script tag verbatim
- * would produce nonsense (`<script src="…\.css">`). `extra` carries the
- * `nonce`/`crossorigin`/`referrerpolicy` source spans captured from the
- * original element at parse time so the same CSP and fetch policy applies;
- * `defer`/`async`/`type` have no meaning on `<link>` and are dropped.
- * @param {string} extra the copyable CSP/fetch attribute source text (leading-space-prefixed, or empty)
- * @param {string} href URL for the stylesheet
- * @param {string} crossOrigin ` crossorigin="…"` to append from `output.crossOriginLoading`, or `""`
- * @param {string} integrity ` integrity="<sentinel>"` to append when `output.html.integrity` is on, or `""`
- * @returns {string} the sibling `<link>` tag's HTML
- */
- const buildStylesheetLink = (extra, href, crossOrigin, integrity) => {
- const safeHref = href.replace(/"/g, """);
- return `<link rel="stylesheet" href="${safeHref}"${extra}${crossOrigin}${integrity}>`;
- };
- /**
- * Build a fresh `<script src="…">` for a JS chunk pulled in by a custom
- * (non-`<script>`) element that the user mapped to `type: "script"` /
- * `"script-module"`. Cloning the custom element verbatim and appending
- * `</script>` (as `cloneTagWithUrl` does for a real `<script>`) would emit
- * mismatched, non-executing markup such as `<my-script src="…"></script>`,
- * so synthesize a real script element instead — copying only the CSP/fetch
- * attributes (`extra`, captured at parse time), since the custom element's
- * other attributes have no defined meaning on a `<script>`.
- * @param {string} extra the copyable CSP/fetch attribute source text (leading-space-prefixed, or empty)
- * @param {string} src URL for the script
- * @param {boolean} isModule whether to emit `type="module"`
- * @param {string} crossOrigin ` crossorigin="…"` to append from `output.crossOriginLoading`, or `""`
- * @param {string} integrity ` integrity="<sentinel>"` to append when `output.html.integrity` is on, or `""`
- * @returns {string} the sibling `<script>` tag's HTML
- */
- const buildScriptTag = (extra, src, isModule, crossOrigin, integrity) => {
- const safeSrc = src.replace(/"/g, """);
- return `<script${isModule ? ' type="module"' : ""} src="${safeSrc}"${extra}${crossOrigin}${integrity}></script>`;
- };
- /**
- * Clone the original `<script>`/`<link>` opening tag with its `src`/`href`
- * value swapped for a different chunk URL. Reusing the source text verbatim
- * preserves attributes such as `nonce`, `crossorigin`, `referrerpolicy`,
- * `defer`, and `async` so the sibling tags load with the same semantics as
- * the entry tag that's already there. `integrity` is dropped because it's
- * content-specific. When the original tag was upgraded to a module script
- * (either by the author or by the `output.module` auto-upgrade in
- * `HtmlParser`), the sibling is forced to `type="module"` regardless of what
- * the source originally said. The `integrity`/`type` spans come from the
- * parser (`HtmlParser` captured them off the AST); edits are applied
- * right-to-left so earlier offsets stay valid — no re-parsing of the tag.
- * @param {string} originalTag the opening tag's source text including `>`
- * @param {number} srcStartInTag offset of the src/href value start within `originalTag`
- * @param {number} srcEndInTag offset of the src/href value end within `originalTag`
- * @param {string} newUrl URL to put into the cloned tag's src/href slot
- * @param {HtmlEntryElementKind} elementKind shape of the originating tag
- * @param {string} crossOrigin ` crossorigin="…"` to insert from `output.crossOriginLoading`, or `""`
- * @param {number} tagNameEndInTag offset right after the tag name within `originalTag`, where `crossOrigin`/`integrity` are inserted
- * @param {Range | null} integrityRange `integrity` attribute span within `originalTag` to drop, or null
- * @param {Range | null} typeValueRange `type` value span within `originalTag` to overwrite with `module`, or null to insert `type="module"`
- * @param {string} integrity ` integrity="<sentinel>"` to insert when `output.html.integrity` is on, or `""`
- * @returns {string} the sibling tag's HTML (including a closing `</script>` for script tags)
- */
- const cloneTagWithUrl = (
- originalTag,
- srcStartInTag,
- srcEndInTag,
- newUrl,
- elementKind,
- crossOrigin,
- tagNameEndInTag,
- integrityRange,
- typeValueRange,
- integrity
- ) => {
- const isLink =
- elementKind === "modulepreload" || elementKind === "stylesheet";
- /** @type {{ start: number, end: number, text: string }[]} */
- const edits = [{ start: srcStartInTag, end: srcEndInTag, text: newUrl }];
- // Drop the author's `integrity` — content-specific, wrong for another
- // chunk's file. A correct per-chunk one is re-inserted below from
- // `output.html.integrity`.
- if (integrityRange) {
- edits.push({ start: integrityRange[0], end: integrityRange[1], text: "" });
- }
- // Inserted right after the tag name: `crossorigin` from
- // `output.crossOriginLoading`, then a forced `type="module"` when the tag
- // has no `type` value to rewrite in place.
- let afterName = crossOrigin;
- if (elementKind === "script-module") {
- if (typeValueRange) {
- edits.push({
- start: typeValueRange[0],
- end: typeValueRange[1],
- text: "module"
- });
- } else {
- afterName += ' type="module"';
- }
- }
- // Per-chunk `integrity` sentinel goes in alongside `crossorigin`/`type`.
- afterName += integrity;
- if (afterName) {
- edits.push({
- start: tagNameEndInTag,
- end: tagNameEndInTag,
- text: afterName
- });
- }
- edits.sort((a, b) => b.start - a.start);
- let body = originalTag;
- for (const edit of edits) {
- body = body.slice(0, edit.start) + edit.text + body.slice(edit.end);
- }
- // `<link>` is a void element — no closing tag. `<script>` needs `</script>`.
- return isLink ? body : `${body}</script>`;
- };
- // ES5 `<link rel="modulepreload">` polyfill (Safari <17, Firefox <115, …):
- // on browsers without native support, fetch each modulepreload target with the
- // tag's own credentials/integrity so the pending module import reuses it. The
- // `.ep` guard makes it idempotent, and the observer catches any later-inserted
- // links. Emitted only when the target environment lacks native support.
- const MODULEPRELOAD_POLYFILL = [
- "(function(){",
- 'var s=document.createElement("link").relList;',
- 'if(s&&s.supports&&s.supports("modulepreload"))return;',
- "function f(e){",
- "if(e.ep)return;e.ep=1;",
- "var o={};",
- "if(e.integrity)o.integrity=e.integrity;",
- "if(e.referrerPolicy)o.referrerPolicy=e.referrerPolicy;",
- 'o.credentials=e.crossOrigin==="use-credentials"?"include":e.crossOrigin==="anonymous"?"omit":"same-origin";',
- "fetch(e.href,o)}",
- "var l=document.querySelectorAll('link[rel=\"modulepreload\"]');",
- "for(var i=0;i<l.length;i++)f(l[i]);",
- "new MutationObserver(function(m){",
- "for(var j=0;j<m.length;j++)",
- "for(var a=m[j].addedNodes,k=0;k<a.length;k++)",
- 'if(a[k].tagName==="LINK"&&a[k].rel==="modulepreload")f(a[k])',
- "}).observe(document,{childList:true,subtree:true})",
- "})();"
- ].join("");
- // One polyfill `<script>` per rendered page. Keyed by the `ReplaceSource` of a
- // single HTML code-generation pass (shared by all of a page's entry tags, fresh
- // per pass), so multi-`<script>` pages get exactly one and the cache stays correct.
- /** @type {WeakSet<ReplaceSource>} */
- const polyfilledSources = new WeakSet();
- /** @typedef {"preload" | "prefetch" | "modulepreload" | "preconnect" | "dns-prefetch"} HtmlResourceHintRel */
- /**
- * A custom resource-hint `<link>` (`output.html.resourceHints`). Exactly one of
- * `href` / `chunk` / `entry` names the target; `chunk`/`entry` URLs (hash and
- * public path) are resolved by webpack, `href` is used verbatim.
- * @typedef {object} HtmlResourceHint
- * @property {HtmlResourceHintRel} rel hint relationship
- * @property {string=} href literal URL (external, `preconnect`, or an already-hashed asset)
- * @property {string=} chunk chunk name to hint
- * @property {string=} entry entrypoint name to hint (expands to one hint per initial chunk)
- * @property {string=} as the `as` attribute; defaults to `script` for chunk/entry refs
- * @property {string=} type the `type` attribute (MIME)
- * @property {(boolean | "anonymous" | "use-credentials")=} crossorigin CORS mode (`true` → `anonymous`)
- * @property {string=} media the `media` attribute
- * @property {boolean=} integrity SRI for a chunk/entry ref; follows `output.html.integrity` by default, `false` opts out
- * @property {("low" | "high" | "auto")=} fetchPriority the `fetchpriority` attribute (allowed on `preload`/`modulepreload`/`prefetch`)
- */
- /**
- * @typedef {object} HtmlResourceHintContext
- * @property {string} entryName name of the entry this page loads
- * @property {Entrypoint} entrypoint the entrypoint
- * @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)
- * @property {import("../Compilation")} compilation the compilation
- * @property {{ rel: "modulepreload" | "preload" | "prefetch", chunk: string, hostChunks: string[] }[]} defaultHints the auto initial-graph hints (`chunk` / `hostChunks` name the referencing chunk — Vite's `hostId`)
- */
- /**
- * @param {string} value attribute value
- * @returns {string} value with `"` escaped so it can't break out of the attribute
- */
- const escapeAttr = (value) => String(value).replace(/"/g, """);
- /**
- * @param {boolean | "anonymous" | "use-credentials" | undefined} value crossorigin option
- * @returns {string | undefined} ` crossorigin="…"`, or undefined when unset/false
- */
- const crossOriginAttr = (value) => {
- if (value === undefined || value === false) return undefined;
- return ` crossorigin="${value === true ? "anonymous" : value}"`;
- };
- /**
- * Build one resource-hint `<link>` tag from a resolved href and attributes.
- * @param {HtmlResourceHintRel} rel the `rel`
- * @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)
- * @returns {string} the `<link>` tag
- */
- const buildLinkTag = (rel, attrs) =>
- `<link rel="${rel}"${attrs.as ? ` as="${escapeAttr(attrs.as)}"` : ""}${
- attrs.type ? ` type="${escapeAttr(attrs.type)}"` : ""
- } href="${escapeAttr(attrs.href)}"${attrs.crossorigin || ""}${
- attrs.integrity || ""
- }${attrs.media ? ` media="${escapeAttr(attrs.media)}"` : ""}${
- attrs.fetchpriority
- ? ` fetchpriority="${escapeAttr(attrs.fetchpriority)}"`
- : ""
- }>`;
- /**
- * Resolve one `output.html.resourceHints` descriptor to zero or more `<link>`
- * tags. A `chunk`/`entry` reference becomes the emitted chunk URL (a sentinel
- * resolved with the final hash and public path), an `href` is used verbatim, and
- * `preconnect`/`dns-prefetch` use only `href`. Unknown chunk/entry names are skipped.
- * @param {HtmlResourceHint} desc the descriptor
- * @param {{ compilation: import("../Compilation"), chunkGraph: ChunkGraph, chunkByName: Map<string, Chunk>, outputCrossOrigin: string, integrityOn: boolean }} ctx resolution context
- * @returns {string[]} the resolved tags
- */
- const resolveResourceHint = (desc, ctx) => {
- const rel = desc.rel;
- if (rel === "preconnect" || rel === "dns-prefetch") {
- if (!desc.href) return [];
- return [
- buildLinkTag(rel, {
- href: desc.href,
- crossorigin: crossOriginAttr(desc.crossorigin),
- media: desc.media
- })
- ];
- }
- if (desc.href) {
- return [
- buildLinkTag(rel, {
- href: desc.href,
- as: desc.as,
- type: desc.type,
- crossorigin: crossOriginAttr(desc.crossorigin),
- media: desc.media,
- fetchpriority: desc.fetchPriority
- })
- ];
- }
- /** @type {Chunk[]} */
- let chunks;
- if (desc.entry) {
- const entrypoint = ctx.compilation.entrypoints.get(desc.entry);
- if (!entrypoint) return [];
- chunks = getEntrypointChunksInLoadOrder(entrypoint);
- } else if (desc.chunk) {
- const chunk = ctx.chunkByName.get(desc.chunk);
- if (!chunk) return [];
- chunks = [chunk];
- } else {
- return [];
- }
- const kind = desc.as === "style" ? "css" : "javascript";
- const as = desc.as || (rel === "modulepreload" ? undefined : "script");
- // A hint targeting another chunk must match how that chunk is fetched, so
- // inherit `output.crossOriginLoading` unless the descriptor overrides it.
- const crossorigin =
- desc.crossorigin !== undefined
- ? crossOriginAttr(desc.crossorigin)
- : ctx.outputCrossOrigin || undefined;
- /** @type {string[]} */
- const tags = [];
- for (const chunk of chunks) {
- const has =
- kind === "css"
- ? chunkHasCss(chunk, ctx.chunkGraph)
- : chunkHasJs(chunk, ctx.chunkGraph);
- if (!has) continue;
- tags.push(
- buildLinkTag(rel, {
- href: getHtmlGenerator().makeChunkUrlSentinel(chunk, kind),
- as,
- type: desc.type,
- crossorigin,
- // SRI follows the global `output.html.integrity` policy (like the
- // auto hints); `integrity: false` opts a single hint out. Only
- // `preload`/`modulepreload` are SRI-eligible — `prefetch` is not.
- integrity:
- (rel === "preload" || rel === "modulepreload") &&
- desc.integrity !== false &&
- ctx.integrityOn
- ? ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(chunk, kind)}"`
- : undefined,
- media: desc.media,
- fetchpriority: desc.fetchPriority
- })
- );
- }
- return tags;
- };
- HtmlEntryDependency.Template = class HtmlEntryDependencyTemplate extends (
- ModuleDependency.Template
- ) {
- /**
- * Applies the plugin by registering its hooks on the compiler.
- * @param {Dependency} dependency the dependency for which the template should be applied
- * @param {ReplaceSource} source the current replace source which can be modified
- * @param {DependencyTemplateContext} templateContext the context object
- * @returns {void}
- */
- apply(dependency, source, templateContext) {
- const { runtimeTemplate } = templateContext;
- const dep = /** @type {HtmlEntryDependency} */ (dependency);
- const compilation = runtimeTemplate.compilation;
- // A `type: "html"` link points at another emitted page, not a JS/CSS
- // chunk: rewrite the attribute to that page's filename (resolved by
- // HtmlModulesPlugin once it's built) and emit no sibling tags — a page
- // link loads only itself.
- if (dep.elementKind === "html") {
- source.replace(
- dep.range[0],
- dep.range[1] - 1,
- getHtmlGenerator().makeHtmlPageUrlSentinel(dep.entryName)
- );
- return;
- }
- const { chunkGraph } = compilation;
- const { crossOriginLoading } = compilation.outputOptions;
- const entrypoint = /** @type {Entrypoint | undefined} */ (
- compilation.entrypoints.get(dep.entryName)
- );
- if (!entrypoint) {
- source.replace(dep.range[0], dep.range[1] - 1, "data:,");
- return;
- }
- const orderedChunks = getEntrypointChunksInLoadOrder(entrypoint);
- const entryChunk = orderedChunks[orderedChunks.length - 1];
- // `crossorigin` mirrors `output.crossOriginLoading` onto every injected
- // tag. Empty when the option is off or the originating tag already set
- // `crossorigin` (author value wins, flagged at parse time). Mirrors
- // webpack's runtime, which sets `crossOrigin` on chunk-loading scripts,
- // and matches Vite, which emits it on every injected script/stylesheet.
- const crossOrigin =
- crossOriginLoading && !dep.hasOwnCrossOrigin
- ? ` crossorigin="${crossOriginLoading}"`
- : "";
- // Per-chunk SRI: emit an `integrity` sentinel resolved late (after the
- // chunks' final bytes exist) by `resolveChunkIntegritySentinels`. Tags are
- // generated per HTML module, but one module can back pages with different
- // `integrity` options, so sentinels are emitted whenever any page wants SRI
- // and each page then resolves or strips them on emit.
- const htmlOption = compilation.outputOptions.html;
- const integrityOn = getHtmlGenerator().hasIntegritySentinels(compilation);
- const inject =
- typeof htmlOption === "object" ? htmlOption.inject : undefined;
- /**
- * Mirror `crossorigin`/`integrity` onto a native entry tag, inserted right
- * after the tag name so they sit alongside any `type="module"` the parser
- * injected. The author's content-specific `integrity` is dropped first (as
- * clones do) so the tag never carries two.
- * @param {Chunk} chunk chunk whose SRI hash the tag references
- * @param {"javascript" | "css"} contentHashType which chunk asset to hash
- * @returns {void}
- */
- const applyEntryTagAttrs = (chunk, contentHashType) => {
- if (!dep.tagIsNative || dep.tagNameEnd < 0) return;
- if (crossOrigin) source.insert(dep.tagNameEnd, crossOrigin);
- if (integrityOn) {
- if (dep.integrityRange) {
- source.replace(
- dep.tagStart + dep.integrityRange[0],
- dep.tagStart + dep.integrityRange[1] - 1,
- ""
- );
- }
- source.insert(
- dep.tagNameEnd,
- ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(
- chunk,
- contentHashType
- )}"`
- );
- }
- };
- // `rel="preload"/"prefetch"` is a resource hint: rewrite the `href` to the
- // built chunk's URL (CSS for `as="style"`, JS otherwise) and stop — no
- // sibling tags and no execution.
- if (dep.elementKind === "preload" || dep.elementKind === "prefetch") {
- const contentHashType =
- dep.category === "css-import" ? "css" : "javascript";
- source.replace(
- dep.range[0],
- dep.range[1] - 1,
- getHtmlGenerator().makeChunkUrlSentinel(entryChunk, contentHashType)
- );
- // `preload` is integrity-eligible per the SRI spec, so mirror
- // `crossorigin`/`integrity` onto it (SRI requires CORS). `prefetch` is a
- // navigation cache hint the spec doesn't cover — leave it a bare href.
- if (dep.elementKind === "preload") {
- applyEntryTagAttrs(entryChunk, contentHashType);
- }
- return;
- }
- const isStylesheet = dep.elementKind === "stylesheet";
- // Inlined scripts are `type="module"` only for ESM output; classic-IIFE
- // chunks (the default) must stay a plain `<script>`.
- const scriptTypeAttr =
- compilation.outputOptions.module === true ? ' type="module"' : "";
- const entryContentHashType = isStylesheet ? "css" : "javascript";
- const inlineOption =
- typeof htmlOption === "object" ? htmlOption.inline : undefined;
- /** @type {RegExp[] | null} */
- const inlinePatterns = Array.isArray(inlineOption) ? inlineOption : null;
- /**
- * @param {Chunk} chunk chunk to test
- * @param {"javascript" | "css"} contentType which asset of the chunk is being emitted
- * @returns {boolean} true when this chunk's content should be inlined
- */
- const isChunkInlined = (chunk, contentType) => {
- // A `<link rel="modulepreload">` is a void fetch hint — inlining it would
- // emit an unclosed, executing `<script>`.
- if (dep.elementKind === "modulepreload") return false;
- if (dep.forceInline) return true;
- if (!inlineOption) return false;
- if (inlineOption === true) return true;
- // `"script"`/`"style"` inline by asset type, not chunk name — a chunk's
- // JS and CSS share a name, so only the emitted `contentType` tells them
- // apart.
- if (inlineOption === "script") return contentType === "javascript";
- if (inlineOption === "style") return contentType === "css";
- const name = chunk.name || String(chunk.id);
- return /** @type {RegExp[]} */ (inlinePatterns).some((re) =>
- re.test(name)
- );
- };
- // Only inline when the HTML module is emitted as a standalone page (extract mode).
- // If it's exported as a JS string, sentinels would end up embedded in the JS chunk.
- const isExtractedHtml = chunkGraph
- .getModuleSourceTypes(
- /** @type {import("../Module")} */ (templateContext.module)
- )
- .has("html");
- const entryInlined =
- isExtractedHtml &&
- dep.tagStart >= 0 &&
- dep.tagOpenEnd > dep.tagStart &&
- isChunkInlined(entryChunk, entryContentHashType);
- if (entryInlined) {
- const inlineSentinel = getHtmlGenerator().makeChunkInlineSentinel(
- entryChunk,
- entryContentHashType
- );
- if (isStylesheet) {
- source.replace(
- dep.tagStart,
- dep.tagOpenEnd - 1,
- `<style>${inlineSentinel}</style>`
- );
- } else {
- source.replace(
- dep.tagStart,
- dep.tagOpenEnd - 1,
- `<script${scriptTypeAttr}>${inlineSentinel}`
- );
- }
- } else {
- source.replace(
- dep.range[0],
- dep.range[1] - 1,
- getHtmlGenerator().makeChunkUrlSentinel(
- entryChunk,
- entryContentHashType
- )
- );
- }
- if (dep.tagStart < 0 || dep.tagOpenEnd <= dep.tagStart) {
- return;
- }
- const originalContent = /** @type {string} */ (source.original().source());
- const originalTag = originalContent.slice(dep.tagStart, dep.tagOpenEnd);
- const srcStartInTag = dep.range[0] - dep.tagStart;
- const srcEndInTag = dep.range[1] - dep.tagStart;
- const tagIsNative = dep.tagIsNative;
- const tagNameEndInTag = dep.tagNameEnd - dep.tagStart;
- // Mirror `crossorigin`/`integrity` onto the non-inlined entry tag
- // (siblings get them via the builders below).
- if (!entryInlined) applyEntryTagAttrs(entryChunk, entryContentHashType);
- // `inject: false` suppresses sibling-chunk tags only; resource hints
- // below still apply (the way to reference the suppressed chunks).
- if (inject !== false) {
- /**
- * @param {Chunk} chunk chunk to emit a sibling tag for
- * @param {"javascript" | "css"} kind content type slice of the chunk to emit
- * @returns {string} a single sibling tag's HTML
- */
- const buildSibling = (chunk, kind) => {
- if (isChunkInlined(chunk, kind)) {
- const sentinel = getHtmlGenerator().makeChunkInlineSentinel(
- chunk,
- kind
- );
- return kind === "css"
- ? `<style>${sentinel}</style>`
- : `<script${scriptTypeAttr}>${sentinel}</script>`;
- }
- const url = getHtmlGenerator().makeChunkUrlSentinel(chunk, kind);
- const integrityAttr = integrityOn
- ? ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(chunk, kind)}"`
- : "";
- if (kind === "css") {
- // A CSS chunk is always loaded via `<link rel="stylesheet">`.
- // Clone only when the entry tag is itself a native `<link
- // rel="stylesheet">` (so attributes like `media` carry over);
- // a `<script>` entry that imported CSS, or a custom element,
- // gets a freshly synthesized `<link>`.
- if (isStylesheet && tagIsNative) {
- return cloneTagWithUrl(
- originalTag,
- srcStartInTag,
- srcEndInTag,
- url,
- dep.elementKind,
- crossOrigin,
- tagNameEndInTag,
- dep.integrityRange,
- dep.typeValueRange,
- integrityAttr
- );
- }
- return buildStylesheetLink(
- dep.copyableAttrsText,
- url,
- crossOrigin,
- integrityAttr
- );
- }
- // A JS chunk is loaded via `<script>`. Clone a native `<script>`
- // verbatim; synthesize a real `<script>` for custom elements that
- // were mapped to `type: script`/`script-module`.
- if (tagIsNative) {
- return cloneTagWithUrl(
- originalTag,
- srcStartInTag,
- srcEndInTag,
- url,
- dep.elementKind,
- crossOrigin,
- tagNameEndInTag,
- dep.integrityRange,
- dep.typeValueRange,
- integrityAttr
- );
- }
- return buildScriptTag(
- dep.copyableAttrsText,
- url,
- dep.elementKind === "script-module",
- crossOrigin,
- integrityAttr
- );
- };
- const siblings = [];
- // Parser-derived anchor (a source text scan would match `</head>` inside
- // comments or raw text).
- const headClose = inject === "head" ? dep.headAnchor : -1;
- // When the entry tag is already inside <head> (tagStart < headClose),
- // inserting siblings at headClose would place them after the entry,
- // reversing runtime-before-entry execution order for deferred scripts.
- // Insert before the entry tag instead so the load order is preserved.
- const headAt =
- headClose < 0
- ? -1
- : dep.tagStart < headClose
- ? dep.tagStart
- : headClose;
- let cssHtml = "";
- let cssAt = -1;
- if (isStylesheet) {
- // `<link rel="stylesheet">` entries are CSS, but a shared/runtime
- // JS-only chunk (e.g. `optimization.runtimeChunk`) can still sit in
- // the entrypoint — only clone the `<link>` for siblings that actually
- // produce a `.css` asset (mirrors the JS branch below), otherwise the
- // link points at a non-existent `.css` file (a 404, or an empty
- // `<style>` when inlined). Attributes like `media` still carry over.
- for (let i = 0; i < orderedChunks.length - 1; i++) {
- if (chunkHasCss(orderedChunks[i], chunkGraph)) {
- siblings.push(buildSibling(orderedChunks[i], "css"));
- }
- }
- } else {
- // CSS chunks are emitted before JS chunks so the cascade is set
- // up before any script runs. Within CSS the order needs to match
- // the source's import order — `entrypoint.chunks` alone doesn't
- // give us that for arbitrary splitChunks layouts (splitChunks
- // inserts each new chunk before the entry chunk via
- // `insertChunk(_, before)`, so split CSS siblings end up in
- // *reverse* of the order they were processed — exactly the
- // html-webpack-plugin#1838 / mini-css-extract#959 symptom). We
- // re-derive the order from the entrypoint's module post-order
- // index, which mirrors the dependency walk and so reflects the
- // import order.
- /** @type {{ chunk: Chunk, index: number, key: string }[]} */
- const cssChunkOrder = [];
- /** @type {Chunk[]} */
- const jsChunks = [];
- for (let i = 0; i < orderedChunks.length - 1; i++) {
- const chunk = orderedChunks[i];
- const hasCss = chunkHasCss(chunk, chunkGraph);
- const hasJs = chunkHasJs(chunk, chunkGraph);
- if (hasCss) {
- cssChunkOrder.push({
- chunk,
- index: firstCssModulePostOrderIndex(
- chunk,
- entrypoint,
- chunkGraph
- ),
- key: cssChunkSortKey(chunk)
- });
- }
- // Anything that isn't CSS-only stays on the JS lane, in the
- // `orderedChunks` order — that preserves the runtime-first /
- // vendor-before-entry invariant of `getEntrypointChunksInLoadOrder`.
- // Chunks that produce no `.js` and no `.css` (e.g. wasm-only
- // or asset-only) still get a `<script>` clone here so we
- // keep prior behavior for users who relied on it.
- if (hasJs || !hasCss) jsChunks.push(chunk);
- }
- // If the entry chunk itself contains CSS (entry JS imports CSS
- // without splitChunks separating it), fold it into the same CSS
- // ordering so the entry-chunk `<link>` lands in the correct
- // cascade position relative to sibling CSS chunks.
- if (chunkHasCss(entryChunk, chunkGraph)) {
- cssChunkOrder.push({
- chunk: entryChunk,
- index: firstCssModulePostOrderIndex(
- entryChunk,
- entrypoint,
- chunkGraph
- ),
- key: cssChunkSortKey(entryChunk)
- });
- }
- cssChunkOrder.sort(compareCssChunkOrder);
- // Stylesheets always land in <head> when the page has one. A
- // classic blocking script pins them ahead of itself (`cssAnchor`)
- // so its execution never awaits later CSS; when every script is
- // `defer`/module they go to the *end* of head, after the script
- // tags — Vite's and html-webpack-plugin's order: the scripts
- // start fetching earlier yet still execute after parsing and
- // after pending stylesheets.
- cssAt =
- dep.headAnchor >= 0
- ? dep.cssAnchor >= 0 && dep.cssAnchor < dep.headAnchor
- ? dep.cssAnchor
- : dep.headAnchor
- : dep.cssAnchor >= 0
- ? dep.cssAnchor
- : dep.tagStart;
- cssHtml = cssChunkOrder
- .map(({ chunk }) => buildSibling(chunk, "css"))
- .join("");
- for (const chunk of jsChunks) {
- siblings.push(buildSibling(chunk, "javascript"));
- }
- }
- // Same-offset inserts keep call order: CSS goes in before the
- // sibling scripts only when a blocking script demands CSS-first.
- if (cssHtml !== "" && dep.cssAnchor >= 0) source.insert(cssAt, cssHtml);
- if (siblings.length > 0) {
- source.insert(headAt >= 0 ? headAt : dep.tagStart, siblings.join(""));
- }
- if (cssHtml !== "" && dep.cssAnchor < 0) source.insert(cssAt, cssHtml);
- }
- // Resource hints are opt-in — webpack already loads the initial chunks
- // via parallel `<script>` tags, so there is no waterfall to prevent by
- // default. `output.resourceHints` requests them: `true` preloads the
- // entry's initial dependency chunks (runtime, vendor, split); an array
- // or function adds custom hints (a function may spread the auto
- // `defaultHints`). Only for extracted pages — a JS-string HTML export
- // would embed the sentinels in the JS chunk. URL-referenced assets
- // (fonts, images) carrying `webpackPrefetch` / `webpackPreload`
- // (magic-comment or `parser.<type>.urlHints` rule) also emit here.
- const resourceHintResolver =
- ResourceHintPlugin.getCompilationResolver(compilation);
- const resourceHints = resourceHintResolver.hints;
- if (
- isExtractedHtml &&
- (dep.elementKind === "script" || dep.elementKind === "script-module")
- ) {
- const isModuleOutput = compilation.outputOptions.module === true;
- /** @type {string[]} */
- const hints = [];
- // === preconnect channel — cross-origin publicPath (resourceHints.preconnect) ===
- const rhOptions = compilation.outputOptions.resourceHints;
- if (rhOptions && rhOptions.preconnect) {
- const pp = compilation.outputOptions.publicPath;
- const originMatch =
- typeof pp === "string" ? /^(https?:)?\/\/[^/?#]+/i.exec(pp) : null;
- if (originMatch) {
- hints.push(
- `<link rel="preconnect" href="${escapeAttr(originMatch[0])}"${crossOrigin}>`
- );
- }
- }
- // === chunks channel — the entry's own initial dependency graph ===
- if (resourceHints !== undefined && resourceHints !== false) {
- // The auto initial-graph preload as descriptors carrying their exact
- // tag, built only when needed (`true`/`"prefetch"` emits them, a
- // function may spread them). The entry chunk is skipped — it's
- // already the `<script src>`. ESM uses `modulepreload` (fetch +
- // parse), classic output `preload as="script"`, and `"prefetch"`
- // opts into `<link rel="prefetch">` (idle-time fetch, no `as` / no
- // `integrity` — the spec doesn't list them). Async `import()` chunks
- // aren't covered here; use `module.parser.javascript.dynamicImport*`
- // which routes through webpack's on-demand chunk-load runtime.
- const prefetchMode = resourceHints === "prefetch";
- const preloadMode = resourceHints === true;
- /** @type {"modulepreload" | "preload" | "prefetch"} */
- const rel = prefetchMode
- ? "prefetch"
- : isModuleOutput
- ? "modulepreload"
- : "preload";
- /** @type {{ rel: "modulepreload" | "preload" | "prefetch", chunk: string, hostChunks: string[], _tag: string }[]} */
- const defaultHints = [];
- if (
- preloadMode ||
- prefetchMode ||
- typeof resourceHints === "function"
- ) {
- for (const chunk of orderedChunks) {
- if (
- chunk === entryChunk ||
- isChunkInlined(chunk, "javascript") ||
- !chunkHasJs(chunk, chunkGraph)
- ) {
- continue;
- }
- const href = getHtmlGenerator().makeChunkUrlSentinel(
- chunk,
- "javascript"
- );
- const integrityAttr =
- integrityOn && !prefetchMode
- ? ` integrity="${getHtmlGenerator().makeChunkIntegritySentinel(
- chunk,
- "javascript"
- )}"`
- : "";
- // Mirror the entry tag's `nonce`/`crossorigin`/`referrerpolicy`
- // (and `output.crossOriginLoading`) so the request matches the
- // sibling `<script>`'s and the browser reuses it instead of
- // fetching twice.
- const attrs = `${dep.copyableAttrsText}${crossOrigin}${integrityAttr}`;
- const tag = prefetchMode
- ? `<link rel="prefetch" href="${href}"${attrs}>`
- : isModuleOutput
- ? `<link rel="modulepreload" href="${href}"${attrs}>`
- : `<link rel="preload" as="script" href="${href}"${attrs}>`;
- const chunkName = chunk.name || String(chunk.id);
- defaultHints.push({
- rel,
- chunk: chunkName,
- hostChunks: [chunkName],
- _tag: tag
- });
- }
- }
- if (preloadMode || prefetchMode) {
- for (const d of defaultHints) {
- hints.push(d._tag);
- }
- } else {
- // Array (custom only) or function (returns the final list, may spread
- // `defaultHints`). A returned default hint keeps its prebuilt tag; any
- // other descriptor is resolved through the URL/SRI pipeline.
- let descriptors = resourceHints;
- if (typeof resourceHints === "function") {
- // The page's own entry name (its named entry chunk), not the synthetic
- // `<script>` entry — that's what users key on.
- let entryName = dep.entryName;
- for (const c of chunkGraph.getModuleChunks(
- /** @type {import("../Module")} */ (templateContext.module)
- )) {
- if (c.name) {
- entryName = c.name;
- break;
- }
- }
- descriptors = resourceHints({
- entryName,
- entrypoint,
- hostType: "html",
- compilation,
- defaultHints
- });
- }
- if (Array.isArray(descriptors) && descriptors.length > 0) {
- /** @type {Map<string, Chunk>} */
- const chunkByName = new Map();
- for (const c of compilation.chunks) {
- if (c.name) chunkByName.set(c.name, c);
- }
- const outputCrossOrigin = crossOriginLoading
- ? ` crossorigin="${crossOriginLoading}"`
- : "";
- const ctx = {
- compilation,
- chunkGraph,
- chunkByName,
- outputCrossOrigin,
- integrityOn
- };
- for (const desc of descriptors) {
- if (!desc || typeof desc !== "object") continue;
- const prebuilt = /** @type {{ _tag?: string }} */ (desc)._tag;
- if (prebuilt) {
- hints.push(prebuilt);
- } else if (desc.rel) {
- for (const tag of resolveResourceHint(desc, ctx)) {
- hints.push(tag);
- }
- }
- }
- }
- }
- }
- // === assets channel — URL-referenced assets carrying `prefetch`/`preload` ===
- // The plugin has already scanned the entrypoint's chunks × modules
- // once and cached the URL/CSS/HTML asset deps for this entry name;
- // consume that list here so this template doesn't repeat the walk.
- // Each unique asset emits one `<link rel="prefetch|preload">` into
- // the HTML `<head>`; the JS chunk-startup runtime skips it (no
- // double `<link>` in DOM). URLs are sentinels resolved from asset
- // codegen data during `HtmlModulesPlugin` render — the asset
- // module might not have been codegen'd yet at this point.
- const outputCrossOriginAttr = crossOriginLoading
- ? ` crossorigin="${crossOriginLoading}"`
- : "";
- for (const { module, dep: md } of resourceHintResolver.getHtmlHinted(
- dep.entryName
- )) {
- const rel = md.preload ? "preload" : "prefetch";
- const asAttr =
- md.asAttribute ||
- ResourceHintRuntimeModule.guessAsAttribute(
- /** @type {import("./URLDependency")} */ (md).request || ""
- );
- const typeAttr = md.typeAttribute;
- const mediaAttr = md.mediaAttribute;
- const fetchPriority = md.fetchPriority;
- const href = getHtmlGenerator().makeAssetUrlSentinel(
- module.identifier()
- );
- const attrs =
- `${asAttr ? ` as="${escapeAttr(asAttr)}"` : ""}` +
- `${typeAttr ? ` type="${escapeAttr(typeAttr)}"` : ""}` +
- ` href="${escapeAttr(href)}"${
- outputCrossOriginAttr
- }${mediaAttr ? ` media="${escapeAttr(mediaAttr)}"` : ""}` +
- `${fetchPriority ? ` fetchpriority="${escapeAttr(fetchPriority)}"` : ""}`;
- hints.push(`<link rel="${rel}"${attrs}>`);
- }
- if (hints.length > 0) {
- // De-duplicate (a custom hint may repeat an auto one), preserving order.
- /** @type {Set<string>} */
- const seen = new Set();
- /** @type {string[]} */
- const finalHints = [];
- for (const h of hints) {
- if (!seen.has(h)) {
- seen.add(h);
- finalHints.push(h);
- }
- }
- // When the target environment lacks native `modulepreload` (an
- // `output.environment` capability, like the JS-feature checks), append
- // a tiny polyfill once per page so the hints still warm the cache there.
- // `resourceHints.modulePreloadPolyfill` defaults from the target's
- // modulepreload support (`output.environment.modulePreload`); `false`
- // opts out (strict CSP).
- if (
- rhOptions &&
- rhOptions.modulePreloadPolyfill &&
- !polyfilledSources.has(source) &&
- finalHints.some((h) => h.startsWith('<link rel="modulepreload"'))
- ) {
- polyfilledSources.add(source);
- finalHints.push(`<script>${MODULEPRELOAD_POLYFILL}</script>`);
- }
- // Insert right after the `<head>` open tag (parser-derived) so the
- // hints are the document's first fetches; fall back to the first
- // blocking script (else the entry tag) when the page has no
- // written `<head>` (e.g. an HTML fragment).
- const hintAt =
- dep.headOpenEnd >= 0
- ? dep.headOpenEnd
- : dep.cssAnchor === -1
- ? dep.tagStart
- : dep.cssAnchor;
- source.insert(hintAt, finalHints.join(""));
- }
- }
- }
- };
- makeSerializable(
- HtmlEntryDependency,
- "webpack/lib/dependencies/HtmlEntryDependency"
- );
- module.exports = HtmlEntryDependency;
|