This commit is contained in:
Ryuya 2025-10-03 23:39:10 +03:00 committed by GitHub
commit ec806be71c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 870 additions and 17 deletions

View File

@ -90,6 +90,7 @@
"externref",
"fetchpriority",
"filebase",
"flac",
"fileoverview",
"filepath",
"finalizer",

View File

@ -312,6 +312,11 @@ module.exports.nodeModuleDecorator = "__webpack_require__.nmd";
*/
module.exports.onChunksLoaded = "__webpack_require__.O";
/**
* the asset prefetch function
*/
module.exports.prefetchAsset = "__webpack_require__.PA";
/**
* the chunk prefetch function
*/
@ -322,6 +327,11 @@ module.exports.prefetchChunk = "__webpack_require__.E";
*/
module.exports.prefetchChunkHandlers = "__webpack_require__.F";
/**
* the asset preload function
*/
module.exports.preloadAsset = "__webpack_require__.LA";
/**
* the chunk preload function
*/

View File

@ -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");
@ -200,6 +202,21 @@ class WebpackOptionsApply extends OptionsApply {
new ChunkPrefetchPreloadPlugin().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) {
case "array-push": {

View File

@ -0,0 +1,55 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const RuntimeGlobals = require("../RuntimeGlobals");
const ResourcePrefetchRuntimeModule = require("../prefetch/ResourcePrefetchRuntimeModule");
/** @typedef {import("../Compiler")} Compiler */
const PLUGIN_NAME = "AssetResourcePrefetchPlugin";
class AssetResourcePrefetchPlugin {
/**
* @param {Compiler} compiler the compiler
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
// 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 ResourcePrefetchRuntimeModule("prefetch")
);
return true;
});
// 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 ResourcePrefetchRuntimeModule("preload")
);
return true;
});
});
}
}
module.exports = AssetResourcePrefetchPlugin;

View File

@ -5,6 +5,7 @@
"use strict";
const InitFragment = require("../InitFragment");
const RuntimeGlobals = require("../RuntimeGlobals");
const RawDataUrlModule = require("../asset/RawDataUrlModule");
const {
@ -43,6 +44,15 @@ class URLDependency extends ModuleDependency {
this.relative = relative || false;
/** @type {UsedByExports | undefined} */
this.usedByExports = undefined;
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() {
@ -81,6 +91,12 @@ 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);
write(this.preloadType);
write(this.preloadMedia);
super.serialize(context);
}
@ -92,6 +108,12 @@ 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();
this.preloadType = read();
this.preloadMedia = read();
super.deserialize(context);
}
}
@ -99,6 +121,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
@ -111,9 +159,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)) {
@ -125,38 +176,87 @@ URLDependency.Template = class URLDependencyTemplate extends (
return;
}
runtimeRequirements.add(RuntimeGlobals.require);
// 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 detectedAssetType = URLDependencyTemplate._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);
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}, ${asArg}, ${fetchPriorityArg}, ${typeArg}, ${mediaArg}, ${dep.relative});\n`,
InitFragment.STAGE_CONSTANTS,
-10,
`asset_preload_${moduleId}`
)
);
} 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}, ${asArg}, ${fetchPriorityArg}, undefined, undefined, ${dep.relative});\n`,
InitFragment.STAGE_CONSTANTS,
-5,
`asset_prefetch_${moduleId}`
)
);
}
}
}
}
};

View File

@ -38,6 +38,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 */
@ -223,9 +224,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,
@ -299,6 +303,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));
@ -360,6 +368,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 (
@ -388,6 +450,7 @@ class WorkerPlugin {
}
const block = new AsyncDependenciesBlock({
...groupOptions,
name: entryOptions.name,
entryOptions: {
chunkLoading: this._chunkLoading,

View File

@ -0,0 +1,105 @@
/*
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 ResourcePrefetchRuntimeModule 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;
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);"
]),
"}",
"",
"if (type) link.type = type;",
"if (media) link.media = media;",
"",
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, type, media, 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, type, media, relative",
code
)};`
]);
}
}
module.exports = ResourcePrefetchRuntimeModule;

View File

@ -182,6 +182,91 @@ class URLParserPlugin {
relative
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
// Parse magic comments with simplified rules
if (importOptions) {
// 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}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
// 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}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
// webpackFetchPriority: "high" | "low" | "auto"
if (
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}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
// webpackPreloadAs: allow override of the "as" attribute for preload
if (importOptions.webpackPreloadAs !== undefined) {
if (typeof importOptions.webpackPreloadAs === "string") {
dep.preloadAs = importOptions.webpackPreloadAs;
} else {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPreloadAs\` expected a string, 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
parser.state.current.addDependency(dep);
InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e));
return true;

View File

@ -0,0 +1,3 @@
body {
background-color: #f0f0f0;
}

View File

@ -0,0 +1,4 @@
.typed-element {
color: #333;
font-size: 16px;
}

View File

@ -0,0 +1,30 @@
"use strict";
// 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);
// Invalid preloadAs (non-string) - should generate warning
const invalidPreloadAs = new URL(
/* webpackPreload: true */
/* webpackPreloadAs: 123 */
"./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 {};

View File

@ -0,0 +1,178 @@
"use strict";
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.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);
expect(link.fetchPriority).toBe(expectations.fetchPriority);
} else {
expect(link._attributes.fetchpriority).toBeUndefined();
expect(link.fetchPriority).toBeUndefined();
}
}
if (expectations.href) {
expect(link.href.toString()).toMatch(expectations.href);
}
}
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
),
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
),
preloadFont: new URL(
/* webpackPreload: true */
"./assets/fonts/test.woff2",
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 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
});
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$/
});
});
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$/
});
});

View File

@ -0,0 +1,4 @@
"use strict";
// Test asset file
console.log("priority-auto.js loaded");

View File

@ -0,0 +1,66 @@
"use strict";
// 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.type = undefined;
element.media = undefined;
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) {
// Make document available in the module scope
scope.document = global.document;
}
};

View File

@ -0,0 +1,4 @@
"use strict";
// Test JavaScript file
console.log("test.js loaded");

View File

@ -0,0 +1,14 @@
"use strict";
module.exports = [
// Invalid fetchPriority value warning
[
/`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./
],
// 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)
[/`webpackPreloadMedia` expected a string, but received: 456\./]
];

View File

@ -0,0 +1,24 @@
"use strict";
/** @type {import("../../../../types").Configuration} */
module.exports = {
mode: "development",
entry: {
main: "./index.js",
warnings: "./generate-warnings.js"
},
output: {
filename: "[name].js",
assetModuleFilename: "[name][ext]",
publicPath: "/public/"
},
target: "web",
module: {
rules: [
{
test: /\.(png|jpg|css|woff2)$/,
type: "asset/resource"
}
]
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,49 @@
// Test cases for new URL() prefetch/preload support
it("should prefetch an image asset", () => {
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 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$/);
});
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$/);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,7 @@
"use strict";
module.exports = {
findBundle() {
return ["main.js"];
}
};

View File

@ -0,0 +1,5 @@
"use strict";
const supportsWorker = require("../../../helpers/supportsWorker");
module.exports = () => supportsWorker();

View File

@ -0,0 +1,8 @@
"use strict";
module.exports = [
// Invalid fetch priority
[
/`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./
]
];

View File

@ -0,0 +1,18 @@
"use strict";
/** @type {import("../../../../types").Configuration} */
module.exports = {
output: {
filename: "[name].js",
assetModuleFilename: "[name][ext]"
},
target: "web",
module: {
rules: [
{
test: /\.png$/,
type: "asset/resource"
}
]
}
};

2
types.d.ts vendored
View File

@ -18769,8 +18769,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";