ResourceHintPlugin.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const { RawSource } = require("webpack-sources");
  6. const ModuleFilenameHelpers = require("../ModuleFilenameHelpers");
  7. const {
  8. ASSET_URL_TYPE,
  9. JAVASCRIPT_TYPE
  10. } = require("../ModuleSourceTypeConstants");
  11. const RuntimeGlobals = require("../RuntimeGlobals");
  12. const CssUrlDependency = require("../dependencies/CssUrlDependency");
  13. const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
  14. const URLDependency = require("../dependencies/URLDependency");
  15. const WorkerDependency = require("../dependencies/WorkerDependency");
  16. const lazyModule = require("../util/lazyModule");
  17. const memoize = require("../util/memoize");
  18. const {
  19. PUBLIC_PATH_AUTO,
  20. PUBLIC_PATH_FULL_HASH
  21. } = require("../util/publicPathPlaceholder");
  22. const { guessAsAttribute } = require("./parseResourceHintOptions");
  23. const parseResourceHintOptions = require("./parseResourceHintOptions");
  24. /** @import Chunk from "../Chunk" */
  25. /** @import ChunkGraph from "../ChunkGraph" */
  26. /** @import CodeGenerationResults from "../CodeGenerationResults" */
  27. /** @import Compilation from "../Compilation" */
  28. /** @import Compiler from "../Compiler" */
  29. /** @import Module, { RuntimeRequirements } from "../Module" */
  30. /**
  31. * The attributes a `<link>` carries besides its href and `as`.
  32. * @typedef {object} StartupHintAttributes
  33. * @property {("low" | "high" | "auto" | undefined)=} fetchPriority
  34. * @property {string=} type
  35. * @property {string=} media
  36. */
  37. /**
  38. * One asset a chunk hints at startup, merged across every reference to it.
  39. * @typedef {object} AssetHintEntry
  40. * @property {Module} assetModule the asset the `<link>` points at
  41. * @property {Module} originModule the chunk module the reference was written in, which is what a baked href is relative to
  42. * @property {string} request the original request, for guessing the `as` attribute
  43. * @property {boolean} preload preload rather than prefetch
  44. * @property {("low" | "high" | "auto" | undefined)} fetchPriority
  45. * @property {(string | undefined)} as
  46. * @property {(string | undefined)} type
  47. * @property {(string | undefined)} media
  48. */
  49. /**
  50. * @import {
  51. * ResourceHintsInitial as ResourceHintsConfig,
  52. * ResourceHintsOptions,
  53. * UrlHintRule
  54. * } from "../../declarations/WebpackOptions"
  55. */
  56. /** @import { HtmlResourceHint } from "../dependencies/HtmlEntryDependency" */
  57. /** @import { ResourceHintOptions } from "./parseResourceHintOptions" */
  58. /**
  59. * @typedef {object} ResolvedResourceHints
  60. * @property {boolean=} prefetch project-wide default for `webpackPrefetch`
  61. * @property {boolean=} preload project-wide default for `webpackPreload`
  62. * @property {("low" | "high" | "auto" | false)=} fetchPriority project-wide default for `webpackFetchPriority`
  63. * @property {string=} as project-wide default for `webpackAs`
  64. * @property {string=} type project-wide default for `webpackType`
  65. * @property {string=} media project-wide default for `webpackMedia`
  66. */
  67. /**
  68. * A URL-referenced-asset dependency that can carry resource-hint state.
  69. * `URLDependency`, `CssUrlDependency` and `HtmlSourceDependency` all share
  70. * these field names — the `applyResourceHints` statics mutate them in place.
  71. * @typedef {object} ResourceHintDep
  72. * @property {true | undefined} prefetch
  73. * @property {true | undefined} preload
  74. * @property {("low" | "high" | "auto" | undefined)} fetchPriority
  75. * @property {string | undefined} asAttribute
  76. * @property {string | undefined} typeAttribute
  77. * @property {string | undefined} mediaAttribute
  78. */
  79. /**
  80. * A URL-referenced asset an HTML entry should emit as `<link>` in its `<head>`.
  81. * @typedef {object} HtmlHintedAsset
  82. * @property {Module} module the asset module (source-of-truth for the URL)
  83. * @property {ResourceHintDep} dep the URL/CSS/HTML source dep carrying the hint flags
  84. */
  85. /**
  86. * A resolved resource-hint descriptor emitted for an entrypoint — the shape
  87. * consumed by `stats.entrypoints[name].resourceHints` and by SSR frameworks
  88. * that render the initial HTML themselves.
  89. * @typedef {object} EntrypointHint
  90. * @property {"preload" | "prefetch" | "modulepreload" | "preconnect"} rel
  91. * @property {string} href emitted URL (public path applied)
  92. * @property {string=} as
  93. * @property {string=} type
  94. * @property {string=} media
  95. * @property {("low" | "high" | "auto")=} fetchPriority
  96. * @property {(boolean | "anonymous" | "use-credentials")=} crossorigin
  97. * @property {string[]=} hostChunks names of the entrypoint chunks this hint originates from (Vite's `hostId`) — lets the callback rewrite per referencing chunk
  98. */
  99. /**
  100. * Origin of `output.publicPath` when it's an absolute cross-origin URL, for
  101. * `output.autoPreconnect`. `undefined` for relative / `"auto"` public paths.
  102. * @param {Compilation} compilation compilation
  103. * @returns {string | undefined} `scheme://host[:port]` or undefined
  104. */
  105. const getPublicPathOrigin = (compilation) => {
  106. const publicPath = compilation.outputOptions.publicPath;
  107. if (typeof publicPath !== "string") return undefined;
  108. const match = /^(https?:)?\/\/[^/?#]+/i.exec(publicPath);
  109. return match ? match[0] : undefined;
  110. };
  111. /**
  112. * @typedef {object} CompilationResolver
  113. * @property {ResourceHintsConfig | undefined} hints the effective `output.resourceHints` value (`undefined` when unset)
  114. * @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>`
  115. * @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
  116. * @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`.
  117. */
  118. // only a build that actually emits a resource hint needs these
  119. const getResourceHintRuntimeModule = lazyModule(() =>
  120. require("./ResourceHintRuntimeModule")
  121. );
  122. const getStartupAssetHintRuntimeModule = lazyModule(() =>
  123. require("./StartupAssetHintRuntimeModule")
  124. );
  125. // only reached once a build has an html module, so a js-only build never
  126. // pays for the html generator this drags in
  127. const getHtmlEntryDependency = memoize(() =>
  128. require("../dependencies/HtmlEntryDependency")
  129. );
  130. const PLUGIN_NAME = "ResourceHintPlugin";
  131. /** @type {WeakMap<Compilation, CompilationResolver>} */
  132. const compilationResolvers = new WeakMap();
  133. // Sane default-exclude for URL hint rules: manifests, PDFs, plain text are
  134. // almost never wanted as `<link rel="preload/prefetch">` targets. Explicit
  135. // magic comments still work — they route through `applyParsedHints`.
  136. const DEFAULT_ASSETS_EXCLUDE_REGEXP = /\.(?:webmanifest|pdf|txt)(?:\?.*)?$/i;
  137. // Initial-graph auto hints target JS chunk output files only.
  138. const JS_CHUNK_FILE_REGEXP = /\.m?jsx?$/i;
  139. /**
  140. * True for the URL-referenced-asset deps that can carry resource-hint flags.
  141. * Type guard so callers keep the narrowed union without re-listing the classes.
  142. * @param {import("../Dependency")} dep dependency
  143. * @returns {dep is URLDependency | CssUrlDependency | HtmlSourceDependency} whether it is a URL asset dep
  144. */
  145. const isUrlAssetDep = (dep) =>
  146. dep instanceof URLDependency ||
  147. dep instanceof CssUrlDependency ||
  148. dep instanceof HtmlSourceDependency;
  149. /**
  150. * Entry chunk name for a hint's `hostChunks` (Vite's `hostId`); the id is a
  151. * stable fallback for unnamed chunks.
  152. * @param {import("../Chunk")} chunk chunk
  153. * @returns {string} name or stringified id
  154. */
  155. const chunkHostName = (chunk) => chunk.name || String(chunk.id);
  156. /**
  157. * Walk `chunks` × their modules × dependencies and yield each distinct
  158. * URL-asset module carrying a prefetch/preload flag, deduping targets within a
  159. * single call. One shared walk for the entrypoint-hint and HTML-hint passes,
  160. * which would otherwise each re-implement the same 4-level nesting.
  161. * @param {Compilation} compilation compilation
  162. * @param {Iterable<import("../Chunk")>} chunks chunks to scan
  163. * @returns {IterableIterator<{ dep: URLDependency | CssUrlDependency | HtmlSourceDependency, target: Module, chunk: import("../Chunk") }>} hinted assets
  164. */
  165. function* iterateHintedUrlAssets(compilation, chunks) {
  166. const { chunkGraph, moduleGraph } = compilation;
  167. /** @type {WeakSet<Module>} */
  168. const seen = new WeakSet();
  169. for (const chunk of chunks) {
  170. for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
  171. const deps = module.dependencies;
  172. if (!deps) continue;
  173. for (const dep of deps) {
  174. if (!isUrlAssetDep(dep)) continue;
  175. if (!dep.prefetch && !dep.preload) continue;
  176. const target = moduleGraph.getModule(dep);
  177. if (!target || seen.has(target)) continue;
  178. seen.add(target);
  179. yield { dep, target, chunk };
  180. }
  181. }
  182. }
  183. }
  184. /**
  185. * Merge the matching `UrlHintRule`s for a request. Rules match by
  186. * `test`/`include`/`exclude` (omit all three → matches everything); later
  187. * matches override earlier ones for defined fields.
  188. * @param {UrlHintRule[] | undefined} rules parser-scoped `urlHints`
  189. * @param {string} request request URL
  190. * @returns {ResolvedResourceHints} merged defaults
  191. */
  192. const matchUrlHints = (rules, request) => {
  193. if (!rules || rules.length === 0) return {};
  194. if (DEFAULT_ASSETS_EXCLUDE_REGEXP.test(request)) return {};
  195. /** @type {ResolvedResourceHints} */
  196. const merged = {};
  197. for (const rule of rules) {
  198. if (
  199. (rule.test !== undefined ||
  200. rule.include !== undefined ||
  201. rule.exclude !== undefined) &&
  202. !ModuleFilenameHelpers.matchObject(
  203. /** @type {EXPECTED_ANY} */ ({
  204. test: rule.test,
  205. include: rule.include,
  206. exclude: rule.exclude
  207. }),
  208. request
  209. )
  210. ) {
  211. continue;
  212. }
  213. if (rule.prefetch !== undefined) merged.prefetch = rule.prefetch;
  214. if (rule.preload !== undefined) merged.preload = rule.preload;
  215. if (rule.fetchPriority !== undefined) {
  216. merged.fetchPriority = rule.fetchPriority;
  217. }
  218. if (rule.as !== undefined) merged.as = rule.as;
  219. if (rule.type !== undefined) merged.type = rule.type;
  220. if (rule.media !== undefined) merged.media = rule.media;
  221. }
  222. return merged;
  223. };
  224. /**
  225. * When `output.resourceHints` is a function, invoke it and return its
  226. * descriptors; otherwise pass through. Callback signature — see `ResourceHints`
  227. * schema entry.
  228. * @param {import("../Entrypoint")} entrypoint entrypoint
  229. * @param {ResourceHintsConfig | undefined} hints top-level config
  230. * @param {string} entryName entry name
  231. * @param {"html" | "js"} hostType page type
  232. * @param {Compilation} compilation compilation
  233. * @param {EntrypointHint[]} defaultHints computed default descriptors
  234. * @returns {EntrypointHint[]} descriptors after the user hook (or the defaults untouched)
  235. */
  236. const applyUserHook = (
  237. entrypoint,
  238. hints,
  239. entryName,
  240. hostType,
  241. compilation,
  242. defaultHints
  243. ) => {
  244. if (typeof hints !== "function") return defaultHints;
  245. const out = hints({
  246. entryName,
  247. entrypoint,
  248. hostType,
  249. compilation,
  250. // `collectEntrypointHints` always sets `hostChunks`; the callback type
  251. // declares it required, so assert it here.
  252. defaultHints:
  253. /** @type {(HtmlResourceHint & { hostChunks: string[] })[]} */ (
  254. defaultHints
  255. )
  256. });
  257. return Array.isArray(out) ? /** @type {EntrypointHint[]} */ (out) : [];
  258. };
  259. /**
  260. * Collect the resolved `<link>`-shaped hint descriptors for an entrypoint —
  261. * combining the auto initial-graph hints (from a truthy `output.resourceHints`)
  262. * with the URL-referenced-asset hints (fonts, images, workers) carried on
  263. * `URLDependency` / `CssUrlDependency` / `HtmlSourceDependency`. Backs
  264. * `stats.entrypoints[name].resourceHints`. Works for any entrypoint, HTML or
  265. * JS-only — SSR frameworks read this to inject `<link>` server-side without a
  266. * separate manifest.
  267. * @param {import("../Compilation")} compilation compilation
  268. * @param {string} entryName entrypoint name
  269. * @param {ResourceHintsConfig | undefined} hints `output.resourceHints`
  270. * @returns {EntrypointHint[]} descriptors
  271. */
  272. const collectEntrypointHints = (compilation, entryName, hints) => {
  273. // `"none"` is a hard off switch — no hints anywhere (stats / manifest / DOM).
  274. if (hints === "none") return [];
  275. const entrypoint = compilation.entrypoints.get(entryName);
  276. if (!entrypoint) return [];
  277. /** @type {EntrypointHint[]} */
  278. const out = [];
  279. /** @type {Set<string>} */
  280. const seenKeys = new Set();
  281. const push = (/** @type {EntrypointHint} */ h) => {
  282. const key = `${h.rel}\0${h.href}`;
  283. if (seenKeys.has(key)) return;
  284. seenKeys.add(key);
  285. out.push(h);
  286. };
  287. const publicPath =
  288. typeof compilation.outputOptions.publicPath === "string" &&
  289. compilation.outputOptions.publicPath !== "auto"
  290. ? compilation.outputOptions.publicPath
  291. : "";
  292. // `resourceHints.preconnect`: warm the connection to a cross-origin
  293. // publicPath (the origin every chunk / asset is fetched from), emitted first.
  294. const outputResourceHints = compilation.outputOptions.resourceHints;
  295. if (outputResourceHints && outputResourceHints.preconnect) {
  296. const origin = getPublicPathOrigin(compilation);
  297. if (origin) {
  298. /** @type {EntrypointHint} */
  299. const h = { rel: "preconnect", href: origin };
  300. const crossOrigin = compilation.outputOptions.crossOriginLoading;
  301. if (crossOrigin) h.crossorigin = crossOrigin;
  302. push(h);
  303. }
  304. }
  305. // Auto initial-graph hints — computed for `true`, `"prefetch"`, or a
  306. // function (the callback receives them as `defaultHints`). The array
  307. // form supplies its own list; nothing is auto-emitted here for it.
  308. if (hints === true || hints === "prefetch" || typeof hints === "function") {
  309. const isModuleOutput = compilation.outputOptions.module === true;
  310. const prefetch = hints === "prefetch";
  311. const rel = prefetch
  312. ? "prefetch"
  313. : isModuleOutput
  314. ? "modulepreload"
  315. : "preload";
  316. const entryChunk = entrypoint.getEntrypointChunk();
  317. for (const chunk of entrypoint.chunks) {
  318. if (chunk === entryChunk) continue;
  319. for (const file of chunk.files) {
  320. if (!JS_CHUNK_FILE_REGEXP.test(file)) continue;
  321. const href = publicPath + file;
  322. /** @type {EntrypointHint} */
  323. const h = {
  324. rel,
  325. href,
  326. hostChunks: [chunkHostName(chunk)]
  327. };
  328. if (rel === "preload") h.as = "script";
  329. push(h);
  330. }
  331. }
  332. }
  333. // URL-referenced deps carrying prefetch/preload. Walk entrypoint's initial
  334. // chunks × modules × deps (async chunks are handled by the on-demand
  335. // runtime via `dynamicImportPrefetch/Preload` parser options).
  336. /** @type {Set<import("../Chunk")>} */
  337. const chunkSet = new Set(entrypoint.chunks);
  338. const runtimeChunk = entrypoint.getRuntimeChunk();
  339. if (runtimeChunk) chunkSet.add(runtimeChunk);
  340. for (const { dep, target, chunk } of iterateHintedUrlAssets(
  341. compilation,
  342. chunkSet
  343. )) {
  344. const buildInfo =
  345. /** @type {{ filename?: string }} */
  346. (target.buildInfo);
  347. if (!buildInfo || !buildInfo.filename) continue;
  348. const asAttribute = dep.asAttribute || guessAsAttribute(dep.request);
  349. /** @type {EntrypointHint} */
  350. const h = {
  351. rel: dep.preload ? "preload" : "prefetch",
  352. href: publicPath + buildInfo.filename,
  353. hostChunks: [chunkHostName(chunk)]
  354. };
  355. if (asAttribute) h.as = asAttribute;
  356. if (dep.typeAttribute) h.type = dep.typeAttribute;
  357. if (dep.mediaAttribute) h.media = dep.mediaAttribute;
  358. if (dep.fetchPriority) h.fetchPriority = dep.fetchPriority;
  359. push(h);
  360. }
  361. // `hostType`: `"html"` iff the entrypoint has an extracted HTML page
  362. // (any `HtmlEntryDependency` with elementKind `script`/`script-module` on
  363. // any HTML module points at this entryName); SSR frameworks reading
  364. // `stats.entrypoints[name].resourceHints` see `"js"`.
  365. let hostType = /** @type {"html" | "js"} */ ("js");
  366. outer: for (const module of compilation.modules) {
  367. if (!module.getSourceTypes || !module.getSourceTypes().has("html")) {
  368. continue;
  369. }
  370. const presDeps = module.presentationalDependencies;
  371. if (!presDeps) continue;
  372. const HtmlEntryDependency = getHtmlEntryDependency();
  373. for (const dep of presDeps) {
  374. if (
  375. dep instanceof HtmlEntryDependency &&
  376. dep.entryName === entryName &&
  377. (dep.elementKind === "script" || dep.elementKind === "script-module")
  378. ) {
  379. hostType = "html";
  380. break outer;
  381. }
  382. }
  383. }
  384. return applyUserHook(
  385. entrypoint,
  386. hints,
  387. entryName,
  388. hostType,
  389. compilation,
  390. out
  391. );
  392. };
  393. /**
  394. * Renders the `<link>` calls a chunk fires at startup, for every asset and worker
  395. * chunk it references with a resource hint. Run while runtime requirements are still
  396. * open, so what an href reads is added to `set` rather than assumed: an href spelled
  397. * as a literal leaves `.p` and `.u` out of the chunk entirely.
  398. * @param {Chunk} chunk the chunk the calls are emitted into
  399. * @param {Compilation} compilation the compilation
  400. * @param {ChunkGraph} chunkGraph the chunk graph
  401. * @param {CodeGenerationResults} codeGenerationResults the code generation results
  402. * @param {RuntimeRequirements} set the chunk's runtime requirements
  403. * @returns {string[]} one call per hinted asset, in emission order
  404. */
  405. const collectStartupAssetHintLines = (
  406. chunk,
  407. compilation,
  408. chunkGraph,
  409. codeGenerationResults,
  410. set
  411. ) => {
  412. const { moduleGraph, runtimeTemplate } = compilation;
  413. const resolver =
  414. /** @type {CompilationResolver} */
  415. (compilationResolvers.get(compilation));
  416. /** @type {string[]} */
  417. const lines = [];
  418. /**
  419. * @param {string} fn the `<link>` helper
  420. * @param {string} href the href expression
  421. * @param {string} as the `as` attribute
  422. * @param {StartupHintAttributes} attributes the remaining attributes
  423. * @returns {void}
  424. */
  425. const push = (fn, href, as, attributes) => {
  426. set.add(fn);
  427. lines.push(
  428. `${fn}(${href}, ${JSON.stringify(as)}, ${
  429. attributes.type ? JSON.stringify(attributes.type) : "undefined"
  430. }, ${attributes.media ? JSON.stringify(attributes.media) : "undefined"}, ${
  431. attributes.fetchPriority
  432. ? JSON.stringify(attributes.fetchPriority)
  433. : "undefined"
  434. });`
  435. );
  436. };
  437. // The analyzable worker form can't wrap its own href without losing the literal
  438. // specifier, so its `<link>` fires here — keyed by chunk, hence a separate pass.
  439. /** @type {Set<string | number>} */
  440. const seenWorkerChunks = new Set();
  441. for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
  442. for (const block of module.blocks) {
  443. for (const dep of block.dependencies) {
  444. if (!(dep instanceof WorkerDependency)) continue;
  445. const hint = dep.options.resourceHint;
  446. if (!hint || (!hint.preload && !hint.prefetch)) continue;
  447. const group = chunkGraph.getBlockChunkGroup(block);
  448. if (!group) continue;
  449. const workerChunk =
  450. /** @type {import("../Entrypoint")} */
  451. (group).getEntrypointChunk();
  452. if (workerChunk.id === null || seenWorkerChunks.has(workerChunk.id)) {
  453. continue;
  454. }
  455. seenWorkerChunks.add(workerChunk.id);
  456. // The same specifier the `new Worker(...)` call site bakes, asked of the
  457. // same module, so the two agree on whether a literal is spellable.
  458. const specifier = runtimeTemplate.supportsAnalyzable(
  459. "url",
  460. chunkGraph,
  461. module
  462. )
  463. ? runtimeTemplate._getAnalyzableChunkSpecifier(
  464. dep.options.publicPath,
  465. workerChunk,
  466. module,
  467. chunkGraph,
  468. set
  469. )
  470. : null;
  471. // Without one the call site keeps the runtime form, where it wraps its own
  472. // href — firing the `<link>` here too would emit the call twice.
  473. if (specifier === null) continue;
  474. push(
  475. hint.preload
  476. ? RuntimeGlobals.preloadAsset
  477. : RuntimeGlobals.prefetchAsset,
  478. `${runtimeTemplate.importMetaUrl(specifier)}.href`,
  479. hint.as || "script",
  480. hint
  481. );
  482. }
  483. }
  484. }
  485. // Dedupe per asset module so two references can't emit two `<link>`s: `preload`
  486. // wins over `prefetch`, and the last explicit attribute override sticks.
  487. /** @type {Map<string, AssetHintEntry>} */
  488. const perAsset = new Map();
  489. for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
  490. const deps =
  491. /** @type {{ dependencies?: import("../Dependency")[] }} */
  492. (module).dependencies;
  493. if (!deps) continue;
  494. for (const dep of deps) {
  495. if (!isUrlAssetDep(dep)) continue;
  496. if (!dep.prefetch && !dep.preload) continue;
  497. const assetModule = moduleGraph.getModule(dep);
  498. if (!assetModule) continue;
  499. // Already a `<link>` in the html `<head>` emitted by `HtmlEntryDependency`,
  500. // so the runtime hint would leave the DOM with two tags for one url.
  501. if (resolver.isHtmlHinted(assetModule)) continue;
  502. const key = assetModule.identifier();
  503. const urlDep =
  504. /** @type {URLDependency & Partial<CssUrlDependency> & Partial<HtmlSourceDependency>} */
  505. (dep);
  506. const existing = perAsset.get(key);
  507. if (existing) {
  508. if (dep.preload) existing.preload = true;
  509. if (dep.fetchPriority) existing.fetchPriority = dep.fetchPriority;
  510. if (urlDep.asAttribute) existing.as = urlDep.asAttribute;
  511. if (urlDep.typeAttribute) existing.type = urlDep.typeAttribute;
  512. if (urlDep.mediaAttribute) existing.media = urlDep.mediaAttribute;
  513. } else {
  514. perAsset.set(key, {
  515. assetModule: /** @type {Module} */ (assetModule),
  516. originModule: module,
  517. request: dep.request,
  518. preload: Boolean(dep.preload),
  519. fetchPriority: dep.fetchPriority,
  520. as: urlDep.asAttribute,
  521. type: urlDep.typeAttribute,
  522. media: urlDep.mediaAttribute
  523. });
  524. }
  525. }
  526. }
  527. for (const entry of perAsset.values()) {
  528. const { assetModule } = entry;
  529. let href;
  530. // An asset with a javascript wrapper exposes its url through it; one without has
  531. // no `__webpack_modules__` entry to require.
  532. if (assetModule.getSourceTypes().has(JAVASCRIPT_TYPE)) {
  533. href = runtimeTemplate.moduleRaw({
  534. chunkGraph,
  535. module: assetModule,
  536. request: entry.request,
  537. runtimeRequirements: set,
  538. weak: false
  539. });
  540. } else {
  541. const has = codeGenerationResults.has(assetModule, chunk.runtime);
  542. const urlData = has
  543. ? codeGenerationResults.getData(assetModule, chunk.runtime, "url")
  544. : undefined;
  545. const resolved = urlData && urlData[ASSET_URL_TYPE];
  546. // A generator `publicPath` overrides `output.publicPath`, so prefer it; only the
  547. // two placeholders below are still unresolved when this runs.
  548. const templated =
  549. typeof resolved === "string" &&
  550. (resolved.includes(PUBLIC_PATH_AUTO) ||
  551. resolved.includes(PUBLIC_PATH_FULL_HASH));
  552. if (typeof resolved === "string" && !templated) {
  553. href = JSON.stringify(resolved);
  554. } else {
  555. const filename = has
  556. ? codeGenerationResults.getData(
  557. assetModule,
  558. chunk.runtime,
  559. "filename"
  560. )
  561. : undefined;
  562. if (typeof filename !== "string") continue;
  563. // The literal the `new URL()` call site bakes, asked of the same module —
  564. // but written into a runtime module, which no `eval` devtool wraps.
  565. const specifier = runtimeTemplate.supportsAnalyzable(
  566. "url-runtime",
  567. chunkGraph,
  568. entry.originModule
  569. )
  570. ? runtimeTemplate.getAnalyzableAssetUrl(
  571. entry.originModule,
  572. chunkGraph,
  573. filename,
  574. chunk.runtime
  575. )
  576. : null;
  577. if (specifier !== null) {
  578. href = `${runtimeTemplate.importMetaUrl(specifier)}.href`;
  579. } else {
  580. set.add(RuntimeGlobals.publicPath);
  581. href = `${RuntimeGlobals.publicPath} + ${JSON.stringify(filename)}`;
  582. }
  583. }
  584. }
  585. push(
  586. entry.preload
  587. ? RuntimeGlobals.preloadAsset
  588. : RuntimeGlobals.prefetchAsset,
  589. href,
  590. entry.as || guessAsAttribute(entry.request),
  591. entry
  592. );
  593. }
  594. return lines;
  595. };
  596. /**
  597. * Adds runtime support for `__webpack_require__.PA` / `__webpack_require__.LA`,
  598. * the helpers that inject `<link rel="prefetch">` / `<link rel="preload">`
  599. * tags for asset modules referenced via `new URL(..., import.meta.url)`, CSS
  600. * `url(...)`, and HTML `<img src>` / `<link href>`. Also stores the top-level
  601. * `output.resourceHints` value so `HtmlEntryDependency` can emit its `<link>`
  602. * tags into the extracted HTML `<head>`.
  603. */
  604. class ResourceHintPlugin {
  605. /**
  606. * @param {ResourceHintsOptions=} options normalized `output.resourceHints`
  607. */
  608. constructor(options) {
  609. /** @type {ResourceHintsConfig | undefined} */
  610. this._hints = options ? options.initial : undefined;
  611. }
  612. /**
  613. * Returns the per-compilation resolver. `.hints` is the effective
  614. * `output.resourceHints` value (used by `HtmlEntryDependency` template);
  615. * `.isHtmlHinted(assetModule)` skips the JS chunk-startup `<link>` when the
  616. * HTML `<head>` already emits it.
  617. * @param {Compilation} compilation compilation
  618. * @returns {CompilationResolver} resolver
  619. */
  620. static getCompilationResolver(compilation) {
  621. const entry = compilationResolvers.get(compilation);
  622. if (entry) return entry;
  623. return {
  624. hints: undefined,
  625. getHtmlHinted: () => [],
  626. isHtmlHinted: () => false,
  627. getEntrypointHints: () => []
  628. };
  629. }
  630. /**
  631. * Match `parser.<type>.urlHints` rules for a request. Returns the merged
  632. * defaults; the caller passes them to `applyDefaults`. Exposed so parsers
  633. * that own their own URL dep creation (JS/CSS/HTML) can share one matcher.
  634. * @param {UrlHintRule[] | undefined} rules rules array
  635. * @param {string} request request URL
  636. * @returns {ResolvedResourceHints} matched defaults
  637. */
  638. static matchUrlHints(rules, request) {
  639. return matchUrlHints(rules, request);
  640. }
  641. /**
  642. * Apply `urlHints` rule defaults to a URL asset dep. Used from URL-emitting
  643. * parsers when they have per-request defaults but no comment options to
  644. * parse (e.g. the HTML parser's `pendingHints` flow, where the comment was
  645. * parsed earlier).
  646. * @param {ResourceHintDep} dep dep to mutate
  647. * @param {ResolvedResourceHints} defaults `matchUrlHints(rules, request)` result
  648. * @returns {void}
  649. */
  650. static applyDefaults(dep, defaults) {
  651. if (defaults.prefetch) dep.prefetch = true;
  652. if (defaults.preload) dep.preload = true;
  653. if (defaults.fetchPriority) dep.fetchPriority = defaults.fetchPriority;
  654. if (defaults.as !== undefined) dep.asAttribute = defaults.as;
  655. if (defaults.type !== undefined) dep.typeAttribute = defaults.type;
  656. if (defaults.media !== undefined) dep.mediaAttribute = defaults.media;
  657. }
  658. /**
  659. * Apply already-parsed `webpackPrefetch` / `webpackPreload` /
  660. * `webpackFetchPriority` / `webpackAs` / `webpackType` / `webpackMedia`
  661. * overrides to a URL asset dep. Each field wins over the project-wide
  662. * default only when it's explicitly set on the magic comment.
  663. * @param {ResourceHintDep} dep dep to mutate
  664. * @param {ResourceHintOptions} hints parsed hint options
  665. * @returns {void}
  666. */
  667. static applyParsedHints(dep, hints) {
  668. if (hints.prefetch !== undefined) dep.prefetch = hints.prefetch;
  669. if (hints.preload !== undefined) dep.preload = hints.preload;
  670. if (hints.fetchPriority !== undefined) {
  671. dep.fetchPriority = hints.fetchPriority;
  672. }
  673. if (hints.as !== undefined) dep.asAttribute = hints.as;
  674. if (hints.type !== undefined) dep.typeAttribute = hints.type;
  675. if (hints.media !== undefined) dep.mediaAttribute = hints.media;
  676. }
  677. /**
  678. * Match `urlHints` for `request` + apply per-URL magic-comment overrides
  679. * to a URL asset dep. Called from every URL-emitting parser (JS
  680. * `new URL(...)`, CSS `url(...)`, HTML `<img src>` / `<link href>`,
  681. * `new Worker(new URL(...))`) so all sources share the same precedence:
  682. * rule defaults first, magic comments win.
  683. * @param {ResourceHintDep} dep dep to mutate in place
  684. * @param {UrlHintRule[] | undefined} rules parser-scoped `urlHints` rules
  685. * @param {string} request the asset request (for rule matching)
  686. * @param {Record<string, EXPECTED_ANY> | null | undefined} commentOptions parsed magic-comment options (`null` / `undefined` skips override phase)
  687. * @param {import("../NormalModule")} module module for emitting warnings on invalid comments
  688. * @param {import("../Dependency").DependencyLocation} loc loc for warnings
  689. * @returns {void}
  690. */
  691. static applyResourceHints(dep, rules, request, commentOptions, module, loc) {
  692. ResourceHintPlugin.applyDefaults(dep, matchUrlHints(rules, request));
  693. if (!commentOptions) return;
  694. ResourceHintPlugin.applyParsedHints(
  695. dep,
  696. parseResourceHintOptions(commentOptions, module, loc)
  697. );
  698. }
  699. /**
  700. * @param {Compiler} compiler the compiler
  701. * @returns {void}
  702. */
  703. apply(compiler) {
  704. const hints = this._hints;
  705. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  706. // Lazy-computed on first access — walk every HTML module's
  707. // `HtmlEntryDependency`s, resolve each named entry, and collect
  708. // every URL asset dep target reachable from its ordered chunks.
  709. // One pass produces two views: a per-entry list (consumed by
  710. // `HtmlEntryDependency.Template` to emit `<link>` tags) and a
  711. // global `WeakSet` (consumed by `StartupAssetHintRuntimeModule`
  712. // to skip the JS runtime `<link>` for the same asset).
  713. // Deterministic (module graph is fixed by seal time) and cheap
  714. // (only fires when a JS chunk actually produces hints).
  715. /** @type {Map<string, HtmlHintedAsset[]> | undefined} */
  716. let perEntry;
  717. /** @type {WeakSet<Module> | undefined} */
  718. let anyHtmlHinted;
  719. const build = () => {
  720. perEntry = new Map();
  721. anyHtmlHinted = new WeakSet();
  722. for (const module of compilation.modules) {
  723. if (!module.getSourceTypes || !module.getSourceTypes().has("html")) {
  724. continue;
  725. }
  726. // HtmlEntry deps live on `presentationalDependencies` (added
  727. // by the HTML parser via `addPresentationalDependency`) —
  728. // they don't affect module resolution, only rendering.
  729. const presDeps = module.presentationalDependencies;
  730. if (!presDeps) continue;
  731. const HtmlEntryDependency = getHtmlEntryDependency();
  732. for (const dep of presDeps) {
  733. if (
  734. !(dep instanceof HtmlEntryDependency) ||
  735. (dep.elementKind !== "script" &&
  736. dep.elementKind !== "script-module")
  737. ) {
  738. continue;
  739. }
  740. const entrypoint = compilation.entrypoints.get(dep.entryName);
  741. if (!entrypoint) continue;
  742. // Mirror `getEntrypointChunksInLoadOrder`: entry chunk +
  743. // runtime chunk + all initial siblings (`entrypoint.chunks`
  744. // includes splitChunks output). Async `import()` chunks
  745. // stay on the on-demand runtime and are NOT HTML-hinted.
  746. /** @type {Set<import("../Chunk")>} */
  747. const chunks = new Set(entrypoint.chunks);
  748. const entry = entrypoint.getEntrypointChunk();
  749. if (entry) chunks.add(entry);
  750. const runtimeChunk = entrypoint.getRuntimeChunk();
  751. if (runtimeChunk) chunks.add(runtimeChunk);
  752. /** @type {HtmlHintedAsset[]} */
  753. const hinted = [];
  754. for (const { dep: assetDep, target } of iterateHintedUrlAssets(
  755. compilation,
  756. chunks
  757. )) {
  758. anyHtmlHinted.add(target);
  759. hinted.push({ module: target, dep: assetDep });
  760. }
  761. // Multiple HtmlEntryDeps may share an entryName (e.g. a
  762. // `<script>` and a `<link rel="modulepreload">` for the
  763. // same chunk). Merge lists so no asset is duplicated.
  764. const existing = perEntry.get(dep.entryName);
  765. if (existing) {
  766. for (const h of hinted) existing.push(h);
  767. } else {
  768. perEntry.set(dep.entryName, hinted);
  769. }
  770. }
  771. }
  772. };
  773. compilationResolvers.set(compilation, {
  774. hints,
  775. getHtmlHinted: (entryName) => {
  776. if (hints === "none") return [];
  777. if (!perEntry) build();
  778. return (
  779. /** @type {Map<string, HtmlHintedAsset[]>} */ (perEntry).get(
  780. entryName
  781. ) || []
  782. );
  783. },
  784. isHtmlHinted: (assetModule) => {
  785. if (hints === "none") return false;
  786. if (!anyHtmlHinted) build();
  787. return /** @type {WeakSet<Module>} */ (anyHtmlHinted).has(
  788. assetModule
  789. );
  790. },
  791. getEntrypointHints: (entryName) =>
  792. collectEntrypointHints(compilation, entryName, hints)
  793. });
  794. // Rendered here, not in the runtime module: requirements are still open, so a
  795. // literal href costs no `.p`/`.u`, and async chunks are reached as well.
  796. compilation.hooks.additionalChunkRuntimeRequirements.tap(
  797. PLUGIN_NAME,
  798. (chunk, set, { chunkGraph, codeGenerationResults }) => {
  799. // Browser markup, and no DOM to put it in — the call sites skip their
  800. // hints under build-time execution too (`URLDependency`).
  801. if (hints === "none" || chunkGraph.buildTimeExecution) return;
  802. const lines = collectStartupAssetHintLines(
  803. chunk,
  804. compilation,
  805. chunkGraph,
  806. codeGenerationResults,
  807. set
  808. );
  809. if (lines.length === 0) return;
  810. set.add(RuntimeGlobals.startupAssetHints);
  811. compilation.addLazyRuntimeModule(
  812. chunk,
  813. getStartupAssetHintRuntimeModule,
  814. (Ctor) => new Ctor(lines)
  815. );
  816. }
  817. );
  818. for (const [rel, runtimeGlobal] of /** @type {const} */ ([
  819. ["prefetch", RuntimeGlobals.prefetchAsset],
  820. ["preload", RuntimeGlobals.preloadAsset]
  821. ])) {
  822. compilation.hooks.runtimeRequirementInTree
  823. .for(runtimeGlobal)
  824. .tap(PLUGIN_NAME, (chunk) => {
  825. compilation.addLazyRuntimeModule(
  826. chunk,
  827. getResourceHintRuntimeModule,
  828. (Ctor) => new Ctor(rel)
  829. );
  830. });
  831. }
  832. // SSR manifest: serialize each entrypoint's resolved hints to a JSON
  833. // asset (the same descriptors as `stats.entrypoints[].resourceHints`)
  834. // so an SSR server can inject the `<link>` tags itself. `"none"`
  835. // yields empty lists (via `collectEntrypointHints`).
  836. const rhOptions = compilation.outputOptions.resourceHints;
  837. const manifestPath = rhOptions && rhOptions.manifest;
  838. if (manifestPath) {
  839. const Compilation = require("../Compilation");
  840. compilation.hooks.processAssets.tap(
  841. {
  842. name: PLUGIN_NAME,
  843. stage: Compilation.PROCESS_ASSETS_STAGE_REPORT
  844. },
  845. () => {
  846. /** @type {Record<string, EntrypointHint[]>} */
  847. const manifest = {};
  848. for (const name of compilation.entrypoints.keys()) {
  849. manifest[name] = collectEntrypointHints(compilation, name, hints);
  850. }
  851. const source = new RawSource(
  852. `${JSON.stringify(manifest, null, 2)}\n`
  853. );
  854. if (compilation.getAsset(manifestPath)) {
  855. compilation.updateAsset(manifestPath, source);
  856. } else {
  857. compilation.emitAsset(manifestPath, source);
  858. }
  859. }
  860. );
  861. }
  862. });
  863. }
  864. }
  865. module.exports = ResourceHintPlugin;