fix: move asset prefetch/preload execution from inline to startup time

This commit is contained in:
Ryuya 2025-07-26 23:14:03 -07:00
parent 9532eea79c
commit 43e8a85399
8 changed files with 525 additions and 156 deletions

View File

@ -62,6 +62,7 @@ const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin");
const JsonModulesPlugin = require("./json/JsonModulesPlugin"); const JsonModulesPlugin = require("./json/JsonModulesPlugin");
const AssetPrefetchStartupPlugin = require("./prefetch/AssetPrefetchStartupPlugin");
const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin"); const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin");
const AssetPrefetchPreloadPlugin = require("./runtime/AssetPrefetchPreloadPlugin"); const AssetPrefetchPreloadPlugin = require("./runtime/AssetPrefetchPreloadPlugin");
@ -225,6 +226,7 @@ class WebpackOptionsApply extends OptionsApply {
new ChunkPrefetchPreloadPlugin().apply(compiler); new ChunkPrefetchPreloadPlugin().apply(compiler);
new AssetPrefetchPreloadPlugin().apply(compiler); new AssetPrefetchPreloadPlugin().apply(compiler);
new AssetPrefetchStartupPlugin().apply(compiler);
if (typeof options.output.chunkFormat === "string") { if (typeof options.output.chunkFormat === "string") {
switch (options.output.chunkFormat) { switch (options.output.chunkFormat) {

View File

@ -76,6 +76,18 @@ class URLDependency extends ModuleDependency {
this.relative = relative || false; this.relative = relative || false;
/** @type {Set<string> | boolean | undefined} */ /** @type {Set<string> | boolean | undefined} */
this.usedByExports = 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() { get type() {
@ -174,7 +186,8 @@ URLDependency.Template = class URLDependencyTemplate extends (
const needsPrefetch = dep.prefetch !== undefined && dep.prefetch !== false; const needsPrefetch = dep.prefetch !== undefined && dep.prefetch !== false;
const needsPreload = dep.preload !== undefined && dep.preload !== 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 // Get the module to determine asset type
const module = moduleGraph.getModule(dep); const module = moduleGraph.getModule(dep);
let asType = ""; let asType = "";
@ -247,6 +260,43 @@ URLDependency.Template = class URLDependencyTemplate extends (
})(), ${RuntimeGlobals.baseURI}` })(), ${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) { } else if (dep.relative) {
// No prefetch/preload - use original code // No prefetch/preload - use original code
runtimeRequirements.add(RuntimeGlobals.relativeUrl); runtimeRequirements.add(RuntimeGlobals.relativeUrl);

View File

@ -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;

View File

@ -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;

View File

@ -1,157 +1,125 @@
"use strict"; "use strict";
// Warnings are generated in generate-warnings.js to avoid duplication function verifyLink(link, expectations) {
expect(link._type).toBe("link");
expect(link.rel).toBe(expectations.rel);
// Clear document.head before each test if (expectations.as) {
beforeEach(() => { expect(link.as).toBe(expectations.as);
if (global.document && global.document.head) {
global.document.head._children = [];
} }
});
it("should generate prefetch link with fetchPriority for new URL() assets", () => { if (expectations.fetchPriority !== undefined) {
// Test high priority prefetch if (expectations.fetchPriority) {
const imageHighUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/priority-high.png", import.meta.url); expect(link._attributes.fetchpriority).toBe(expectations.fetchPriority);
expect(link.fetchPriority).toBe(expectations.fetchPriority);
} else {
expect(link._attributes.fetchpriority).toBeUndefined();
expect(link.fetchPriority).toBeUndefined();
}
}
expect(document.head._children).toHaveLength(1); if (expectations.type) {
const link1 = document.head._children[0]; expect(link.type).toBe(expectations.type);
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", () => { if (expectations.href) {
// Test low priority preload expect(link.href.toString()).toMatch(expectations.href);
const styleLowUrl = new URL(/* webpackPreload: true */ /* webpackFetchPriority: "low" */ "./assets/styles/priority-low.css", import.meta.url); }
}
expect(document.head._children).toHaveLength(1); it("should generate all prefetch and preload links", () => {
const link1 = document.head._children[0]; const urls = {
expect(link1._type).toBe("link"); prefetchHigh: new URL(
expect(link1.rel).toBe("preload"); /* webpackPrefetch: true */ /* webpackFetchPriority: "high" */
expect(link1.as).toBe("style"); "./assets/images/priority-high.png",
expect(link1._attributes.fetchpriority).toBe("low"); import.meta.url
expect(link1.fetchPriority).toBe("low"); ),
expect(link1.href.toString()).toMatch(/priority-low\.css$/); 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
)
};
it("should handle auto fetchPriority", () => { const prefetchHighLink = document.head._children.find(
const scriptAutoUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "auto" */ "./priority-auto.js", import.meta.url); 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$/
});
expect(document.head._children).toHaveLength(1); const preloadLowLink = document.head._children.find(
const link1 = document.head._children[0]; link => link.href.includes("priority-low.css") && link.rel === "preload"
expect(link1._type).toBe("link"); );
expect(link1.rel).toBe("prefetch"); expect(preloadLowLink).toBeTruthy();
expect(link1.as).toBe("script"); verifyLink(preloadLowLink, {
expect(link1._attributes.fetchpriority).toBe("auto"); rel: "preload",
expect(link1.fetchPriority).toBe("auto"); as: "style",
}); fetchPriority: "low",
href: /priority-low\.css$/
});
it("should not set fetchPriority for invalid values", () => { const prefetchAutoLink = document.head._children.find(
// Note: The actual invalid value is tested in generate-warnings.js link => link.href.includes("priority-auto.js") && link.rel === "prefetch"
// Here we just verify that invalid values are filtered out );
const invalidUrl = new URL(/* webpackPrefetch: true */ "./assets/images/test.png", import.meta.url); expect(prefetchAutoLink).toBeTruthy();
verifyLink(prefetchAutoLink, {
rel: "prefetch",
as: "script",
fetchPriority: "auto"
});
expect(document.head._children).toHaveLength(1); const preloadTypedLink = document.head._children.find(
const link1 = document.head._children[0]; link => link.href.includes("typed.css") && link.rel === "preload"
expect(link1._type).toBe("link"); );
expect(link1.rel).toBe("prefetch"); expect(preloadTypedLink).toBeTruthy();
// When there's no fetchPriority, it should be undefined verifyLink(preloadTypedLink, {
expect(link1._attributes.fetchpriority).toBeUndefined(); rel: "preload",
expect(link1.fetchPriority).toBeUndefined(); as: "style",
}); type: "text/css",
href: /typed\.css$/
});
it("should handle multiple URLs with different priorities", () => { const bothHintsLink = document.head._children.find(
const url1 = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ "./assets/images/image-1.png", import.meta.url); link => link.href.includes("both-hints.png")
);
expect(bothHintsLink).toBeTruthy();
expect(bothHintsLink.rel).toBe("preload");
expect(bothHintsLink._attributes.fetchpriority).toBe("high");
const url2 = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "low" */ "./assets/images/image-2.png", import.meta.url); const noPriorityLink = document.head._children.find(
link => link.href.includes("test.png") && link.rel === "prefetch" &&
const url3 = new URL(/* webpackPrefetch: true */ "./assets/images/image-3.png", import.meta.url); !link._attributes.fetchpriority
);
expect(document.head._children).toHaveLength(3); expect(noPriorityLink).toBeTruthy();
verifyLink(noPriorityLink, {
// First link - high priority rel: "prefetch",
const link1 = document.head._children[0]; as: "image",
expect(link1._attributes.fetchpriority).toBe("high"); fetchPriority: undefined
// 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 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");
});
});

View File

@ -1,4 +0,0 @@
"use strict";
// Test file for verifying prefetch order
export const ordered = true;

View File

@ -58,6 +58,8 @@ module.exports = {
}, },
moduleScope(scope) { moduleScope(scope) {
// Make document available in the module scope
scope.document = global.document;
// Inject runtime globals that would normally be provided by webpack // Inject runtime globals that would normally be provided by webpack
scope.__webpack_require__ = { scope.__webpack_require__ = {
PA(url, as, fetchPriority, type) { PA(url, as, fetchPriority, type) {

View File

@ -9,7 +9,8 @@ module.exports = {
}, },
output: { output: {
filename: "[name].js", filename: "[name].js",
assetModuleFilename: "[name][ext]" assetModuleFilename: "[name][ext]",
publicPath: "/public/"
}, },
target: "web", target: "web",
module: { module: {