mirror of https://github.com/webpack/webpack.git
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:
parent
a47f4443d1
commit
e97ae49459
|
@ -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) {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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\./]
|
||||
]
|
||||
];
|
||||
|
|
Loading…
Reference in New Issue