/* MIT License http://www.opensource.org/licenses/mit-license.php */ "use strict"; const { RawSource } = require("webpack-sources"); const ModuleFilenameHelpers = require("../ModuleFilenameHelpers"); const { ASSET_URL_TYPE, JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants"); const RuntimeGlobals = require("../RuntimeGlobals"); const CssUrlDependency = require("../dependencies/CssUrlDependency"); const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency"); const URLDependency = require("../dependencies/URLDependency"); const WorkerDependency = require("../dependencies/WorkerDependency"); const lazyModule = require("../util/lazyModule"); const memoize = require("../util/memoize"); const { PUBLIC_PATH_AUTO, PUBLIC_PATH_FULL_HASH } = require("../util/publicPathPlaceholder"); const { guessAsAttribute } = require("./parseResourceHintOptions"); const parseResourceHintOptions = require("./parseResourceHintOptions"); /** @import Chunk from "../Chunk" */ /** @import ChunkGraph from "../ChunkGraph" */ /** @import CodeGenerationResults from "../CodeGenerationResults" */ /** @import Compilation from "../Compilation" */ /** @import Compiler from "../Compiler" */ /** @import Module, { RuntimeRequirements } from "../Module" */ /** * The attributes a `` carries besides its href and `as`. * @typedef {object} StartupHintAttributes * @property {("low" | "high" | "auto" | undefined)=} fetchPriority * @property {string=} type * @property {string=} media */ /** * One asset a chunk hints at startup, merged across every reference to it. * @typedef {object} AssetHintEntry * @property {Module} assetModule the asset the `` points at * @property {Module} originModule the chunk module the reference was written in, which is what a baked href is relative to * @property {string} request the original request, for guessing the `as` attribute * @property {boolean} preload preload rather than prefetch * @property {("low" | "high" | "auto" | undefined)} fetchPriority * @property {(string | undefined)} as * @property {(string | undefined)} type * @property {(string | undefined)} media */ /** * @import { * ResourceHintsInitial as ResourceHintsConfig, * ResourceHintsOptions, * UrlHintRule * } from "../../declarations/WebpackOptions" */ /** @import { HtmlResourceHint } from "../dependencies/HtmlEntryDependency" */ /** @import { ResourceHintOptions } from "./parseResourceHintOptions" */ /** * @typedef {object} ResolvedResourceHints * @property {boolean=} prefetch project-wide default for `webpackPrefetch` * @property {boolean=} preload project-wide default for `webpackPreload` * @property {("low" | "high" | "auto" | false)=} fetchPriority project-wide default for `webpackFetchPriority` * @property {string=} as project-wide default for `webpackAs` * @property {string=} type project-wide default for `webpackType` * @property {string=} media project-wide default for `webpackMedia` */ /** * A URL-referenced-asset dependency that can carry resource-hint state. * `URLDependency`, `CssUrlDependency` and `HtmlSourceDependency` all share * these field names — the `applyResourceHints` statics mutate them in place. * @typedef {object} ResourceHintDep * @property {true | undefined} prefetch * @property {true | undefined} preload * @property {("low" | "high" | "auto" | undefined)} fetchPriority * @property {string | undefined} asAttribute * @property {string | undefined} typeAttribute * @property {string | undefined} mediaAttribute */ /** * A URL-referenced asset an HTML entry should emit as `` in its ``. * @typedef {object} HtmlHintedAsset * @property {Module} module the asset module (source-of-truth for the URL) * @property {ResourceHintDep} dep the URL/CSS/HTML source dep carrying the hint flags */ /** * A resolved resource-hint descriptor emitted for an entrypoint — the shape * consumed by `stats.entrypoints[name].resourceHints` and by SSR frameworks * that render the initial HTML themselves. * @typedef {object} EntrypointHint * @property {"preload" | "prefetch" | "modulepreload" | "preconnect"} rel * @property {string} href emitted URL (public path applied) * @property {string=} as * @property {string=} type * @property {string=} media * @property {("low" | "high" | "auto")=} fetchPriority * @property {(boolean | "anonymous" | "use-credentials")=} crossorigin * @property {string[]=} hostChunks names of the entrypoint chunks this hint originates from (Vite's `hostId`) — lets the callback rewrite per referencing chunk */ /** * Origin of `output.publicPath` when it's an absolute cross-origin URL, for * `output.autoPreconnect`. `undefined` for relative / `"auto"` public paths. * @param {Compilation} compilation compilation * @returns {string | undefined} `scheme://host[:port]` or undefined */ const getPublicPathOrigin = (compilation) => { const publicPath = compilation.outputOptions.publicPath; if (typeof publicPath !== "string") return undefined; const match = /^(https?:)?\/\/[^/?#]+/i.exec(publicPath); return match ? match[0] : undefined; }; /** * @typedef {object} CompilationResolver * @property {ResourceHintsConfig | undefined} hints the effective `output.resourceHints` value (`undefined` when unset) * @property {(entryName: string) => HtmlHintedAsset[]} getHtmlHinted URL asset descriptors reachable from an HTML entrypoint's initial chunks — `HtmlEntryDependency` template consumes this list to emit `` tags into the extracted HTML `` * @property {(assetModule: Module) => boolean} isHtmlHinted true when `assetModule` appears in *any* HTML entry's hinted list — the JS chunk-startup runtime skips it so the DOM never carries two tags for one URL * @property {(entryName: string) => EntrypointHint[]} getEntrypointHints resolved ``-shaped descriptors for the given entry — auto initial-graph hints plus URL asset hints, then filtered / rewritten through the user function (when `output.resourceHints` is a function). Reads via `stats.entrypoints[name].resourceHints`. */ // only a build that actually emits a resource hint needs these const getResourceHintRuntimeModule = lazyModule(() => require("./ResourceHintRuntimeModule") ); const getStartupAssetHintRuntimeModule = lazyModule(() => require("./StartupAssetHintRuntimeModule") ); // only reached once a build has an html module, so a js-only build never // pays for the html generator this drags in const getHtmlEntryDependency = memoize(() => require("../dependencies/HtmlEntryDependency") ); const PLUGIN_NAME = "ResourceHintPlugin"; /** @type {WeakMap} */ const compilationResolvers = new WeakMap(); // Sane default-exclude for URL hint rules: manifests, PDFs, plain text are // almost never wanted as `` targets. Explicit // magic comments still work — they route through `applyParsedHints`. const DEFAULT_ASSETS_EXCLUDE_REGEXP = /\.(?:webmanifest|pdf|txt)(?:\?.*)?$/i; // Initial-graph auto hints target JS chunk output files only. const JS_CHUNK_FILE_REGEXP = /\.m?jsx?$/i; /** * True for the URL-referenced-asset deps that can carry resource-hint flags. * Type guard so callers keep the narrowed union without re-listing the classes. * @param {import("../Dependency")} dep dependency * @returns {dep is URLDependency | CssUrlDependency | HtmlSourceDependency} whether it is a URL asset dep */ const isUrlAssetDep = (dep) => dep instanceof URLDependency || dep instanceof CssUrlDependency || dep instanceof HtmlSourceDependency; /** * Entry chunk name for a hint's `hostChunks` (Vite's `hostId`); the id is a * stable fallback for unnamed chunks. * @param {import("../Chunk")} chunk chunk * @returns {string} name or stringified id */ const chunkHostName = (chunk) => chunk.name || String(chunk.id); /** * Walk `chunks` × their modules × dependencies and yield each distinct * URL-asset module carrying a prefetch/preload flag, deduping targets within a * single call. One shared walk for the entrypoint-hint and HTML-hint passes, * which would otherwise each re-implement the same 4-level nesting. * @param {Compilation} compilation compilation * @param {Iterable} chunks chunks to scan * @returns {IterableIterator<{ dep: URLDependency | CssUrlDependency | HtmlSourceDependency, target: Module, chunk: import("../Chunk") }>} hinted assets */ function* iterateHintedUrlAssets(compilation, chunks) { const { chunkGraph, moduleGraph } = compilation; /** @type {WeakSet} */ const seen = new WeakSet(); for (const chunk of chunks) { for (const module of chunkGraph.getChunkModulesIterable(chunk)) { const deps = module.dependencies; if (!deps) continue; for (const dep of deps) { if (!isUrlAssetDep(dep)) continue; if (!dep.prefetch && !dep.preload) continue; const target = moduleGraph.getModule(dep); if (!target || seen.has(target)) continue; seen.add(target); yield { dep, target, chunk }; } } } } /** * Merge the matching `UrlHintRule`s for a request. Rules match by * `test`/`include`/`exclude` (omit all three → matches everything); later * matches override earlier ones for defined fields. * @param {UrlHintRule[] | undefined} rules parser-scoped `urlHints` * @param {string} request request URL * @returns {ResolvedResourceHints} merged defaults */ const matchUrlHints = (rules, request) => { if (!rules || rules.length === 0) return {}; if (DEFAULT_ASSETS_EXCLUDE_REGEXP.test(request)) return {}; /** @type {ResolvedResourceHints} */ const merged = {}; for (const rule of rules) { if ( (rule.test !== undefined || rule.include !== undefined || rule.exclude !== undefined) && !ModuleFilenameHelpers.matchObject( /** @type {EXPECTED_ANY} */ ({ test: rule.test, include: rule.include, exclude: rule.exclude }), request ) ) { continue; } if (rule.prefetch !== undefined) merged.prefetch = rule.prefetch; if (rule.preload !== undefined) merged.preload = rule.preload; if (rule.fetchPriority !== undefined) { merged.fetchPriority = rule.fetchPriority; } if (rule.as !== undefined) merged.as = rule.as; if (rule.type !== undefined) merged.type = rule.type; if (rule.media !== undefined) merged.media = rule.media; } return merged; }; /** * When `output.resourceHints` is a function, invoke it and return its * descriptors; otherwise pass through. Callback signature — see `ResourceHints` * schema entry. * @param {import("../Entrypoint")} entrypoint entrypoint * @param {ResourceHintsConfig | undefined} hints top-level config * @param {string} entryName entry name * @param {"html" | "js"} hostType page type * @param {Compilation} compilation compilation * @param {EntrypointHint[]} defaultHints computed default descriptors * @returns {EntrypointHint[]} descriptors after the user hook (or the defaults untouched) */ const applyUserHook = ( entrypoint, hints, entryName, hostType, compilation, defaultHints ) => { if (typeof hints !== "function") return defaultHints; const out = hints({ entryName, entrypoint, hostType, compilation, // `collectEntrypointHints` always sets `hostChunks`; the callback type // declares it required, so assert it here. defaultHints: /** @type {(HtmlResourceHint & { hostChunks: string[] })[]} */ ( defaultHints ) }); return Array.isArray(out) ? /** @type {EntrypointHint[]} */ (out) : []; }; /** * Collect the resolved ``-shaped hint descriptors for an entrypoint — * combining the auto initial-graph hints (from a truthy `output.resourceHints`) * with the URL-referenced-asset hints (fonts, images, workers) carried on * `URLDependency` / `CssUrlDependency` / `HtmlSourceDependency`. Backs * `stats.entrypoints[name].resourceHints`. Works for any entrypoint, HTML or * JS-only — SSR frameworks read this to inject `` server-side without a * separate manifest. * @param {import("../Compilation")} compilation compilation * @param {string} entryName entrypoint name * @param {ResourceHintsConfig | undefined} hints `output.resourceHints` * @returns {EntrypointHint[]} descriptors */ const collectEntrypointHints = (compilation, entryName, hints) => { // `"none"` is a hard off switch — no hints anywhere (stats / manifest / DOM). if (hints === "none") return []; const entrypoint = compilation.entrypoints.get(entryName); if (!entrypoint) return []; /** @type {EntrypointHint[]} */ const out = []; /** @type {Set} */ const seenKeys = new Set(); const push = (/** @type {EntrypointHint} */ h) => { const key = `${h.rel}\0${h.href}`; if (seenKeys.has(key)) return; seenKeys.add(key); out.push(h); }; const publicPath = typeof compilation.outputOptions.publicPath === "string" && compilation.outputOptions.publicPath !== "auto" ? compilation.outputOptions.publicPath : ""; // `resourceHints.preconnect`: warm the connection to a cross-origin // publicPath (the origin every chunk / asset is fetched from), emitted first. const outputResourceHints = compilation.outputOptions.resourceHints; if (outputResourceHints && outputResourceHints.preconnect) { const origin = getPublicPathOrigin(compilation); if (origin) { /** @type {EntrypointHint} */ const h = { rel: "preconnect", href: origin }; const crossOrigin = compilation.outputOptions.crossOriginLoading; if (crossOrigin) h.crossorigin = crossOrigin; push(h); } } // Auto initial-graph hints — computed for `true`, `"prefetch"`, or a // function (the callback receives them as `defaultHints`). The array // form supplies its own list; nothing is auto-emitted here for it. if (hints === true || hints === "prefetch" || typeof hints === "function") { const isModuleOutput = compilation.outputOptions.module === true; const prefetch = hints === "prefetch"; const rel = prefetch ? "prefetch" : isModuleOutput ? "modulepreload" : "preload"; const entryChunk = entrypoint.getEntrypointChunk(); for (const chunk of entrypoint.chunks) { if (chunk === entryChunk) continue; for (const file of chunk.files) { if (!JS_CHUNK_FILE_REGEXP.test(file)) continue; const href = publicPath + file; /** @type {EntrypointHint} */ const h = { rel, href, hostChunks: [chunkHostName(chunk)] }; if (rel === "preload") h.as = "script"; push(h); } } } // URL-referenced deps carrying prefetch/preload. Walk entrypoint's initial // chunks × modules × deps (async chunks are handled by the on-demand // runtime via `dynamicImportPrefetch/Preload` parser options). /** @type {Set} */ const chunkSet = new Set(entrypoint.chunks); const runtimeChunk = entrypoint.getRuntimeChunk(); if (runtimeChunk) chunkSet.add(runtimeChunk); for (const { dep, target, chunk } of iterateHintedUrlAssets( compilation, chunkSet )) { const buildInfo = /** @type {{ filename?: string }} */ (target.buildInfo); if (!buildInfo || !buildInfo.filename) continue; const asAttribute = dep.asAttribute || guessAsAttribute(dep.request); /** @type {EntrypointHint} */ const h = { rel: dep.preload ? "preload" : "prefetch", href: publicPath + buildInfo.filename, hostChunks: [chunkHostName(chunk)] }; if (asAttribute) h.as = asAttribute; if (dep.typeAttribute) h.type = dep.typeAttribute; if (dep.mediaAttribute) h.media = dep.mediaAttribute; if (dep.fetchPriority) h.fetchPriority = dep.fetchPriority; push(h); } // `hostType`: `"html"` iff the entrypoint has an extracted HTML page // (any `HtmlEntryDependency` with elementKind `script`/`script-module` on // any HTML module points at this entryName); SSR frameworks reading // `stats.entrypoints[name].resourceHints` see `"js"`. let hostType = /** @type {"html" | "js"} */ ("js"); outer: for (const module of compilation.modules) { if (!module.getSourceTypes || !module.getSourceTypes().has("html")) { continue; } const presDeps = module.presentationalDependencies; if (!presDeps) continue; const HtmlEntryDependency = getHtmlEntryDependency(); for (const dep of presDeps) { if ( dep instanceof HtmlEntryDependency && dep.entryName === entryName && (dep.elementKind === "script" || dep.elementKind === "script-module") ) { hostType = "html"; break outer; } } } return applyUserHook( entrypoint, hints, entryName, hostType, compilation, out ); }; /** * Renders the `` calls a chunk fires at startup, for every asset and worker * chunk it references with a resource hint. Run while runtime requirements are still * open, so what an href reads is added to `set` rather than assumed: an href spelled * as a literal leaves `.p` and `.u` out of the chunk entirely. * @param {Chunk} chunk the chunk the calls are emitted into * @param {Compilation} compilation the compilation * @param {ChunkGraph} chunkGraph the chunk graph * @param {CodeGenerationResults} codeGenerationResults the code generation results * @param {RuntimeRequirements} set the chunk's runtime requirements * @returns {string[]} one call per hinted asset, in emission order */ const collectStartupAssetHintLines = ( chunk, compilation, chunkGraph, codeGenerationResults, set ) => { const { moduleGraph, runtimeTemplate } = compilation; const resolver = /** @type {CompilationResolver} */ (compilationResolvers.get(compilation)); /** @type {string[]} */ const lines = []; /** * @param {string} fn the `` helper * @param {string} href the href expression * @param {string} as the `as` attribute * @param {StartupHintAttributes} attributes the remaining attributes * @returns {void} */ const push = (fn, href, as, attributes) => { set.add(fn); lines.push( `${fn}(${href}, ${JSON.stringify(as)}, ${ attributes.type ? JSON.stringify(attributes.type) : "undefined" }, ${attributes.media ? JSON.stringify(attributes.media) : "undefined"}, ${ attributes.fetchPriority ? JSON.stringify(attributes.fetchPriority) : "undefined" });` ); }; // The analyzable worker form can't wrap its own href without losing the literal // specifier, so its `` fires here — keyed by chunk, hence a separate pass. /** @type {Set} */ const seenWorkerChunks = new Set(); for (const module of chunkGraph.getChunkModulesIterable(chunk)) { for (const block of module.blocks) { for (const dep of block.dependencies) { if (!(dep instanceof WorkerDependency)) continue; const hint = dep.options.resourceHint; if (!hint || (!hint.preload && !hint.prefetch)) continue; const group = chunkGraph.getBlockChunkGroup(block); if (!group) continue; const workerChunk = /** @type {import("../Entrypoint")} */ (group).getEntrypointChunk(); if (workerChunk.id === null || seenWorkerChunks.has(workerChunk.id)) { continue; } seenWorkerChunks.add(workerChunk.id); // The same specifier the `new Worker(...)` call site bakes, asked of the // same module, so the two agree on whether a literal is spellable. const specifier = runtimeTemplate.supportsAnalyzable( "url", chunkGraph, module ) ? runtimeTemplate._getAnalyzableChunkSpecifier( dep.options.publicPath, workerChunk, module, chunkGraph, set ) : null; // Without one the call site keeps the runtime form, where it wraps its own // href — firing the `` here too would emit the call twice. if (specifier === null) continue; push( hint.preload ? RuntimeGlobals.preloadAsset : RuntimeGlobals.prefetchAsset, `${runtimeTemplate.importMetaUrl(specifier)}.href`, hint.as || "script", hint ); } } } // Dedupe per asset module so two references can't emit two ``s: `preload` // wins over `prefetch`, and the last explicit attribute override sticks. /** @type {Map} */ const perAsset = new Map(); for (const module of chunkGraph.getChunkModulesIterable(chunk)) { const deps = /** @type {{ dependencies?: import("../Dependency")[] }} */ (module).dependencies; if (!deps) continue; for (const dep of deps) { if (!isUrlAssetDep(dep)) continue; if (!dep.prefetch && !dep.preload) continue; const assetModule = moduleGraph.getModule(dep); if (!assetModule) continue; // Already a `` in the html `` emitted by `HtmlEntryDependency`, // so the runtime hint would leave the DOM with two tags for one url. if (resolver.isHtmlHinted(assetModule)) continue; const key = assetModule.identifier(); const urlDep = /** @type {URLDependency & Partial & Partial} */ (dep); const existing = perAsset.get(key); if (existing) { if (dep.preload) existing.preload = true; if (dep.fetchPriority) existing.fetchPriority = dep.fetchPriority; if (urlDep.asAttribute) existing.as = urlDep.asAttribute; if (urlDep.typeAttribute) existing.type = urlDep.typeAttribute; if (urlDep.mediaAttribute) existing.media = urlDep.mediaAttribute; } else { perAsset.set(key, { assetModule: /** @type {Module} */ (assetModule), originModule: module, request: dep.request, preload: Boolean(dep.preload), fetchPriority: dep.fetchPriority, as: urlDep.asAttribute, type: urlDep.typeAttribute, media: urlDep.mediaAttribute }); } } } for (const entry of perAsset.values()) { const { assetModule } = entry; let href; // An asset with a javascript wrapper exposes its url through it; one without has // no `__webpack_modules__` entry to require. if (assetModule.getSourceTypes().has(JAVASCRIPT_TYPE)) { href = runtimeTemplate.moduleRaw({ chunkGraph, module: assetModule, request: entry.request, runtimeRequirements: set, weak: false }); } else { const has = codeGenerationResults.has(assetModule, chunk.runtime); const urlData = has ? codeGenerationResults.getData(assetModule, chunk.runtime, "url") : undefined; const resolved = urlData && urlData[ASSET_URL_TYPE]; // A generator `publicPath` overrides `output.publicPath`, so prefer it; only the // two placeholders below are still unresolved when this runs. const templated = typeof resolved === "string" && (resolved.includes(PUBLIC_PATH_AUTO) || resolved.includes(PUBLIC_PATH_FULL_HASH)); if (typeof resolved === "string" && !templated) { href = JSON.stringify(resolved); } else { const filename = has ? codeGenerationResults.getData( assetModule, chunk.runtime, "filename" ) : undefined; if (typeof filename !== "string") continue; // The literal the `new URL()` call site bakes, asked of the same module — // but written into a runtime module, which no `eval` devtool wraps. const specifier = runtimeTemplate.supportsAnalyzable( "url-runtime", chunkGraph, entry.originModule ) ? runtimeTemplate.getAnalyzableAssetUrl( entry.originModule, chunkGraph, filename, chunk.runtime ) : null; if (specifier !== null) { href = `${runtimeTemplate.importMetaUrl(specifier)}.href`; } else { set.add(RuntimeGlobals.publicPath); href = `${RuntimeGlobals.publicPath} + ${JSON.stringify(filename)}`; } } } push( entry.preload ? RuntimeGlobals.preloadAsset : RuntimeGlobals.prefetchAsset, href, entry.as || guessAsAttribute(entry.request), entry ); } return lines; }; /** * Adds runtime support for `__webpack_require__.PA` / `__webpack_require__.LA`, * the helpers that inject `` / `` * tags for asset modules referenced via `new URL(..., import.meta.url)`, CSS * `url(...)`, and HTML `` / ``. Also stores the top-level * `output.resourceHints` value so `HtmlEntryDependency` can emit its `` * tags into the extracted HTML ``. */ class ResourceHintPlugin { /** * @param {ResourceHintsOptions=} options normalized `output.resourceHints` */ constructor(options) { /** @type {ResourceHintsConfig | undefined} */ this._hints = options ? options.initial : undefined; } /** * Returns the per-compilation resolver. `.hints` is the effective * `output.resourceHints` value (used by `HtmlEntryDependency` template); * `.isHtmlHinted(assetModule)` skips the JS chunk-startup `` when the * HTML `` already emits it. * @param {Compilation} compilation compilation * @returns {CompilationResolver} resolver */ static getCompilationResolver(compilation) { const entry = compilationResolvers.get(compilation); if (entry) return entry; return { hints: undefined, getHtmlHinted: () => [], isHtmlHinted: () => false, getEntrypointHints: () => [] }; } /** * Match `parser..urlHints` rules for a request. Returns the merged * defaults; the caller passes them to `applyDefaults`. Exposed so parsers * that own their own URL dep creation (JS/CSS/HTML) can share one matcher. * @param {UrlHintRule[] | undefined} rules rules array * @param {string} request request URL * @returns {ResolvedResourceHints} matched defaults */ static matchUrlHints(rules, request) { return matchUrlHints(rules, request); } /** * Apply `urlHints` rule defaults to a URL asset dep. Used from URL-emitting * parsers when they have per-request defaults but no comment options to * parse (e.g. the HTML parser's `pendingHints` flow, where the comment was * parsed earlier). * @param {ResourceHintDep} dep dep to mutate * @param {ResolvedResourceHints} defaults `matchUrlHints(rules, request)` result * @returns {void} */ static applyDefaults(dep, defaults) { if (defaults.prefetch) dep.prefetch = true; if (defaults.preload) dep.preload = true; if (defaults.fetchPriority) dep.fetchPriority = defaults.fetchPriority; if (defaults.as !== undefined) dep.asAttribute = defaults.as; if (defaults.type !== undefined) dep.typeAttribute = defaults.type; if (defaults.media !== undefined) dep.mediaAttribute = defaults.media; } /** * Apply already-parsed `webpackPrefetch` / `webpackPreload` / * `webpackFetchPriority` / `webpackAs` / `webpackType` / `webpackMedia` * overrides to a URL asset dep. Each field wins over the project-wide * default only when it's explicitly set on the magic comment. * @param {ResourceHintDep} dep dep to mutate * @param {ResourceHintOptions} hints parsed hint options * @returns {void} */ static applyParsedHints(dep, hints) { if (hints.prefetch !== undefined) dep.prefetch = hints.prefetch; if (hints.preload !== undefined) dep.preload = hints.preload; if (hints.fetchPriority !== undefined) { dep.fetchPriority = hints.fetchPriority; } if (hints.as !== undefined) dep.asAttribute = hints.as; if (hints.type !== undefined) dep.typeAttribute = hints.type; if (hints.media !== undefined) dep.mediaAttribute = hints.media; } /** * Match `urlHints` for `request` + apply per-URL magic-comment overrides * to a URL asset dep. Called from every URL-emitting parser (JS * `new URL(...)`, CSS `url(...)`, HTML `` / ``, * `new Worker(new URL(...))`) so all sources share the same precedence: * rule defaults first, magic comments win. * @param {ResourceHintDep} dep dep to mutate in place * @param {UrlHintRule[] | undefined} rules parser-scoped `urlHints` rules * @param {string} request the asset request (for rule matching) * @param {Record | null | undefined} commentOptions parsed magic-comment options (`null` / `undefined` skips override phase) * @param {import("../NormalModule")} module module for emitting warnings on invalid comments * @param {import("../Dependency").DependencyLocation} loc loc for warnings * @returns {void} */ static applyResourceHints(dep, rules, request, commentOptions, module, loc) { ResourceHintPlugin.applyDefaults(dep, matchUrlHints(rules, request)); if (!commentOptions) return; ResourceHintPlugin.applyParsedHints( dep, parseResourceHintOptions(commentOptions, module, loc) ); } /** * @param {Compiler} compiler the compiler * @returns {void} */ apply(compiler) { const hints = this._hints; compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { // Lazy-computed on first access — walk every HTML module's // `HtmlEntryDependency`s, resolve each named entry, and collect // every URL asset dep target reachable from its ordered chunks. // One pass produces two views: a per-entry list (consumed by // `HtmlEntryDependency.Template` to emit `` tags) and a // global `WeakSet` (consumed by `StartupAssetHintRuntimeModule` // to skip the JS runtime `` for the same asset). // Deterministic (module graph is fixed by seal time) and cheap // (only fires when a JS chunk actually produces hints). /** @type {Map | undefined} */ let perEntry; /** @type {WeakSet | undefined} */ let anyHtmlHinted; const build = () => { perEntry = new Map(); anyHtmlHinted = new WeakSet(); for (const module of compilation.modules) { if (!module.getSourceTypes || !module.getSourceTypes().has("html")) { continue; } // HtmlEntry deps live on `presentationalDependencies` (added // by the HTML parser via `addPresentationalDependency`) — // they don't affect module resolution, only rendering. const presDeps = module.presentationalDependencies; if (!presDeps) continue; const HtmlEntryDependency = getHtmlEntryDependency(); for (const dep of presDeps) { if ( !(dep instanceof HtmlEntryDependency) || (dep.elementKind !== "script" && dep.elementKind !== "script-module") ) { continue; } const entrypoint = compilation.entrypoints.get(dep.entryName); if (!entrypoint) continue; // Mirror `getEntrypointChunksInLoadOrder`: entry chunk + // runtime chunk + all initial siblings (`entrypoint.chunks` // includes splitChunks output). Async `import()` chunks // stay on the on-demand runtime and are NOT HTML-hinted. /** @type {Set} */ const chunks = new Set(entrypoint.chunks); const entry = entrypoint.getEntrypointChunk(); if (entry) chunks.add(entry); const runtimeChunk = entrypoint.getRuntimeChunk(); if (runtimeChunk) chunks.add(runtimeChunk); /** @type {HtmlHintedAsset[]} */ const hinted = []; for (const { dep: assetDep, target } of iterateHintedUrlAssets( compilation, chunks )) { anyHtmlHinted.add(target); hinted.push({ module: target, dep: assetDep }); } // Multiple HtmlEntryDeps may share an entryName (e.g. a // `