mirror of https://github.com/webpack/webpack.git
fix: move asset prefetch/preload execution from inline to startup time
This commit is contained in:
parent
9532eea79c
commit
43e8a85399
|
@ -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) {
|
||||
|
|
|
@ -76,6 +76,18 @@ class URLDependency extends ModuleDependency {
|
|||
this.relative = relative || false;
|
||||
/** @type {Set<string> | 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);
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,157 +1,125 @@
|
|||
"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
|
||||
beforeEach(() => {
|
||||
if (global.document && global.document.head) {
|
||||
global.document.head._children = [];
|
||||
if (expectations.as) {
|
||||
expect(link.as).toBe(expectations.as);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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$/);
|
||||
});
|
||||
if (expectations.type) {
|
||||
expect(link.type).toBe(expectations.type);
|
||||
}
|
||||
|
||||
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);
|
||||
if (expectations.href) {
|
||||
expect(link.href.toString()).toMatch(expectations.href);
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
)
|
||||
};
|
||||
|
||||
it("should handle auto fetchPriority", () => {
|
||||
const scriptAutoUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "auto" */ "./priority-auto.js", 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$/
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
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$/
|
||||
});
|
||||
|
||||
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);
|
||||
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"
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
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$/
|
||||
});
|
||||
|
||||
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 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 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");
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
// Test file for verifying prefetch order
|
||||
export const ordered = true;
|
|
@ -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) {
|
||||
|
|
|
@ -9,7 +9,8 @@ module.exports = {
|
|||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
assetModuleFilename: "[name][ext]"
|
||||
assetModuleFilename: "[name][ext]",
|
||||
publicPath: "/public/"
|
||||
},
|
||||
target: "web",
|
||||
module: {
|
||||
|
|
Loading…
Reference in New Issue