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
|
* @property {AssetInfo[]} preload
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @typedef {import("../Chunk") & { _assetPrefetchInfo?: AssetPrefetchInfo }} ChunkWithAssetInfo */
|
|
||||||
|
|
||||||
const PLUGIN_NAME = "AssetPrefetchStartupPlugin";
|
const PLUGIN_NAME = "AssetPrefetchStartupPlugin";
|
||||||
|
|
||||||
class AssetPrefetchStartupPlugin {
|
class AssetPrefetchStartupPlugin {
|
||||||
|
|
@ -39,9 +37,8 @@ class AssetPrefetchStartupPlugin {
|
||||||
*/
|
*/
|
||||||
apply(compiler) {
|
apply(compiler) {
|
||||||
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
|
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 assetPrefetchMap = new WeakMap();
|
||||||
|
const chunkAssetInfoMap = new WeakMap();
|
||||||
|
|
||||||
// Hook into finishModules to collect all URLDependencies
|
// Hook into finishModules to collect all URLDependencies
|
||||||
compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => {
|
compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => {
|
||||||
|
|
@ -96,9 +93,17 @@ class AssetPrefetchStartupPlugin {
|
||||||
).request;
|
).request;
|
||||||
if (!request) continue;
|
if (!request) continue;
|
||||||
|
|
||||||
// Get the relative asset path (webpack will handle as relative to runtime)
|
// Get the actual asset filename from module buildInfo
|
||||||
// We just need the filename, not the full path
|
let assetUrl;
|
||||||
const assetUrl = request.split("/").pop() || request;
|
if (
|
||||||
|
resolvedModule.buildInfo &&
|
||||||
|
resolvedModule.buildInfo.filename
|
||||||
|
) {
|
||||||
|
assetUrl = resolvedModule.buildInfo.filename;
|
||||||
|
} else {
|
||||||
|
// Fallback to extracting from request
|
||||||
|
assetUrl = request.split(/[\\/]/).pop() || request;
|
||||||
|
}
|
||||||
|
|
||||||
const assetType =
|
const assetType =
|
||||||
AssetPrefetchStartupPlugin._getAssetType(request);
|
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) {
|
if (assetInfo.prefetch.length > 0 || assetInfo.preload.length > 0) {
|
||||||
const chunkWithInfo = /** @type {ChunkWithAssetInfo} */ (chunk);
|
const existing = chunkAssetInfoMap.get(chunk);
|
||||||
if (!chunkWithInfo._assetPrefetchInfo) {
|
if (!existing) {
|
||||||
chunkWithInfo._assetPrefetchInfo = assetInfo;
|
chunkAssetInfoMap.set(chunk, assetInfo);
|
||||||
} else {
|
} else {
|
||||||
// Merge with existing info
|
existing.prefetch.push(...assetInfo.prefetch);
|
||||||
chunkWithInfo._assetPrefetchInfo.prefetch.push(
|
existing.preload.push(...assetInfo.preload);
|
||||||
...assetInfo.prefetch
|
|
||||||
);
|
|
||||||
chunkWithInfo._assetPrefetchInfo.preload.push(
|
|
||||||
...assetInfo.preload
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add runtime requirements and modules
|
|
||||||
compilation.hooks.additionalChunkRuntimeRequirements.tap(
|
compilation.hooks.additionalChunkRuntimeRequirements.tap(
|
||||||
PLUGIN_NAME,
|
PLUGIN_NAME,
|
||||||
(chunk, set) => {
|
(chunk, set) => {
|
||||||
const chunkWithInfo = /** @type {ChunkWithAssetInfo} */ (chunk);
|
const assetInfo = chunkAssetInfoMap.get(chunk);
|
||||||
if (!chunkWithInfo._assetPrefetchInfo) return;
|
if (!assetInfo) return;
|
||||||
|
|
||||||
const { prefetch, preload } = chunkWithInfo._assetPrefetchInfo;
|
const { prefetch, preload } = assetInfo;
|
||||||
|
|
||||||
if (prefetch.length > 0) {
|
if (prefetch.length > 0) {
|
||||||
set.add(RuntimeGlobals.prefetchAsset);
|
set.add(RuntimeGlobals.prefetchAsset);
|
||||||
|
|
@ -153,13 +151,10 @@ class AssetPrefetchStartupPlugin {
|
||||||
set.add(RuntimeGlobals.preloadAsset);
|
set.add(RuntimeGlobals.preloadAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add startup runtime module for assets
|
|
||||||
if (prefetch.length > 0 || preload.length > 0) {
|
if (prefetch.length > 0 || preload.length > 0) {
|
||||||
compilation.addRuntimeModule(
|
compilation.addRuntimeModule(
|
||||||
chunk,
|
chunk,
|
||||||
new AssetPrefetchStartupRuntimeModule(
|
new AssetPrefetchStartupRuntimeModule(assetInfo)
|
||||||
chunkWithInfo._assetPrefetchInfo
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,15 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
||||||
this.assetInfo = assetInfo;
|
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
|
* @returns {string | null} runtime code
|
||||||
*/
|
*/
|
||||||
|
|
@ -45,14 +54,13 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
||||||
|
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
|
||||||
// Helper to serialize asset info
|
|
||||||
/**
|
/**
|
||||||
* @param {AssetInfo} asset The asset information to serialize
|
* @param {AssetInfo} asset The asset information to serialize
|
||||||
* @returns {string} Serialized arguments for prefetch/preload function
|
* @returns {string} Serialized arguments for prefetch/preload function
|
||||||
*/
|
*/
|
||||||
const serializeAsset = (asset) => {
|
const serializeAsset = (asset) => {
|
||||||
const args = [
|
const args = [
|
||||||
`${RuntimeGlobals.publicPath} + "${asset.url}"`,
|
`${RuntimeGlobals.publicPath} + ${JSON.stringify(asset.url)}`,
|
||||||
`"${asset.as}"`
|
`"${asset.as}"`
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -69,21 +77,20 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
||||||
return args.join(", ");
|
return args.join(", ");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate prefetch code
|
|
||||||
if (assetInfo.prefetch.length > 0) {
|
if (assetInfo.prefetch.length > 0) {
|
||||||
const prefetchCode =
|
const prefetchCode =
|
||||||
assetInfo.prefetch.length <= 2
|
assetInfo.prefetch.length <= 2
|
||||||
? // For few assets, generate direct calls
|
? assetInfo.prefetch.map(
|
||||||
assetInfo.prefetch.map(
|
|
||||||
(asset) =>
|
(asset) =>
|
||||||
`${RuntimeGlobals.prefetchAsset}(${serializeAsset(asset)});`
|
`${RuntimeGlobals.prefetchAsset}(${serializeAsset(asset)});`
|
||||||
)
|
)
|
||||||
: // For many assets, use array iteration
|
: Template.asString([
|
||||||
Template.asString([
|
|
||||||
`[${assetInfo.prefetch
|
`[${assetInfo.prefetch
|
||||||
.map(
|
.map(
|
||||||
(asset) =>
|
(asset) =>
|
||||||
`{ url: ${RuntimeGlobals.publicPath} + "${asset.url}", as: "${asset.as}"${
|
`{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify(
|
||||||
|
asset.url
|
||||||
|
)}, as: "${asset.as}"${
|
||||||
asset.fetchPriority
|
asset.fetchPriority
|
||||||
? `, fetchPriority: "${asset.fetchPriority}"`
|
? `, fetchPriority: "${asset.fetchPriority}"`
|
||||||
: ""
|
: ""
|
||||||
|
|
@ -101,21 +108,20 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate preload code with higher priority
|
|
||||||
if (assetInfo.preload.length > 0) {
|
if (assetInfo.preload.length > 0) {
|
||||||
const preloadCode =
|
const preloadCode =
|
||||||
assetInfo.preload.length <= 2
|
assetInfo.preload.length <= 2
|
||||||
? // For few assets, generate direct calls
|
? assetInfo.preload.map(
|
||||||
assetInfo.preload.map(
|
|
||||||
(asset) =>
|
(asset) =>
|
||||||
`${RuntimeGlobals.preloadAsset}(${serializeAsset(asset)});`
|
`${RuntimeGlobals.preloadAsset}(${serializeAsset(asset)});`
|
||||||
)
|
)
|
||||||
: // For many assets, use array iteration
|
: Template.asString([
|
||||||
Template.asString([
|
|
||||||
`[${assetInfo.preload
|
`[${assetInfo.preload
|
||||||
.map(
|
.map(
|
||||||
(asset) =>
|
(asset) =>
|
||||||
`{ url: ${RuntimeGlobals.publicPath} + "${asset.url}", as: "${asset.as}"${
|
`{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify(
|
||||||
|
asset.url
|
||||||
|
)}, as: "${asset.as}"${
|
||||||
asset.fetchPriority
|
asset.fetchPriority
|
||||||
? `, fetchPriority: "${asset.fetchPriority}"`
|
? `, fetchPriority: "${asset.fetchPriority}"`
|
||||||
: ""
|
: ""
|
||||||
|
|
@ -135,6 +141,13 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule {
|
||||||
|
|
||||||
return Template.asString(lines);
|
return Template.asString(lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean} true, if the runtime module should get it's own scope
|
||||||
|
*/
|
||||||
|
shouldIsolate() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AssetPrefetchStartupRuntimeModule;
|
module.exports = AssetPrefetchStartupRuntimeModule;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ it("should generate all prefetch and preload links", () => {
|
||||||
/* webpackPrefetch: true */
|
/* webpackPrefetch: true */
|
||||||
"./assets/images/test.png",
|
"./assets/images/test.png",
|
||||||
import.meta.url
|
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",
|
as: "image",
|
||||||
fetchPriority: undefined
|
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