mirror of https://github.com/webpack/webpack.git
feat: implement webpackPrefetch/webpackPreload/webpackFetchPriority support for new URL() syntax
This commit is contained in:
parent
5d233d5389
commit
42b9e8462f
|
@ -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) {
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -188,50 +188,30 @@ 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) {
|
||||
if (
|
||||
importOptions.webpackPrefetch !== undefined &&
|
||||
importOptions.webpackPrefetch !== true
|
||||
) {
|
||||
parser.state.module.addWarning(
|
||||
new UnsupportedFeatureWarning(
|
||||
`\`webpackPrefetch\` order must be non-negative, but received: ${importOptions.webpackPrefetch}.`,
|
||||
`\`webpackPrefetch\` expected true, 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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate webpackPreload
|
||||
if (importOptions.webpackPreload !== undefined) {
|
||||
if (importOptions.webpackPreload === true) {
|
||||
// Valid
|
||||
} else if (typeof importOptions.webpackPreload === "number") {
|
||||
if (importOptions.webpackPreload < 0) {
|
||||
if (
|
||||
importOptions.webpackPreload !== undefined &&
|
||||
importOptions.webpackPreload !== true
|
||||
) {
|
||||
parser.state.module.addWarning(
|
||||
new UnsupportedFeatureWarning(
|
||||
`\`webpackPreload\` order must be non-negative, but received: ${importOptions.webpackPreload}.`,
|
||||
`\`webpackPreload\` expected true, 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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate webpackFetchPriority
|
||||
if (
|
||||
|
|
|
@ -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 {};
|
|
@ -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");
|
||||
});
|
||||
*/
|
|
@ -0,0 +1,2 @@
|
|||
// Test file for verifying prefetch order
|
||||
export const ordered = true;
|
|
@ -0,0 +1,2 @@
|
|||
// Test asset file
|
||||
console.log("priority-auto.js loaded");
|
|
@ -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
|
||||
};
|
||||
}
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
// Test JavaScript file
|
||||
console.log("test.js loaded");
|
|
@ -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\./
|
||||
]
|
||||
];
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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\./
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;");
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue