From 1e70bf7d49f45423b114ccf721bda940d2f572ba Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 13 Jul 2025 01:02:26 -0700 Subject: [PATCH 01/23] feat: add webpackPrefetch/webpackPreload/webpackFetchPriority support for Worker syntax --- lib/dependencies/WorkerPlugin.js | 96 +++++++- .../chunk-name-worker.js | 4 + .../classic-prefetch-worker.js | 4 + .../classic-preload-worker.js | 3 + .../classic-priority-worker.js | 3 + .../fetch-priority-worker.js | 4 + .../worker/worker-prefetch-preload/index.js | 219 ++++++++++++++++++ .../invalid-prefetch-worker.js | 3 + .../invalid-priority-worker.js | 3 + .../low-priority-worker.js | 4 + .../multi-hint-worker.js | 3 + .../worker-prefetch-preload/normal-worker.js | 4 + .../prefetch-order-worker.js | 4 + .../prefetch-worker.js | 4 + .../preload-order-worker.js | 4 + .../worker-prefetch-preload/preload-worker.js | 4 + .../runtime-prefetch-worker.js | 7 + .../runtime-preload-worker.js | 8 + .../runtime-priority-worker.js | 7 + .../worker-prefetch-preload/runtime-test.js | 94 ++++++++ .../worker-prefetch-preload/shared-module.js | 4 + .../shared-worker-preload.js | 9 + .../shared-worker-test.js | 38 +++ .../worker-prefetch-preload/shared-worker.js | 9 + .../worker-prefetch-preload/test.config.js | 5 + .../worker-prefetch-preload/test.filter.js | 3 + .../worker-prefetch-preload/warnings.js | 12 + .../worker-prefetch-preload/webpack.config.js | 7 + 28 files changed, 566 insertions(+), 3 deletions(-) create mode 100644 test/configCases/worker/worker-prefetch-preload/chunk-name-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/classic-prefetch-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/classic-preload-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/classic-priority-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/fetch-priority-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/index.js create mode 100644 test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/low-priority-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/normal-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/prefetch-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/preload-order-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/preload-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-test.js create mode 100644 test/configCases/worker/worker-prefetch-preload/shared-module.js create mode 100644 test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js create mode 100644 test/configCases/worker/worker-prefetch-preload/shared-worker-test.js create mode 100644 test/configCases/worker/worker-prefetch-preload/shared-worker.js create mode 100644 test/configCases/worker/worker-prefetch-preload/test.config.js create mode 100644 test/configCases/worker/worker-prefetch-preload/test.filter.js create mode 100644 test/configCases/worker/worker-prefetch-preload/warnings.js create mode 100644 test/configCases/worker/worker-prefetch-preload/webpack.config.js diff --git a/lib/dependencies/WorkerPlugin.js b/lib/dependencies/WorkerPlugin.js index 6d28cfbe0..6e666aae8 100644 --- a/lib/dependencies/WorkerPlugin.js +++ b/lib/dependencies/WorkerPlugin.js @@ -40,6 +40,7 @@ const WorkerDependency = require("./WorkerDependency"); /** @typedef {import("../../declarations/WebpackOptions").WasmLoading} WasmLoading */ /** @typedef {import("../../declarations/WebpackOptions").WorkerPublicPath} WorkerPublicPath */ /** @typedef {import("../Compiler")} Compiler */ +/** @typedef {import("../ChunkGroup").RawChunkGroupOptions} RawChunkGroupOptions */ /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */ /** @typedef {import("../Entrypoint").EntryOptions} EntryOptions */ /** @typedef {import("../NormalModule")} NormalModule */ @@ -226,9 +227,12 @@ class WorkerPlugin { } } const insertType = expr.properties.length > 0 ? "comma" : "single"; - const insertLocation = /** @type {Range} */ ( - expr.properties[expr.properties.length - 1].range - )[1]; + const insertLocation = + expr.properties.length > 0 + ? /** @type {Range} */ ( + expr.properties[expr.properties.length - 1].range + )[1] + : /** @type {Range} */ (expr.range)[0] + 1; return { expressions, otherElements, @@ -302,6 +306,10 @@ class WorkerPlugin { ? /** @type {Range} */ (arg2.range) : /** @type {Range} */ (arg1.range)[1] }; + + /** @type {RawChunkGroupOptions} */ + const groupOptions = {}; + const { options: importOptions, errors: commentErrors } = parser.parseCommentOptions(/** @type {Range} */ (expr.range)); @@ -363,6 +371,87 @@ class WorkerPlugin { entryOptions.name = importOptions.webpackChunkName; } } + if (importOptions.webpackPrefetch !== undefined) { + if (importOptions.webpackPrefetch === true) { + groupOptions.prefetchOrder = 0; + } else if (typeof importOptions.webpackPrefetch === "number") { + // Validate prefetch order is within reasonable bounds + if (importOptions.webpackPrefetch < 0) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` order must be non-negative, but received: ${importOptions.webpackPrefetch}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } else { + groupOptions.prefetchOrder = importOptions.webpackPrefetch; + } + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` expected true or a number, but received: ${importOptions.webpackPrefetch}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + if (importOptions.webpackPreload !== undefined) { + if (importOptions.webpackPreload === true) { + groupOptions.preloadOrder = 0; + } else if (typeof importOptions.webpackPreload === "number") { + // Validate preload order is within reasonable bounds + if (importOptions.webpackPreload < 0) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` order must be non-negative, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } else { + groupOptions.preloadOrder = importOptions.webpackPreload; + } + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + if (importOptions.webpackFetchPriority !== undefined) { + if ( + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes( + importOptions.webpackFetchPriority + ) + ) { + groupOptions.fetchPriority = + /** @type {"low" | "high" | "auto"} */ + (importOptions.webpackFetchPriority); + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + // Warn if both prefetch and preload are specified + if ( + importOptions.webpackPrefetch !== undefined && + importOptions.webpackPreload !== undefined + ) { + parser.state.module.addWarning( + new CommentCompilationWarning( + "Both webpackPrefetch and webpackPreload are specified. " + + "webpackPreload will take precedence for immediate loading, " + + "while webpackPrefetch is typically used for future navigation.", + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } } if ( @@ -394,6 +483,7 @@ class WorkerPlugin { } const block = new AsyncDependenciesBlock({ + ...groupOptions, name: entryOptions.name, entryOptions: { chunkLoading: this._chunkLoading, diff --git a/test/configCases/worker/worker-prefetch-preload/chunk-name-worker.js b/test/configCases/worker/worker-prefetch-preload/chunk-name-worker.js new file mode 100644 index 000000000..4c08f7081 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/chunk-name-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("chunk-name-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/classic-prefetch-worker.js b/test/configCases/worker/worker-prefetch-preload/classic-prefetch-worker.js new file mode 100644 index 000000000..923c2e0cf --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/classic-prefetch-worker.js @@ -0,0 +1,4 @@ +// Classic (non-module) worker +self.onmessage = function(e) { + self.postMessage("classic-prefetch-worker: " + e.data); +}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/classic-preload-worker.js b/test/configCases/worker/worker-prefetch-preload/classic-preload-worker.js new file mode 100644 index 000000000..fa9dfffc4 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/classic-preload-worker.js @@ -0,0 +1,3 @@ +self.onmessage = function(e) { + self.postMessage("classic-preload-worker: " + e.data); +}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/classic-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/classic-priority-worker.js new file mode 100644 index 000000000..2c6abb52e --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/classic-priority-worker.js @@ -0,0 +1,3 @@ +self.onmessage = function(e) { + self.postMessage("classic-priority-worker: " + e.data); +}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/fetch-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/fetch-priority-worker.js new file mode 100644 index 000000000..272dac0db --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/fetch-priority-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("fetch-priority-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/index.js b/test/configCases/worker/worker-prefetch-preload/index.js new file mode 100644 index 000000000..40e261303 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/index.js @@ -0,0 +1,219 @@ +it("should allow to create a Worker with webpackPrefetch", async () => { + const worker = new Worker(new URL("./prefetch-worker.js", import.meta.url), { + type: "module", + /* webpackPrefetch: true */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("prefetch-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a Worker with webpackPrefetch order", async () => { + const worker = new Worker(new URL("./prefetch-order-worker.js", import.meta.url), { + type: "module", + /* webpackPrefetch: 2 */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("prefetch-order-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a Worker with webpackPreload", async () => { + const worker = new Worker(new URL("./preload-worker.js", import.meta.url), { + type: "module", + /* webpackPreload: true */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("preload-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a Worker with webpackFetchPriority", async () => { + const worker = new Worker(new URL("./fetch-priority-worker.js", import.meta.url), { + type: "module", + /* webpackPreload: true */ + /* webpackFetchPriority: "high" */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("fetch-priority-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a Worker with webpackPreload order", async () => { + const worker = new Worker(new URL("./preload-order-worker.js", import.meta.url), { + type: "module", + /* webpackPreload: 5 */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("preload-order-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a normal Worker without hints", async () => { + const worker = new Worker(new URL("./normal-worker.js", import.meta.url), { + type: "module" + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("normal-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a Worker with chunk name", async () => { + const worker = new Worker(new URL("./chunk-name-worker.js", import.meta.url), { + type: "module", + /* webpackChunkName: "custom-worker-chunk" */ + /* webpackPrefetch: true */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("chunk-name-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a Worker with low fetchPriority", async () => { + const worker = new Worker(new URL("./low-priority-worker.js", import.meta.url), { + type: "module", + /* webpackPreload: true */ + /* webpackFetchPriority: "low" */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("low-priority-worker: test"); + await worker.terminate(); +}); + +// Classic (non-module) Worker tests +it("should allow to create a classic Worker with webpackPrefetch", async () => { + const worker = new Worker(new URL("./classic-prefetch-worker.js", import.meta.url), { + // type: "classic" is the default, so we can omit it + /* webpackPrefetch: true */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("classic-prefetch-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a classic Worker with webpackPreload", async () => { + const worker = new Worker(new URL("./classic-preload-worker.js", import.meta.url), { + type: "classic", // explicitly set to classic + /* webpackPreload: true */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("classic-preload-worker: test"); + await worker.terminate(); +}); + +it("should allow to create a classic Worker with fetchPriority", async () => { + const worker = new Worker(new URL("./classic-priority-worker.js", import.meta.url), { + // no type specified = classic worker + /* webpackPreload: true */ + /* webpackFetchPriority: "high" */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("classic-priority-worker: test"); + await worker.terminate(); +}); + +// Warning tests - these should generate warnings during compilation +it("should handle negative prefetch values", async () => { + const worker = new Worker(new URL("./invalid-prefetch-worker.js", import.meta.url), { + type: "module", + /* webpackPrefetch: -1 */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + // Worker should still work, but webpack should have generated a warning + expect(result).toBe("invalid-prefetch-worker: test"); + await worker.terminate(); +}); + +it("should handle invalid fetchPriority values", async () => { + const worker = new Worker(new URL("./invalid-priority-worker.js", import.meta.url), { + type: "module", + /* webpackPreload: true */ + /* webpackFetchPriority: "invalid" */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + // Worker should still work, but webpack should have generated a warning + expect(result).toBe("invalid-priority-worker: test"); + await worker.terminate(); +}); + +it("should warn when both prefetch and preload are specified", async () => { + const worker = new Worker(new URL("./multi-hint-worker.js", import.meta.url), { + type: "module", + /* webpackPrefetch: true */ + /* webpackPreload: true */ + }); + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + // Worker should still work with preload taking precedence + expect(result).toBe("multi-hint-worker: test"); + await worker.terminate(); +}); diff --git a/test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js b/test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js new file mode 100644 index 000000000..f1fe6882b --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js @@ -0,0 +1,3 @@ +self.onmessage = function(e) { + self.postMessage("invalid-prefetch-worker: " + e.data); +}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js new file mode 100644 index 000000000..226b87575 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js @@ -0,0 +1,3 @@ +self.onmessage = function(e) { + self.postMessage("invalid-priority-worker: " + e.data); +}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/low-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/low-priority-worker.js new file mode 100644 index 000000000..e8a6aeac7 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/low-priority-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("low-priority-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js b/test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js new file mode 100644 index 000000000..7032a6349 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js @@ -0,0 +1,3 @@ +self.onmessage = function(e) { + self.postMessage("multi-hint-worker: " + e.data); +}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/normal-worker.js b/test/configCases/worker/worker-prefetch-preload/normal-worker.js new file mode 100644 index 000000000..503ba5a2c --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/normal-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("normal-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js b/test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js new file mode 100644 index 000000000..27ab9a722 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("prefetch-order-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/prefetch-worker.js b/test/configCases/worker/worker-prefetch-preload/prefetch-worker.js new file mode 100644 index 000000000..633fcdf04 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/prefetch-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("prefetch-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/preload-order-worker.js b/test/configCases/worker/worker-prefetch-preload/preload-order-worker.js new file mode 100644 index 000000000..46627adde --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/preload-order-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("preload-order-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/preload-worker.js b/test/configCases/worker/worker-prefetch-preload/preload-worker.js new file mode 100644 index 000000000..19ed4ce7e --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/preload-worker.js @@ -0,0 +1,4 @@ +self.onmessage = function(e) { + self.postMessage("preload-worker: " + e.data); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js b/test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js new file mode 100644 index 000000000..24c674a32 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js @@ -0,0 +1,7 @@ +// Add some imports to ensure this creates a separate chunk +import { processData } from "./shared-module.js"; + +self.onmessage = function(e) { + const processed = processData(e.data); + self.postMessage("runtime-prefetch-worker: " + processed); +}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js b/test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js new file mode 100644 index 000000000..55bcb8462 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js @@ -0,0 +1,8 @@ +// Add some imports to ensure this creates a separate chunk +import { processData } from "./shared-module.js"; + +self.onmessage = function(e) { + const processed = processData(e.data); + self.postMessage("runtime-preload-worker: " + processed); +}; + diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js new file mode 100644 index 000000000..80f702549 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js @@ -0,0 +1,7 @@ +// Add some imports to ensure this creates a separate chunk +import { processData } from "./shared-module.js"; + +self.onmessage = function(e) { + const processed = processData(e.data); + self.postMessage("runtime-priority-worker: " + processed); +}; diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-test.js b/test/configCases/worker/worker-prefetch-preload/runtime-test.js new file mode 100644 index 000000000..f6c1f44ff --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/runtime-test.js @@ -0,0 +1,94 @@ +it("should create prefetch link tags for workers", async () => { + // Clear any existing link tags + const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); + existingLinks.forEach(link => link.remove()); + + // Create worker with prefetch + const worker = new Worker(new URL("./runtime-prefetch-worker.js", import.meta.url), { + type: "module", + /* webpackPrefetch: true */ + /* webpackChunkName: "runtime-prefetch-worker" */ + }); + + // Wait a bit for the link to be created + await new Promise(resolve => setTimeout(resolve, 100)); + + // Check if prefetch link was created + const prefetchLinks = document.querySelectorAll('link[rel="prefetch"]'); + const hasPrefetchLink = Array.from(prefetchLinks).some(link => + link.href.includes("runtime-prefetch-worker") + ); + + expect(hasPrefetchLink).toBe(true); + + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("runtime-prefetch-worker: test"); + await worker.terminate(); +}); + +it("should create preload link tags for workers", async () => { + // Clear any existing link tags + const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); + existingLinks.forEach(link => link.remove()); + + // Create worker with preload + const worker = new Worker(new URL("./runtime-preload-worker.js", import.meta.url), { + type: "module", + /* webpackPreload: true */ + /* webpackChunkName: "runtime-preload-worker" */ + }); + + // Check if preload link was created immediately + const preloadLinks = document.querySelectorAll('link[rel="preload"]'); + const hasPreloadLink = Array.from(preloadLinks).some(link => + link.href.includes("runtime-preload-worker") + ); + + expect(hasPreloadLink).toBe(true); + + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("runtime-preload-worker: test"); + await worker.terminate(); +}); + +it("should create preload link with fetchpriority attribute", async () => { + // Clear any existing link tags + const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); + existingLinks.forEach(link => link.remove()); + + // Create worker with preload and fetch priority + const worker = new Worker(new URL("./runtime-priority-worker.js", import.meta.url), { + type: "module", + /* webpackPreload: true */ + /* webpackFetchPriority: "high" */ + /* webpackChunkName: "runtime-priority-worker" */ + }); + + // Check if preload link was created with fetchpriority + const preloadLinks = document.querySelectorAll('link[rel="preload"]'); + const priorityLink = Array.from(preloadLinks).find(link => + link.href.includes("runtime-priority-worker") + ); + + expect(priorityLink).toBeTruthy(); + expect(priorityLink.getAttribute("fetchpriority")).toBe("high"); + + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("runtime-priority-worker: test"); + await worker.terminate(); +}); diff --git a/test/configCases/worker/worker-prefetch-preload/shared-module.js b/test/configCases/worker/worker-prefetch-preload/shared-module.js new file mode 100644 index 000000000..91bdcf464 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/shared-module.js @@ -0,0 +1,4 @@ +export function processData(data) { + // Simulate some processing + return data.toUpperCase ? data.toUpperCase() : data; +} diff --git a/test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js b/test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js new file mode 100644 index 000000000..19d48e341 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js @@ -0,0 +1,9 @@ +self.onconnect = function(e) { + const port = e.ports[0]; + + port.onmessage = function(event) { + port.postMessage("shared-worker-preload: " + event.data); + }; + + port.start(); +}; diff --git a/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js b/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js new file mode 100644 index 000000000..1b3804740 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js @@ -0,0 +1,38 @@ +it("should allow SharedWorker with webpackPrefetch", async () => { + const worker = new SharedWorker(new URL("./shared-worker.js", import.meta.url), { + type: "module", + name: "shared-worker-instance", + /* webpackPrefetch: true */ + /* webpackChunkName: "shared-worker-prefetch" */ + }); + + const messagePromise = new Promise(resolve => { + worker.port.onmessage = event => { + resolve(event.data); + }; + }); + + worker.port.postMessage("test"); + const result = await messagePromise; + expect(result).toBe("shared-worker: test"); +}); + +it("should allow SharedWorker with webpackPreload and fetchPriority", async () => { + const worker = new SharedWorker(new URL("./shared-worker-preload.js", import.meta.url), { + type: "module", + name: "shared-worker-preload-instance", + /* webpackPreload: true */ + /* webpackFetchPriority: "high" */ + /* webpackChunkName: "shared-worker-preload" */ + }); + + const messagePromise = new Promise(resolve => { + worker.port.onmessage = event => { + resolve(event.data); + }; + }); + + worker.port.postMessage("test-preload"); + const result = await messagePromise; + expect(result).toBe("shared-worker-preload: test-preload"); +}); diff --git a/test/configCases/worker/worker-prefetch-preload/shared-worker.js b/test/configCases/worker/worker-prefetch-preload/shared-worker.js new file mode 100644 index 000000000..94a6629ad --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/shared-worker.js @@ -0,0 +1,9 @@ +self.onconnect = function(e) { + const port = e.ports[0]; + + port.onmessage = function(event) { + port.postMessage("shared-worker: " + event.data); + }; + + port.start(); +}; diff --git a/test/configCases/worker/worker-prefetch-preload/test.config.js b/test/configCases/worker/worker-prefetch-preload/test.config.js new file mode 100644 index 000000000..0c4cdb953 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle(i, options) { + return ["main.js"]; + } +}; diff --git a/test/configCases/worker/worker-prefetch-preload/test.filter.js b/test/configCases/worker/worker-prefetch-preload/test.filter.js new file mode 100644 index 000000000..d456e8870 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/test.filter.js @@ -0,0 +1,3 @@ +const supportsWorker = require("../../../helpers/supportsWorker"); + +module.exports = () => supportsWorker(); diff --git a/test/configCases/worker/worker-prefetch-preload/warnings.js b/test/configCases/worker/worker-prefetch-preload/warnings.js new file mode 100644 index 000000000..59e7619ad --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/warnings.js @@ -0,0 +1,12 @@ +module.exports = [ + // Test for negative prefetch values + [/`webpackPrefetch` order must be non-negative, but received: -1\./], + // Test for invalid fetchPriority values + [ + /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ + ], + // Test for both prefetch and preload specified + [ + /Both webpackPrefetch and webpackPreload are specified\. webpackPreload will take precedence/ + ] +]; diff --git a/test/configCases/worker/worker-prefetch-preload/webpack.config.js b/test/configCases/worker/worker-prefetch-preload/webpack.config.js new file mode 100644 index 000000000..798707403 --- /dev/null +++ b/test/configCases/worker/worker-prefetch-preload/webpack.config.js @@ -0,0 +1,7 @@ +/** @type {import("../../../../types").Configuration} */ +module.exports = { + output: { + filename: "[name].js" + }, + target: "web" +}; From 979b7f81e066fce0946eb122166ce0e2bd98410a Mon Sep 17 00:00:00 2001 From: Ryuya Date: Mon, 14 Jul 2025 07:54:06 -0700 Subject: [PATCH 02/23] test: update Worker magic comment tests to follow webpack conventions --- .../worker/worker-prefetch-preload/index.js | 152 +++++++++++++----- .../worker-prefetch-preload/runtime-test.js | 63 ++++++-- .../shared-worker-test.js | 22 ++- 3 files changed, 181 insertions(+), 56 deletions(-) diff --git a/test/configCases/worker/worker-prefetch-preload/index.js b/test/configCases/worker/worker-prefetch-preload/index.js index 40e261303..ca5d026f4 100644 --- a/test/configCases/worker/worker-prefetch-preload/index.js +++ b/test/configCases/worker/worker-prefetch-preload/index.js @@ -1,8 +1,11 @@ it("should allow to create a Worker with webpackPrefetch", async () => { - const worker = new Worker(new URL("./prefetch-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPrefetch: true */ - }); + new URL("./prefetch-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -14,10 +17,13 @@ it("should allow to create a Worker with webpackPrefetch", async () => { }); it("should allow to create a Worker with webpackPrefetch order", async () => { - const worker = new Worker(new URL("./prefetch-order-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPrefetch: 2 */ - }); + new URL("./prefetch-order-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -29,10 +35,13 @@ it("should allow to create a Worker with webpackPrefetch order", async () => { }); it("should allow to create a Worker with webpackPreload", async () => { - const worker = new Worker(new URL("./preload-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPreload: true */ - }); + new URL("./preload-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -44,11 +53,14 @@ it("should allow to create a Worker with webpackPreload", async () => { }); it("should allow to create a Worker with webpackFetchPriority", async () => { - const worker = new Worker(new URL("./fetch-priority-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPreload: true */ /* webpackFetchPriority: "high" */ - }); + new URL("./fetch-priority-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -60,10 +72,13 @@ it("should allow to create a Worker with webpackFetchPriority", async () => { }); it("should allow to create a Worker with webpackPreload order", async () => { - const worker = new Worker(new URL("./preload-order-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPreload: 5 */ - }); + new URL("./preload-order-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -89,11 +104,14 @@ it("should allow to create a normal Worker without hints", async () => { }); it("should allow to create a Worker with chunk name", async () => { - const worker = new Worker(new URL("./chunk-name-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackChunkName: "custom-worker-chunk" */ /* webpackPrefetch: true */ - }); + new URL("./chunk-name-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -105,11 +123,14 @@ it("should allow to create a Worker with chunk name", async () => { }); it("should allow to create a Worker with low fetchPriority", async () => { - const worker = new Worker(new URL("./low-priority-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPreload: true */ /* webpackFetchPriority: "low" */ - }); + new URL("./low-priority-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -122,10 +143,11 @@ it("should allow to create a Worker with low fetchPriority", async () => { // Classic (non-module) Worker tests it("should allow to create a classic Worker with webpackPrefetch", async () => { - const worker = new Worker(new URL("./classic-prefetch-worker.js", import.meta.url), { - // type: "classic" is the default, so we can omit it + const worker = new Worker( /* webpackPrefetch: true */ - }); + new URL("./classic-prefetch-worker.js", import.meta.url) + // type: "classic" is the default, so we can omit options + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -137,10 +159,13 @@ it("should allow to create a classic Worker with webpackPrefetch", async () => { }); it("should allow to create a classic Worker with webpackPreload", async () => { - const worker = new Worker(new URL("./classic-preload-worker.js", import.meta.url), { - type: "classic", // explicitly set to classic + const worker = new Worker( /* webpackPreload: true */ - }); + new URL("./classic-preload-worker.js", import.meta.url), + { + type: "classic" // explicitly set to classic + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -152,11 +177,12 @@ it("should allow to create a classic Worker with webpackPreload", async () => { }); it("should allow to create a classic Worker with fetchPriority", async () => { - const worker = new Worker(new URL("./classic-priority-worker.js", import.meta.url), { - // no type specified = classic worker + const worker = new Worker( /* webpackPreload: true */ /* webpackFetchPriority: "high" */ - }); + new URL("./classic-priority-worker.js", import.meta.url) + // no type specified = classic worker + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -169,10 +195,13 @@ it("should allow to create a classic Worker with fetchPriority", async () => { // Warning tests - these should generate warnings during compilation it("should handle negative prefetch values", async () => { - const worker = new Worker(new URL("./invalid-prefetch-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPrefetch: -1 */ - }); + new URL("./invalid-prefetch-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -185,11 +214,14 @@ it("should handle negative prefetch values", async () => { }); it("should handle invalid fetchPriority values", async () => { - const worker = new Worker(new URL("./invalid-priority-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPreload: true */ /* webpackFetchPriority: "invalid" */ - }); + new URL("./invalid-priority-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -202,11 +234,14 @@ it("should handle invalid fetchPriority values", async () => { }); it("should warn when both prefetch and preload are specified", async () => { - const worker = new Worker(new URL("./multi-hint-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPrefetch: true */ /* webpackPreload: true */ - }); + new URL("./multi-hint-worker.js", import.meta.url), + { + type: "module" + } + ); worker.postMessage("test"); const result = await new Promise(resolve => { worker.onmessage = event => { @@ -217,3 +252,42 @@ it("should warn when both prefetch and preload are specified", async () => { expect(result).toBe("multi-hint-worker: test"); await worker.terminate(); }); + +// Test both comment placement patterns +it("should support magic comments in options object", async () => { + const worker = new Worker( + new URL("./prefetch-worker.js", import.meta.url), + { + type: "module", + /* webpackPrefetch: true */ + } + ); + worker.postMessage("test-options"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("prefetch-worker: test-options"); + await worker.terminate(); +}); + +it("should support multiple magic comments in different positions", async () => { + const worker = new Worker( + /* webpackChunkName: "mixed-position-worker" */ + new URL("./fetch-priority-worker.js", import.meta.url), + { + type: "module", + /* webpackPreload: true */ + /* webpackFetchPriority: "high" */ + } + ); + worker.postMessage("test-mixed"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("fetch-priority-worker: test-mixed"); + await worker.terminate(); +}); diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-test.js b/test/configCases/worker/worker-prefetch-preload/runtime-test.js index f6c1f44ff..0d1230215 100644 --- a/test/configCases/worker/worker-prefetch-preload/runtime-test.js +++ b/test/configCases/worker/worker-prefetch-preload/runtime-test.js @@ -4,11 +4,14 @@ it("should create prefetch link tags for workers", async () => { existingLinks.forEach(link => link.remove()); // Create worker with prefetch - const worker = new Worker(new URL("./runtime-prefetch-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPrefetch: true */ /* webpackChunkName: "runtime-prefetch-worker" */ - }); + new URL("./runtime-prefetch-worker.js", import.meta.url), + { + type: "module" + } + ); // Wait a bit for the link to be created await new Promise(resolve => setTimeout(resolve, 100)); @@ -37,11 +40,14 @@ it("should create preload link tags for workers", async () => { existingLinks.forEach(link => link.remove()); // Create worker with preload - const worker = new Worker(new URL("./runtime-preload-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPreload: true */ /* webpackChunkName: "runtime-preload-worker" */ - }); + new URL("./runtime-preload-worker.js", import.meta.url), + { + type: "module" + } + ); // Check if preload link was created immediately const preloadLinks = document.querySelectorAll('link[rel="preload"]'); @@ -67,12 +73,15 @@ it("should create preload link with fetchpriority attribute", async () => { existingLinks.forEach(link => link.remove()); // Create worker with preload and fetch priority - const worker = new Worker(new URL("./runtime-priority-worker.js", import.meta.url), { - type: "module", + const worker = new Worker( /* webpackPreload: true */ /* webpackFetchPriority: "high" */ /* webpackChunkName: "runtime-priority-worker" */ - }); + new URL("./runtime-priority-worker.js", import.meta.url), + { + type: "module" + } + ); // Check if preload link was created with fetchpriority const preloadLinks = document.querySelectorAll('link[rel="preload"]'); @@ -92,3 +101,39 @@ it("should create preload link with fetchpriority attribute", async () => { expect(result).toBe("runtime-priority-worker: test"); await worker.terminate(); }); + +it("should support magic comments in options object for runtime", async () => { + // Clear any existing link tags + const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); + existingLinks.forEach(link => link.remove()); + + // Create worker with comments in options + const worker = new Worker( + new URL("./runtime-prefetch-worker.js", import.meta.url), + { + type: "module", + /* webpackPrefetch: true */ + /* webpackChunkName: "runtime-options-worker" */ + } + ); + + // Wait a bit for the link to be created + await new Promise(resolve => setTimeout(resolve, 100)); + + // Check if prefetch link was created + const prefetchLinks = document.querySelectorAll('link[rel="prefetch"]'); + const hasPrefetchLink = Array.from(prefetchLinks).some(link => + link.href.includes("runtime-prefetch-worker") || link.href.includes("runtime-options-worker") + ); + + expect(hasPrefetchLink).toBe(true); + + worker.postMessage("test"); + const result = await new Promise(resolve => { + worker.onmessage = event => { + resolve(event.data); + }; + }); + expect(result).toBe("runtime-prefetch-worker: test"); + await worker.terminate(); +}); diff --git a/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js b/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js index 1b3804740..9b824aa8f 100644 --- a/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js +++ b/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js @@ -1,10 +1,13 @@ it("should allow SharedWorker with webpackPrefetch", async () => { - const worker = new SharedWorker(new URL("./shared-worker.js", import.meta.url), { - type: "module", - name: "shared-worker-instance", + const worker = new SharedWorker( /* webpackPrefetch: true */ /* webpackChunkName: "shared-worker-prefetch" */ - }); + new URL("./shared-worker.js", import.meta.url), + { + type: "module", + name: "shared-worker-instance" + } + ); const messagePromise = new Promise(resolve => { worker.port.onmessage = event => { @@ -18,13 +21,16 @@ it("should allow SharedWorker with webpackPrefetch", async () => { }); it("should allow SharedWorker with webpackPreload and fetchPriority", async () => { - const worker = new SharedWorker(new URL("./shared-worker-preload.js", import.meta.url), { - type: "module", - name: "shared-worker-preload-instance", + const worker = new SharedWorker( /* webpackPreload: true */ /* webpackFetchPriority: "high" */ /* webpackChunkName: "shared-worker-preload" */ - }); + new URL("./shared-worker-preload.js", import.meta.url), + { + type: "module", + name: "shared-worker-preload-instance" + } + ); const messagePromise = new Promise(resolve => { worker.port.onmessage = event => { From 4cab6a07d5eee671f7d56dafc126e90c0e7ff1d7 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 20 Jul 2025 03:06:39 -0700 Subject: [PATCH 03/23] refactor: move prefetch/preload support from Worker to new URL() syntax --- lib/dependencies/WorkerPlugin.js | 81 ----- lib/url/URLParserPlugin.js | 100 ++++++ .../url-prefetch-preload/both-hints-image.png | Bin 0 -> 68 bytes .../parsing/url-prefetch-preload/index.js | 68 ++++ .../invalid-priority-image.png | Bin 0 -> 68 bytes .../url-prefetch-preload/negative-image.png | Bin 0 -> 68 bytes .../url-prefetch-preload/order-image.png | Bin 0 -> 68 bytes .../url-prefetch-preload/prefetch-image.png | Bin 0 -> 68 bytes .../url-prefetch-preload/preload-image.png | Bin 0 -> 68 bytes .../url-prefetch-preload/priority-image.png | Bin 0 -> 68 bytes .../url-prefetch-preload}/test.config.js | 2 +- .../url-prefetch-preload}/test.filter.js | 0 .../url-prefetch-preload}/warnings.js | 8 +- .../url-prefetch-preload/webpack.config.js | 16 + .../chunk-name-worker.js | 4 - .../classic-prefetch-worker.js | 4 - .../classic-preload-worker.js | 3 - .../classic-priority-worker.js | 3 - .../fetch-priority-worker.js | 4 - .../worker/worker-prefetch-preload/index.js | 293 ------------------ .../invalid-prefetch-worker.js | 3 - .../invalid-priority-worker.js | 3 - .../low-priority-worker.js | 4 - .../multi-hint-worker.js | 3 - .../worker-prefetch-preload/normal-worker.js | 4 - .../prefetch-order-worker.js | 4 - .../prefetch-worker.js | 4 - .../preload-order-worker.js | 4 - .../worker-prefetch-preload/preload-worker.js | 4 - .../runtime-prefetch-worker.js | 7 - .../runtime-preload-worker.js | 8 - .../runtime-priority-worker.js | 7 - .../worker-prefetch-preload/runtime-test.js | 139 --------- .../worker-prefetch-preload/shared-module.js | 4 - .../shared-worker-preload.js | 9 - .../shared-worker-test.js | 44 --- .../worker-prefetch-preload/shared-worker.js | 9 - .../worker-prefetch-preload/webpack.config.js | 7 - 38 files changed, 189 insertions(+), 664 deletions(-) create mode 100644 test/configCases/parsing/url-prefetch-preload/both-hints-image.png create mode 100644 test/configCases/parsing/url-prefetch-preload/index.js create mode 100644 test/configCases/parsing/url-prefetch-preload/invalid-priority-image.png create mode 100644 test/configCases/parsing/url-prefetch-preload/negative-image.png create mode 100644 test/configCases/parsing/url-prefetch-preload/order-image.png create mode 100644 test/configCases/parsing/url-prefetch-preload/prefetch-image.png create mode 100644 test/configCases/parsing/url-prefetch-preload/preload-image.png create mode 100644 test/configCases/parsing/url-prefetch-preload/priority-image.png rename test/configCases/{worker/worker-prefetch-preload => parsing/url-prefetch-preload}/test.config.js (64%) rename test/configCases/{worker/worker-prefetch-preload => parsing/url-prefetch-preload}/test.filter.js (100%) rename test/configCases/{worker/worker-prefetch-preload => parsing/url-prefetch-preload}/warnings.js (61%) create mode 100644 test/configCases/parsing/url-prefetch-preload/webpack.config.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/chunk-name-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/classic-prefetch-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/classic-preload-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/classic-priority-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/fetch-priority-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/index.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/low-priority-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/normal-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/prefetch-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/preload-order-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/preload-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/runtime-test.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/shared-module.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/shared-worker-test.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/shared-worker.js delete mode 100644 test/configCases/worker/worker-prefetch-preload/webpack.config.js diff --git a/lib/dependencies/WorkerPlugin.js b/lib/dependencies/WorkerPlugin.js index 6e666aae8..26d7e225c 100644 --- a/lib/dependencies/WorkerPlugin.js +++ b/lib/dependencies/WorkerPlugin.js @@ -371,87 +371,6 @@ class WorkerPlugin { entryOptions.name = importOptions.webpackChunkName; } } - if (importOptions.webpackPrefetch !== undefined) { - if (importOptions.webpackPrefetch === true) { - groupOptions.prefetchOrder = 0; - } else if (typeof importOptions.webpackPrefetch === "number") { - // Validate prefetch order is within reasonable bounds - if (importOptions.webpackPrefetch < 0) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPrefetch\` order must be non-negative, but received: ${importOptions.webpackPrefetch}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } else { - groupOptions.prefetchOrder = importOptions.webpackPrefetch; - } - } else { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPrefetch\` expected true or a number, but received: ${importOptions.webpackPrefetch}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - } - if (importOptions.webpackPreload !== undefined) { - if (importOptions.webpackPreload === true) { - groupOptions.preloadOrder = 0; - } else if (typeof importOptions.webpackPreload === "number") { - // Validate preload order is within reasonable bounds - if (importOptions.webpackPreload < 0) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPreload\` order must be non-negative, but received: ${importOptions.webpackPreload}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } else { - groupOptions.preloadOrder = importOptions.webpackPreload; - } - } else { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - } - if (importOptions.webpackFetchPriority !== undefined) { - if ( - typeof importOptions.webpackFetchPriority === "string" && - ["high", "low", "auto"].includes( - importOptions.webpackFetchPriority - ) - ) { - groupOptions.fetchPriority = - /** @type {"low" | "high" | "auto"} */ - (importOptions.webpackFetchPriority); - } else { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - } - // Warn if both prefetch and preload are specified - if ( - importOptions.webpackPrefetch !== undefined && - importOptions.webpackPreload !== undefined - ) { - parser.state.module.addWarning( - new CommentCompilationWarning( - "Both webpackPrefetch and webpackPreload are specified. " + - "webpackPreload will take precedence for immediate loading, " + - "while webpackPrefetch is typically used for future navigation.", - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } } if ( diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 7a864134e..6907310ef 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -184,6 +184,106 @@ class URLParserPlugin { relative ); dep.loc = /** @type {DependencyLocation} */ (expr.loc); + + // 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) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` order must be non-negative, 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) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` order must be non-negative, 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 ( + importOptions.webpackFetchPriority !== undefined && + (typeof importOptions.webpackFetchPriority !== "string" || + !["high", "low", "auto"].includes( + importOptions.webpackFetchPriority + )) + ) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + + // Validate webpackPreloadAs + if ( + importOptions.webpackPreloadAs !== undefined && + typeof importOptions.webpackPreloadAs !== "string" + ) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadAs\` expected a string, but received: ${importOptions.webpackPreloadAs}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + + // Warn if both prefetch and preload are specified + if ( + importOptions.webpackPrefetch !== undefined && + importOptions.webpackPreload !== undefined + ) { + parser.state.module.addWarning( + new CommentCompilationWarning( + "Both webpackPrefetch and webpackPreload are specified. " + + "webpackPreload will take precedence for immediate loading.", + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + + // Store hints on the dependency for later use + dep.prefetch = importOptions.webpackPrefetch; + dep.preload = importOptions.webpackPreload; + dep.fetchPriority = importOptions.webpackFetchPriority; + dep.preloadAs = importOptions.webpackPreloadAs; + } + + // Add dependency directly parser.state.current.addDependency(dep); InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e)); return true; diff --git a/test/configCases/parsing/url-prefetch-preload/both-hints-image.png b/test/configCases/parsing/url-prefetch-preload/both-hints-image.png new file mode 100644 index 0000000000000000000000000000000000000000..5c169832ed55b84b586ff7fb0103704d1b28528e GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar-fhfA9k(7#Ub4?j8n; PGI+ZBxvX { + const url = new URL( + /* webpackPrefetch: true */ + "./prefetch-image.png", + import.meta.url + ); + expect(url.href).toMatch(/prefetch-image\.png$/); +}); + +it("should preload an image asset", () => { + const url = new URL( + /* webpackPreload: true */ + "./preload-image.png", + import.meta.url + ); + expect(url.href).toMatch(/preload-image\.png$/); +}); + +it("should prefetch with order", () => { + const url = new URL( + /* webpackPrefetch: 10 */ + "./order-image.png", + import.meta.url + ); + expect(url.href).toMatch(/order-image\.png$/); +}); + +it("should preload with fetch priority", () => { + const url = new URL( + /* webpackPreload: true */ + /* webpackFetchPriority: "high" */ + "./priority-image.png", + import.meta.url + ); + expect(url.href).toMatch(/priority-image\.png$/); +}); + +// Warning test cases +it("should handle negative prefetch values", () => { + const url1 = new URL( + /* webpackPrefetch: -1 */ + "./negative-image.png", + import.meta.url + ); + expect(url1.href).toMatch(/negative-image\.png$/); +}); + +it("should handle invalid fetch priority", () => { + const url2 = new URL( + /* webpackPreload: true */ + /* webpackFetchPriority: "invalid" */ + "./invalid-priority-image.png", + import.meta.url + ); + expect(url2.href).toMatch(/invalid-priority-image\.png$/); +}); + +it("should handle both prefetch and preload", () => { + const url3 = new URL( + /* webpackPrefetch: true */ + /* webpackPreload: true */ + "./both-hints-image.png", + import.meta.url + ); + expect(url3.href).toMatch(/both-hints-image\.png$/); +}); \ No newline at end of file diff --git a/test/configCases/parsing/url-prefetch-preload/invalid-priority-image.png b/test/configCases/parsing/url-prefetch-preload/invalid-priority-image.png new file mode 100644 index 0000000000000000000000000000000000000000..5c169832ed55b84b586ff7fb0103704d1b28528e GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar-fhfA9k(7#Ub4?j8n; PGI+ZBxvX { - const worker = new Worker( - /* webpackPrefetch: true */ - new URL("./prefetch-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("prefetch-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a Worker with webpackPrefetch order", async () => { - const worker = new Worker( - /* webpackPrefetch: 2 */ - new URL("./prefetch-order-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("prefetch-order-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a Worker with webpackPreload", async () => { - const worker = new Worker( - /* webpackPreload: true */ - new URL("./preload-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("preload-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a Worker with webpackFetchPriority", async () => { - const worker = new Worker( - /* webpackPreload: true */ - /* webpackFetchPriority: "high" */ - new URL("./fetch-priority-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("fetch-priority-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a Worker with webpackPreload order", async () => { - const worker = new Worker( - /* webpackPreload: 5 */ - new URL("./preload-order-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("preload-order-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a normal Worker without hints", async () => { - const worker = new Worker(new URL("./normal-worker.js", import.meta.url), { - type: "module" - }); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("normal-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a Worker with chunk name", async () => { - const worker = new Worker( - /* webpackChunkName: "custom-worker-chunk" */ - /* webpackPrefetch: true */ - new URL("./chunk-name-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("chunk-name-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a Worker with low fetchPriority", async () => { - const worker = new Worker( - /* webpackPreload: true */ - /* webpackFetchPriority: "low" */ - new URL("./low-priority-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("low-priority-worker: test"); - await worker.terminate(); -}); - -// Classic (non-module) Worker tests -it("should allow to create a classic Worker with webpackPrefetch", async () => { - const worker = new Worker( - /* webpackPrefetch: true */ - new URL("./classic-prefetch-worker.js", import.meta.url) - // type: "classic" is the default, so we can omit options - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("classic-prefetch-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a classic Worker with webpackPreload", async () => { - const worker = new Worker( - /* webpackPreload: true */ - new URL("./classic-preload-worker.js", import.meta.url), - { - type: "classic" // explicitly set to classic - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("classic-preload-worker: test"); - await worker.terminate(); -}); - -it("should allow to create a classic Worker with fetchPriority", async () => { - const worker = new Worker( - /* webpackPreload: true */ - /* webpackFetchPriority: "high" */ - new URL("./classic-priority-worker.js", import.meta.url) - // no type specified = classic worker - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("classic-priority-worker: test"); - await worker.terminate(); -}); - -// Warning tests - these should generate warnings during compilation -it("should handle negative prefetch values", async () => { - const worker = new Worker( - /* webpackPrefetch: -1 */ - new URL("./invalid-prefetch-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - // Worker should still work, but webpack should have generated a warning - expect(result).toBe("invalid-prefetch-worker: test"); - await worker.terminate(); -}); - -it("should handle invalid fetchPriority values", async () => { - const worker = new Worker( - /* webpackPreload: true */ - /* webpackFetchPriority: "invalid" */ - new URL("./invalid-priority-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - // Worker should still work, but webpack should have generated a warning - expect(result).toBe("invalid-priority-worker: test"); - await worker.terminate(); -}); - -it("should warn when both prefetch and preload are specified", async () => { - const worker = new Worker( - /* webpackPrefetch: true */ - /* webpackPreload: true */ - new URL("./multi-hint-worker.js", import.meta.url), - { - type: "module" - } - ); - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - // Worker should still work with preload taking precedence - expect(result).toBe("multi-hint-worker: test"); - await worker.terminate(); -}); - -// Test both comment placement patterns -it("should support magic comments in options object", async () => { - const worker = new Worker( - new URL("./prefetch-worker.js", import.meta.url), - { - type: "module", - /* webpackPrefetch: true */ - } - ); - worker.postMessage("test-options"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("prefetch-worker: test-options"); - await worker.terminate(); -}); - -it("should support multiple magic comments in different positions", async () => { - const worker = new Worker( - /* webpackChunkName: "mixed-position-worker" */ - new URL("./fetch-priority-worker.js", import.meta.url), - { - type: "module", - /* webpackPreload: true */ - /* webpackFetchPriority: "high" */ - } - ); - worker.postMessage("test-mixed"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("fetch-priority-worker: test-mixed"); - await worker.terminate(); -}); diff --git a/test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js b/test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js deleted file mode 100644 index f1fe6882b..000000000 --- a/test/configCases/worker/worker-prefetch-preload/invalid-prefetch-worker.js +++ /dev/null @@ -1,3 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("invalid-prefetch-worker: " + e.data); -}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js deleted file mode 100644 index 226b87575..000000000 --- a/test/configCases/worker/worker-prefetch-preload/invalid-priority-worker.js +++ /dev/null @@ -1,3 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("invalid-priority-worker: " + e.data); -}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/low-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/low-priority-worker.js deleted file mode 100644 index e8a6aeac7..000000000 --- a/test/configCases/worker/worker-prefetch-preload/low-priority-worker.js +++ /dev/null @@ -1,4 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("low-priority-worker: " + e.data); -}; - diff --git a/test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js b/test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js deleted file mode 100644 index 7032a6349..000000000 --- a/test/configCases/worker/worker-prefetch-preload/multi-hint-worker.js +++ /dev/null @@ -1,3 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("multi-hint-worker: " + e.data); -}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/normal-worker.js b/test/configCases/worker/worker-prefetch-preload/normal-worker.js deleted file mode 100644 index 503ba5a2c..000000000 --- a/test/configCases/worker/worker-prefetch-preload/normal-worker.js +++ /dev/null @@ -1,4 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("normal-worker: " + e.data); -}; - diff --git a/test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js b/test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js deleted file mode 100644 index 27ab9a722..000000000 --- a/test/configCases/worker/worker-prefetch-preload/prefetch-order-worker.js +++ /dev/null @@ -1,4 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("prefetch-order-worker: " + e.data); -}; - diff --git a/test/configCases/worker/worker-prefetch-preload/prefetch-worker.js b/test/configCases/worker/worker-prefetch-preload/prefetch-worker.js deleted file mode 100644 index 633fcdf04..000000000 --- a/test/configCases/worker/worker-prefetch-preload/prefetch-worker.js +++ /dev/null @@ -1,4 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("prefetch-worker: " + e.data); -}; - diff --git a/test/configCases/worker/worker-prefetch-preload/preload-order-worker.js b/test/configCases/worker/worker-prefetch-preload/preload-order-worker.js deleted file mode 100644 index 46627adde..000000000 --- a/test/configCases/worker/worker-prefetch-preload/preload-order-worker.js +++ /dev/null @@ -1,4 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("preload-order-worker: " + e.data); -}; - diff --git a/test/configCases/worker/worker-prefetch-preload/preload-worker.js b/test/configCases/worker/worker-prefetch-preload/preload-worker.js deleted file mode 100644 index 19ed4ce7e..000000000 --- a/test/configCases/worker/worker-prefetch-preload/preload-worker.js +++ /dev/null @@ -1,4 +0,0 @@ -self.onmessage = function(e) { - self.postMessage("preload-worker: " + e.data); -}; - diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js b/test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js deleted file mode 100644 index 24c674a32..000000000 --- a/test/configCases/worker/worker-prefetch-preload/runtime-prefetch-worker.js +++ /dev/null @@ -1,7 +0,0 @@ -// Add some imports to ensure this creates a separate chunk -import { processData } from "./shared-module.js"; - -self.onmessage = function(e) { - const processed = processData(e.data); - self.postMessage("runtime-prefetch-worker: " + processed); -}; \ No newline at end of file diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js b/test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js deleted file mode 100644 index 55bcb8462..000000000 --- a/test/configCases/worker/worker-prefetch-preload/runtime-preload-worker.js +++ /dev/null @@ -1,8 +0,0 @@ -// Add some imports to ensure this creates a separate chunk -import { processData } from "./shared-module.js"; - -self.onmessage = function(e) { - const processed = processData(e.data); - self.postMessage("runtime-preload-worker: " + processed); -}; - diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js b/test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js deleted file mode 100644 index 80f702549..000000000 --- a/test/configCases/worker/worker-prefetch-preload/runtime-priority-worker.js +++ /dev/null @@ -1,7 +0,0 @@ -// Add some imports to ensure this creates a separate chunk -import { processData } from "./shared-module.js"; - -self.onmessage = function(e) { - const processed = processData(e.data); - self.postMessage("runtime-priority-worker: " + processed); -}; diff --git a/test/configCases/worker/worker-prefetch-preload/runtime-test.js b/test/configCases/worker/worker-prefetch-preload/runtime-test.js deleted file mode 100644 index 0d1230215..000000000 --- a/test/configCases/worker/worker-prefetch-preload/runtime-test.js +++ /dev/null @@ -1,139 +0,0 @@ -it("should create prefetch link tags for workers", async () => { - // Clear any existing link tags - const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); - existingLinks.forEach(link => link.remove()); - - // Create worker with prefetch - const worker = new Worker( - /* webpackPrefetch: true */ - /* webpackChunkName: "runtime-prefetch-worker" */ - new URL("./runtime-prefetch-worker.js", import.meta.url), - { - type: "module" - } - ); - - // Wait a bit for the link to be created - await new Promise(resolve => setTimeout(resolve, 100)); - - // Check if prefetch link was created - const prefetchLinks = document.querySelectorAll('link[rel="prefetch"]'); - const hasPrefetchLink = Array.from(prefetchLinks).some(link => - link.href.includes("runtime-prefetch-worker") - ); - - expect(hasPrefetchLink).toBe(true); - - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("runtime-prefetch-worker: test"); - await worker.terminate(); -}); - -it("should create preload link tags for workers", async () => { - // Clear any existing link tags - const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); - existingLinks.forEach(link => link.remove()); - - // Create worker with preload - const worker = new Worker( - /* webpackPreload: true */ - /* webpackChunkName: "runtime-preload-worker" */ - new URL("./runtime-preload-worker.js", import.meta.url), - { - type: "module" - } - ); - - // Check if preload link was created immediately - const preloadLinks = document.querySelectorAll('link[rel="preload"]'); - const hasPreloadLink = Array.from(preloadLinks).some(link => - link.href.includes("runtime-preload-worker") - ); - - expect(hasPreloadLink).toBe(true); - - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("runtime-preload-worker: test"); - await worker.terminate(); -}); - -it("should create preload link with fetchpriority attribute", async () => { - // Clear any existing link tags - const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); - existingLinks.forEach(link => link.remove()); - - // Create worker with preload and fetch priority - const worker = new Worker( - /* webpackPreload: true */ - /* webpackFetchPriority: "high" */ - /* webpackChunkName: "runtime-priority-worker" */ - new URL("./runtime-priority-worker.js", import.meta.url), - { - type: "module" - } - ); - - // Check if preload link was created with fetchpriority - const preloadLinks = document.querySelectorAll('link[rel="preload"]'); - const priorityLink = Array.from(preloadLinks).find(link => - link.href.includes("runtime-priority-worker") - ); - - expect(priorityLink).toBeTruthy(); - expect(priorityLink.getAttribute("fetchpriority")).toBe("high"); - - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("runtime-priority-worker: test"); - await worker.terminate(); -}); - -it("should support magic comments in options object for runtime", async () => { - // Clear any existing link tags - const existingLinks = document.querySelectorAll('link[rel="prefetch"], link[rel="preload"]'); - existingLinks.forEach(link => link.remove()); - - // Create worker with comments in options - const worker = new Worker( - new URL("./runtime-prefetch-worker.js", import.meta.url), - { - type: "module", - /* webpackPrefetch: true */ - /* webpackChunkName: "runtime-options-worker" */ - } - ); - - // Wait a bit for the link to be created - await new Promise(resolve => setTimeout(resolve, 100)); - - // Check if prefetch link was created - const prefetchLinks = document.querySelectorAll('link[rel="prefetch"]'); - const hasPrefetchLink = Array.from(prefetchLinks).some(link => - link.href.includes("runtime-prefetch-worker") || link.href.includes("runtime-options-worker") - ); - - expect(hasPrefetchLink).toBe(true); - - worker.postMessage("test"); - const result = await new Promise(resolve => { - worker.onmessage = event => { - resolve(event.data); - }; - }); - expect(result).toBe("runtime-prefetch-worker: test"); - await worker.terminate(); -}); diff --git a/test/configCases/worker/worker-prefetch-preload/shared-module.js b/test/configCases/worker/worker-prefetch-preload/shared-module.js deleted file mode 100644 index 91bdcf464..000000000 --- a/test/configCases/worker/worker-prefetch-preload/shared-module.js +++ /dev/null @@ -1,4 +0,0 @@ -export function processData(data) { - // Simulate some processing - return data.toUpperCase ? data.toUpperCase() : data; -} diff --git a/test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js b/test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js deleted file mode 100644 index 19d48e341..000000000 --- a/test/configCases/worker/worker-prefetch-preload/shared-worker-preload.js +++ /dev/null @@ -1,9 +0,0 @@ -self.onconnect = function(e) { - const port = e.ports[0]; - - port.onmessage = function(event) { - port.postMessage("shared-worker-preload: " + event.data); - }; - - port.start(); -}; diff --git a/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js b/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js deleted file mode 100644 index 9b824aa8f..000000000 --- a/test/configCases/worker/worker-prefetch-preload/shared-worker-test.js +++ /dev/null @@ -1,44 +0,0 @@ -it("should allow SharedWorker with webpackPrefetch", async () => { - const worker = new SharedWorker( - /* webpackPrefetch: true */ - /* webpackChunkName: "shared-worker-prefetch" */ - new URL("./shared-worker.js", import.meta.url), - { - type: "module", - name: "shared-worker-instance" - } - ); - - const messagePromise = new Promise(resolve => { - worker.port.onmessage = event => { - resolve(event.data); - }; - }); - - worker.port.postMessage("test"); - const result = await messagePromise; - expect(result).toBe("shared-worker: test"); -}); - -it("should allow SharedWorker with webpackPreload and fetchPriority", async () => { - const worker = new SharedWorker( - /* webpackPreload: true */ - /* webpackFetchPriority: "high" */ - /* webpackChunkName: "shared-worker-preload" */ - new URL("./shared-worker-preload.js", import.meta.url), - { - type: "module", - name: "shared-worker-preload-instance" - } - ); - - const messagePromise = new Promise(resolve => { - worker.port.onmessage = event => { - resolve(event.data); - }; - }); - - worker.port.postMessage("test-preload"); - const result = await messagePromise; - expect(result).toBe("shared-worker-preload: test-preload"); -}); diff --git a/test/configCases/worker/worker-prefetch-preload/shared-worker.js b/test/configCases/worker/worker-prefetch-preload/shared-worker.js deleted file mode 100644 index 94a6629ad..000000000 --- a/test/configCases/worker/worker-prefetch-preload/shared-worker.js +++ /dev/null @@ -1,9 +0,0 @@ -self.onconnect = function(e) { - const port = e.ports[0]; - - port.onmessage = function(event) { - port.postMessage("shared-worker: " + event.data); - }; - - port.start(); -}; diff --git a/test/configCases/worker/worker-prefetch-preload/webpack.config.js b/test/configCases/worker/worker-prefetch-preload/webpack.config.js deleted file mode 100644 index 798707403..000000000 --- a/test/configCases/worker/worker-prefetch-preload/webpack.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import("../../../../types").Configuration} */ -module.exports = { - output: { - filename: "[name].js" - }, - target: "web" -}; From 5d233d538955ff19ef4f5b8e10fdc3b3bd22018e Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 20 Jul 2025 03:44:53 -0700 Subject: [PATCH 04/23] feat: implement URL-based prefetch/preload support with runtime modules --- cspell.json | 1 + lib/RuntimeGlobals.js | 10 ++ lib/WebpackOptionsApply.js | 2 + lib/dependencies/URLDependency.js | 96 ++++++++++++++++++- lib/runtime/AssetPrefetchPreloadPlugin.js | 45 +++++++++ .../AssetPrefetchPreloadRuntimeModule.js | 59 ++++++++++++ 6 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 lib/runtime/AssetPrefetchPreloadPlugin.js create mode 100644 lib/runtime/AssetPrefetchPreloadRuntimeModule.js diff --git a/cspell.json b/cspell.json index 9f5e92d11..9ce10a6cb 100644 --- a/cspell.json +++ b/cspell.json @@ -90,6 +90,7 @@ "externref", "fetchpriority", "filebase", + "flac", "fileoverview", "filepath", "finalizer", diff --git a/lib/RuntimeGlobals.js b/lib/RuntimeGlobals.js index 26178a0e5..8cfe4eefd 100644 --- a/lib/RuntimeGlobals.js +++ b/lib/RuntimeGlobals.js @@ -315,6 +315,7 @@ module.exports.onChunksLoaded = "__webpack_require__.O"; /** * the chunk prefetch function */ +module.exports.prefetchAsset = "__webpack_require__.PA"; module.exports.prefetchChunk = "__webpack_require__.E"; /** @@ -325,6 +326,7 @@ module.exports.prefetchChunkHandlers = "__webpack_require__.F"; /** * the chunk preload function */ +module.exports.preloadAsset = "__webpack_require__.LA"; module.exports.preloadChunk = "__webpack_require__.G"; /** @@ -332,6 +334,14 @@ module.exports.preloadChunk = "__webpack_require__.G"; */ module.exports.preloadChunkHandlers = "__webpack_require__.H"; +/** + * the asset prefetch function + */ + +/** + * the asset preload function + */ + /** * the bundle public path */ diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index 906451d35..1fb178bb3 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -63,6 +63,7 @@ const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); const JsonModulesPlugin = require("./json/JsonModulesPlugin"); const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin"); +const AssetPrefetchPreloadPlugin = require("./runtime/AssetPrefetchPreloadPlugin"); const DataUriPlugin = require("./schemes/DataUriPlugin"); const FileUriPlugin = require("./schemes/FileUriPlugin"); @@ -223,6 +224,7 @@ class WebpackOptionsApply extends OptionsApply { } new ChunkPrefetchPreloadPlugin().apply(compiler); + new AssetPrefetchPreloadPlugin().apply(compiler); if (typeof options.output.chunkFormat === "string") { switch (options.output.chunkFormat) { diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index f105e0cf5..2c3acbbe3 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -132,7 +132,101 @@ URLDependency.Template = class URLDependencyTemplate extends ( runtimeRequirements.add(RuntimeGlobals.require); - if (dep.relative) { + // Check if we need to add prefetch/preload runtime + const needsPrefetch = dep.prefetch !== undefined && dep.prefetch !== false; + const needsPreload = dep.preload !== undefined && dep.preload !== false; + + if (needsPrefetch || needsPreload) { + // Get the module to determine asset type + const module = moduleGraph.getModule(dep); + let asType = ""; + + if (module) { + const request = module.request || ""; + // Determine the 'as' attribute based on file extension + // Reference: MDN rel=preload documentation (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload) + // Valid 'as' values: fetch, font, image, script, style, track + // Note: audio/video are in spec but not supported by major browsers as of 2025 + if (/\.(png|jpe?g|gif|svg|webp|avif|bmp|ico|tiff?)$/i.test(request)) { + asType = "image"; + } else if (/\.(woff2?|ttf|otf|eot)$/i.test(request)) { + asType = "font"; + } else if (/\.(js|mjs|jsx|ts|tsx)$/i.test(request)) { + asType = "script"; + } else if (/\.css$/i.test(request)) { + asType = "style"; + } else if (/\.vtt$/i.test(request)) { + asType = "track"; // WebVTT files for video subtitles/captions + } else if ( + /\.(mp4|webm|ogg|mp3|wav|flac|aac|m4a|avi|mov|wmv|mkv)$/i.test( + request + ) + ) { + // Audio/video files: use 'fetch' as fallback since as='audio'/'video' not supported + // Reference: https://github.com/mdn/browser-compat-data/issues/9577 + asType = "fetch"; + } else if (/\.(json|xml|txt|csv|pdf|doc|docx|wasm)$/i.test(request)) { + asType = "fetch"; // Data files, documents, WebAssembly + } else { + asType = "fetch"; // Generic fetch for unknown types + } + } + + // Generate the module expression (just the module id) + const moduleExpr = runtimeTemplate.moduleRaw({ + chunkGraph, + module: moduleGraph.getModule(dep), + request: dep.request, + runtimeRequirements, + weak: false + }); + + // Build the prefetch/preload code + const hintCode = []; + const fetchPriority = dep.fetchPriority + ? `"${dep.fetchPriority}"` + : "undefined"; + + if (needsPrefetch && !needsPreload) { + // Only prefetch + runtimeRequirements.add(RuntimeGlobals.prefetchAsset); + hintCode.push( + `${RuntimeGlobals.prefetchAsset}(url, "${asType}", ${fetchPriority});` + ); + } else if (needsPreload) { + // Preload (takes precedence over prefetch) + runtimeRequirements.add(RuntimeGlobals.preloadAsset); + hintCode.push( + `${RuntimeGlobals.preloadAsset}(url, "${asType}", ${fetchPriority});` + ); + } + + // Wrap in IIFE to execute hint code and return URL + if (dep.relative) { + runtimeRequirements.add(RuntimeGlobals.relativeUrl); + source.replace( + dep.outerRange[0], + dep.outerRange[1] - 1, + `/* asset import */ (function() { + var url = new ${RuntimeGlobals.relativeUrl}(${moduleExpr}); + ${hintCode.join("\n")} + return url; + })()` + ); + } else { + runtimeRequirements.add(RuntimeGlobals.baseURI); + source.replace( + dep.range[0], + dep.range[1] - 1, + `/* asset import */ (function() { + var url = new URL(${moduleExpr}, ${RuntimeGlobals.baseURI}); + ${hintCode.join("\n")} + return url; + })(), ${RuntimeGlobals.baseURI}` + ); + } + } else if (dep.relative) { + // No prefetch/preload - use original code runtimeRequirements.add(RuntimeGlobals.relativeUrl); source.replace( dep.outerRange[0], diff --git a/lib/runtime/AssetPrefetchPreloadPlugin.js b/lib/runtime/AssetPrefetchPreloadPlugin.js new file mode 100644 index 000000000..c8e80122b --- /dev/null +++ b/lib/runtime/AssetPrefetchPreloadPlugin.js @@ -0,0 +1,45 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const AssetPrefetchPreloadRuntimeModule = require("./AssetPrefetchPreloadRuntimeModule"); + +/** @typedef {import("../Compiler")} Compiler */ + +const PLUGIN_NAME = "AssetPrefetchPreloadPlugin"; + +class AssetPrefetchPreloadPlugin { + /** + * @param {Compiler} compiler the compiler + * @returns {void} + */ + apply(compiler) { + compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { + // Add runtime module for asset prefetch + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.prefetchAsset) + .tap(PLUGIN_NAME, (chunk, set) => { + compilation.addRuntimeModule( + chunk, + new AssetPrefetchPreloadRuntimeModule("prefetch") + ); + }); + + // Add runtime module for asset preload + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.preloadAsset) + .tap(PLUGIN_NAME, (chunk, set) => { + compilation.addRuntimeModule( + chunk, + new AssetPrefetchPreloadRuntimeModule("preload") + ); + }); + }); + } +} + +module.exports = AssetPrefetchPreloadPlugin; diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js new file mode 100644 index 000000000..0955b7387 --- /dev/null +++ b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js @@ -0,0 +1,59 @@ +/* + 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("../Compilation")} Compilation */ + +class AssetPrefetchPreloadRuntimeModule extends RuntimeModule { + /** + * @param {string} type "prefetch" or "preload" + */ + constructor(type) { + super(`asset ${type}`); + this._type = type; + } + + /** + * @returns {string | null} runtime code + */ + generate() { + const { compilation } = this; + const { runtimeTemplate } = compilation; + const fn = + this._type === "prefetch" + ? RuntimeGlobals.prefetchAsset + : RuntimeGlobals.preloadAsset; + + return Template.asString([ + `${fn} = ${runtimeTemplate.basicFunction("url, as, fetchPriority", [ + "var link = document.createElement('link');", + this._type === "prefetch" + ? "link.rel = 'prefetch';" + : "link.rel = 'preload';", + "if(as) link.as = as;", + "link.href = url;", + "if(fetchPriority) link.fetchPriority = fetchPriority;", + // Add nonce if needed + compilation.outputOptions.crossOriginLoading + ? Template.asString([ + "if(__webpack_require__.nc) {", + Template.indent( + "link.setAttribute('nonce', __webpack_require__.nc);" + ), + "}" + ]) + : "", + "document.head.appendChild(link);" + ])};` + ]); + } +} + +module.exports = AssetPrefetchPreloadRuntimeModule; From 42b9e8462f8ed2b8394397fe4296daf63851379f Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 20 Jul 2025 22:58:28 -0700 Subject: [PATCH 05/23] feat: implement webpackPrefetch/webpackPreload/webpackFetchPriority support for new URL() syntax --- lib/dependencies/URLDependency.js | 17 +- .../AssetPrefetchPreloadRuntimeModule.js | 7 +- lib/url/URLParserPlugin.js | 60 ++--- .../assets/fonts/test.woff2 | 0 .../assets/images/both-hints.png | 0 .../assets/images/image-1.png | 0 .../assets/images/image-2.png | 0 .../assets/images/image-3.png | 0 .../assets/images/order-1.png | 0 .../assets/images/order-2.png | 0 .../assets/images/order-3.png | 0 .../assets/images/order-4.png | 0 .../assets/images/order-5.png | 0 .../assets/images/priority-high.png | 0 .../assets/images/priority-invalid.png | 0 .../assets/images/test.png | 0 .../assets/scripts/worker.js | 0 .../assets/styles/priority-low.css | 0 .../assets/styles/test.css | 0 .../generate-warnings.js | 9 + .../index.js | 188 ++++++++++++++++ .../order-test.js | 2 + .../priority-auto.js | 2 + .../test.config.js | 86 +++++++ .../test.js | 2 + .../warnings.js | 10 + .../webpack.config.js | 21 ++ .../parsing/url-prefetch-preload/index.js | 2 +- .../parsing/url-prefetch-preload/warnings.js | 4 +- test/dependencies/URLDependency.unittest.js | 212 ++++++++++++++++++ ...etPrefetchPreloadRuntimeModule.unittest.js | 142 ++++++++++++ 31 files changed, 719 insertions(+), 45 deletions(-) create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/fonts/test.woff2 create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/both-hints.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-1.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-2.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-3.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-1.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-2.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-3.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-4.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-5.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-high.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-invalid.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/test.png create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/scripts/worker.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/priority-low.css create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/test.css create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js create mode 100644 test/dependencies/URLDependency.unittest.js create mode 100644 test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 2c3acbbe3..b59bb74e6 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -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) { diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js index 0955b7387..7488ba417 100644 --- a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js +++ b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js @@ -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([ diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 6907310ef..119b78832 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -188,49 +188,29 @@ 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) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPrefetch\` order must be non-negative, 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) - ) - ); - } + if ( + importOptions.webpackPrefetch !== undefined && + importOptions.webpackPrefetch !== true + ) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` expected true, 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) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPreload\` order must be non-negative, 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) - ) - ); - } + if ( + importOptions.webpackPreload !== undefined && + importOptions.webpackPreload !== true + ) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` expected true, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); } // Validate webpackFetchPriority diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/fonts/test.woff2 b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/fonts/test.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/both-hints.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/both-hints.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-1.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-1.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-2.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-2.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-3.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-3.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-1.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-1.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-2.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-2.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-3.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-3.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-4.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-4.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-5.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-5.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-high.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-high.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-invalid.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-invalid.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/test.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/test.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/scripts/worker.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/scripts/worker.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/priority-low.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/priority-low.css new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/test.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/test.css new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js new file mode 100644 index 000000000..4dd5612b3 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -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 {}; \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js new file mode 100644 index 000000000..d96f12ac6 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -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"); +}); +*/ \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js new file mode 100644 index 000000000..45d3febf8 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js @@ -0,0 +1,2 @@ +// Test file for verifying prefetch order +export const ordered = true; \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js new file mode 100644 index 000000000..46e78c704 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js @@ -0,0 +1,2 @@ +// Test asset file +console.log("priority-auto.js loaded"); \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js new file mode 100644 index 000000000..7161d0d01 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -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 + }; + } +}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js new file mode 100644 index 000000000..6801c5fc5 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js @@ -0,0 +1,2 @@ +// Test JavaScript file +console.log("test.js loaded"); \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js new file mode 100644 index 000000000..e380916a6 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -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\./ + ] +]; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js new file mode 100644 index 000000000..c486c0b10 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js @@ -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" + } + ] + } +}; diff --git a/test/configCases/parsing/url-prefetch-preload/index.js b/test/configCases/parsing/url-prefetch-preload/index.js index 9aa343461..8d69d829b 100644 --- a/test/configCases/parsing/url-prefetch-preload/index.js +++ b/test/configCases/parsing/url-prefetch-preload/index.js @@ -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", diff --git a/test/configCases/parsing/url-prefetch-preload/warnings.js b/test/configCases/parsing/url-prefetch-preload/warnings.js index f0baf6bb0..ccab8a5fc 100644 --- a/test/configCases/parsing/url-prefetch-preload/warnings.js +++ b/test/configCases/parsing/url-prefetch-preload/warnings.js @@ -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\./ diff --git a/test/dependencies/URLDependency.unittest.js b/test/dependencies/URLDependency.unittest.js new file mode 100644 index 000000000..be4a56fe7 --- /dev/null +++ b/test/dependencies/URLDependency.unittest.js @@ -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(); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js b/test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js new file mode 100644 index 000000000..1cc3a7178 --- /dev/null +++ b/test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js @@ -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;"); + }); + }); +}); \ No newline at end of file From ceac24b37f759c6ae9bea36cbea9c77d669ca921 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 20 Jul 2025 23:29:59 -0700 Subject: [PATCH 06/23] fix: resolve linting errors in test files --- lib/RuntimeGlobals.js | 20 +++---- lib/dependencies/URLDependency.js | 4 +- lib/runtime/AssetPrefetchPreloadPlugin.js | 2 +- .../AssetPrefetchPreloadRuntimeModule.js | 1 + .../generate-warnings.js | 4 +- .../index.js | 60 +++++++------------ .../order-test.js | 4 +- .../priority-auto.js | 4 +- .../test.config.js | 4 +- .../test.js | 4 +- .../warnings.js | 2 + .../webpack.config.js | 2 + .../url-prefetch-preload/test.config.js | 2 + .../url-prefetch-preload/test.filter.js | 2 + .../parsing/url-prefetch-preload/warnings.js | 2 + .../url-prefetch-preload/webpack.config.js | 2 + types.d.ts | 2 + 17 files changed, 65 insertions(+), 56 deletions(-) diff --git a/lib/RuntimeGlobals.js b/lib/RuntimeGlobals.js index 8cfe4eefd..9e100d2e7 100644 --- a/lib/RuntimeGlobals.js +++ b/lib/RuntimeGlobals.js @@ -313,9 +313,13 @@ module.exports.nodeModuleDecorator = "__webpack_require__.nmd"; module.exports.onChunksLoaded = "__webpack_require__.O"; /** - * the chunk prefetch function + * the asset prefetch function */ module.exports.prefetchAsset = "__webpack_require__.PA"; + +/** + * the chunk prefetch function + */ module.exports.prefetchChunk = "__webpack_require__.E"; /** @@ -324,9 +328,13 @@ module.exports.prefetchChunk = "__webpack_require__.E"; module.exports.prefetchChunkHandlers = "__webpack_require__.F"; /** - * the chunk preload function + * the asset preload function */ module.exports.preloadAsset = "__webpack_require__.LA"; + +/** + * the chunk preload function + */ module.exports.preloadChunk = "__webpack_require__.G"; /** @@ -334,14 +342,6 @@ module.exports.preloadChunk = "__webpack_require__.G"; */ module.exports.preloadChunkHandlers = "__webpack_require__.H"; -/** - * the asset prefetch function - */ - -/** - * the asset preload function - */ - /** * the bundle public path */ diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index b59bb74e6..1f0354aba 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -150,7 +150,9 @@ URLDependency.Template = class URLDependencyTemplate extends ( let asType = ""; if (module) { - const request = module.request || ""; + const request = /** @type {string} */ ( + /** @type {{ request?: string }} */ (module).request || "" + ); // Determine the 'as' attribute based on file extension // Reference: MDN rel=preload documentation (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload) // Valid 'as' values: fetch, font, image, script, style, track diff --git a/lib/runtime/AssetPrefetchPreloadPlugin.js b/lib/runtime/AssetPrefetchPreloadPlugin.js index c8e80122b..d373f9117 100644 --- a/lib/runtime/AssetPrefetchPreloadPlugin.js +++ b/lib/runtime/AssetPrefetchPreloadPlugin.js @@ -18,7 +18,7 @@ class AssetPrefetchPreloadPlugin { * @returns {void} */ apply(compiler) { - compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { // Add runtime module for asset prefetch compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.prefetchAsset) diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js index 7488ba417..725797f39 100644 --- a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js +++ b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js @@ -25,6 +25,7 @@ class AssetPrefetchPreloadRuntimeModule extends RuntimeModule { */ generate() { const { compilation } = this; + if (!compilation) return null; const { runtimeTemplate } = compilation; const fn = this._type === "prefetch" diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js index 4dd5612b3..57d1806f0 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -1,3 +1,5 @@ +"use strict"; + // This file is used to generate expected warnings during compilation // Invalid fetchPriority value - should generate warning @@ -6,4 +8,4 @@ const invalidPriorityUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPr // 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 {}; \ No newline at end of file +export default {}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index d96f12ac6..71f387fc2 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -1,3 +1,5 @@ +"use strict"; + // Warnings are generated in generate-warnings.js to avoid duplication // Mock for document.head structure @@ -35,7 +37,7 @@ beforeEach(() => { 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"); @@ -49,7 +51,7 @@ it("should generate prefetch link with fetchPriority for new URL() assets", () = 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"); @@ -62,7 +64,7 @@ it("should generate preload link with fetchPriority for new URL() assets", () => 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"); @@ -76,7 +78,7 @@ 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"); @@ -88,21 +90,21 @@ it("should not set fetchPriority for invalid values", () => { 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(); @@ -112,7 +114,7 @@ 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"); @@ -123,24 +125,24 @@ it("should prefer preload over prefetch when both are specified", () => { 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"); @@ -155,10 +157,10 @@ it("should handle prefetch with boolean values only", () => { 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"); @@ -166,23 +168,3 @@ it("should handle prefetch with boolean values only", () => { 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"); -}); -*/ \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js index 45d3febf8..41737ca18 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js @@ -1,2 +1,4 @@ +"use strict"; + // Test file for verifying prefetch order -export const ordered = true; \ No newline at end of file +export const ordered = true; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js index 46e78c704..88088be87 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js @@ -1,2 +1,4 @@ +"use strict"; + // Test asset file -console.log("priority-auto.js loaded"); \ No newline at end of file +console.log("priority-auto.js loaded"); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js index 7161d0d01..dabb2d27b 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -1,5 +1,7 @@ +"use strict"; + // Mock document.head structure for testing -const mockCreateElement = tagName => { +const mockCreateElement = (tagName) => { const element = { _type: tagName, _attributes: {}, diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js index 6801c5fc5..0cca6613b 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js @@ -1,2 +1,4 @@ +"use strict"; + // Test JavaScript file -console.log("test.js loaded"); \ No newline at end of file +console.log("test.js loaded"); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js index e380916a6..723e22c92 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = [ // Invalid fetchPriority value warning [ diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js index c486c0b10..a2d74d218 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js @@ -1,3 +1,5 @@ +"use strict"; + /** @type {import("../../../../types").Configuration} */ module.exports = { mode: "development", diff --git a/test/configCases/parsing/url-prefetch-preload/test.config.js b/test/configCases/parsing/url-prefetch-preload/test.config.js index 78a59a588..a8755bed9 100644 --- a/test/configCases/parsing/url-prefetch-preload/test.config.js +++ b/test/configCases/parsing/url-prefetch-preload/test.config.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = { findBundle() { return ["main.js"]; diff --git a/test/configCases/parsing/url-prefetch-preload/test.filter.js b/test/configCases/parsing/url-prefetch-preload/test.filter.js index d456e8870..fc9b5e2ce 100644 --- a/test/configCases/parsing/url-prefetch-preload/test.filter.js +++ b/test/configCases/parsing/url-prefetch-preload/test.filter.js @@ -1,3 +1,5 @@ +"use strict"; + const supportsWorker = require("../../../helpers/supportsWorker"); module.exports = () => supportsWorker(); diff --git a/test/configCases/parsing/url-prefetch-preload/warnings.js b/test/configCases/parsing/url-prefetch-preload/warnings.js index ccab8a5fc..d385e700b 100644 --- a/test/configCases/parsing/url-prefetch-preload/warnings.js +++ b/test/configCases/parsing/url-prefetch-preload/warnings.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = [ // Numeric prefetch value (not boolean) [/`webpackPrefetch` expected true, but received: 10\./], diff --git a/test/configCases/parsing/url-prefetch-preload/webpack.config.js b/test/configCases/parsing/url-prefetch-preload/webpack.config.js index ff49bd329..0e5fe3076 100644 --- a/test/configCases/parsing/url-prefetch-preload/webpack.config.js +++ b/test/configCases/parsing/url-prefetch-preload/webpack.config.js @@ -1,3 +1,5 @@ +"use strict"; + /** @type {import("../../../../types").Configuration} */ module.exports = { output: { diff --git a/types.d.ts b/types.d.ts index 776167f4f..c8602848d 100644 --- a/types.d.ts +++ b/types.d.ts @@ -17758,8 +17758,10 @@ declare namespace exports { export let moduleLoaded: "module.loaded"; export let nodeModuleDecorator: "__webpack_require__.nmd"; export let onChunksLoaded: "__webpack_require__.O"; + export let prefetchAsset: "__webpack_require__.PA"; export let prefetchChunk: "__webpack_require__.E"; export let prefetchChunkHandlers: "__webpack_require__.F"; + export let preloadAsset: "__webpack_require__.LA"; export let preloadChunk: "__webpack_require__.G"; export let preloadChunkHandlers: "__webpack_require__.H"; export let publicPath: "__webpack_require__.p"; From 5c9de0cd7207201599b241dd2e2addd0443361f5 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sat, 26 Jul 2025 17:01:01 -0700 Subject: [PATCH 07/23] refactor: remove prefetch/preload conflict warning and use RuntimeGlobals.scriptNonce --- lib/runtime/AssetPrefetchPreloadRuntimeModule.js | 4 ++-- lib/url/URLParserPlugin.js | 14 -------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js index 725797f39..03d10a460 100644 --- a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js +++ b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js @@ -49,9 +49,9 @@ class AssetPrefetchPreloadRuntimeModule extends RuntimeModule { // Add nonce if needed compilation.outputOptions.crossOriginLoading ? Template.asString([ - "if(__webpack_require__.nc) {", + `if(${RuntimeGlobals.scriptNonce}) {`, Template.indent( - "link.setAttribute('nonce', __webpack_require__.nc);" + `link.setAttribute('nonce', ${RuntimeGlobals.scriptNonce});` ), "}" ]) diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 119b78832..bba3d4b4c 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -242,20 +242,6 @@ class URLParserPlugin { ); } - // Warn if both prefetch and preload are specified - if ( - importOptions.webpackPrefetch !== undefined && - importOptions.webpackPreload !== undefined - ) { - parser.state.module.addWarning( - new CommentCompilationWarning( - "Both webpackPrefetch and webpackPreload are specified. " + - "webpackPreload will take precedence for immediate loading.", - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - // Store hints on the dependency for later use dep.prefetch = importOptions.webpackPrefetch; dep.preload = importOptions.webpackPreload; From 9c1e12dc025eaea0f4740c3b550f003ccf6b8d04 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sat, 26 Jul 2025 18:49:36 -0700 Subject: [PATCH 08/23] refactor: extract asset type determination logic into separate function --- lib/dependencies/URLDependency.js | 59 +++++++++++++++++-------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 1f0354aba..d3b235688 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -34,6 +34,37 @@ const getIgnoredRawDataUrlModule = memoize( () => new RawDataUrlModule("data:,", "ignored-asset", "(ignored asset)") ); +/** + * 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 + */ +const getAssetType = (request) => { + // Reference: MDN rel=preload documentation (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload) + // Valid 'as' values: fetch, font, image, script, style, track + // Note: audio/video are in spec but not supported by major browsers as of 2025 + 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"; // WebVTT files for video subtitles/captions + } else if ( + /\.(mp4|webm|ogg|mp3|wav|flac|aac|m4a|avi|mov|wmv|mkv)$/i.test(request) + ) { + // Audio/video files: use 'fetch' as fallback since as='audio'/'video' not supported + // Reference: https://github.com/mdn/browser-compat-data/issues/9577 + return "fetch"; + } else if (/\.(json|xml|txt|csv|pdf|doc|docx|wasm)$/i.test(request)) { + return "fetch"; // Data files, documents, WebAssembly + } + return "fetch"; // Generic fetch for unknown types +}; + class URLDependency extends ModuleDependency { /** * @param {string} request request @@ -153,33 +184,7 @@ URLDependency.Template = class URLDependencyTemplate extends ( const request = /** @type {string} */ ( /** @type {{ request?: string }} */ (module).request || "" ); - // Determine the 'as' attribute based on file extension - // Reference: MDN rel=preload documentation (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload) - // Valid 'as' values: fetch, font, image, script, style, track - // Note: audio/video are in spec but not supported by major browsers as of 2025 - if (/\.(png|jpe?g|gif|svg|webp|avif|bmp|ico|tiff?)$/i.test(request)) { - asType = "image"; - } else if (/\.(woff2?|ttf|otf|eot)$/i.test(request)) { - asType = "font"; - } else if (/\.(js|mjs|jsx|ts|tsx)$/i.test(request)) { - asType = "script"; - } else if (/\.css$/i.test(request)) { - asType = "style"; - } else if (/\.vtt$/i.test(request)) { - asType = "track"; // WebVTT files for video subtitles/captions - } else if ( - /\.(mp4|webm|ogg|mp3|wav|flac|aac|m4a|avi|mov|wmv|mkv)$/i.test( - request - ) - ) { - // Audio/video files: use 'fetch' as fallback since as='audio'/'video' not supported - // Reference: https://github.com/mdn/browser-compat-data/issues/9577 - asType = "fetch"; - } else if (/\.(json|xml|txt|csv|pdf|doc|docx|wasm)$/i.test(request)) { - asType = "fetch"; // Data files, documents, WebAssembly - } else { - asType = "fetch"; // Generic fetch for unknown types - } + asType = getAssetType(request); } // Generate the module expression (just the module id) From fb8bd910f0579000c0f0432fd5a8b4b00442d92a Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sat, 26 Jul 2025 20:11:29 -0700 Subject: [PATCH 09/23] test: clean up tests and remove outdated functionality --- .../generate-warnings.js | 2 +- .../index.js | 31 +-- .../warnings.js | 4 - .../parsing/url-prefetch-preload/index.js | 19 -- .../url-prefetch-preload/negative-image.png | Bin 68 -> 0 bytes .../url-prefetch-preload/order-image.png | Bin 68 -> 0 bytes .../parsing/url-prefetch-preload/warnings.js | 8 - test/dependencies/URLDependency.unittest.js | 212 ------------------ 8 files changed, 4 insertions(+), 272 deletions(-) delete mode 100644 test/configCases/parsing/url-prefetch-preload/negative-image.png delete mode 100644 test/configCases/parsing/url-prefetch-preload/order-image.png delete mode 100644 test/dependencies/URLDependency.unittest.js diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js index 57d1806f0..9ac0c63e8 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -5,7 +5,7 @@ // 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 +// Both prefetch and preload specified - no warning anymore (preload takes precedence) const bothHintsUrl = new URL(/* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackFetchPriority: "high" */ "./assets/images/both-hints.png", import.meta.url); export default {}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index 71f387fc2..c5030b837 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -2,36 +2,11 @@ // 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 = []; + if (global.document && global.document.head) { + global.document.head._children = []; + } }); it("should generate prefetch link with fetchPriority for new URL() assets", () => { diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js index 723e22c92..eb4c2050e 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -4,9 +4,5 @@ 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\./ ] ]; diff --git a/test/configCases/parsing/url-prefetch-preload/index.js b/test/configCases/parsing/url-prefetch-preload/index.js index 8d69d829b..db6c3207d 100644 --- a/test/configCases/parsing/url-prefetch-preload/index.js +++ b/test/configCases/parsing/url-prefetch-preload/index.js @@ -18,15 +18,6 @@ it("should preload an image asset", () => { expect(url.href).toMatch(/preload-image\.png$/); }); -it("should handle numeric prefetch values with warning", () => { - const url = new URL( - /* webpackPrefetch: 10 */ - "./order-image.png", - import.meta.url - ); - expect(url.href).toMatch(/order-image\.png$/); -}); - it("should preload with fetch priority", () => { const url = new URL( /* webpackPreload: true */ @@ -37,16 +28,6 @@ it("should preload with fetch priority", () => { expect(url.href).toMatch(/priority-image\.png$/); }); -// Warning test cases -it("should handle negative prefetch values", () => { - const url1 = new URL( - /* webpackPrefetch: -1 */ - "./negative-image.png", - import.meta.url - ); - expect(url1.href).toMatch(/negative-image\.png$/); -}); - it("should handle invalid fetch priority", () => { const url2 = new URL( /* webpackPreload: true */ diff --git a/test/configCases/parsing/url-prefetch-preload/negative-image.png b/test/configCases/parsing/url-prefetch-preload/negative-image.png deleted file mode 100644 index 5c169832ed55b84b586ff7fb0103704d1b28528e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryJf1F&Ar-fhfA9k(7#Ub4?j8n; PGI+ZBxvX { - 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(); - }); - }); - }); -}); \ No newline at end of file From 9532eea79c54155a8ce87707d556136fce81a9a5 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sat, 26 Jul 2025 20:48:35 -0700 Subject: [PATCH 10/23] feat: add webpackPreloadType support for new URL() syntax --- lib/dependencies/URLDependency.js | 14 ++++++++------ .../AssetPrefetchPreloadRuntimeModule.js | 3 ++- lib/url/URLParserPlugin.js | 14 ++++++++++++++ .../assets/styles/invalid-type.css | 3 +++ .../assets/styles/typed.css | 4 ++++ .../generate-warnings.js | 4 ++-- .../index.js | 18 +++++++++++++++--- .../test.config.js | 6 ++++-- .../warnings.js | 4 +++- 9 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/invalid-type.css create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/typed.css diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index d3b235688..43680963b 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -40,9 +40,8 @@ const getIgnoredRawDataUrlModule = memoize( * @returns {string} The 'as' attribute value */ const getAssetType = (request) => { - // Reference: MDN rel=preload documentation (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload) + // Reference: MDN rel=preload documentation (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded) // Valid 'as' values: fetch, font, image, script, style, track - // Note: audio/video are in spec but not supported by major browsers as of 2025 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)) { @@ -59,8 +58,6 @@ const getAssetType = (request) => { // Audio/video files: use 'fetch' as fallback since as='audio'/'video' not supported // Reference: https://github.com/mdn/browser-compat-data/issues/9577 return "fetch"; - } else if (/\.(json|xml|txt|csv|pdf|doc|docx|wasm)$/i.test(request)) { - return "fetch"; // Data files, documents, WebAssembly } return "fetch"; // Generic fetch for unknown types }; @@ -121,6 +118,7 @@ class URLDependency extends ModuleDependency { write(this.preload); write(this.fetchPriority); write(this.preloadAs); + write(this.preloadType); super.serialize(context); } @@ -136,6 +134,7 @@ class URLDependency extends ModuleDependency { this.preload = read(); this.fetchPriority = read(); this.preloadAs = read(); + this.preloadType = read(); super.deserialize(context); } } @@ -206,18 +205,21 @@ URLDependency.Template = class URLDependencyTemplate extends ( const fetchPriority = validFetchPriority ? `"${validFetchPriority}"` : "undefined"; + const preloadType = dep.preloadType + ? `"${dep.preloadType}"` + : "undefined"; if (needsPrefetch && !needsPreload) { // Only prefetch runtimeRequirements.add(RuntimeGlobals.prefetchAsset); hintCode.push( - `${RuntimeGlobals.prefetchAsset}(url, "${asType}", ${fetchPriority});` + `${RuntimeGlobals.prefetchAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` ); } else if (needsPreload) { // Preload (takes precedence over prefetch) runtimeRequirements.add(RuntimeGlobals.preloadAsset); hintCode.push( - `${RuntimeGlobals.preloadAsset}(url, "${asType}", ${fetchPriority});` + `${RuntimeGlobals.preloadAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` ); } diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js index 03d10a460..8fc49ef7a 100644 --- a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js +++ b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js @@ -33,12 +33,13 @@ class AssetPrefetchPreloadRuntimeModule extends RuntimeModule { : RuntimeGlobals.preloadAsset; return Template.asString([ - `${fn} = ${runtimeTemplate.basicFunction("url, as, fetchPriority", [ + `${fn} = ${runtimeTemplate.basicFunction("url, as, fetchPriority, type", [ "var link = document.createElement('link');", this._type === "prefetch" ? "link.rel = 'prefetch';" : "link.rel = 'preload';", "if(as) link.as = as;", + "if(type) link.type = type;", "link.href = url;", "if(fetchPriority) {", Template.indent([ diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index bba3d4b4c..2ab0b5362 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -242,11 +242,25 @@ class URLParserPlugin { ); } + // Validate webpackPreloadType + if ( + importOptions.webpackPreloadType !== undefined && + typeof importOptions.webpackPreloadType !== "string" + ) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadType\` expected a string, but received: ${importOptions.webpackPreloadType}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + // Store hints on the dependency for later use dep.prefetch = importOptions.webpackPrefetch; dep.preload = importOptions.webpackPreload; dep.fetchPriority = importOptions.webpackFetchPriority; dep.preloadAs = importOptions.webpackPreloadAs; + dep.preloadType = importOptions.webpackPreloadType; } // Add dependency directly diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/invalid-type.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/invalid-type.css new file mode 100644 index 000000000..6fab1b6a8 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/invalid-type.css @@ -0,0 +1,3 @@ +body { + background-color: #f0f0f0; +} \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/typed.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/typed.css new file mode 100644 index 000000000..287e8e44a --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/typed.css @@ -0,0 +1,4 @@ +.typed-element { + color: #333; + font-size: 16px; +} \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js index 9ac0c63e8..0053ffe74 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -5,7 +5,7 @@ // 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 - no warning anymore (preload takes precedence) -const bothHintsUrl = new URL(/* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackFetchPriority: "high" */ "./assets/images/both-hints.png", import.meta.url); +// Invalid webpackPreloadType value - should generate warning +const invalidTypeUrl = new URL(/* webpackPreload: true */ /* webpackPreloadType: 123 */ "./assets/styles/invalid-type.css", import.meta.url); export default {}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index c5030b837..c846f8986 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -86,9 +86,8 @@ it("should handle multiple URLs with different priorities", () => { }); 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); + // 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]; @@ -97,6 +96,19 @@ it("should prefer preload over prefetch when both are specified", () => { 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); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js index dabb2d27b..eadb3011d 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -60,10 +60,11 @@ module.exports = { moduleScope(scope) { // Inject runtime globals that would normally be provided by webpack scope.__webpack_require__ = { - PA(url, as, fetchPriority) { + PA(url, as, fetchPriority, type) { const link = global.document.createElement("link"); link.rel = "prefetch"; if (as) link.as = as; + if (type) link.type = type; link.href = url; if (fetchPriority) { link.fetchPriority = fetchPriority; @@ -71,10 +72,11 @@ module.exports = { } global.document.head.appendChild(link); }, - LA(url, as, fetchPriority) { + LA(url, as, fetchPriority, type) { const link = global.document.createElement("link"); link.rel = "preload"; if (as) link.as = as; + if (type) link.type = type; link.href = url; if (fetchPriority) { link.fetchPriority = fetchPriority; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js index eb4c2050e..c41bfc5f8 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -4,5 +4,7 @@ module.exports = [ // Invalid fetchPriority value warning [ /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ - ] + ], + // Invalid webpackPreloadType value warning + [/`webpackPreloadType` expected a string, but received: 123\./] ]; From 43e8a85399619d5c0f4c80638c8fd99b327d76c6 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sat, 26 Jul 2025 23:14:03 -0700 Subject: [PATCH 11/23] fix: move asset prefetch/preload execution from inline to startup time --- lib/WebpackOptionsApply.js | 2 + lib/dependencies/URLDependency.js | 52 +++- lib/prefetch/AssetPrefetchStartupPlugin.js | 210 ++++++++++++++ .../AssetPrefetchStartupRuntimeModule.js | 140 +++++++++ .../index.js | 268 ++++++++---------- .../order-test.js | 4 - .../test.config.js | 2 + .../webpack.config.js | 3 +- 8 files changed, 525 insertions(+), 156 deletions(-) create mode 100644 lib/prefetch/AssetPrefetchStartupPlugin.js create mode 100644 lib/prefetch/AssetPrefetchStartupRuntimeModule.js delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index 1fb178bb3..bb528d32e 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -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) { diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 43680963b..393f547ca 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -76,6 +76,18 @@ class URLDependency extends ModuleDependency { this.relative = relative || false; /** @type {Set | 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); diff --git a/lib/prefetch/AssetPrefetchStartupPlugin.js b/lib/prefetch/AssetPrefetchStartupPlugin.js new file mode 100644 index 000000000..14f12b767 --- /dev/null +++ b/lib/prefetch/AssetPrefetchStartupPlugin.js @@ -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; diff --git a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js new file mode 100644 index 000000000..e290c8b38 --- /dev/null +++ b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js @@ -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; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index c846f8986..9f5aa9777 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -1,157 +1,125 @@ "use strict"; -// Warnings are generated in generate-warnings.js to avoid duplication - -// Clear document.head before each test -beforeEach(() => { - if (global.document && global.document.head) { - global.document.head._children = []; +function verifyLink(link, expectations) { + expect(link._type).toBe("link"); + expect(link.rel).toBe(expectations.rel); + + if (expectations.as) { + expect(link.as).toBe(expectations.as); } -}); + + 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(); + } + } + + if (expectations.type) { + expect(link.type).toBe(expectations.type); + } + + if (expectations.href) { + expect(link.href.toString()).toMatch(expectations.href); + } +} -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", () => { - // 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 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 + ) + }; + + 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$/ + }); + + 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$/ + }); + + 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" + }); + + 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$/ + }); + + 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 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"); - }); -}); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js deleted file mode 100644 index 41737ca18..000000000 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/order-test.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; - -// Test file for verifying prefetch order -export const ordered = true; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js index eadb3011d..c5391ce61 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -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) { diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js index a2d74d218..c43fa438f 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js @@ -9,7 +9,8 @@ module.exports = { }, output: { filename: "[name].js", - assetModuleFilename: "[name][ext]" + assetModuleFilename: "[name][ext]", + publicPath: "/public/" }, target: "web", module: { From 4aaaf13071f2eeb5ad54df797be556b2add048de Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 27 Jul 2025 04:10:59 -0700 Subject: [PATCH 12/23] fix: resolve cache invalidation issues in asset prefetch implementation --- lib/prefetch/AssetPrefetchStartupPlugin.js | 47 +++--- .../AssetPrefetchStartupRuntimeModule.js | 41 +++-- .../assets/images/image-1.png | 0 .../assets/images/image-2.png | 0 .../assets/images/image-3.png | 0 .../assets/images/order-1.png | 0 .../assets/images/order-2.png | 0 .../assets/images/order-3.png | 0 .../assets/images/order-4.png | 0 .../assets/images/order-5.png | 0 .../index.js | 15 ++ ...etPrefetchPreloadRuntimeModule.unittest.js | 142 ------------------ 12 files changed, 63 insertions(+), 182 deletions(-) delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-1.png delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-2.png delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-3.png delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-1.png delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-2.png delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-3.png delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-4.png delete mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-5.png delete mode 100644 test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js diff --git a/lib/prefetch/AssetPrefetchStartupPlugin.js b/lib/prefetch/AssetPrefetchStartupPlugin.js index 14f12b767..befb06ebd 100644 --- a/lib/prefetch/AssetPrefetchStartupPlugin.js +++ b/lib/prefetch/AssetPrefetchStartupPlugin.js @@ -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) ); } } diff --git a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js index e290c8b38..0dae9b867 100644 --- a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js +++ b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js @@ -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; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-1.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-1.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-2.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-2.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-3.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/image-3.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-1.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-1.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-2.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-2.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-3.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-3.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-4.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-4.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-5.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/order-5.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index 9f5aa9777..1158ad239 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -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$/ + }); }); diff --git a/test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js b/test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js deleted file mode 100644 index 1cc3a7178..000000000 --- a/test/runtime/AssetPrefetchPreloadRuntimeModule.unittest.js +++ /dev/null @@ -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;"); - }); - }); -}); \ No newline at end of file From c94b2e9a1b4f9d9c1afc86065e0d69581ca02a12 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 27 Jul 2025 21:51:50 -0700 Subject: [PATCH 13/23] refactor: extract asset type determination logic into util module --- lib/dependencies/URLDependency.js | 29 +---------------- lib/prefetch/AssetPrefetchStartupPlugin.js | 28 ++--------------- lib/util/assetType.js | 36 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 54 deletions(-) create mode 100644 lib/util/assetType.js diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 393f547ca..31b4fb148 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -10,6 +10,7 @@ const RawDataUrlModule = require("../asset/RawDataUrlModule"); const { getDependencyUsedByExportsCondition } = require("../optimize/InnerGraph"); +const getAssetType = require("../util/assetType"); const makeSerializable = require("../util/makeSerializable"); const memoize = require("../util/memoize"); const ModuleDependency = require("./ModuleDependency"); @@ -34,34 +35,6 @@ const getIgnoredRawDataUrlModule = memoize( () => new RawDataUrlModule("data:,", "ignored-asset", "(ignored asset)") ); -/** - * 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 - */ -const getAssetType = (request) => { - // Reference: MDN rel=preload documentation (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded) - // Valid 'as' values: fetch, font, image, script, style, track - 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"; // WebVTT files for video subtitles/captions - } else if ( - /\.(mp4|webm|ogg|mp3|wav|flac|aac|m4a|avi|mov|wmv|mkv)$/i.test(request) - ) { - // Audio/video files: use 'fetch' as fallback since as='audio'/'video' not supported - // Reference: https://github.com/mdn/browser-compat-data/issues/9577 - return "fetch"; - } - return "fetch"; // Generic fetch for unknown types -}; - class URLDependency extends ModuleDependency { /** * @param {string} request request diff --git a/lib/prefetch/AssetPrefetchStartupPlugin.js b/lib/prefetch/AssetPrefetchStartupPlugin.js index befb06ebd..bc4d247ad 100644 --- a/lib/prefetch/AssetPrefetchStartupPlugin.js +++ b/lib/prefetch/AssetPrefetchStartupPlugin.js @@ -6,6 +6,7 @@ "use strict"; const RuntimeGlobals = require("../RuntimeGlobals"); +const getAssetType = require("../util/assetType"); const AssetPrefetchStartupRuntimeModule = require("./AssetPrefetchStartupRuntimeModule"); /** @typedef {import("../Chunk")} Chunk */ @@ -105,8 +106,7 @@ class AssetPrefetchStartupPlugin { assetUrl = request.split(/[\\/]/).pop() || request; } - const assetType = - AssetPrefetchStartupPlugin._getAssetType(request); + const assetType = getAssetType(request); const info = { url: assetUrl, as: assetType, @@ -176,30 +176,6 @@ class AssetPrefetchStartupPlugin { }); }); } - - /** - * 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; diff --git a/lib/util/assetType.js b/lib/util/assetType.js new file mode 100644 index 000000000..13bfc8c6c --- /dev/null +++ b/lib/util/assetType.js @@ -0,0 +1,36 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +/** + * Determines the 'as' attribute value for prefetch/preload based on file extension + * Reference: MDN rel=preload documentation + * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded + * @param {string} request The module request string or filename + * @returns {string} The 'as' attribute value for link element + */ +const 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) + ) { + // Audio/video files: use 'fetch' as fallback since as='audio'/'video' not supported + // Reference: https://github.com/mdn/browser-compat-data/issues/9577 + return "fetch"; + } + return "fetch"; +}; + +module.exports = getAssetType; From a47f4443d194083197452ae92c9b1abe6a528650 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 27 Jul 2025 22:44:29 -0700 Subject: [PATCH 14/23] chore: comments --- lib/dependencies/URLDependency.js | 24 +++++++++---------- lib/prefetch/AssetPrefetchStartupPlugin.js | 17 ++++++------- .../AssetPrefetchStartupRuntimeModule.js | 4 ++-- lib/runtime/AssetPrefetchPreloadPlugin.js | 4 ++-- .../AssetPrefetchPreloadRuntimeModule.js | 2 +- lib/url/URLParserPlugin.js | 16 ++++++------- lib/util/assetType.js | 8 +++---- 7 files changed, 35 insertions(+), 40 deletions(-) diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 31b4fb148..0f3369e25 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -155,13 +155,13 @@ URLDependency.Template = class URLDependencyTemplate extends ( runtimeRequirements.add(RuntimeGlobals.require); - // Check if we need to add prefetch/preload runtime + // Determine if prefetch/preload hints are specified const needsPrefetch = dep.prefetch !== undefined && dep.prefetch !== false; const needsPreload = dep.preload !== undefined && dep.preload !== false; - // Skip inline prefetch/preload if handled by startup module + // Generate inline prefetch/preload code if not handled by startup module if ((needsPrefetch || needsPreload) && !dep._startupPrefetch) { - // Get the module to determine asset type + // Resolve module to determine appropriate asset type const module = moduleGraph.getModule(dep); let asType = ""; @@ -172,7 +172,7 @@ URLDependency.Template = class URLDependencyTemplate extends ( asType = getAssetType(request); } - // Generate the module expression (just the module id) + // Get the module ID for runtime code generation const moduleExpr = runtimeTemplate.moduleRaw({ chunkGraph, module: moduleGraph.getModule(dep), @@ -181,9 +181,9 @@ URLDependency.Template = class URLDependencyTemplate extends ( weak: false }); - // Build the prefetch/preload code + // Construct prefetch/preload function calls const hintCode = []; - // Only pass valid fetchPriority values + // Validate fetchPriority against allowed values const validFetchPriority = dep.fetchPriority && ["high", "low", "auto"].includes(dep.fetchPriority) ? dep.fetchPriority @@ -196,20 +196,20 @@ URLDependency.Template = class URLDependencyTemplate extends ( : "undefined"; if (needsPrefetch && !needsPreload) { - // Only prefetch + // Generate prefetch call runtimeRequirements.add(RuntimeGlobals.prefetchAsset); hintCode.push( `${RuntimeGlobals.prefetchAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` ); } else if (needsPreload) { - // Preload (takes precedence over prefetch) + // Generate preload call (overrides prefetch if both specified) runtimeRequirements.add(RuntimeGlobals.preloadAsset); hintCode.push( `${RuntimeGlobals.preloadAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` ); } - // Wrap in IIFE to execute hint code and return URL + // Create IIFE that generates URL and adds resource hints if (dep.relative) { runtimeRequirements.add(RuntimeGlobals.relativeUrl); source.replace( @@ -234,7 +234,7 @@ URLDependency.Template = class URLDependencyTemplate extends ( ); } } else if ((needsPrefetch || needsPreload) && dep._startupPrefetch) { - // Prefetch/preload handled by startup module - generate standard URL + // Generate standard URL when prefetch/preload is handled by startup module if (dep.relative) { runtimeRequirements.add(RuntimeGlobals.relativeUrl); source.replace( @@ -264,14 +264,14 @@ URLDependency.Template = class URLDependencyTemplate extends ( })}, ${RuntimeGlobals.baseURI}` ); } - // Still need to add runtime requirements for prefetch/preload + // Register runtime requirements for prefetch/preload functions if (needsPrefetch && !needsPreload) { runtimeRequirements.add(RuntimeGlobals.prefetchAsset); } else if (needsPreload) { runtimeRequirements.add(RuntimeGlobals.preloadAsset); } } else if (dep.relative) { - // No prefetch/preload - use original code + // Standard URL generation without resource hints runtimeRequirements.add(RuntimeGlobals.relativeUrl); source.replace( dep.outerRange[0], diff --git a/lib/prefetch/AssetPrefetchStartupPlugin.js b/lib/prefetch/AssetPrefetchStartupPlugin.js index bc4d247ad..1204c6c62 100644 --- a/lib/prefetch/AssetPrefetchStartupPlugin.js +++ b/lib/prefetch/AssetPrefetchStartupPlugin.js @@ -41,12 +41,12 @@ class AssetPrefetchStartupPlugin { const assetPrefetchMap = new WeakMap(); const chunkAssetInfoMap = new WeakMap(); - // Hook into finishModules to collect all URLDependencies + // Collect URLDependencies with prefetch/preload hints during module finalization compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => { for (const module of modules) { if (!module.dependencies) continue; - // Collect URLDependencies with prefetch/preload + // Find all URL dependencies that have prefetch or preload hints const assetDeps = []; for (const dep of module.dependencies) { if (dep.constructor.name === "URLDependency") { @@ -64,7 +64,7 @@ class AssetPrefetchStartupPlugin { } }); - // Process assets when chunks are being optimized + // Aggregate prefetch/preload assets by chunk during optimization compilation.hooks.optimizeChunks.tap( { name: PLUGIN_NAME, stage: 1 }, (chunks) => { @@ -77,13 +77,13 @@ class AssetPrefetchStartupPlugin { preload: /** @type {AssetInfo[]} */ ([]) }; - // Process all modules in this chunk + // Iterate through all modules in the 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 + // Flag this dependency as handled by startup module to prevent inline generation dep._startupPrefetch = true; const resolvedModule = moduleGraph.getModule(dep); @@ -94,7 +94,7 @@ class AssetPrefetchStartupPlugin { ).request; if (!request) continue; - // Get the actual asset filename from module buildInfo + // Extract the asset filename from module metadata let assetUrl; if ( resolvedModule.buildInfo && @@ -102,7 +102,7 @@ class AssetPrefetchStartupPlugin { ) { assetUrl = resolvedModule.buildInfo.filename; } else { - // Fallback to extracting from request + // Fall back to filename from request path assetUrl = request.split(/[\\/]/).pop() || request; } @@ -160,18 +160,15 @@ class AssetPrefetchStartupPlugin { } ); - // 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); }); }); diff --git a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js index 0dae9b867..94aa06777 100644 --- a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js +++ b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js @@ -55,8 +55,8 @@ class AssetPrefetchStartupRuntimeModule extends RuntimeModule { const lines = []; /** - * @param {AssetInfo} asset The asset information to serialize - * @returns {string} Serialized arguments for prefetch/preload function + * @param {AssetInfo} asset asset info object + * @returns {string} serialized function arguments */ const serializeAsset = (asset) => { const args = [ diff --git a/lib/runtime/AssetPrefetchPreloadPlugin.js b/lib/runtime/AssetPrefetchPreloadPlugin.js index d373f9117..4403d96b9 100644 --- a/lib/runtime/AssetPrefetchPreloadPlugin.js +++ b/lib/runtime/AssetPrefetchPreloadPlugin.js @@ -19,7 +19,7 @@ class AssetPrefetchPreloadPlugin { */ apply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - // Add runtime module for asset prefetch + // Register runtime module for asset prefetch compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.prefetchAsset) .tap(PLUGIN_NAME, (chunk, set) => { @@ -29,7 +29,7 @@ class AssetPrefetchPreloadPlugin { ); }); - // Add runtime module for asset preload + // Register runtime module for asset preload compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.preloadAsset) .tap(PLUGIN_NAME, (chunk, set) => { diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js index 8fc49ef7a..6eaed6275 100644 --- a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js +++ b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js @@ -47,7 +47,7 @@ class AssetPrefetchPreloadRuntimeModule extends RuntimeModule { "link.setAttribute('fetchpriority', fetchPriority);" ]), "}", - // Add nonce if needed + // Apply nonce attribute for CSP if configured compilation.outputOptions.crossOriginLoading ? Template.asString([ `if(${RuntimeGlobals.scriptNonce}) {`, diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 2ab0b5362..7588dbaad 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -185,9 +185,9 @@ class URLParserPlugin { ); dep.loc = /** @type {DependencyLocation} */ (expr.loc); - // Handle prefetch/preload hints + // Process magic comments for prefetch/preload hints if (importOptions) { - // Validate webpackPrefetch + // webpackPrefetch should be boolean true if ( importOptions.webpackPrefetch !== undefined && importOptions.webpackPrefetch !== true @@ -200,7 +200,7 @@ class URLParserPlugin { ); } - // Validate webpackPreload + // webpackPreload should be boolean true if ( importOptions.webpackPreload !== undefined && importOptions.webpackPreload !== true @@ -213,7 +213,7 @@ class URLParserPlugin { ); } - // Validate webpackFetchPriority + // webpackFetchPriority should be one of: high, low, auto if ( importOptions.webpackFetchPriority !== undefined && (typeof importOptions.webpackFetchPriority !== "string" || @@ -229,7 +229,7 @@ class URLParserPlugin { ); } - // Validate webpackPreloadAs + // webpackPreloadAs should be a string if ( importOptions.webpackPreloadAs !== undefined && typeof importOptions.webpackPreloadAs !== "string" @@ -242,7 +242,7 @@ class URLParserPlugin { ); } - // Validate webpackPreloadType + // webpackPreloadType should be a string if ( importOptions.webpackPreloadType !== undefined && typeof importOptions.webpackPreloadType !== "string" @@ -255,7 +255,7 @@ class URLParserPlugin { ); } - // Store hints on the dependency for later use + // Store magic comment values on dependency dep.prefetch = importOptions.webpackPrefetch; dep.preload = importOptions.webpackPreload; dep.fetchPriority = importOptions.webpackFetchPriority; @@ -263,7 +263,7 @@ class URLParserPlugin { dep.preloadType = importOptions.webpackPreloadType; } - // Add dependency directly + // Register the dependency parser.state.current.addDependency(dep); InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e)); return true; diff --git a/lib/util/assetType.js b/lib/util/assetType.js index 13bfc8c6c..ff85fa5a4 100644 --- a/lib/util/assetType.js +++ b/lib/util/assetType.js @@ -7,10 +7,9 @@ /** * Determines the 'as' attribute value for prefetch/preload based on file extension - * Reference: MDN rel=preload documentation * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded - * @param {string} request The module request string or filename - * @returns {string} The 'as' attribute value for link element + * @param {string} request module request string or filename + * @returns {string} asset type for link element 'as' attribute */ const getAssetType = (request) => { if (/\.(png|jpe?g|gif|svg|webp|avif|bmp|ico|tiff?)$/i.test(request)) { @@ -26,8 +25,7 @@ const getAssetType = (request) => { } else if ( /\.(mp4|webm|ogg|mp3|wav|flac|aac|m4a|avi|mov|wmv|mkv)$/i.test(request) ) { - // Audio/video files: use 'fetch' as fallback since as='audio'/'video' not supported - // Reference: https://github.com/mdn/browser-compat-data/issues/9577 + // Audio/video files use 'fetch' as browser support varies return "fetch"; } return "fetch"; From e97ae49459f49b8332c086ce3cb2cde62ffc17f8 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sat, 9 Aug 2025 17:33:58 -0700 Subject: [PATCH 15/23] refactor: simplify asset prefetch/preload implementation - Consolidate multiple runtime modules into unified AssetResourcePrefetchPlugin - Remove complex startup prefetch mechanism in favor of simpler inline approach --- lib/WebpackOptionsApply.js | 7 +- .../AssetResourcePrefetchPlugin.js} | 26 ++- .../AssetResourcePrefetchRuntimeModule.js | 82 +++++++ lib/dependencies/URLDependency.js | 206 ++++++------------ lib/dependencies/WorkerPlugin.js | 56 ++++- lib/prefetch/AssetPrefetchStartupPlugin.js | 178 --------------- .../AssetPrefetchStartupRuntimeModule.js | 153 ------------- .../AssetPrefetchPreloadRuntimeModule.js | 66 ------ lib/url/URLParserPlugin.js | 65 ++---- .../generate-warnings.js | 3 - .../index.js | 43 +--- .../test.config.js | 29 +-- .../warnings.js | 4 +- 13 files changed, 248 insertions(+), 670 deletions(-) rename lib/{runtime/AssetPrefetchPreloadPlugin.js => asset/AssetResourcePrefetchPlugin.js} (51%) create mode 100644 lib/asset/AssetResourcePrefetchRuntimeModule.js delete mode 100644 lib/prefetch/AssetPrefetchStartupPlugin.js delete mode 100644 lib/prefetch/AssetPrefetchStartupRuntimeModule.js delete mode 100644 lib/runtime/AssetPrefetchPreloadRuntimeModule.js diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index bb528d32e..72ddf39c0 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -34,6 +34,8 @@ const WebpackIsIncludedPlugin = require("./WebpackIsIncludedPlugin"); const AssetModulesPlugin = require("./asset/AssetModulesPlugin"); +const AssetResourcePrefetchPlugin = require("./asset/AssetResourcePrefetchPlugin"); + const InferAsyncModulesPlugin = require("./async-modules/InferAsyncModulesPlugin"); const ResolverCachePlugin = require("./cache/ResolverCachePlugin"); @@ -62,9 +64,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"); const DataUriPlugin = require("./schemes/DataUriPlugin"); const FileUriPlugin = require("./schemes/FileUriPlugin"); @@ -225,8 +225,7 @@ class WebpackOptionsApply extends OptionsApply { } new ChunkPrefetchPreloadPlugin().apply(compiler); - new AssetPrefetchPreloadPlugin().apply(compiler); - new AssetPrefetchStartupPlugin().apply(compiler); + new AssetResourcePrefetchPlugin().apply(compiler); if (typeof options.output.chunkFormat === "string") { switch (options.output.chunkFormat) { diff --git a/lib/runtime/AssetPrefetchPreloadPlugin.js b/lib/asset/AssetResourcePrefetchPlugin.js similarity index 51% rename from lib/runtime/AssetPrefetchPreloadPlugin.js rename to lib/asset/AssetResourcePrefetchPlugin.js index 4403d96b9..6f44feab5 100644 --- a/lib/runtime/AssetPrefetchPreloadPlugin.js +++ b/lib/asset/AssetResourcePrefetchPlugin.js @@ -6,40 +6,50 @@ "use strict"; const RuntimeGlobals = require("../RuntimeGlobals"); -const AssetPrefetchPreloadRuntimeModule = require("./AssetPrefetchPreloadRuntimeModule"); +const AssetResourcePrefetchRuntimeModule = require("./AssetResourcePrefetchRuntimeModule"); /** @typedef {import("../Compiler")} Compiler */ -const PLUGIN_NAME = "AssetPrefetchPreloadPlugin"; +const PLUGIN_NAME = "AssetResourcePrefetchPlugin"; -class AssetPrefetchPreloadPlugin { +class AssetResourcePrefetchPlugin { /** * @param {Compiler} compiler the compiler * @returns {void} */ apply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - // Register runtime module for asset prefetch + // prefetchAsset compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.prefetchAsset) .tap(PLUGIN_NAME, (chunk, set) => { + set.add(RuntimeGlobals.publicPath); + set.add(RuntimeGlobals.require); + set.add(RuntimeGlobals.baseURI); + set.add(RuntimeGlobals.relativeUrl); compilation.addRuntimeModule( chunk, - new AssetPrefetchPreloadRuntimeModule("prefetch") + new AssetResourcePrefetchRuntimeModule("prefetch") ); + return true; }); - // Register runtime module for asset preload + // preloadAsset compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.preloadAsset) .tap(PLUGIN_NAME, (chunk, set) => { + set.add(RuntimeGlobals.publicPath); + set.add(RuntimeGlobals.require); + set.add(RuntimeGlobals.baseURI); + set.add(RuntimeGlobals.relativeUrl); compilation.addRuntimeModule( chunk, - new AssetPrefetchPreloadRuntimeModule("preload") + new AssetResourcePrefetchRuntimeModule("preload") ); + return true; }); }); } } -module.exports = AssetPrefetchPreloadPlugin; +module.exports = AssetResourcePrefetchPlugin; diff --git a/lib/asset/AssetResourcePrefetchRuntimeModule.js b/lib/asset/AssetResourcePrefetchRuntimeModule.js new file mode 100644 index 000000000..38ea4300f --- /dev/null +++ b/lib/asset/AssetResourcePrefetchRuntimeModule.js @@ -0,0 +1,82 @@ +/* + 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("../Compilation")} Compilation */ + +class AssetResourcePrefetchRuntimeModule extends RuntimeModule { + /** + * @param {string} type "prefetch" or "preload" + */ + constructor(type) { + super(`asset ${type}`, RuntimeModule.STAGE_ATTACH); + this._type = type; + } + + /** + * @returns {string | null} runtime code + */ + generate() { + const { compilation } = this; + if (!compilation) return null; + + const { runtimeTemplate, outputOptions } = compilation; + const fnName = + this._type === "prefetch" + ? RuntimeGlobals.prefetchAsset + : RuntimeGlobals.preloadAsset; + + const crossOriginLoading = outputOptions.crossOriginLoading; + + return Template.asString([ + `${fnName} = ${runtimeTemplate.basicFunction( + "moduleId, as, fetchPriority, relative", + [ + "var url;", + "if (relative) {", + Template.indent([ + `url = new ${RuntimeGlobals.relativeUrl}(${RuntimeGlobals.require}(moduleId));` + ]), + "} else {", + Template.indent([ + `url = new URL(${RuntimeGlobals.require}(moduleId), ${RuntimeGlobals.baseURI});` + ]), + "}", + "", + "var link = document.createElement('link');", + `link.rel = '${this._type}';`, + "if (as) link.as = as;", + "link.href = url.href;", + "", + "if (fetchPriority) {", + Template.indent([ + "link.fetchPriority = fetchPriority;", + "link.setAttribute('fetchpriority', fetchPriority);" + ]), + "}", + "", + crossOriginLoading + ? Template.asString([ + "if (link.href.indexOf(window.location.origin + '/') !== 0) {", + Template.indent([ + `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};` + ]), + "}" + ]) + : "", + "", + "document.head.appendChild(link);" + ] + )};` + ]); + } +} + +module.exports = AssetResourcePrefetchRuntimeModule; diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 0f3369e25..a16218f62 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -5,6 +5,7 @@ "use strict"; +const InitFragment = require("../InitFragment"); const RuntimeGlobals = require("../RuntimeGlobals"); const RawDataUrlModule = require("../asset/RawDataUrlModule"); const { @@ -49,18 +50,9 @@ class URLDependency extends ModuleDependency { this.relative = relative || false; /** @type {Set | 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() { @@ -102,8 +94,6 @@ class URLDependency extends ModuleDependency { write(this.prefetch); write(this.preload); write(this.fetchPriority); - write(this.preloadAs); - write(this.preloadType); super.serialize(context); } @@ -118,8 +108,6 @@ class URLDependency extends ModuleDependency { this.prefetch = read(); this.preload = read(); this.fetchPriority = read(); - this.preloadAs = read(); - this.preloadType = read(); super.deserialize(context); } } @@ -139,9 +127,12 @@ URLDependency.Template = class URLDependencyTemplate extends ( moduleGraph, runtimeRequirements, runtimeTemplate, - runtime + runtime, + initFragments } = templateContext; const dep = /** @type {URLDependency} */ (dependency); + + const module = moduleGraph.getModule(dep); const connection = moduleGraph.getConnection(dep); // Skip rendering depending when dependency is conditional if (connection && !connection.isTargetActive(runtime)) { @@ -153,154 +144,81 @@ URLDependency.Template = class URLDependencyTemplate extends ( return; } - runtimeRequirements.add(RuntimeGlobals.require); - - // Determine if prefetch/preload hints are specified - const needsPrefetch = dep.prefetch !== undefined && dep.prefetch !== false; - const needsPreload = dep.preload !== undefined && dep.preload !== false; - - // Generate inline prefetch/preload code if not handled by startup module - if ((needsPrefetch || needsPreload) && !dep._startupPrefetch) { - // Resolve module to determine appropriate asset type - const module = moduleGraph.getModule(dep); - let asType = ""; - - if (module) { - const request = /** @type {string} */ ( - /** @type {{ request?: string }} */ (module).request || "" - ); - asType = getAssetType(request); - } - - // Get the module ID for runtime code generation - const moduleExpr = runtimeTemplate.moduleRaw({ - chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, - runtimeRequirements, - weak: false - }); - - // Construct prefetch/preload function calls - const hintCode = []; - // Validate fetchPriority against allowed values - const validFetchPriority = - dep.fetchPriority && ["high", "low", "auto"].includes(dep.fetchPriority) - ? dep.fetchPriority - : undefined; - const fetchPriority = validFetchPriority - ? `"${validFetchPriority}"` - : "undefined"; - const preloadType = dep.preloadType - ? `"${dep.preloadType}"` - : "undefined"; - - if (needsPrefetch && !needsPreload) { - // Generate prefetch call - runtimeRequirements.add(RuntimeGlobals.prefetchAsset); - hintCode.push( - `${RuntimeGlobals.prefetchAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` - ); - } else if (needsPreload) { - // Generate preload call (overrides prefetch if both specified) - runtimeRequirements.add(RuntimeGlobals.preloadAsset); - hintCode.push( - `${RuntimeGlobals.preloadAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` - ); - } - - // Create IIFE that generates URL and adds resource hints - if (dep.relative) { - runtimeRequirements.add(RuntimeGlobals.relativeUrl); - source.replace( - dep.outerRange[0], - dep.outerRange[1] - 1, - `/* asset import */ (function() { - var url = new ${RuntimeGlobals.relativeUrl}(${moduleExpr}); - ${hintCode.join("\n")} - return url; - })()` - ); - } else { - runtimeRequirements.add(RuntimeGlobals.baseURI); - source.replace( - dep.range[0], - dep.range[1] - 1, - `/* asset import */ (function() { - var url = new URL(${moduleExpr}, ${RuntimeGlobals.baseURI}); - ${hintCode.join("\n")} - return url; - })(), ${RuntimeGlobals.baseURI}` - ); - } - } else if ((needsPrefetch || needsPreload) && dep._startupPrefetch) { - // Generate standard URL when prefetch/preload is handled by startup module - 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}` - ); - } - // Register runtime requirements for prefetch/preload functions - if (needsPrefetch && !needsPreload) { - runtimeRequirements.add(RuntimeGlobals.prefetchAsset); - } else if (needsPreload) { - runtimeRequirements.add(RuntimeGlobals.preloadAsset); - } - } else if (dep.relative) { - // Standard URL generation without resource hints + // Standard URL generation + 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 - })})` + `/* asset import */ new ${RuntimeGlobals.relativeUrl}(${runtimeTemplate.moduleRaw( + { + chunkGraph, + module, + 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), + module, request: dep.request, runtimeRequirements, weak: false })}, ${RuntimeGlobals.baseURI}` ); } + + // Prefetch/Preload via InitFragment + if ((dep.prefetch || dep.preload) && module) { + const request = dep.request; + const assetType = getAssetType(request); + const id = chunkGraph.getModuleId(module); + if (id !== null) { + const moduleId = runtimeTemplate.moduleId({ + module, + chunkGraph, + request: dep.request, + weak: false + }); + + if (dep.preload) { + runtimeRequirements.add(RuntimeGlobals.preloadAsset); + initFragments.push( + new InitFragment( + `${RuntimeGlobals.preloadAsset}(${moduleId}, ${JSON.stringify( + assetType + )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ + dep.relative + });\n`, + InitFragment.STAGE_CONSTANTS, + -10, + `asset_preload_${moduleId}` + ) + ); + } else if (dep.prefetch) { + runtimeRequirements.add(RuntimeGlobals.prefetchAsset); + initFragments.push( + new InitFragment( + `${RuntimeGlobals.prefetchAsset}(${moduleId}, ${JSON.stringify( + assetType + )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ + dep.relative + });\n`, + InitFragment.STAGE_CONSTANTS, + -5, + `asset_prefetch_${moduleId}` + ) + ); + } + } + } } }; diff --git a/lib/dependencies/WorkerPlugin.js b/lib/dependencies/WorkerPlugin.js index 26d7e225c..64bb80eb9 100644 --- a/lib/dependencies/WorkerPlugin.js +++ b/lib/dependencies/WorkerPlugin.js @@ -371,6 +371,60 @@ class WorkerPlugin { entryOptions.name = importOptions.webpackChunkName; } } + + // Support webpackPrefetch (true | number) + if (importOptions.webpackPrefetch !== undefined) { + if (importOptions.webpackPrefetch === true) { + groupOptions.prefetchOrder = 0; + } else if (typeof importOptions.webpackPrefetch === "number") { + groupOptions.prefetchOrder = importOptions.webpackPrefetch; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` expected true or a number, but received: ${importOptions.webpackPrefetch}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // Support webpackPreload (true | number) + if (importOptions.webpackPreload !== undefined) { + if (importOptions.webpackPreload === true) { + groupOptions.preloadOrder = 0; + } else if (typeof importOptions.webpackPreload === "number") { + groupOptions.preloadOrder = importOptions.webpackPreload; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // Support webpackFetchPriority ("high" | "low" | "auto") + if (importOptions.webpackFetchPriority !== undefined) { + if ( + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes( + importOptions.webpackFetchPriority + ) + ) { + groupOptions.fetchPriority = + /** @type {"auto" | "high" | "low"} */ ( + importOptions.webpackFetchPriority + ); + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } } if ( @@ -407,7 +461,7 @@ class WorkerPlugin { entryOptions: { chunkLoading: this._chunkLoading, wasmLoading: this._wasmLoading, - ...entryOptions + runtime: entryOptions.runtime } }); block.loc = expr.loc; diff --git a/lib/prefetch/AssetPrefetchStartupPlugin.js b/lib/prefetch/AssetPrefetchStartupPlugin.js deleted file mode 100644 index 1204c6c62..000000000 --- a/lib/prefetch/AssetPrefetchStartupPlugin.js +++ /dev/null @@ -1,178 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ - -"use strict"; - -const RuntimeGlobals = require("../RuntimeGlobals"); -const getAssetType = require("../util/assetType"); -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 - */ - -const PLUGIN_NAME = "AssetPrefetchStartupPlugin"; - -class AssetPrefetchStartupPlugin { - /** - * @param {Compiler} compiler the compiler - * @returns {void} - */ - apply(compiler) { - compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - const assetPrefetchMap = new WeakMap(); - const chunkAssetInfoMap = new WeakMap(); - - // Collect URLDependencies with prefetch/preload hints during module finalization - compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => { - for (const module of modules) { - if (!module.dependencies) continue; - - // Find all URL dependencies that have prefetch or preload hints - 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); - } - } - }); - - // Aggregate prefetch/preload assets by chunk during optimization - 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[]} */ ([]) - }; - - // Iterate through all modules in the chunk - for (const module of chunkGraph.getChunkModules(chunk)) { - const urlDeps = assetPrefetchMap.get(module); - if (!urlDeps) continue; - - for (const dep of urlDeps) { - // Flag this dependency as handled by startup module to prevent inline generation - dep._startupPrefetch = true; - - const resolvedModule = moduleGraph.getModule(dep); - if (!resolvedModule) continue; - - const request = /** @type {{ request?: string }} */ ( - resolvedModule - ).request; - if (!request) continue; - - // Extract the asset filename from module metadata - let assetUrl; - if ( - resolvedModule.buildInfo && - resolvedModule.buildInfo.filename - ) { - assetUrl = resolvedModule.buildInfo.filename; - } else { - // Fall back to filename from request path - assetUrl = request.split(/[\\/]/).pop() || request; - } - - const assetType = 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); - } - } - } - - if (assetInfo.prefetch.length > 0 || assetInfo.preload.length > 0) { - const existing = chunkAssetInfoMap.get(chunk); - if (!existing) { - chunkAssetInfoMap.set(chunk, assetInfo); - } else { - existing.prefetch.push(...assetInfo.prefetch); - existing.preload.push(...assetInfo.preload); - } - } - } - } - ); - - compilation.hooks.additionalChunkRuntimeRequirements.tap( - PLUGIN_NAME, - (chunk, set) => { - const assetInfo = chunkAssetInfoMap.get(chunk); - if (!assetInfo) return; - - const { prefetch, preload } = assetInfo; - - if (prefetch.length > 0) { - set.add(RuntimeGlobals.prefetchAsset); - } - - if (preload.length > 0) { - set.add(RuntimeGlobals.preloadAsset); - } - - if (prefetch.length > 0 || preload.length > 0) { - compilation.addRuntimeModule( - chunk, - new AssetPrefetchStartupRuntimeModule(assetInfo) - ); - } - } - ); - - compilation.hooks.runtimeRequirementInTree - .for(RuntimeGlobals.prefetchAsset) - .tap(PLUGIN_NAME, (chunk, set) => { - set.add(RuntimeGlobals.publicPath); - }); - - compilation.hooks.runtimeRequirementInTree - .for(RuntimeGlobals.preloadAsset) - .tap(PLUGIN_NAME, (chunk, set) => { - set.add(RuntimeGlobals.publicPath); - }); - }); - } -} - -module.exports = AssetPrefetchStartupPlugin; diff --git a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js deleted file mode 100644 index 94aa06777..000000000 --- a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - 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} a unique identifier of the module - */ - identifier() { - return `webpack/runtime/asset-prefetch-startup|${JSON.stringify( - this.assetInfo - )}`; - } - - /** - * @returns {string | null} runtime code - */ - generate() { - const { assetInfo } = this; - const compilation = /** @type {Compilation} */ (this.compilation); - const { runtimeTemplate } = compilation; - - const lines = []; - - /** - * @param {AssetInfo} asset asset info object - * @returns {string} serialized function arguments - */ - const serializeAsset = (asset) => { - const args = [ - `${RuntimeGlobals.publicPath} + ${JSON.stringify(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(", "); - }; - - if (assetInfo.prefetch.length > 0) { - const prefetchCode = - assetInfo.prefetch.length <= 2 - ? assetInfo.prefetch.map( - (asset) => - `${RuntimeGlobals.prefetchAsset}(${serializeAsset(asset)});` - ) - : Template.asString([ - `[${assetInfo.prefetch - .map( - (asset) => - `{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify( - 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); - } - } - - if (assetInfo.preload.length > 0) { - const preloadCode = - assetInfo.preload.length <= 2 - ? assetInfo.preload.map( - (asset) => - `${RuntimeGlobals.preloadAsset}(${serializeAsset(asset)});` - ) - : Template.asString([ - `[${assetInfo.preload - .map( - (asset) => - `{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify( - 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); - } - - /** - * @returns {boolean} true, if the runtime module should get it's own scope - */ - shouldIsolate() { - return false; - } -} - -module.exports = AssetPrefetchStartupRuntimeModule; diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js deleted file mode 100644 index 6eaed6275..000000000 --- a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - 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("../Compilation")} Compilation */ - -class AssetPrefetchPreloadRuntimeModule extends RuntimeModule { - /** - * @param {string} type "prefetch" or "preload" - */ - constructor(type) { - super(`asset ${type}`); - this._type = type; - } - - /** - * @returns {string | null} runtime code - */ - generate() { - const { compilation } = this; - if (!compilation) return null; - const { runtimeTemplate } = compilation; - const fn = - this._type === "prefetch" - ? RuntimeGlobals.prefetchAsset - : RuntimeGlobals.preloadAsset; - - return Template.asString([ - `${fn} = ${runtimeTemplate.basicFunction("url, as, fetchPriority, type", [ - "var link = document.createElement('link');", - this._type === "prefetch" - ? "link.rel = 'prefetch';" - : "link.rel = 'preload';", - "if(as) link.as = as;", - "if(type) link.type = type;", - "link.href = url;", - "if(fetchPriority) {", - Template.indent([ - "link.fetchPriority = fetchPriority;", - "link.setAttribute('fetchpriority', fetchPriority);" - ]), - "}", - // Apply nonce attribute for CSP if configured - compilation.outputOptions.crossOriginLoading - ? Template.asString([ - `if(${RuntimeGlobals.scriptNonce}) {`, - Template.indent( - `link.setAttribute('nonce', ${RuntimeGlobals.scriptNonce});` - ), - "}" - ]) - : "", - "document.head.appendChild(link);" - ])};` - ]); - } -} - -module.exports = AssetPrefetchPreloadRuntimeModule; diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 7588dbaad..3576b1e0f 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -184,14 +184,12 @@ class URLParserPlugin { relative ); dep.loc = /** @type {DependencyLocation} */ (expr.loc); - - // Process magic comments for prefetch/preload hints + // Parse magic comments with simplified rules if (importOptions) { - // webpackPrefetch should be boolean true - if ( - importOptions.webpackPrefetch !== undefined && - importOptions.webpackPrefetch !== true - ) { + // Accept only boolean true for webpackPrefetch + if (importOptions.webpackPrefetch === true) { + dep.prefetch = true; + } else if (importOptions.webpackPrefetch !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackPrefetch\` expected true, but received: ${importOptions.webpackPrefetch}.`, @@ -200,11 +198,10 @@ class URLParserPlugin { ); } - // webpackPreload should be boolean true - if ( - importOptions.webpackPreload !== undefined && - importOptions.webpackPreload !== true - ) { + // Accept only boolean true for webpackPreload + if (importOptions.webpackPreload === true) { + dep.preload = true; + } else if (importOptions.webpackPreload !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackPreload\` expected true, but received: ${importOptions.webpackPreload}.`, @@ -213,14 +210,13 @@ class URLParserPlugin { ); } - // webpackFetchPriority should be one of: high, low, auto + // webpackFetchPriority: "high" | "low" | "auto" if ( - importOptions.webpackFetchPriority !== undefined && - (typeof importOptions.webpackFetchPriority !== "string" || - !["high", "low", "auto"].includes( - importOptions.webpackFetchPriority - )) + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes(importOptions.webpackFetchPriority) ) { + dep.fetchPriority = importOptions.webpackFetchPriority; + } else if (importOptions.webpackFetchPriority !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, @@ -228,39 +224,6 @@ class URLParserPlugin { ) ); } - - // webpackPreloadAs should be a string - if ( - importOptions.webpackPreloadAs !== undefined && - typeof importOptions.webpackPreloadAs !== "string" - ) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPreloadAs\` expected a string, but received: ${importOptions.webpackPreloadAs}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - - // webpackPreloadType should be a string - if ( - importOptions.webpackPreloadType !== undefined && - typeof importOptions.webpackPreloadType !== "string" - ) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPreloadType\` expected a string, but received: ${importOptions.webpackPreloadType}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - - // Store magic comment values on dependency - dep.prefetch = importOptions.webpackPrefetch; - dep.preload = importOptions.webpackPreload; - dep.fetchPriority = importOptions.webpackFetchPriority; - dep.preloadAs = importOptions.webpackPreloadAs; - dep.preloadType = importOptions.webpackPreloadType; } // Register the dependency diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js index 0053ffe74..b741df1e0 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -5,7 +5,4 @@ // Invalid fetchPriority value - should generate warning const invalidPriorityUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "invalid" */ "./assets/images/priority-invalid.png", import.meta.url); -// Invalid webpackPreloadType value - should generate warning -const invalidTypeUrl = new URL(/* webpackPreload: true */ /* webpackPreloadType: 123 */ "./assets/styles/invalid-type.css", import.meta.url); - export default {}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index 1158ad239..93e0f8760 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -3,11 +3,11 @@ function verifyLink(link, expectations) { expect(link._type).toBe("link"); expect(link.rel).toBe(expectations.rel); - + if (expectations.as) { expect(link.as).toBe(expectations.as); } - + if (expectations.fetchPriority !== undefined) { if (expectations.fetchPriority) { expect(link._attributes.fetchpriority).toBe(expectations.fetchPriority); @@ -17,11 +17,8 @@ function verifyLink(link, expectations) { expect(link.fetchPriority).toBeUndefined(); } } - - if (expectations.type) { - expect(link.type).toBe(expectations.type); - } - + + if (expectations.href) { expect(link.href.toString()).toMatch(expectations.href); } @@ -44,11 +41,6 @@ it("should generate all prefetch and preload links", () => { "./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", @@ -65,7 +57,7 @@ it("should generate all prefetch and preload links", () => { import.meta.url ) }; - + const prefetchHighLink = document.head._children.find( link => link.href.includes("priority-high.png") && link.rel === "prefetch" ); @@ -76,7 +68,7 @@ it("should generate all prefetch and preload links", () => { fetchPriority: "high", href: /priority-high\.png$/ }); - + const preloadLowLink = document.head._children.find( link => link.href.includes("priority-low.css") && link.rel === "preload" ); @@ -87,7 +79,7 @@ it("should generate all prefetch and preload links", () => { fetchPriority: "low", href: /priority-low\.css$/ }); - + const prefetchAutoLink = document.head._children.find( link => link.href.includes("priority-auto.js") && link.rel === "prefetch" ); @@ -97,28 +89,17 @@ it("should generate all prefetch and preload links", () => { as: "script", fetchPriority: "auto" }); - - 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$/ - }); - + 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 noPriorityLink = document.head._children.find( - link => link.href.includes("test.png") && link.rel === "prefetch" && - !link._attributes.fetchpriority + link => link.href.includes("test.png") && link.rel === "prefetch" && + !link._attributes.fetchpriority ); expect(noPriorityLink).toBeTruthy(); verifyLink(noPriorityLink, { @@ -126,7 +107,7 @@ 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" ); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js index c5391ce61..80ef2b4e7 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -22,6 +22,7 @@ const mockCreateElement = (tagName) => { element.rel = ""; element.as = ""; element.href = ""; + element.type = undefined; element.fetchPriority = undefined; } else if (tagName === "script") { element.src = ""; @@ -60,33 +61,5 @@ 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) { - const link = global.document.createElement("link"); - link.rel = "prefetch"; - if (as) link.as = as; - if (type) link.type = type; - link.href = url; - if (fetchPriority) { - link.fetchPriority = fetchPriority; - link.setAttribute("fetchpriority", fetchPriority); - } - global.document.head.appendChild(link); - }, - LA(url, as, fetchPriority, type) { - const link = global.document.createElement("link"); - link.rel = "preload"; - if (as) link.as = as; - if (type) link.type = type; - link.href = url; - if (fetchPriority) { - link.fetchPriority = fetchPriority; - link.setAttribute("fetchpriority", fetchPriority); - } - global.document.head.appendChild(link); - }, - b: "https://test.example.com/" // baseURI - }; } }; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js index c41bfc5f8..eb4c2050e 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -4,7 +4,5 @@ module.exports = [ // Invalid fetchPriority value warning [ /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ - ], - // Invalid webpackPreloadType value warning - [/`webpackPreloadType` expected a string, but received: 123\./] + ] ]; From e7c0382087e9ace904e9a2006f2bf9f09749a754 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Sun, 10 Aug 2025 12:07:56 -0700 Subject: [PATCH 16/23] fix: only enable URL asset prefetch/preload hints for web targets --- lib/dependencies/URLPlugin.js | 9 ++++++++- lib/url/URLParserPlugin.js | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/dependencies/URLPlugin.js b/lib/dependencies/URLPlugin.js index 709465887..03fa15057 100644 --- a/lib/dependencies/URLPlugin.js +++ b/lib/dependencies/URLPlugin.js @@ -50,7 +50,14 @@ class URLPlugin { */ const handler = (parser, parserOptions) => { if (parserOptions.url === false) return; - new URLParserPlugin(parserOptions).apply(parser); + const externalsPresets = compilation.options.externalsPresets || {}; + const enableUrlAssetHints = Boolean( + externalsPresets.web || externalsPresets.webAsync + ); + new URLParserPlugin({ + ...parserOptions, + enableUrlAssetHints + }).apply(parser); }; normalModuleFactory.hooks.parser diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 3576b1e0f..f8ed143d5 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -106,6 +106,7 @@ class URLParserPlugin { */ apply(parser) { const relative = this.options.url === "relative"; + const enableUrlAssetHints = Boolean(this.options.enableUrlAssetHints); parser.hooks.canRename.for("URL").tap(PLUGIN_NAME, approve); parser.hooks.evaluateNewExpression.for("URL").tap(PLUGIN_NAME, (expr) => { @@ -188,7 +189,7 @@ class URLParserPlugin { if (importOptions) { // Accept only boolean true for webpackPrefetch if (importOptions.webpackPrefetch === true) { - dep.prefetch = true; + if (enableUrlAssetHints) dep.prefetch = true; } else if (importOptions.webpackPrefetch !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( @@ -200,7 +201,7 @@ class URLParserPlugin { // Accept only boolean true for webpackPreload if (importOptions.webpackPreload === true) { - dep.preload = true; + if (enableUrlAssetHints) dep.preload = true; } else if (importOptions.webpackPreload !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( @@ -215,7 +216,9 @@ class URLParserPlugin { typeof importOptions.webpackFetchPriority === "string" && ["high", "low", "auto"].includes(importOptions.webpackFetchPriority) ) { - dep.fetchPriority = importOptions.webpackFetchPriority; + if (enableUrlAssetHints) { + dep.fetchPriority = importOptions.webpackFetchPriority; + } } else if (importOptions.webpackFetchPriority !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( From 0e3dfa1a38ae64d6c40d59ee829fe75473f32cfd Mon Sep 17 00:00:00 2001 From: Ryuya Date: Tue, 2 Sep 2025 17:43:02 -0700 Subject: [PATCH 17/23] refactor: replace `AssetResourcePrefetchRuntimeModule` with `ResourcePrefetchRuntimeModule` --- lib/asset/AssetResourcePrefetchPlugin.js | 6 +++--- .../ResourcePrefetchRuntimeModule.js} | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename lib/{asset/AssetResourcePrefetchRuntimeModule.js => prefetch/ResourcePrefetchRuntimeModule.js} (94%) diff --git a/lib/asset/AssetResourcePrefetchPlugin.js b/lib/asset/AssetResourcePrefetchPlugin.js index 6f44feab5..17cf65a70 100644 --- a/lib/asset/AssetResourcePrefetchPlugin.js +++ b/lib/asset/AssetResourcePrefetchPlugin.js @@ -6,7 +6,7 @@ "use strict"; const RuntimeGlobals = require("../RuntimeGlobals"); -const AssetResourcePrefetchRuntimeModule = require("./AssetResourcePrefetchRuntimeModule"); +const ResourcePrefetchRuntimeModule = require("../prefetch/ResourcePrefetchRuntimeModule"); /** @typedef {import("../Compiler")} Compiler */ @@ -29,7 +29,7 @@ class AssetResourcePrefetchPlugin { set.add(RuntimeGlobals.relativeUrl); compilation.addRuntimeModule( chunk, - new AssetResourcePrefetchRuntimeModule("prefetch") + new ResourcePrefetchRuntimeModule("prefetch") ); return true; }); @@ -44,7 +44,7 @@ class AssetResourcePrefetchPlugin { set.add(RuntimeGlobals.relativeUrl); compilation.addRuntimeModule( chunk, - new AssetResourcePrefetchRuntimeModule("preload") + new ResourcePrefetchRuntimeModule("preload") ); return true; }); diff --git a/lib/asset/AssetResourcePrefetchRuntimeModule.js b/lib/prefetch/ResourcePrefetchRuntimeModule.js similarity index 94% rename from lib/asset/AssetResourcePrefetchRuntimeModule.js rename to lib/prefetch/ResourcePrefetchRuntimeModule.js index 38ea4300f..03be98bb9 100644 --- a/lib/asset/AssetResourcePrefetchRuntimeModule.js +++ b/lib/prefetch/ResourcePrefetchRuntimeModule.js @@ -11,7 +11,7 @@ const Template = require("../Template"); /** @typedef {import("../Compilation")} Compilation */ -class AssetResourcePrefetchRuntimeModule extends RuntimeModule { +class ResourcePrefetchRuntimeModule extends RuntimeModule { /** * @param {string} type "prefetch" or "preload" */ @@ -79,4 +79,4 @@ class AssetResourcePrefetchRuntimeModule extends RuntimeModule { } } -module.exports = AssetResourcePrefetchRuntimeModule; +module.exports = ResourcePrefetchRuntimeModule; From 719e2b13ca77aa9d5c44ac19d9f46a27c702b193 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Tue, 2 Sep 2025 18:02:44 -0700 Subject: [PATCH 18/23] refactor: move asset type determination logic from util module to URLDependency class --- lib/dependencies/URLDependency.js | 29 ++++++++++++++++++++++++-- lib/util/assetType.js | 34 ------------------------------- 2 files changed, 27 insertions(+), 36 deletions(-) delete mode 100644 lib/util/assetType.js diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index a16218f62..5de787427 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -11,7 +11,6 @@ const RawDataUrlModule = require("../asset/RawDataUrlModule"); const { getDependencyUsedByExportsCondition } = require("../optimize/InnerGraph"); -const getAssetType = require("../util/assetType"); const makeSerializable = require("../util/makeSerializable"); const memoize = require("../util/memoize"); const ModuleDependency = require("./ModuleDependency"); @@ -115,6 +114,32 @@ class URLDependency extends ModuleDependency { URLDependency.Template = class URLDependencyTemplate extends ( ModuleDependency.Template ) { + /** + * Determines the 'as' attribute value for prefetch/preload based on file extension + * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded + * @param {string} request module request string or filename + * @returns {string} asset type for link element 'as' attribute + */ + 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) + ) { + // Audio/video files use 'fetch' as browser support varies + return "fetch"; + } + return "fetch"; + } + /** * @param {Dependency} dependency the dependency for which the template should be applied * @param {ReplaceSource} source the current replace source which can be modified @@ -178,7 +203,7 @@ URLDependency.Template = class URLDependencyTemplate extends ( // Prefetch/Preload via InitFragment if ((dep.prefetch || dep.preload) && module) { const request = dep.request; - const assetType = getAssetType(request); + const assetType = URLDependencyTemplate._getAssetType(request); const id = chunkGraph.getModuleId(module); if (id !== null) { const moduleId = runtimeTemplate.moduleId({ diff --git a/lib/util/assetType.js b/lib/util/assetType.js deleted file mode 100644 index ff85fa5a4..000000000 --- a/lib/util/assetType.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ - -"use strict"; - -/** - * Determines the 'as' attribute value for prefetch/preload based on file extension - * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded - * @param {string} request module request string or filename - * @returns {string} asset type for link element 'as' attribute - */ -const 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) - ) { - // Audio/video files use 'fetch' as browser support varies - return "fetch"; - } - return "fetch"; -}; - -module.exports = getAssetType; From 37632364c5e3b02d05b051e9c9ba269d8991add2 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Tue, 2 Sep 2025 18:22:45 -0700 Subject: [PATCH 19/23] feat(runtime): improve asset prefetch/preload for universal target --- lib/WebpackOptionsApply.js | 16 +++- lib/dependencies/URLPlugin.js | 9 +- lib/prefetch/ResourcePrefetchRuntimeModule.js | 92 +++++++++++-------- lib/url/URLParserPlugin.js | 9 +- 4 files changed, 75 insertions(+), 51 deletions(-) diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index 72ddf39c0..43d97c7f2 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -225,7 +225,21 @@ class WebpackOptionsApply extends OptionsApply { } new ChunkPrefetchPreloadPlugin().apply(compiler); - new AssetResourcePrefetchPlugin().apply(compiler); + + // Apply AssetResourcePrefetchPlugin only for web targets or universal targets + // Check if we're targeting web environment + const externalsPresets = options.externalsPresets || {}; + const isTargetingWeb = Boolean( + externalsPresets.web || + externalsPresets.webAsync || + externalsPresets.electronRenderer + ); + + // Apply the plugin if we're targeting web environment + // For universal targets (["web", "node"]), the runtime module will handle platform detection using isNeutralPlatform + if (isTargetingWeb || !externalsPresets.node) { + new AssetResourcePrefetchPlugin().apply(compiler); + } if (typeof options.output.chunkFormat === "string") { switch (options.output.chunkFormat) { diff --git a/lib/dependencies/URLPlugin.js b/lib/dependencies/URLPlugin.js index 03fa15057..709465887 100644 --- a/lib/dependencies/URLPlugin.js +++ b/lib/dependencies/URLPlugin.js @@ -50,14 +50,7 @@ class URLPlugin { */ const handler = (parser, parserOptions) => { if (parserOptions.url === false) return; - const externalsPresets = compilation.options.externalsPresets || {}; - const enableUrlAssetHints = Boolean( - externalsPresets.web || externalsPresets.webAsync - ); - new URLParserPlugin({ - ...parserOptions, - enableUrlAssetHints - }).apply(parser); + new URLParserPlugin(parserOptions).apply(parser); }; normalModuleFactory.hooks.parser diff --git a/lib/prefetch/ResourcePrefetchRuntimeModule.js b/lib/prefetch/ResourcePrefetchRuntimeModule.js index 03be98bb9..b9cc5cf20 100644 --- a/lib/prefetch/ResourcePrefetchRuntimeModule.js +++ b/lib/prefetch/ResourcePrefetchRuntimeModule.js @@ -34,46 +34,66 @@ class ResourcePrefetchRuntimeModule extends RuntimeModule { : RuntimeGlobals.preloadAsset; const crossOriginLoading = outputOptions.crossOriginLoading; + const isNeutralPlatform = runtimeTemplate.isNeutralPlatform(); + // For neutral platform (universal targets), generate code that checks for document at runtime + const code = [ + "var url;", + "if (relative) {", + Template.indent([ + `url = new ${RuntimeGlobals.relativeUrl}(${RuntimeGlobals.require}(moduleId));` + ]), + "} else {", + Template.indent([ + `url = new URL(${RuntimeGlobals.require}(moduleId), ${RuntimeGlobals.baseURI});` + ]), + "}", + "", + "var link = document.createElement('link');", + `link.rel = '${this._type}';`, + "if (as) link.as = as;", + "link.href = url.href;", + "", + "if (fetchPriority) {", + Template.indent([ + "link.fetchPriority = fetchPriority;", + "link.setAttribute('fetchpriority', fetchPriority);" + ]), + "}", + "", + crossOriginLoading + ? Template.asString([ + "if (link.href.indexOf(window.location.origin + '/') !== 0) {", + Template.indent([ + `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};` + ]), + "}" + ]) + : "", + "", + "document.head.appendChild(link);" + ]; + + // For neutral platform, wrap the code to check for document availability + if (isNeutralPlatform) { + return Template.asString([ + `${fnName} = ${runtimeTemplate.basicFunction( + "moduleId, as, fetchPriority, relative", + [ + "// Only execute in browser environment", + "if (typeof document !== 'undefined') {", + Template.indent(code), + "}" + ] + )};` + ]); + } + + // For browser-only targets, generate code without the check return Template.asString([ `${fnName} = ${runtimeTemplate.basicFunction( "moduleId, as, fetchPriority, relative", - [ - "var url;", - "if (relative) {", - Template.indent([ - `url = new ${RuntimeGlobals.relativeUrl}(${RuntimeGlobals.require}(moduleId));` - ]), - "} else {", - Template.indent([ - `url = new URL(${RuntimeGlobals.require}(moduleId), ${RuntimeGlobals.baseURI});` - ]), - "}", - "", - "var link = document.createElement('link');", - `link.rel = '${this._type}';`, - "if (as) link.as = as;", - "link.href = url.href;", - "", - "if (fetchPriority) {", - Template.indent([ - "link.fetchPriority = fetchPriority;", - "link.setAttribute('fetchpriority', fetchPriority);" - ]), - "}", - "", - crossOriginLoading - ? Template.asString([ - "if (link.href.indexOf(window.location.origin + '/') !== 0) {", - Template.indent([ - `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};` - ]), - "}" - ]) - : "", - "", - "document.head.appendChild(link);" - ] + code )};` ]); } diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index f8ed143d5..3576b1e0f 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -106,7 +106,6 @@ class URLParserPlugin { */ apply(parser) { const relative = this.options.url === "relative"; - const enableUrlAssetHints = Boolean(this.options.enableUrlAssetHints); parser.hooks.canRename.for("URL").tap(PLUGIN_NAME, approve); parser.hooks.evaluateNewExpression.for("URL").tap(PLUGIN_NAME, (expr) => { @@ -189,7 +188,7 @@ class URLParserPlugin { if (importOptions) { // Accept only boolean true for webpackPrefetch if (importOptions.webpackPrefetch === true) { - if (enableUrlAssetHints) dep.prefetch = true; + dep.prefetch = true; } else if (importOptions.webpackPrefetch !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( @@ -201,7 +200,7 @@ class URLParserPlugin { // Accept only boolean true for webpackPreload if (importOptions.webpackPreload === true) { - if (enableUrlAssetHints) dep.preload = true; + dep.preload = true; } else if (importOptions.webpackPreload !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( @@ -216,9 +215,7 @@ class URLParserPlugin { typeof importOptions.webpackFetchPriority === "string" && ["high", "low", "auto"].includes(importOptions.webpackFetchPriority) ) { - if (enableUrlAssetHints) { - dep.fetchPriority = importOptions.webpackFetchPriority; - } + dep.fetchPriority = importOptions.webpackFetchPriority; } else if (importOptions.webpackFetchPriority !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( From 3c30c24d8f9c309d0c133b2b31c6937ea67bfcb6 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Mon, 15 Sep 2025 06:24:36 -0700 Subject: [PATCH 20/23] feat: add preload options for enhanced asset loading - Introduced `preloadAs`, `preloadType`, and `preloadMedia` properties to `URLDependency` for better control over asset preloading. --- cspell.json | 1 + lib/dependencies/URLDependency.js | 40 ++++++++--- lib/prefetch/ResourcePrefetchRuntimeModule.js | 7 +- lib/url/URLParserPlugin.js | 69 +++++++++++++++++++ 4 files changed, 104 insertions(+), 13 deletions(-) diff --git a/cspell.json b/cspell.json index 9ce10a6cb..2d3369659 100644 --- a/cspell.json +++ b/cspell.json @@ -231,6 +231,7 @@ "serializables", "serializer", "serializers", + "serviceworker", "shama", "skypack", "snapshotting", diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 5de787427..ab934493a 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -52,6 +52,12 @@ class URLDependency extends ModuleDependency { this.prefetch = undefined; this.preload = undefined; this.fetchPriority = undefined; + /** @type {string|undefined} */ + this.preloadAs = undefined; + /** @type {string|undefined} */ + this.preloadType = undefined; + /** @type {string|undefined} */ + this.preloadMedia = undefined; } get type() { @@ -93,6 +99,9 @@ class URLDependency extends ModuleDependency { write(this.prefetch); write(this.preload); write(this.fetchPriority); + write(this.preloadAs); + write(this.preloadType); + write(this.preloadMedia); super.serialize(context); } @@ -107,6 +116,9 @@ class URLDependency extends ModuleDependency { this.prefetch = read(); this.preload = read(); this.fetchPriority = read(); + this.preloadAs = read(); + this.preloadType = read(); + this.preloadMedia = read(); super.deserialize(context); } } @@ -203,7 +215,7 @@ URLDependency.Template = class URLDependencyTemplate extends ( // Prefetch/Preload via InitFragment if ((dep.prefetch || dep.preload) && module) { const request = dep.request; - const assetType = URLDependencyTemplate._getAssetType(request); + const detectedAssetType = URLDependencyTemplate._getAssetType(request); const id = chunkGraph.getModuleId(module); if (id !== null) { const moduleId = runtimeTemplate.moduleId({ @@ -215,13 +227,19 @@ URLDependency.Template = class URLDependencyTemplate extends ( if (dep.preload) { runtimeRequirements.add(RuntimeGlobals.preloadAsset); + const asArg = JSON.stringify(dep.preloadAs || detectedAssetType); + const fetchPriorityArg = dep.fetchPriority + ? JSON.stringify(dep.fetchPriority) + : "undefined"; + const typeArg = dep.preloadType + ? JSON.stringify(dep.preloadType) + : "undefined"; + const mediaArg = dep.preloadMedia + ? JSON.stringify(dep.preloadMedia) + : "undefined"; initFragments.push( new InitFragment( - `${RuntimeGlobals.preloadAsset}(${moduleId}, ${JSON.stringify( - assetType - )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ - dep.relative - });\n`, + `${RuntimeGlobals.preloadAsset}(${moduleId}, ${asArg}, ${fetchPriorityArg}, ${typeArg}, ${mediaArg}, ${dep.relative});\n`, InitFragment.STAGE_CONSTANTS, -10, `asset_preload_${moduleId}` @@ -229,13 +247,13 @@ URLDependency.Template = class URLDependencyTemplate extends ( ); } else if (dep.prefetch) { runtimeRequirements.add(RuntimeGlobals.prefetchAsset); + const asArg = JSON.stringify(detectedAssetType); + const fetchPriorityArg = dep.fetchPriority + ? JSON.stringify(dep.fetchPriority) + : "undefined"; initFragments.push( new InitFragment( - `${RuntimeGlobals.prefetchAsset}(${moduleId}, ${JSON.stringify( - assetType - )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ - dep.relative - });\n`, + `${RuntimeGlobals.prefetchAsset}(${moduleId}, ${asArg}, ${fetchPriorityArg}, undefined, undefined, ${dep.relative});\n`, InitFragment.STAGE_CONSTANTS, -5, `asset_prefetch_${moduleId}` diff --git a/lib/prefetch/ResourcePrefetchRuntimeModule.js b/lib/prefetch/ResourcePrefetchRuntimeModule.js index b9cc5cf20..cd9603aec 100644 --- a/lib/prefetch/ResourcePrefetchRuntimeModule.js +++ b/lib/prefetch/ResourcePrefetchRuntimeModule.js @@ -61,6 +61,9 @@ class ResourcePrefetchRuntimeModule extends RuntimeModule { ]), "}", "", + "if (type) link.type = type;", + "if (media) link.media = media;", + "", crossOriginLoading ? Template.asString([ "if (link.href.indexOf(window.location.origin + '/') !== 0) {", @@ -78,7 +81,7 @@ class ResourcePrefetchRuntimeModule extends RuntimeModule { if (isNeutralPlatform) { return Template.asString([ `${fnName} = ${runtimeTemplate.basicFunction( - "moduleId, as, fetchPriority, relative", + "moduleId, as, fetchPriority, type, media, relative", [ "// Only execute in browser environment", "if (typeof document !== 'undefined') {", @@ -92,7 +95,7 @@ class ResourcePrefetchRuntimeModule extends RuntimeModule { // For browser-only targets, generate code without the check return Template.asString([ `${fnName} = ${runtimeTemplate.basicFunction( - "moduleId, as, fetchPriority, relative", + "moduleId, as, fetchPriority, type, media, relative", code )};` ]); diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 3576b1e0f..6fd7d8c01 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -224,6 +224,75 @@ class URLParserPlugin { ) ); } + + // webpackPreloadAs: allow override of the "as" attribute for preload + // Fetch Standard: Request destinations (enumerates destinations used by `as`) https://fetch.spec.whatwg.org/#concept-request-destination + if (importOptions.webpackPreloadAs !== undefined) { + const allowedAs = [ + // Per HTML LS "match-preload-type" + Fetch Standard request destinations + // See references above + "audio", + "audioworklet", + "document", + "embed", + "fetch", + "font", + "image", + "manifest", + "object", + "paintworklet", + "report", + "script", + "sharedworker", + "serviceworker", + "style", + "track", + "video", + "worker", + "xslt" + ]; + if ( + typeof importOptions.webpackPreloadAs === "string" && + allowedAs.includes(importOptions.webpackPreloadAs) + ) { + dep.preloadAs = importOptions.webpackPreloadAs; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadAs\` expected one of ${JSON.stringify(allowedAs)}, but received: ${importOptions.webpackPreloadAs}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // webpackPreloadType: set link.type when provided + if (importOptions.webpackPreloadType !== undefined) { + if (typeof importOptions.webpackPreloadType === "string") { + dep.preloadType = importOptions.webpackPreloadType; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadType\` expected a string, but received: ${importOptions.webpackPreloadType}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // webpackPreloadMedia: set link.media when provided + if (importOptions.webpackPreloadMedia !== undefined) { + if (typeof importOptions.webpackPreloadMedia === "string") { + dep.preloadMedia = importOptions.webpackPreloadMedia; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadMedia\` expected a string, but received: ${importOptions.webpackPreloadMedia}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } } // Register the dependency From 9e28ba7a71533bed9649b90bba5546a327573dfe Mon Sep 17 00:00:00 2001 From: Ryuya Date: Mon, 15 Sep 2025 06:25:44 -0700 Subject: [PATCH 21/23] tests: add test cases for invalid `webpackPreloadAs`, `webpackPreloadType`, and `webpackPreloadMedia` values. --- .../assets/images/override.png | 1 + .../generate-warnings.js | 24 ++++++++ .../index.js | 57 +++++++++++++++++++ .../test.config.js | 1 + .../warnings.js | 8 ++- 5 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/override.png diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/override.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/override.png new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/override.png @@ -0,0 +1 @@ + diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js index b741df1e0..33a862198 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -5,4 +5,28 @@ // Invalid fetchPriority value - should generate warning const invalidPriorityUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "invalid" */ "./assets/images/priority-invalid.png", import.meta.url); +// Invalid preloadAs - should generate warning +const invalidPreloadAs = new URL( + /* webpackPreload: true */ + /* webpackPreloadAs: "invalid-as" */ + "./assets/images/priority-invalid.png", + import.meta.url +); + +// Invalid preloadType (non-string) - should generate warning +const invalidPreloadType = new URL( + /* webpackPreload: true */ + /* webpackPreloadType: 123 */ + "./assets/images/priority-invalid.png", + import.meta.url +); + +// Invalid preloadMedia (non-string) - should generate warning +const invalidPreloadMedia = new URL( + /* webpackPreload: true */ + /* webpackPreloadMedia: 456 */ + "./assets/images/priority-invalid.png", + import.meta.url +); + export default {}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index 93e0f8760..2ad97d3c8 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -8,6 +8,22 @@ function verifyLink(link, expectations) { expect(link.as).toBe(expectations.as); } + if (expectations.type !== undefined) { + if (expectations.type) { + expect(link.type).toBe(expectations.type); + } else { + expect(link.type).toBeUndefined(); + } + } + + if (expectations.media !== undefined) { + if (expectations.media) { + expect(link.media).toBe(expectations.media); + } else { + expect(link.media).toBeUndefined(); + } + } + if (expectations.fetchPriority !== undefined) { if (expectations.fetchPriority) { expect(link._attributes.fetchpriority).toBe(expectations.fetchPriority); @@ -119,3 +135,44 @@ it("should generate all prefetch and preload links", () => { }); }); +it("should allow overriding as/type/media via magic comments", () => { + const override = new URL( + /* webpackPreload: true */ + /* webpackPreloadAs: "font" */ + /* webpackPreloadType: "font/woff2" */ + /* webpackPreloadMedia: "(max-width: 600px)" */ + "./assets/images/override.png", + import.meta.url + ); + + const link = document.head._children.find( + l => l.href.includes("override.png") && l.rel === "preload" + ); + expect(link).toBeTruthy(); + verifyLink(link, { + rel: "preload", + as: "font", + type: "font/woff2", + media: "(max-width: 600px)", + href: /override\.png$/ + }); +}); + +it("should accept additional as tokens from Fetch Standard (e.g., sharedworker)", () => { + const u = new URL( + /* webpackPreload: true */ + /* webpackPreloadAs: "sharedworker" */ + "./priority-auto.js", + import.meta.url + ); + + const link = document.head._children.find( + l => l.href.includes("priority-auto.js") && l.rel === "preload" + ); + expect(link).toBeTruthy(); + verifyLink(link, { + rel: "preload", + as: "sharedworker", + href: /priority-auto\.js$/ + }); +}); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js index 80ef2b4e7..d7e9b5768 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -23,6 +23,7 @@ const mockCreateElement = (tagName) => { element.as = ""; element.href = ""; element.type = undefined; + element.media = undefined; element.fetchPriority = undefined; } else if (tagName === "script") { element.src = ""; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js index eb4c2050e..48ec8ca27 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -4,5 +4,11 @@ module.exports = [ // Invalid fetchPriority value warning [ /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ - ] + ], + // Invalid preloadAs value + [/`webpackPreloadAs` expected one of \[.*\], but received: invalid-as\./], + // Invalid preloadType (non-string) + [/`webpackPreloadType` expected a string, but received: 123\./], + // Invalid preloadMedia (non-string) + [/`webpackPreloadMedia` expected a string, but received: 456\./] ]; From 57bb97cbb5bbae4bb2d32d96a3aa8acc8b4dfbe6 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Mon, 15 Sep 2025 06:43:01 -0700 Subject: [PATCH 22/23] refactor --- lib/dependencies/WorkerPlugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dependencies/WorkerPlugin.js b/lib/dependencies/WorkerPlugin.js index 64bb80eb9..334748440 100644 --- a/lib/dependencies/WorkerPlugin.js +++ b/lib/dependencies/WorkerPlugin.js @@ -461,7 +461,7 @@ class WorkerPlugin { entryOptions: { chunkLoading: this._chunkLoading, wasmLoading: this._wasmLoading, - runtime: entryOptions.runtime + ...entryOptions } }); block.loc = expr.loc; From a8d39c0cdfe243ee8111d8114895eaddfdc197d4 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Mon, 15 Sep 2025 07:14:12 -0700 Subject: [PATCH 23/23] refactor: simplify `webpackPreloadAs` validation and update warnings --- cspell.json | 1 - lib/url/URLParserPlugin.js | 31 ++----------------- .../generate-warnings.js | 6 ++-- .../warnings.js | 4 +-- 4 files changed, 6 insertions(+), 36 deletions(-) diff --git a/cspell.json b/cspell.json index 2d3369659..9ce10a6cb 100644 --- a/cspell.json +++ b/cspell.json @@ -231,7 +231,6 @@ "serializables", "serializer", "serializers", - "serviceworker", "shama", "skypack", "snapshotting", diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 6fd7d8c01..9eb83c7d6 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -226,40 +226,13 @@ class URLParserPlugin { } // webpackPreloadAs: allow override of the "as" attribute for preload - // Fetch Standard: Request destinations (enumerates destinations used by `as`) https://fetch.spec.whatwg.org/#concept-request-destination if (importOptions.webpackPreloadAs !== undefined) { - const allowedAs = [ - // Per HTML LS "match-preload-type" + Fetch Standard request destinations - // See references above - "audio", - "audioworklet", - "document", - "embed", - "fetch", - "font", - "image", - "manifest", - "object", - "paintworklet", - "report", - "script", - "sharedworker", - "serviceworker", - "style", - "track", - "video", - "worker", - "xslt" - ]; - if ( - typeof importOptions.webpackPreloadAs === "string" && - allowedAs.includes(importOptions.webpackPreloadAs) - ) { + if (typeof importOptions.webpackPreloadAs === "string") { dep.preloadAs = importOptions.webpackPreloadAs; } else { parser.state.module.addWarning( new UnsupportedFeatureWarning( - `\`webpackPreloadAs\` expected one of ${JSON.stringify(allowedAs)}, but received: ${importOptions.webpackPreloadAs}.`, + `\`webpackPreloadAs\` expected a string, but received: ${importOptions.webpackPreloadAs}.`, /** @type {DependencyLocation} */ (expr.loc) ) ); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js index 33a862198..e19df05d1 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -4,15 +4,13 @@ // Invalid fetchPriority value - should generate warning const invalidPriorityUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "invalid" */ "./assets/images/priority-invalid.png", import.meta.url); - -// Invalid preloadAs - should generate warning +// Invalid preloadAs (non-string) - should generate warning const invalidPreloadAs = new URL( /* webpackPreload: true */ - /* webpackPreloadAs: "invalid-as" */ + /* webpackPreloadAs: 123 */ "./assets/images/priority-invalid.png", import.meta.url ); - // Invalid preloadType (non-string) - should generate warning const invalidPreloadType = new URL( /* webpackPreload: true */ diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js index 48ec8ca27..5923056d0 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -5,8 +5,8 @@ module.exports = [ [ /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ ], - // Invalid preloadAs value - [/`webpackPreloadAs` expected one of \[.*\], but received: invalid-as\./], + // Invalid preloadAs (non-string) + [/`webpackPreloadAs` expected a string, but received: 123\./], // Invalid preloadType (non-string) [/`webpackPreloadType` expected a string, but received: 123\./], // Invalid preloadMedia (non-string)