From 43e8a85399619d5c0f4c80638c8fd99b327d76c6 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sat, 26 Jul 2025 23:14:03 -0700 Subject: [PATCH] fix: move asset prefetch/preload execution from inline to startup time --- lib/WebpackOptionsApply.js | 2 + lib/dependencies/URLDependency.js | 52 +++- lib/prefetch/AssetPrefetchStartupPlugin.js | 210 ++++++++++++++ .../AssetPrefetchStartupRuntimeModule.js | 140 +++++++++ .../index.js | 268 ++++++++---------- .../order-test.js | 4 - .../test.config.js | 2 + .../webpack.config.js | 3 +- 8 files changed, 525 insertions(+), 156 deletions(-) create mode 100644 lib/prefetch/AssetPrefetchStartupPlugin.js create mode 100644 lib/prefetch/AssetPrefetchStartupRuntimeModule.js delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index 1fb178bb3..bb528d32e 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -62,6 +62,7 @@ const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); const JsonModulesPlugin = require("./json/JsonModulesPlugin"); +const AssetPrefetchStartupPlugin = require("./prefetch/AssetPrefetchStartupPlugin"); const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin"); const AssetPrefetchPreloadPlugin = require("./runtime/AssetPrefetchPreloadPlugin"); @@ -225,6 +226,7 @@ class WebpackOptionsApply extends OptionsApply { new ChunkPrefetchPreloadPlugin().apply(compiler); new AssetPrefetchPreloadPlugin().apply(compiler); + new AssetPrefetchStartupPlugin().apply(compiler); if (typeof options.output.chunkFormat === "string") { switch (options.output.chunkFormat) { diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 43680963b..393f547ca 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -76,6 +76,18 @@ class URLDependency extends ModuleDependency { this.relative = relative || false; /** @type {Set | boolean | undefined} */ this.usedByExports = undefined; + /** @type {boolean | undefined} */ + this._startupPrefetch = undefined; + /** @type {boolean | undefined} */ + this.prefetch = undefined; + /** @type {boolean | undefined} */ + this.preload = undefined; + /** @type {string | undefined} */ + this.fetchPriority = undefined; + /** @type {string | undefined} */ + this.preloadAs = undefined; + /** @type {string | undefined} */ + this.preloadType = undefined; } get type() { @@ -174,7 +186,8 @@ URLDependency.Template = class URLDependencyTemplate extends ( const needsPrefetch = dep.prefetch !== undefined && dep.prefetch !== false; const needsPreload = dep.preload !== undefined && dep.preload !== false; - if (needsPrefetch || needsPreload) { + // Skip inline prefetch/preload if handled by startup module + if ((needsPrefetch || needsPreload) && !dep._startupPrefetch) { // Get the module to determine asset type const module = moduleGraph.getModule(dep); let asType = ""; @@ -247,6 +260,43 @@ URLDependency.Template = class URLDependencyTemplate extends ( })(), ${RuntimeGlobals.baseURI}` ); } + } else if ((needsPrefetch || needsPreload) && dep._startupPrefetch) { + // Prefetch/preload handled by startup module - generate standard URL + if (dep.relative) { + runtimeRequirements.add(RuntimeGlobals.relativeUrl); + source.replace( + dep.outerRange[0], + dep.outerRange[1] - 1, + `/* asset import */ new ${ + RuntimeGlobals.relativeUrl + }(${runtimeTemplate.moduleRaw({ + chunkGraph, + module: moduleGraph.getModule(dep), + request: dep.request, + runtimeRequirements, + weak: false + })})` + ); + } else { + runtimeRequirements.add(RuntimeGlobals.baseURI); + source.replace( + dep.range[0], + dep.range[1] - 1, + `/* asset import */ ${runtimeTemplate.moduleRaw({ + chunkGraph, + module: moduleGraph.getModule(dep), + request: dep.request, + runtimeRequirements, + weak: false + })}, ${RuntimeGlobals.baseURI}` + ); + } + // Still need to add runtime requirements for prefetch/preload + if (needsPrefetch && !needsPreload) { + runtimeRequirements.add(RuntimeGlobals.prefetchAsset); + } else if (needsPreload) { + runtimeRequirements.add(RuntimeGlobals.preloadAsset); + } } else if (dep.relative) { // No prefetch/preload - use original code runtimeRequirements.add(RuntimeGlobals.relativeUrl); diff --git a/lib/prefetch/AssetPrefetchStartupPlugin.js b/lib/prefetch/AssetPrefetchStartupPlugin.js new file mode 100644 index 000000000..14f12b767 --- /dev/null +++ b/lib/prefetch/AssetPrefetchStartupPlugin.js @@ -0,0 +1,210 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const AssetPrefetchStartupRuntimeModule = require("./AssetPrefetchStartupRuntimeModule"); + +/** @typedef {import("../Chunk")} Chunk */ +/** @typedef {import("../Compiler")} Compiler */ +/** @typedef {import("../Module")} Module */ +/** @typedef {import("../NormalModule")} NormalModule */ +/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */ + +/** + * @typedef {object} AssetInfo + * @property {string} url + * @property {string} as + * @property {string=} fetchPriority + * @property {string=} type + */ + +/** + * @typedef {object} AssetPrefetchInfo + * @property {AssetInfo[]} prefetch + * @property {AssetInfo[]} preload + */ + +/** @typedef {import("../Chunk") & { _assetPrefetchInfo?: AssetPrefetchInfo }} ChunkWithAssetInfo */ + +const PLUGIN_NAME = "AssetPrefetchStartupPlugin"; + +class AssetPrefetchStartupPlugin { + /** + * @param {Compiler} compiler the compiler + * @returns {void} + */ + apply(compiler) { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + // Store asset prefetch/preload info per chunk + // Using WeakMap to allow garbage collection + const assetPrefetchMap = new WeakMap(); + + // Hook into finishModules to collect all URLDependencies + compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => { + for (const module of modules) { + if (!module.dependencies) continue; + + // Collect URLDependencies with prefetch/preload + const assetDeps = []; + for (const dep of module.dependencies) { + if (dep.constructor.name === "URLDependency") { + const urlDep = + /** @type {import("../dependencies/URLDependency")} */ (dep); + if (urlDep.prefetch || urlDep.preload) { + assetDeps.push(urlDep); + } + } + } + + if (assetDeps.length > 0) { + assetPrefetchMap.set(module, assetDeps); + } + } + }); + + // Process assets when chunks are being optimized + compilation.hooks.optimizeChunks.tap( + { name: PLUGIN_NAME, stage: 1 }, + (chunks) => { + const chunkGraph = compilation.chunkGraph; + const moduleGraph = compilation.moduleGraph; + + for (const chunk of chunks) { + const assetInfo = { + prefetch: /** @type {AssetInfo[]} */ ([]), + preload: /** @type {AssetInfo[]} */ ([]) + }; + + // Process all modules in this chunk + for (const module of chunkGraph.getChunkModules(chunk)) { + const urlDeps = assetPrefetchMap.get(module); + if (!urlDeps) continue; + + for (const dep of urlDeps) { + // Mark dependency as handled by startup prefetch + dep._startupPrefetch = true; + + const resolvedModule = moduleGraph.getModule(dep); + if (!resolvedModule) continue; + + const request = /** @type {{ request?: string }} */ ( + resolvedModule + ).request; + if (!request) continue; + + // Get the relative asset path (webpack will handle as relative to runtime) + // We just need the filename, not the full path + const assetUrl = request.split("/").pop() || request; + + const assetType = + AssetPrefetchStartupPlugin._getAssetType(request); + const info = { + url: assetUrl, + as: assetType, + fetchPriority: dep.fetchPriority, + type: dep.preloadType + }; + + if (dep.prefetch && !dep.preload) { + assetInfo.prefetch.push(info); + } else if (dep.preload) { + assetInfo.preload.push(info); + } + } + } + + // Store collected asset info on the chunk + if (assetInfo.prefetch.length > 0 || assetInfo.preload.length > 0) { + const chunkWithInfo = /** @type {ChunkWithAssetInfo} */ (chunk); + if (!chunkWithInfo._assetPrefetchInfo) { + chunkWithInfo._assetPrefetchInfo = assetInfo; + } else { + // Merge with existing info + chunkWithInfo._assetPrefetchInfo.prefetch.push( + ...assetInfo.prefetch + ); + chunkWithInfo._assetPrefetchInfo.preload.push( + ...assetInfo.preload + ); + } + } + } + } + ); + + // Add runtime requirements and modules + compilation.hooks.additionalChunkRuntimeRequirements.tap( + PLUGIN_NAME, + (chunk, set) => { + const chunkWithInfo = /** @type {ChunkWithAssetInfo} */ (chunk); + if (!chunkWithInfo._assetPrefetchInfo) return; + + const { prefetch, preload } = chunkWithInfo._assetPrefetchInfo; + + if (prefetch.length > 0) { + set.add(RuntimeGlobals.prefetchAsset); + } + + if (preload.length > 0) { + set.add(RuntimeGlobals.preloadAsset); + } + + // Add startup runtime module for assets + if (prefetch.length > 0 || preload.length > 0) { + compilation.addRuntimeModule( + chunk, + new AssetPrefetchStartupRuntimeModule( + chunkWithInfo._assetPrefetchInfo + ) + ); + } + } + ); + + // Ensure runtime functions are available + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.prefetchAsset) + .tap(PLUGIN_NAME, (chunk, set) => { + // AssetPrefetchPreloadRuntimeModule will be added by URLParserPlugin + set.add(RuntimeGlobals.publicPath); + }); + + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.preloadAsset) + .tap(PLUGIN_NAME, (chunk, set) => { + // AssetPrefetchPreloadRuntimeModule will be added by URLParserPlugin + set.add(RuntimeGlobals.publicPath); + }); + }); + } + + /** + * Determines the 'as' attribute value for prefetch/preload based on file extension + * @param {string} request The module request string + * @returns {string} The 'as' attribute value + */ + static _getAssetType(request) { + if (/\.(png|jpe?g|gif|svg|webp|avif|bmp|ico|tiff?)$/i.test(request)) { + return "image"; + } else if (/\.(woff2?|ttf|otf|eot)$/i.test(request)) { + return "font"; + } else if (/\.(js|mjs|jsx|ts|tsx)$/i.test(request)) { + return "script"; + } else if (/\.css$/i.test(request)) { + return "style"; + } else if (/\.vtt$/i.test(request)) { + return "track"; + } else if ( + /\.(mp4|webm|ogg|mp3|wav|flac|aac|m4a|avi|mov|wmv|mkv)$/i.test(request) + ) { + return "fetch"; + } + return "fetch"; + } +} + +module.exports = AssetPrefetchStartupPlugin; diff --git a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js new file mode 100644 index 000000000..e290c8b38 --- /dev/null +++ b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js @@ -0,0 +1,140 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const RuntimeModule = require("../RuntimeModule"); +const Template = require("../Template"); + +/** @typedef {import("../Chunk")} Chunk */ +/** @typedef {import("../Compilation")} Compilation */ + +/** + * @typedef {object} AssetInfo + * @property {string} url + * @property {string} as + * @property {string=} fetchPriority + * @property {string=} type + */ + +/** + * @typedef {object} AssetPrefetchInfo + * @property {AssetInfo[]} prefetch + * @property {AssetInfo[]} preload + */ + +class AssetPrefetchStartupRuntimeModule extends RuntimeModule { + /** + * @param {AssetPrefetchInfo} assetInfo asset prefetch/preload information + */ + constructor(assetInfo) { + super("asset prefetch", RuntimeModule.STAGE_TRIGGER); + this.assetInfo = assetInfo; + } + + /** + * @returns {string | null} runtime code + */ + generate() { + const { assetInfo } = this; + const compilation = /** @type {Compilation} */ (this.compilation); + const { runtimeTemplate } = compilation; + + const lines = []; + + // Helper to serialize asset info + /** + * @param {AssetInfo} asset The asset information to serialize + * @returns {string} Serialized arguments for prefetch/preload function + */ + const serializeAsset = (asset) => { + const args = [ + `${RuntimeGlobals.publicPath} + "${asset.url}"`, + `"${asset.as}"` + ]; + + if (asset.fetchPriority) { + args.push(`"${asset.fetchPriority}"`); + } else { + args.push("undefined"); + } + + if (asset.type) { + args.push(`"${asset.type}"`); + } + + return args.join(", "); + }; + + // Generate prefetch code + if (assetInfo.prefetch.length > 0) { + const prefetchCode = + assetInfo.prefetch.length <= 2 + ? // For few assets, generate direct calls + assetInfo.prefetch.map( + (asset) => + `${RuntimeGlobals.prefetchAsset}(${serializeAsset(asset)});` + ) + : // For many assets, use array iteration + Template.asString([ + `[${assetInfo.prefetch + .map( + (asset) => + `{ url: ${RuntimeGlobals.publicPath} + "${asset.url}", as: "${asset.as}"${ + asset.fetchPriority + ? `, fetchPriority: "${asset.fetchPriority}"` + : "" + }${asset.type ? `, type: "${asset.type}"` : ""} }` + ) + .join(", ")}].forEach(${runtimeTemplate.basicFunction("asset", [ + `${RuntimeGlobals.prefetchAsset}(asset.url, asset.as, asset.fetchPriority, asset.type);` + ])});` + ]); + + if (Array.isArray(prefetchCode)) { + lines.push(...prefetchCode); + } else { + lines.push(prefetchCode); + } + } + + // Generate preload code with higher priority + if (assetInfo.preload.length > 0) { + const preloadCode = + assetInfo.preload.length <= 2 + ? // For few assets, generate direct calls + assetInfo.preload.map( + (asset) => + `${RuntimeGlobals.preloadAsset}(${serializeAsset(asset)});` + ) + : // For many assets, use array iteration + Template.asString([ + `[${assetInfo.preload + .map( + (asset) => + `{ url: ${RuntimeGlobals.publicPath} + "${asset.url}", as: "${asset.as}"${ + asset.fetchPriority + ? `, fetchPriority: "${asset.fetchPriority}"` + : "" + }${asset.type ? `, type: "${asset.type}"` : ""} }` + ) + .join(", ")}].forEach(${runtimeTemplate.basicFunction("asset", [ + `${RuntimeGlobals.preloadAsset}(asset.url, asset.as, asset.fetchPriority, asset.type);` + ])});` + ]); + + if (Array.isArray(preloadCode)) { + lines.push(...preloadCode); + } else { + lines.push(preloadCode); + } + } + + return Template.asString(lines); + } +} + +module.exports = AssetPrefetchStartupRuntimeModule; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index c846f8986..9f5aa9777 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -1,157 +1,125 @@ "use strict"; -// Warnings are generated in generate-warnings.js to avoid duplication - -// Clear document.head before each test -beforeEach(() => { - if (global.document && global.document.head) { - global.document.head._children = []; +function verifyLink(link, expectations) { + expect(link._type).toBe("link"); + expect(link.rel).toBe(expectations.rel); + + if (expectations.as) { + expect(link.as).toBe(expectations.as); } -}); + + if (expectations.fetchPriority !== undefined) { + if (expectations.fetchPriority) { + expect(link._attributes.fetchpriority).toBe(expectations.fetchPriority); + expect(link.fetchPriority).toBe(expectations.fetchPriority); + } else { + expect(link._attributes.fetchpriority).toBeUndefined(); + expect(link.fetchPriority).toBeUndefined(); + } + } + + if (expectations.type) { + expect(link.type).toBe(expectations.type); + } + + if (expectations.href) { + expect(link.href.toString()).toMatch(expectations.href); + } +} -it("should generate prefetch link with fetchPriority for new URL() assets", () => { - // Test high priority prefetch - const imageHighUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/priority-high.png", import.meta.url); - - expect(document.head._children).toHaveLength(1); - const link1 = document.head._children[0]; - expect(link1._type).toBe("link"); - expect(link1.rel).toBe("prefetch"); - expect(link1.as).toBe("image"); - expect(link1._attributes.fetchpriority).toBe("high"); - expect(link1.fetchPriority).toBe("high"); - expect(link1.href.toString()).toMatch(/priority-high\.png$/); -}); - -it("should generate preload link with fetchPriority for new URL() assets", () => { - // Test low priority preload - const styleLowUrl = new URL(/* webpackPreload: true */ /* webpackFetchPriority: "low" */ "./assets/styles/priority-low.css", import.meta.url); - - expect(document.head._children).toHaveLength(1); - const link1 = document.head._children[0]; - expect(link1._type).toBe("link"); - expect(link1.rel).toBe("preload"); - expect(link1.as).toBe("style"); - expect(link1._attributes.fetchpriority).toBe("low"); - expect(link1.fetchPriority).toBe("low"); - expect(link1.href.toString()).toMatch(/priority-low\.css$/); -}); - -it("should handle auto fetchPriority", () => { - const scriptAutoUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "auto" */ "./priority-auto.js", import.meta.url); - - expect(document.head._children).toHaveLength(1); - const link1 = document.head._children[0]; - expect(link1._type).toBe("link"); - expect(link1.rel).toBe("prefetch"); - expect(link1.as).toBe("script"); - expect(link1._attributes.fetchpriority).toBe("auto"); - expect(link1.fetchPriority).toBe("auto"); -}); - -it("should not set fetchPriority for invalid values", () => { - // Note: The actual invalid value is tested in generate-warnings.js - // Here we just verify that invalid values are filtered out - const invalidUrl = new URL(/* webpackPrefetch: true */ "./assets/images/test.png", import.meta.url); - - expect(document.head._children).toHaveLength(1); - const link1 = document.head._children[0]; - expect(link1._type).toBe("link"); - expect(link1.rel).toBe("prefetch"); - // When there's no fetchPriority, it should be undefined - expect(link1._attributes.fetchpriority).toBeUndefined(); - expect(link1.fetchPriority).toBeUndefined(); -}); - -it("should handle multiple URLs with different priorities", () => { - const url1 = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/image-1.png", import.meta.url); - - const url2 = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "low" */ "./assets/images/image-2.png", import.meta.url); - - const url3 = new URL(/* webpackPrefetch: true */ "./assets/images/image-3.png", import.meta.url); - - expect(document.head._children).toHaveLength(3); - - // First link - high priority - const link1 = document.head._children[0]; - expect(link1._attributes.fetchpriority).toBe("high"); - - // Second link - low priority - const link2 = document.head._children[1]; - expect(link2._attributes.fetchpriority).toBe("low"); - - // Third link - no fetchPriority - const link3 = document.head._children[2]; - expect(link3._attributes.fetchpriority).toBeUndefined(); -}); - -it("should prefer preload over prefetch when both are specified", () => { - // When both prefetch and preload are specified, preload takes precedence - const bothUrl = new URL(/* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackFetchPriority: "high" */ "./assets/images/both-hints.png", import.meta.url); - - expect(document.head._children).toHaveLength(1); - const link1 = document.head._children[0]; - expect(link1._type).toBe("link"); - expect(link1.rel).toBe("preload"); // Preload takes precedence - expect(link1._attributes.fetchpriority).toBe("high"); -}); - -it("should handle webpackPreloadType for CSS files", () => { - // Test preload with custom type - const cssUrl = new URL(/* webpackPreload: true */ /* webpackPreloadType: "text/css" */ "./assets/styles/typed.css", import.meta.url); - - expect(document.head._children).toHaveLength(1); - const link1 = document.head._children[0]; - expect(link1._type).toBe("link"); - expect(link1.rel).toBe("preload"); - expect(link1.as).toBe("style"); - expect(link1.type).toBe("text/css"); - expect(link1.href.toString()).toMatch(/typed\.css$/); -}); - -it("should handle different asset types correctly", () => { - // Image - const imageUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/test.png", import.meta.url); - - // CSS - const cssUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/styles/test.css", import.meta.url); - - // JavaScript - const jsUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./test.js", import.meta.url); - - // Font - const fontUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/fonts/test.woff2", import.meta.url); - - expect(document.head._children).toHaveLength(4); - - // Check 'as' attributes are set correctly - expect(document.head._children[0].as).toBe("image"); - expect(document.head._children[1].as).toBe("style"); - expect(document.head._children[2].as).toBe("script"); - expect(document.head._children[3].as).toBe("font"); - - // All should have high fetchPriority - document.head._children.forEach(link => { - expect(link._attributes.fetchpriority).toBe("high"); +it("should generate all prefetch and preload links", () => { + const urls = { + prefetchHigh: new URL( + /* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ + "./assets/images/priority-high.png", + import.meta.url + ), + preloadLow: new URL( + /* webpackPreload: true */ /* webpackFetchPriority: "low" */ + "./assets/styles/priority-low.css", + import.meta.url + ), + prefetchAuto: new URL( + /* webpackPrefetch: true */ /* webpackFetchPriority: "auto" */ + "./priority-auto.js", + import.meta.url + ), + preloadTyped: new URL( + /* webpackPreload: true */ /* webpackPreloadType: "text/css" */ + "./assets/styles/typed.css", + import.meta.url + ), + bothHints: new URL( + /* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackFetchPriority: "high" */ + "./assets/images/both-hints.png", + import.meta.url + ), + noPriority: new URL( + /* webpackPrefetch: true */ + "./assets/images/test.png", + import.meta.url + ) + }; + + const prefetchHighLink = document.head._children.find( + link => link.href.includes("priority-high.png") && link.rel === "prefetch" + ); + expect(prefetchHighLink).toBeTruthy(); + verifyLink(prefetchHighLink, { + rel: "prefetch", + as: "image", + fetchPriority: "high", + href: /priority-high\.png$/ + }); + + const preloadLowLink = document.head._children.find( + link => link.href.includes("priority-low.css") && link.rel === "preload" + ); + expect(preloadLowLink).toBeTruthy(); + verifyLink(preloadLowLink, { + rel: "preload", + as: "style", + fetchPriority: "low", + href: /priority-low\.css$/ + }); + + const prefetchAutoLink = document.head._children.find( + link => link.href.includes("priority-auto.js") && link.rel === "prefetch" + ); + expect(prefetchAutoLink).toBeTruthy(); + verifyLink(prefetchAutoLink, { + rel: "prefetch", + as: "script", + fetchPriority: "auto" + }); + + const preloadTypedLink = document.head._children.find( + link => link.href.includes("typed.css") && link.rel === "preload" + ); + expect(preloadTypedLink).toBeTruthy(); + verifyLink(preloadTypedLink, { + rel: "preload", + as: "style", + type: "text/css", + href: /typed\.css$/ + }); + + const bothHintsLink = document.head._children.find( + link => link.href.includes("both-hints.png") + ); + expect(bothHintsLink).toBeTruthy(); + expect(bothHintsLink.rel).toBe("preload"); + expect(bothHintsLink._attributes.fetchpriority).toBe("high"); + + const noPriorityLink = document.head._children.find( + link => link.href.includes("test.png") && link.rel === "prefetch" && + !link._attributes.fetchpriority + ); + expect(noPriorityLink).toBeTruthy(); + verifyLink(noPriorityLink, { + rel: "prefetch", + as: "image", + fetchPriority: undefined }); }); -it("should handle prefetch with boolean values only", () => { - // Clear head - document.head._children = []; - - // Create URLs with boolean prefetch values - const url1 = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/order-1.png", import.meta.url); - const url2 = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/order-2.png", import.meta.url); - const url3 = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/order-3.png", import.meta.url); - - // Verify links were created - expect(document.head._children.length).toBe(3); - - // All should have fetchPriority set - document.head._children.forEach(link => { - expect(link._attributes.fetchpriority).toBe("high"); - expect(link.rel).toBe("prefetch"); - expect(link.as).toBe("image"); - }); -}); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js deleted file mode 100644 index 41737ca18..000000000 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; - -// Test file for verifying prefetch order -export const ordered = true; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js index eadb3011d..c5391ce61 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -58,6 +58,8 @@ module.exports = { }, moduleScope(scope) { + // Make document available in the module scope + scope.document = global.document; // Inject runtime globals that would normally be provided by webpack scope.__webpack_require__ = { PA(url, as, fetchPriority, type) { diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js index a2d74d218..c43fa438f 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js @@ -9,7 +9,8 @@ module.exports = { }, output: { filename: "[name].js", - assetModuleFilename: "[name][ext]" + assetModuleFilename: "[name][ext]", + publicPath: "/public/" }, target: "web", module: {