refactor: simplify asset prefetch/preload implementation

- Consolidate multiple runtime modules into unified AssetResourcePrefetchPlugin
- Remove complex startup prefetch mechanism in favor of simpler inline approach
This commit is contained in:
Ryuya 2025-08-09 17:33:58 -07:00
parent a47f4443d1
commit e97ae49459
13 changed files with 248 additions and 670 deletions

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");
@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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<string> | boolean | undefined} */
this.usedByExports = undefined;
/** @type {boolean | undefined} */
this._startupPrefetch = undefined;
/** @type {boolean | undefined} */
this.prefetch = undefined;
/** @type {boolean | undefined} */
this.preload = undefined;
/** @type {string | undefined} */
this.fetchPriority = undefined;
/** @type {string | undefined} */
this.preloadAs = undefined;
/** @type {string | undefined} */
this.preloadType = undefined;
}
get type() {
@ -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,153 +144,80 @@ 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({
// 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),
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,
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
});
// 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)
if (dep.preload) {
runtimeRequirements.add(RuntimeGlobals.preloadAsset);
hintCode.push(
`${RuntimeGlobals.preloadAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});`
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}`
)
);
}
// 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) {
} else if (dep.prefetch) {
runtimeRequirements.add(RuntimeGlobals.prefetchAsset);
} else if (needsPreload) {
runtimeRequirements.add(RuntimeGlobals.preloadAsset);
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}`
)
);
}
}
} else if (dep.relative) {
// Standard URL generation without resource hints
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}`
);
}
}
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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 {};

View File

@ -18,9 +18,6 @@ function verifyLink(link, expectations) {
}
}
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",
@ -98,17 +90,6 @@ it("should generate all prefetch and preload links", () => {
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")
);

View File

@ -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
};
}
};

View File

@ -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\./]
]
];