UNPKG

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.

688 lines (653 loc) 27.6 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php */ "use strict"; const { RawSource } = require("webpack-sources"); const ModuleFilenameHelpers = require("../ModuleFilenameHelpers"); const RuntimeGlobals = require("../RuntimeGlobals"); const CssUrlDependency = require("../dependencies/CssUrlDependency"); const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency"); const URLDependency = require("../dependencies/URLDependency"); const ResourceHintRuntimeModule = require("./ResourceHintRuntimeModule"); const StartupAssetHintRuntimeModule = require("./StartupAssetHintRuntimeModule"); const parseResourceHintOptions = require("./parseResourceHintOptions"); /** @typedef {import("../Compilation")} Compilation */ /** @typedef {import("../Compiler")} Compiler */ /** @typedef {import("../Module")} Module */ /** @typedef {import("../../declarations/WebpackOptions").ResourceHintsInitial} ResourceHintsConfig */ /** @typedef {import("../../declarations/WebpackOptions").ResourceHintsOptions} ResourceHintsOptions */ /** @typedef {import("../../declarations/WebpackOptions").UrlHintRule} UrlHintRule */ /** @typedef {import("../dependencies/HtmlEntryDependency").HtmlResourceHint} HtmlResourceHint */ /** @typedef {import("../dependencies/HtmlEntryDependency").HtmlResourceHintContext} HtmlResourceHintContext */ /** @typedef {import("./parseResourceHintOptions").ResourceHintOptions} ResourceHintOptions */ /** * @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 `<link>` in its `<head>`. * @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 `<link>` tags into the extracted HTML `<head>` * @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 `<link>`-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`. */ const PLUGIN_NAME = "ResourceHintPlugin"; /** @type {WeakMap<Compilation, CompilationResolver>} */ const compilationResolvers = new WeakMap(); // Sane default-exclude for URL hint rules: manifests, PDFs, plain text are // almost never wanted as `<link rel="preload/prefetch">` 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; /** * True for the CSS / HTML URL-asset deps whose templates don't run for JS * output — the plugin lifts their hint flags into JS runtime requirements * itself. JS `URLDependency` lifts its own, so it is excluded here. * @param {import("../Dependency")} dep dependency * @returns {dep is CssUrlDependency | HtmlSourceDependency} whether it is a CSS/HTML URL asset dep */ const isCssOrHtmlUrlAssetDep = (dep) => 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<import("../Chunk")>} 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<Module>} */ 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 `<link>`-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 `<link>` 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<string>} */ 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<import("../Chunk")>} */ 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 || ResourceHintRuntimeModule.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"); const HtmlEntryDependency = require("../dependencies/HtmlEntryDependency"); outer: for (const module of compilation.modules) { if (!module.getSourceTypes || !module.getSourceTypes().has("html")) { continue; } const presDeps = module.presentationalDependencies; if (!presDeps) continue; 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 ); }; /** * Adds runtime support for `__webpack_require__.PA` / `__webpack_require__.LA`, * the helpers that inject `<link rel="prefetch">` / `<link rel="preload">` * tags for asset modules referenced via `new URL(..., import.meta.url)`, CSS * `url(...)`, and HTML `<img src>` / `<link href>`. Also stores the top-level * `output.resourceHints` value so `HtmlEntryDependency` can emit its `<link>` * tags into the extracted HTML `<head>`. */ 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 `<link>` when the * HTML `<head>` 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.<type>.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 `<img src>` / `<link href>`, * `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<string, EXPECTED_ANY> | 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; const HtmlEntryDependency = require("../dependencies/HtmlEntryDependency"); 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 `<link>` tags) and a // global `WeakSet` (consumed by `StartupAssetHintRuntimeModule` // to skip the JS runtime `<link>` 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<string, HtmlHintedAsset[]> | undefined} */ let perEntry; /** @type {WeakSet<Module> | 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; 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<import("../Chunk")>} */ 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 // `<script>` and a `<link rel="modulepreload">` for the // same chunk). Merge lists so no asset is duplicated. const existing = perEntry.get(dep.entryName); if (existing) { for (const h of hinted) existing.push(h); } else { perEntry.set(dep.entryName, hinted); } } } }; compilationResolvers.set(compilation, { hints, getHtmlHinted: (entryName) => { if (hints === "none") return []; if (!perEntry) build(); return ( /** @type {Map<string, HtmlHintedAsset[]>} */ (perEntry).get( entryName ) || [] ); }, isHtmlHinted: (assetModule) => { if (hints === "none") return false; if (!anyHtmlHinted) build(); return /** @type {WeakSet<Module>} */ (anyHtmlHinted).has( assetModule ); }, getEntrypointHints: (entryName) => collectEntrypointHints(compilation, entryName, hints) }); // CSS `url(...)` and HTML `<img src>` / `<link href>` deps store // hint flags but their dep templates don't run for the JS // output, so we need to lift those flags into the JS chunk's // runtime requirements ourselves. Iterate only css/html modules // (pure-JS chunks skip the walk entirely). compilation.hooks.additionalChunkRuntimeRequirements.tap( PLUGIN_NAME, (chunk, set, { chunkGraph }) => { if (hints === "none") return; let hasPrefetch = false; let hasPreload = false; for (const module of chunkGraph.getChunkModulesIterable(chunk)) { const deps = /** @type {{ dependencies?: import("../Dependency")[] }} */ (module).dependencies; if (!deps) continue; for (const dep of deps) { if (!isCssOrHtmlUrlAssetDep(dep)) continue; if (dep.prefetch) hasPrefetch = true; if (dep.preload) hasPreload = true; if (hasPrefetch && hasPreload) break; } if (hasPrefetch && hasPreload) break; } // Attach the startup-hint runtime module to *this* chunk // (covering async chunks too) — `runtimeRequirementInTree` // alone would only attach it to the runtime chunk, so a // hinted `new URL(...)` in an async chunk would never // fire its `<link>` injection. We add it whenever this // chunk already pulls in `startupAssetHints` (via JS // `URLDependency` template) or whenever CSS / HTML lifts // it in here. if ( hasPrefetch || hasPreload || set.has(RuntimeGlobals.startupAssetHints) ) { set.add(RuntimeGlobals.startupAssetHints); // CSS-only asset modules aren't in JS `__webpack_modules__`, // so the startup-hint runtime falls back to // `__webpack_require__.p + "filename"`. Make sure // `publicPath` is in this chunk's runtime. set.add(RuntimeGlobals.publicPath); if (hasPrefetch) set.add(RuntimeGlobals.prefetchAsset); if (hasPreload) set.add(RuntimeGlobals.preloadAsset); compilation.addRuntimeModule( chunk, new StartupAssetHintRuntimeModule() ); } } ); for (const [rel, runtimeGlobal] of /** @type {const} */ ([ ["prefetch", RuntimeGlobals.prefetchAsset], ["preload", RuntimeGlobals.preloadAsset] ])) { compilation.hooks.runtimeRequirementInTree .for(runtimeGlobal) .tap(PLUGIN_NAME, (chunk) => { compilation.addRuntimeModule( chunk, new ResourceHintRuntimeModule(rel) ); }); } // SSR manifest: serialize each entrypoint's resolved hints to a JSON // asset (the same descriptors as `stats.entrypoints[].resourceHints`) // so an SSR server can inject the `<link>` tags itself. `"none"` // yields empty lists (via `collectEntrypointHints`). const rhOptions = compilation.outputOptions.resourceHints; const manifestPath = rhOptions && rhOptions.manifest; if (manifestPath) { const Compilation = require("../Compilation"); compilation.hooks.processAssets.tap( { name: PLUGIN_NAME, stage: Compilation.PROCESS_ASSETS_STAGE_REPORT }, () => { /** @type {Record<string, EntrypointHint[]>} */ const manifest = {}; for (const name of compilation.entrypoints.keys()) { manifest[name] = collectEntrypointHints(compilation, name, hints); } const source = new RawSource( `${JSON.stringify(manifest, null, 2)}\n` ); if (compilation.getAsset(manifestPath)) { compilation.updateAsset(manifestPath, source); } else { compilation.emitAsset(manifestPath, source); } } ); } }); } } module.exports = ResourceHintPlugin;