webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
1,194 lines (1,135 loc) • 53.8 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const {
CSS_IMPORT_TYPE,
CSS_TYPE,
JAVASCRIPT_TYPE
} = require("../ModuleSourceTypeConstants");
const HtmlGenerator = require("../html/HtmlGenerator");
const ResourceHintPlugin = require("../prefetch/ResourceHintPlugin");
const ResourceHintRuntimeModule = require("../prefetch/ResourceHintRuntimeModule");
const makeSerializable = require("../util/makeSerializable");
const ModuleDependency = require("./ModuleDependency");
/** @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: HtmlGenerator.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="${HtmlGenerator.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,
HtmlGenerator.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`. On when
// `output.html.integrity` is `true`, a non-empty array, or a function.
const htmlOption = compilation.outputOptions.html;
const integrity =
typeof htmlOption === "object" ? htmlOption.integrity : undefined;
const integrityOn =
integrity === true ||
typeof integrity === "function" ||
(Array.isArray(integrity) && integrity.length > 0);
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="${HtmlGenerator.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,
HtmlGenerator.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 = HtmlGenerator.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,
HtmlGenerator.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 = HtmlGenerator.makeChunkInlineSentinel(chunk, kind);
return kind === "css"
? `<style>${sentinel}</style>`
: `<script${scriptTypeAttr}>${sentinel}</script>`;
}
const url = HtmlGenerator.makeChunkUrlSentinel(chunk, kind);
const integrityAttr = integrityOn
? ` integrity="${HtmlGenerator.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 = HtmlGenerator.makeChunkUrlSentinel(
chunk,
"javascript"
);
const integrityAttr =
integrityOn && !prefetchMode
? ` integrity="${HtmlGenerator.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,
outputC