mirror of https://github.com/webpack/webpack.git
fix: resolve cache invalidation issues in asset prefetch implementation
This commit is contained in:
parent
43e8a85399
commit
4aaaf13071
|
|
@ -28,8 +28,6 @@ const AssetPrefetchStartupRuntimeModule = require("./AssetPrefetchStartupRuntime
|
|||
* @property {AssetInfo[]} preload
|
||||
*/
|
||||
|
||||
/** @typedef {import("../Chunk") & { _assetPrefetchInfo?: AssetPrefetchInfo }} ChunkWithAssetInfo */
|
||||
|
||||
const PLUGIN_NAME = "AssetPrefetchStartupPlugin";
|
||||
|
||||
class AssetPrefetchStartupPlugin {
|
||||
|
|
@ -39,9 +37,8 @@ class AssetPrefetchStartupPlugin {
|
|||
*/
|
||||
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();
|
||||
const chunkAssetInfoMap = new WeakMap();
|
||||
|
||||
// Hook into finishModules to collect all URLDependencies
|
||||
compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => {
|
||||
|
|
@ -96,9 +93,17 @@ class AssetPrefetchStartupPlugin {
|
|||
).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;
|
||||
// Get the actual asset filename from module buildInfo
|
||||
let assetUrl;
|
||||
if (
|
||||
resolvedModule.buildInfo &&
|
||||
resolvedModule.buildInfo.filename
|
||||
) {
|
||||
assetUrl = resolvedModule.buildInfo.filename;
|
||||
} else {
|
||||
// Fallback to extracting from request
|
||||
assetUrl = request.split(/[\\/]/).pop() || request;
|
||||
}
|
||||
|
||||
const assetType =
|
||||
AssetPrefetchStartupPlugin._getAssetType(request);
|
||||
|
|
@ -117,33 +122,26 @@ class AssetPrefetchStartupPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
const existing = chunkAssetInfoMap.get(chunk);
|
||||
if (!existing) {
|
||||
chunkAssetInfoMap.set(chunk, assetInfo);
|
||||
} else {
|
||||
// Merge with existing info
|
||||
chunkWithInfo._assetPrefetchInfo.prefetch.push(
|
||||
...assetInfo.prefetch
|
||||
);
|
||||
chunkWithInfo._assetPrefetchInfo.preload.push(
|
||||
...assetInfo.preload
|
||||
);
|
||||
existing.prefetch.push(...assetInfo.prefetch);
|
||||
existing.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 assetInfo = chunkAssetInfoMap.get(chunk);
|
||||
if (!assetInfo) return;
|
||||
|
||||
const { prefetch, preload } = chunkWithInfo._assetPrefetchInfo;
|
||||
const { prefetch, preload } = assetInfo;
|
||||
|
||||
if (prefetch.length > 0) {
|
||||
set.add(RuntimeGlobals.prefetchAsset);
|
||||
|
|
@ -153,13 +151,10 @@ class AssetPrefetchStartupPlugin {
|
|||
set.add(RuntimeGlobals.preloadAsset);
|
||||
}
|
||||
|
||||
// Add startup runtime module for assets
|
||||
if (prefetch.length > 0 || preload.length > 0) {
|
||||
compilation.addRuntimeModule(
|
||||
chunk,
|
||||
new AssetPrefetchStartupRuntimeModule(
|
||||
chunkWithInfo._assetPrefetchInfo
|
||||
)
|
||||
new AssetPrefetchStartupRuntimeModule(assetInfo)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,15 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
|||
this.assetInfo = assetInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} a unique identifier of the module
|
||||
*/
|
||||
identifier() {
|
||||
return `webpack/runtime/asset-prefetch-startup|${JSON.stringify(
|
||||
this.assetInfo
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string | null} runtime code
|
||||
*/
|
||||
|
|
@ -45,14 +54,13 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
|||
|
||||
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}"`,
|
||||
`${RuntimeGlobals.publicPath} + ${JSON.stringify(asset.url)}`,
|
||||
`"${asset.as}"`
|
||||
];
|
||||
|
||||
|
|
@ -69,21 +77,20 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
|||
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(
|
||||
? assetInfo.prefetch.map(
|
||||
(asset) =>
|
||||
`${RuntimeGlobals.prefetchAsset}(${serializeAsset(asset)});`
|
||||
)
|
||||
: // For many assets, use array iteration
|
||||
Template.asString([
|
||||
: Template.asString([
|
||||
`[${assetInfo.prefetch
|
||||
.map(
|
||||
(asset) =>
|
||||
`{ url: ${RuntimeGlobals.publicPath} + "${asset.url}", as: "${asset.as}"${
|
||||
`{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify(
|
||||
asset.url
|
||||
)}, as: "${asset.as}"${
|
||||
asset.fetchPriority
|
||||
? `, fetchPriority: "${asset.fetchPriority}"`
|
||||
: ""
|
||||
|
|
@ -101,21 +108,20 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
|||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
? assetInfo.preload.map(
|
||||
(asset) =>
|
||||
`${RuntimeGlobals.preloadAsset}(${serializeAsset(asset)});`
|
||||
)
|
||||
: // For many assets, use array iteration
|
||||
Template.asString([
|
||||
: Template.asString([
|
||||
`[${assetInfo.preload
|
||||
.map(
|
||||
(asset) =>
|
||||
`{ url: ${RuntimeGlobals.publicPath} + "${asset.url}", as: "${asset.as}"${
|
||||
`{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify(
|
||||
asset.url
|
||||
)}, as: "${asset.as}"${
|
||||
asset.fetchPriority
|
||||
? `, fetchPriority: "${asset.fetchPriority}"`
|
||||
: ""
|
||||
|
|
@ -135,6 +141,13 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
|||
|
||||
return Template.asString(lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} true, if the runtime module should get it's own scope
|
||||
*/
|
||||
shouldIsolate() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AssetPrefetchStartupRuntimeModule;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ it("should generate all prefetch and preload links", () => {
|
|||
/* webpackPrefetch: true */
|
||||
"./assets/images/test.png",
|
||||
import.meta.url
|
||||
),
|
||||
preloadFont: new URL(
|
||||
/* webpackPreload: true */
|
||||
"./assets/fonts/test.woff2",
|
||||
import.meta.url
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -121,5 +126,15 @@ it("should generate all prefetch and preload links", () => {
|
|||
as: "image",
|
||||
fetchPriority: undefined
|
||||
});
|
||||
|
||||
const fontPreloadLink = document.head._children.find(
|
||||
link => link.href.includes("test.woff2") && link.rel === "preload"
|
||||
);
|
||||
expect(fontPreloadLink).toBeTruthy();
|
||||
verifyLink(fontPreloadLink, {
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
href: /test\.woff2$/
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
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