feat: implement webpackPrefetch/webpackPreload/webpackFetchPriority support for new URL() syntax

This commit is contained in:
Ryuya 2025-07-20 22:58:28 -07:00
parent 5d233d5389
commit 42b9e8462f
31 changed files with 719 additions and 45 deletions

View File

@ -86,6 +86,10 @@ class URLDependency extends ModuleDependency {
write(this.outerRange);
write(this.relative);
write(this.usedByExports);
write(this.prefetch);
write(this.preload);
write(this.fetchPriority);
write(this.preloadAs);
super.serialize(context);
}
@ -97,6 +101,10 @@ class URLDependency extends ModuleDependency {
this.outerRange = read();
this.relative = read();
this.usedByExports = read();
this.prefetch = read();
this.preload = read();
this.fetchPriority = read();
this.preloadAs = read();
super.deserialize(context);
}
}
@ -183,8 +191,13 @@ URLDependency.Template = class URLDependencyTemplate extends (
// Build the prefetch/preload code
const hintCode = [];
const fetchPriority = dep.fetchPriority
? `"${dep.fetchPriority}"`
// Only pass valid fetchPriority values
const validFetchPriority =
dep.fetchPriority && ["high", "low", "auto"].includes(dep.fetchPriority)
? dep.fetchPriority
: undefined;
const fetchPriority = validFetchPriority
? `"${validFetchPriority}"`
: "undefined";
if (needsPrefetch && !needsPreload) {

View File

@ -39,7 +39,12 @@ class AssetPrefetchPreloadRuntimeModule extends RuntimeModule {
: "link.rel = 'preload';",
"if(as) link.as = as;",
"link.href = url;",
"if(fetchPriority) link.fetchPriority = fetchPriority;",
"if(fetchPriority) {",
Template.indent([
"link.fetchPriority = fetchPriority;",
"link.setAttribute('fetchpriority', fetchPriority);"
]),
"}",
// Add nonce if needed
compilation.outputOptions.crossOriginLoading
? Template.asString([

View File

@ -188,49 +188,29 @@ class URLParserPlugin {
// Handle prefetch/preload hints
if (importOptions) {
// Validate webpackPrefetch
if (importOptions.webpackPrefetch !== undefined) {
if (importOptions.webpackPrefetch === true) {
// Valid
} else if (typeof importOptions.webpackPrefetch === "number") {
if (importOptions.webpackPrefetch < 0) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPrefetch\` order must be non-negative, but received: ${importOptions.webpackPrefetch}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
} else {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPrefetch\` expected true or a number, but received: ${importOptions.webpackPrefetch}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
if (
importOptions.webpackPrefetch !== undefined &&
importOptions.webpackPrefetch !== true
) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPrefetch\` expected true, but received: ${importOptions.webpackPrefetch}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
// Validate webpackPreload
if (importOptions.webpackPreload !== undefined) {
if (importOptions.webpackPreload === true) {
// Valid
} else if (typeof importOptions.webpackPreload === "number") {
if (importOptions.webpackPreload < 0) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPreload\` order must be non-negative, but received: ${importOptions.webpackPreload}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
} else {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
if (
importOptions.webpackPreload !== undefined &&
importOptions.webpackPreload !== true
) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPreload\` expected true, but received: ${importOptions.webpackPreload}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
// Validate webpackFetchPriority

View File

@ -0,0 +1,9 @@
// This file is used to generate expected warnings during compilation
// Invalid fetchPriority value - should generate warning
const invalidPriorityUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "invalid" */ "./assets/images/priority-invalid.png", import.meta.url);
// Both prefetch and preload specified - should generate warning
const bothHintsUrl = new URL(/* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackFetchPriority: "high" */ "./assets/images/both-hints.png", import.meta.url);
export default {};

View File

@ -0,0 +1,188 @@
// Warnings are generated in generate-warnings.js to avoid duplication
// Mock for document.head structure
global.document = {
head: {
_children: [],
appendChild: function(element) {
this._children.push(element);
}
},
createElement: function(tagName) {
const element = {
_type: tagName,
_attributes: {},
setAttribute: function(name, value) {
this._attributes[name] = value;
// Also set as property for fetchPriority
if (name === 'fetchpriority') {
this.fetchPriority = value;
}
},
getAttribute: function(name) {
return this._attributes[name];
}
};
return element;
}
};
// Clear document.head before each test
beforeEach(() => {
document.head._children = [];
});
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", () => {
// Note: The warning for both hints is tested in generate-warnings.js
// Here we just verify that preload takes precedence
const bothUrl = new URL(/* webpackPreload: true */ /* webpackFetchPriority: "high" */ "./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("preload"); // Preload takes precedence
expect(link1._attributes.fetchpriority).toBe("high");
});
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 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");
});
});
// Test for Worker (future implementation)
// TODO: Enable this test when Worker support is implemented
/*
it("should handle Worker with fetchPriority", () => {
const worker = new Worker(
// webpackPrefetch: true
// webpackFetchPriority: "low"
new URL("./assets/scripts/worker.js", import.meta.url),
{ type: "module" }
);
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("low");
});
*/

View File

@ -0,0 +1,2 @@
// Test file for verifying prefetch order
export const ordered = true;

View File

@ -0,0 +1,2 @@
// Test asset file
console.log("priority-auto.js loaded");

View File

@ -0,0 +1,86 @@
// Mock document.head structure for testing
const mockCreateElement = tagName => {
const element = {
_type: tagName,
_attributes: {},
setAttribute(name, value) {
this._attributes[name] = value;
// Also set as property for fetchPriority
if (name === "fetchpriority") {
this.fetchPriority = value;
}
},
getAttribute(name) {
return this._attributes[name];
}
};
// Set properties based on tag type
if (tagName === "link") {
element.rel = "";
element.as = "";
element.href = "";
element.fetchPriority = undefined;
} else if (tagName === "script") {
element.src = "";
element.async = true;
element.fetchPriority = undefined;
}
return element;
};
module.exports = {
beforeExecute: () => {
// Mock document for browser environment
global.document = {
head: {
_children: [],
appendChild(element) {
this._children.push(element);
}
},
createElement: mockCreateElement
};
// Mock window for import.meta.url
global.window = {
location: {
href: "https://test.example.com/"
}
};
},
findBundle() {
return ["main.js"];
},
moduleScope(scope) {
// Inject runtime globals that would normally be provided by webpack
scope.__webpack_require__ = {
PA(url, as, fetchPriority) {
const link = global.document.createElement("link");
link.rel = "prefetch";
if (as) link.as = as;
link.href = url;
if (fetchPriority) {
link.fetchPriority = fetchPriority;
link.setAttribute("fetchpriority", fetchPriority);
}
global.document.head.appendChild(link);
},
LA(url, as, fetchPriority) {
const link = global.document.createElement("link");
link.rel = "preload";
if (as) link.as = as;
link.href = url;
if (fetchPriority) {
link.fetchPriority = fetchPriority;
link.setAttribute("fetchpriority", fetchPriority);
}
global.document.head.appendChild(link);
},
b: "https://test.example.com/" // baseURI
};
}
};

View File

@ -0,0 +1,2 @@
// Test JavaScript file
console.log("test.js loaded");

View File

@ -0,0 +1,10 @@
module.exports = [
// Invalid fetchPriority value warning
[
/`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./
],
// Both prefetch and preload specified
[
/Both webpackPrefetch and webpackPreload are specified\. webpackPreload will take precedence for immediate loading\./
]
];

View File

@ -0,0 +1,21 @@
/** @type {import("../../../../types").Configuration} */
module.exports = {
mode: "development",
entry: {
main: "./index.js",
warnings: "./generate-warnings.js"
},
output: {
filename: "[name].js",
assetModuleFilename: "[name][ext]"
},
target: "web",
module: {
rules: [
{
test: /\.(png|jpg|css|woff2)$/,
type: "asset/resource"
}
]
}
};

View File

@ -18,7 +18,7 @@ it("should preload an image asset", () => {
expect(url.href).toMatch(/preload-image\.png$/);
});
it("should prefetch with order", () => {
it("should handle numeric prefetch values with warning", () => {
const url = new URL(
/* webpackPrefetch: 10 */
"./order-image.png",

View File

@ -1,6 +1,8 @@
module.exports = [
// Numeric prefetch value (not boolean)
[/`webpackPrefetch` expected true, but received: 10\./],
// Negative prefetch value
[/`webpackPrefetch` order must be non-negative, but received: -1\./],
[/`webpackPrefetch` expected true, but received: -1\./],
// Invalid fetch priority
[
/`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./

View File

@ -0,0 +1,212 @@
"use strict";
const URLDependency = require("../../lib/dependencies/URLDependency");
const RuntimeGlobals = require("../../lib/RuntimeGlobals");
describe("URLDependency", () => {
describe("Template", () => {
const mockSource = {
replace: jest.fn()
};
const mockRuntimeTemplate = {
moduleRaw: jest.fn().mockReturnValue("__webpack_require__(123)")
};
const mockModuleGraph = {
getModule: jest.fn().mockReturnValue({
request: "test.png"
})
};
const mockChunkGraph = {};
const mockRuntimeRequirements = new Set();
beforeEach(() => {
mockSource.replace.mockClear();
mockRuntimeTemplate.moduleRaw.mockClear();
mockRuntimeRequirements.clear();
});
it("should handle prefetch with fetchPriority", () => {
const dep = new URLDependency(
"./test.png",
[10, 20],
[0, 30],
"prefetch"
);
dep.prefetch = true;
dep.fetchPriority = "high";
const template = new URLDependency.Template();
template.apply(
dep,
mockSource,
{
runtimeTemplate: mockRuntimeTemplate,
moduleGraph: mockModuleGraph,
chunkGraph: mockChunkGraph,
runtimeRequirements: mockRuntimeRequirements
}
);
expect(mockRuntimeRequirements.has(RuntimeGlobals.prefetchAsset)).toBe(true);
expect(mockRuntimeRequirements.has(RuntimeGlobals.baseURI)).toBe(true);
const replacementCall = mockSource.replace.mock.calls[0];
const replacementCode = replacementCall[2];
expect(replacementCode).toContain("__webpack_require__.PA(url, \"image\", \"high\");");
expect(replacementCode).toContain("new URL(__webpack_require__(123), __webpack_require__.b)");
});
it("should handle preload with fetchPriority", () => {
const dep = new URLDependency(
"./test.css",
[10, 20],
[0, 30],
"preload"
);
dep.preload = true;
dep.fetchPriority = "low";
const template = new URLDependency.Template();
template.apply(
dep,
mockSource,
{
runtimeTemplate: mockRuntimeTemplate,
moduleGraph: {
getModule: jest.fn().mockReturnValue({
request: "test.css"
})
},
chunkGraph: mockChunkGraph,
runtimeRequirements: mockRuntimeRequirements
}
);
expect(mockRuntimeRequirements.has(RuntimeGlobals.preloadAsset)).toBe(true);
const replacementCall = mockSource.replace.mock.calls[0];
const replacementCode = replacementCall[2];
expect(replacementCode).toContain("__webpack_require__.LA(url, \"style\", \"low\");");
});
it("should handle both prefetch and preload (preload takes precedence)", () => {
const dep = new URLDependency(
"./test.js",
[10, 20],
[0, 30],
"both"
);
dep.prefetch = true;
dep.preload = true;
dep.fetchPriority = "high";
const template = new URLDependency.Template();
template.apply(
dep,
mockSource,
{
runtimeTemplate: mockRuntimeTemplate,
moduleGraph: {
getModule: jest.fn().mockReturnValue({
request: "test.js"
})
},
chunkGraph: mockChunkGraph,
runtimeRequirements: mockRuntimeRequirements
}
);
// Should only have preload, not prefetch
expect(mockRuntimeRequirements.has(RuntimeGlobals.preloadAsset)).toBe(true);
expect(mockRuntimeRequirements.has(RuntimeGlobals.prefetchAsset)).toBe(false);
const replacementCall = mockSource.replace.mock.calls[0];
const replacementCode = replacementCall[2];
expect(replacementCode).toContain("__webpack_require__.LA(url, \"script\", \"high\");");
expect(replacementCode).not.toContain("__webpack_require__.PA");
});
it("should handle undefined fetchPriority", () => {
const dep = new URLDependency(
"./test.png",
[10, 20],
[0, 30],
"prefetch"
);
dep.prefetch = true;
// fetchPriority is undefined
const template = new URLDependency.Template();
template.apply(
dep,
mockSource,
{
runtimeTemplate: mockRuntimeTemplate,
moduleGraph: mockModuleGraph,
chunkGraph: mockChunkGraph,
runtimeRequirements: mockRuntimeRequirements
}
);
const replacementCall = mockSource.replace.mock.calls[0];
const replacementCode = replacementCall[2];
expect(replacementCode).toContain("__webpack_require__.PA(url, \"image\", undefined);");
});
it("should correctly determine asset types", () => {
const testCases = [
{ request: "test.png", expectedAs: "image" },
{ request: "test.jpg", expectedAs: "image" },
{ request: "test.webp", expectedAs: "image" },
{ request: "test.css", expectedAs: "style" },
{ request: "test.js", expectedAs: "script" },
{ request: "test.mjs", expectedAs: "script" },
{ request: "test.woff2", expectedAs: "font" },
{ request: "test.ttf", expectedAs: "font" },
{ request: "test.vtt", expectedAs: "track" },
{ request: "test.mp4", expectedAs: "fetch" }, // video uses fetch
{ request: "test.json", expectedAs: "fetch" },
{ request: "test.wasm", expectedAs: "fetch" }
];
testCases.forEach(({ request, expectedAs }) => {
const dep = new URLDependency(
`./${request}`,
[10, 20],
[0, 30],
"prefetch"
);
dep.prefetch = true;
dep.fetchPriority = "high";
const template = new URLDependency.Template();
template.apply(
dep,
mockSource,
{
runtimeTemplate: mockRuntimeTemplate,
moduleGraph: {
getModule: jest.fn().mockReturnValue({ request })
},
chunkGraph: mockChunkGraph,
runtimeRequirements: new Set()
}
);
const replacementCall = mockSource.replace.mock.calls[0];
const replacementCode = replacementCall[2];
expect(replacementCode).toContain(`__webpack_require__.PA(url, "${expectedAs}", "high");`);
mockSource.replace.mockClear();
});
});
});
});

View File

@ -0,0 +1,142 @@
const AssetPrefetchPreloadRuntimeModule = require("../../lib/runtime/AssetPrefetchPreloadRuntimeModule");
const Template = require("../../lib/Template");
describe("AssetPrefetchPreloadRuntimeModule", () => {
const mockCompilation = {
outputOptions: {
crossOriginLoading: false
}
};
const mockRuntimeTemplate = {
basicFunction: (args, body) => {
if (typeof body === "string") {
return `function(${args}){${body}}`;
}
return `function(${args}){${Template.asString(body)}}`;
}
};
beforeEach(() => {
// Mock for runtime environment
global.__webpack_require__ = {
p: "/",
u: id => `${id}.js`
};
global.RuntimeGlobals = {
prefetchAsset: "__webpack_require__.PA",
preloadAsset: "__webpack_require__.LA"
};
global.document = {
head: {
appendChild: jest.fn()
},
createElement: jest.fn(tag => ({
tag,
setAttribute: jest.fn()
}))
};
});
afterEach(() => {
delete global.__webpack_require__;
delete global.RuntimeGlobals;
delete global.document;
});
describe("prefetch module", () => {
it("should generate runtime code for prefetch", () => {
const module = new AssetPrefetchPreloadRuntimeModule("prefetch");
module.compilation = mockCompilation;
module.runtimeTemplate = mockRuntimeTemplate;
const code = module.generate();
expect(code).toContain("__webpack_require__.PAQueue = [];");
expect(code).toContain("__webpack_require__.PAQueueProcessing = false;");
expect(code).toContain("processPrefetchQueue");
expect(code).toContain("link.rel = 'prefetch';");
expect(code).toContain("PAQueue.sort");
expect(code).toContain("order: order || 0");
});
it("should support fetchPriority attribute", () => {
const module = new AssetPrefetchPreloadRuntimeModule("prefetch");
module.compilation = mockCompilation;
module.runtimeTemplate = mockRuntimeTemplate;
const code = module.generate();
expect(code).toContain("if(item.fetchPriority)");
expect(code).toContain("link.fetchPriority = item.fetchPriority;");
expect(code).toContain("link.setAttribute('fetchpriority', item.fetchPriority);");
});
it("should handle numeric order for queue sorting", () => {
const module = new AssetPrefetchPreloadRuntimeModule("prefetch");
module.compilation = mockCompilation;
module.runtimeTemplate = mockRuntimeTemplate;
const code = module.generate();
expect(code).toContain("// Sort queue by order (lower numbers first)");
expect(code).toContain("return a.order - b.order;");
});
});
describe("preload module", () => {
it("should generate runtime code for preload", () => {
const module = new AssetPrefetchPreloadRuntimeModule("preload");
module.compilation = mockCompilation;
module.runtimeTemplate = mockRuntimeTemplate;
const code = module.generate();
expect(code).toContain("__webpack_require__.LAQueue = [];");
expect(code).toContain("__webpack_require__.LAQueueProcessing = false;");
expect(code).toContain("processPreloadQueue");
expect(code).toContain("link.rel = 'preload';");
expect(code).toContain("LAQueue.sort");
expect(code).toContain("order: order || 0");
});
it("should add nonce when crossOriginLoading is enabled", () => {
const module = new AssetPrefetchPreloadRuntimeModule("preload");
module.compilation = {
outputOptions: {
crossOriginLoading: true
}
};
module.runtimeTemplate = mockRuntimeTemplate;
const code = module.generate();
expect(code).toContain("if(__webpack_require__.nc)");
expect(code).toContain("link.setAttribute('nonce', __webpack_require__.nc);");
});
});
describe("queue processing", () => {
it("should process items sequentially with setTimeout", () => {
const module = new AssetPrefetchPreloadRuntimeModule("prefetch");
module.compilation = mockCompilation;
module.runtimeTemplate = mockRuntimeTemplate;
const code = module.generate();
expect(code).toContain("// Process next item after a small delay to avoid blocking");
expect(code).toContain("setTimeout(processNext, 0);");
});
it("should handle empty queue", () => {
const module = new AssetPrefetchPreloadRuntimeModule("prefetch");
module.compilation = mockCompilation;
module.runtimeTemplate = mockRuntimeTemplate;
const code = module.generate();
expect(code).toContain("if (__webpack_require__.PAQueue.length === 0)");
expect(code).toContain("__webpack_require__.PAQueueProcessing = false;");
expect(code).toContain("return;");
});
});
});