/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/dependencies/HtmlEntryDependency.js
1 303 строки
53 KB
Alexander Akait
perf(html): load the html generator only for builds with html modules (#21659)
10 авг 2026, 12:15
Не верифицирован
10 авг 2026, 12:15
38c55a3
Код
Авторство
О чём код?
/* 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")); /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ /** @typedef {import("../Chunk")} Chunk */ /** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */ /** @typedef {import("../Entrypoint")} Entrypoint */ /** @typedef {import("../javascript/JavascriptParser").Range} Range */ /** @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;