feat: support `webpackPrefetch` for `new URL(...)`

This commit is contained in:
alexander.akait 2024-06-20 18:55:30 +03:00
parent 8187e48982
commit 68e7c698b1
10 changed files with 345 additions and 22 deletions

View File

@ -105,6 +105,16 @@ exports.preloadChunk = "__webpack_require__.G";
*/
exports.preloadChunkHandlers = "__webpack_require__.H";
/**
* a flag when a module/chunk/tree has css modules
*/
exports.hasPreloadUrl = "has preload url";
/**
* the url preload function
*/
exports.preloadUrl = "__webpack_require__.B";
/**
* the exported property define getters function
*/

View File

@ -12,6 +12,7 @@ const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin");
const JsonModulesPlugin = require("./json/JsonModulesPlugin");
const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin");
const URLPreloadPlugin = require("./prefetch/URLPreloadPlugin");
const EntryOptionPlugin = require("./EntryOptionPlugin");
const RecordIdsPlugin = require("./RecordIdsPlugin");
@ -180,6 +181,7 @@ class WebpackOptionsApply extends OptionsApply {
}
new ChunkPrefetchPreloadPlugin().apply(compiler);
new URLPreloadPlugin().apply(compiler);
if (typeof options.output.chunkFormat === "string") {
switch (options.output.chunkFormat) {

View File

@ -72,7 +72,7 @@ const getAssetSourceGenerator = memoize(() =>
);
const type = ASSET_MODULE_TYPE;
const plugin = "AssetModulesPlugin";
const PLUGIN_NAME = "AssetModulesPlugin";
class AssetModulesPlugin {
/**
@ -82,11 +82,11 @@ class AssetModulesPlugin {
*/
apply(compiler) {
compiler.hooks.compilation.tap(
plugin,
PLUGIN_NAME,
(compilation, { normalModuleFactory }) => {
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE)
.tap(plugin, parserOptions => {
.tap(PLUGIN_NAME, parserOptions => {
validateParserOptions(parserOptions);
parserOptions = cleverMerge(
compiler.options.module.parser.asset,
@ -107,21 +107,21 @@ class AssetModulesPlugin {
});
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_INLINE)
.tap(plugin, parserOptions => {
.tap(PLUGIN_NAME, parserOptions => {
const AssetParser = getAssetParser();
return new AssetParser(true);
});
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_RESOURCE)
.tap(plugin, parserOptions => {
.tap(PLUGIN_NAME, parserOptions => {
const AssetParser = getAssetParser();
return new AssetParser(false);
});
normalModuleFactory.hooks.createParser
.for(ASSET_MODULE_TYPE_SOURCE)
.tap(plugin, parserOptions => {
.tap(PLUGIN_NAME, parserOptions => {
const AssetSourceParser = getAssetSourceParser();
return new AssetSourceParser();
@ -134,7 +134,7 @@ class AssetModulesPlugin {
]) {
normalModuleFactory.hooks.createGenerator
.for(type)
.tap(plugin, generatorOptions => {
.tap(PLUGIN_NAME, generatorOptions => {
validateGeneratorOptions[type](generatorOptions);
let dataUrl = undefined;
@ -171,13 +171,13 @@ class AssetModulesPlugin {
}
normalModuleFactory.hooks.createGenerator
.for(ASSET_MODULE_TYPE_SOURCE)
.tap(plugin, () => {
.tap(PLUGIN_NAME, () => {
const AssetSourceGenerator = getAssetSourceGenerator();
return new AssetSourceGenerator();
});
compilation.hooks.renderManifest.tap(plugin, (result, options) => {
compilation.hooks.renderManifest.tap(PLUGIN_NAME, (result, options) => {
const { chunkGraph } = compilation;
const { chunk, codeGenerationResults } = options;
@ -217,9 +217,8 @@ class AssetModulesPlugin {
return result;
});
compilation.hooks.prepareModuleExecution.tap(
"AssetModulesPlugin",
PLUGIN_NAME,
(options, context) => {
const { codeGenerationResult } = options;
const source = codeGenerationResult.sources.get(ASSET_MODULE_TYPE);

View File

@ -55,7 +55,7 @@ class CssLoadingRuntimeModule extends RuntimeModule {
* @param {ReadOnlyRuntimeRequirements} runtimeRequirements runtime requirements
*/
constructor(runtimeRequirements) {
super("css loading", 10);
super("css loading", RuntimeModule.STAGE_ATTACH);
this._runtimeRequirements = runtimeRequirements;
}

View File

@ -5,6 +5,7 @@
"use strict";
const InitFragment = require("../InitFragment");
const RuntimeGlobals = require("../RuntimeGlobals");
const RawDataUrlModule = require("../asset/RawDataUrlModule");
const {
@ -30,6 +31,13 @@ const ModuleDependency = require("./ModuleDependency");
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
/**
* @typedef {object} PreloadOptions
* @property {number=} preloadOrder
* @property {string=} preloadAs
* @property {("low" | "high" | "auto")=} fetchPriority
*/
const getIgnoredRawDataUrlModule = memoize(() => {
return new RawDataUrlModule("data:,", `ignored-asset`, `(ignored asset)`);
});
@ -40,12 +48,14 @@ class URLDependency extends ModuleDependency {
* @param {Range} range range of the arguments of new URL( |> ... <| )
* @param {Range} outerRange range of the full |> new URL(...) <|
* @param {boolean=} relative use relative urls instead of absolute with base uri
* @param {PreloadOptions=} groupOptions use relative urls instead of absolute with base uri
*/
constructor(request, range, outerRange, relative) {
constructor(request, range, outerRange, relative, groupOptions) {
super(request);
this.range = range;
this.outerRange = outerRange;
this.relative = relative || false;
this.groupOptions = groupOptions;
/** @type {Set<string> | boolean | undefined} */
this.usedByExports = undefined;
}
@ -85,6 +95,7 @@ class URLDependency extends ModuleDependency {
const { write } = context;
write(this.outerRange);
write(this.relative);
write(this.groupOptions);
write(this.usedByExports);
super.serialize(context);
}
@ -96,6 +107,7 @@ class URLDependency extends ModuleDependency {
const { read } = context;
this.outerRange = read();
this.relative = read();
this.groupOptions = read();
this.usedByExports = read();
super.deserialize(context);
}
@ -116,7 +128,8 @@ URLDependency.Template = class URLDependencyTemplate extends (
moduleGraph,
runtimeRequirements,
runtimeTemplate,
runtime
runtime,
initFragments
} = templateContext;
const dep = /** @type {URLDependency} */ (dependency);
const connection = moduleGraph.getConnection(dep);
@ -132,6 +145,10 @@ URLDependency.Template = class URLDependencyTemplate extends (
runtimeRequirements.add(RuntimeGlobals.require);
const module = moduleGraph.getModule(dep);
const request = dep.request;
const weak = false;
if (dep.relative) {
runtimeRequirements.add(RuntimeGlobals.relativeUrl);
source.replace(
@ -141,10 +158,10 @@ URLDependency.Template = class URLDependencyTemplate extends (
RuntimeGlobals.relativeUrl
}(${runtimeTemplate.moduleRaw({
chunkGraph,
module: moduleGraph.getModule(dep),
request: dep.request,
module,
request,
runtimeRequirements,
weak: false
weak
})})`
);
} else {
@ -155,13 +172,45 @@ URLDependency.Template = class URLDependencyTemplate extends (
dep.range[1] - 1,
`/* asset import */ ${runtimeTemplate.moduleRaw({
chunkGraph,
module: moduleGraph.getModule(dep),
request: dep.request,
module,
request,
runtimeRequirements,
weak: false
weak
})}, ${RuntimeGlobals.baseURI}`
);
}
if (dep.groupOptions && dep.groupOptions.preloadOrder !== undefined) {
runtimeRequirements.add(RuntimeGlobals.hasPreloadUrl);
const moduleId = runtimeTemplate.moduleId({
module,
chunkGraph,
request,
weak
});
const needRelativeParam =
dep.relative || runtimeRequirements.has(RuntimeGlobals.relativeUrl);
const preloadAs = dep.groupOptions.preloadAs;
const fetchPriority = dep.groupOptions.fetchPriority;
initFragments.push(
new InitFragment(
`${RuntimeGlobals.preloadUrl}(${moduleId}, ${JSON.stringify(
preloadAs
)}${
fetchPriority
? `, ${JSON.stringify(fetchPriority)}`
: needRelativeParam
? ", undefined"
: ""
}${needRelativeParam ? `, ${dep.relative}` : ""});\n`,
InitFragment.STAGE_CONSTANTS,
dep.groupOptions.preloadOrder * -1,
`__webpack_url_preload__(${moduleId})`
)
);
}
}
};

View File

@ -6,10 +6,12 @@
"use strict";
const { pathToFileURL } = require("url");
const CommentCompilationWarning = require("../CommentCompilationWarning");
const {
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_ESM
} = require("../ModuleTypeConstants");
const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning");
const BasicEvaluatedExpression = require("../javascript/BasicEvaluatedExpression");
const { approve } = require("../javascript/JavascriptParserHelpers");
const InnerGraph = require("../optimize/InnerGraph");
@ -23,6 +25,7 @@ const URLDependency = require("./URLDependency");
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../javascript/JavascriptParser")} Parser */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {import("./URLDependency").PreloadOptions} PreloadOptions */
const PLUGIN_NAME = "URLPlugin";
@ -105,6 +108,94 @@ class URLPlugin {
if (!request) return;
/** @type {PreloadOptions} */
const preloadOptions = {};
const { urlPreload, urlPreloadAs, urlFetchPriority } =
parserOptions;
if (urlPreload !== undefined && urlPreload !== false)
preloadOptions.preloadOrder =
urlPreload === true ? 0 : urlPreload;
if (urlPreloadAs !== undefined && urlPreloadAs !== false)
preloadOptions.preloadAs = urlPreloadAs;
if (urlFetchPriority !== undefined && urlFetchPriority !== false)
preloadOptions.fetchPriority = urlFetchPriority;
const { options: importOptions, errors: commentErrors } =
parser.parseCommentOptions(/** @type {Range} */ (expr.range));
if (commentErrors) {
for (const e of commentErrors) {
const { comment } = e;
parser.state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
comment.loc
)
);
}
}
if (importOptions) {
if (importOptions.webpackPreload !== undefined) {
if (importOptions.webpackPreload === true) {
preloadOptions.preloadOrder = 0;
} else if (typeof importOptions.webpackPreload === "number") {
preloadOptions.preloadOrder = importOptions.webpackPreload;
} else {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
}
if (importOptions.webpackPreloadAs !== undefined) {
if (typeof importOptions.webpackPreloadAs === "string") {
preloadOptions.preloadAs = importOptions.webpackPreloadAs;
} else {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPreloadAs\` expected string, but received: ${importOptions.webpackPreloadAs}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
}
if (importOptions.webpackFetchPriority !== undefined) {
if (
typeof importOptions.webpackFetchPriority === "string" &&
["high", "low", "auto"].includes(
importOptions.webpackFetchPriority
)
) {
preloadOptions.fetchPriority =
importOptions.webpackFetchPriority;
} else {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackFetchPriority\` expected true or "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
}
}
if (
preloadOptions.preloadOrder !== undefined &&
preloadOptions.preloadAs === undefined
) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackPreload\` for \`new URL(...)\` expected \`webpackPreloadAs\` comment.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
// TODO - `type` and `media` support
const [arg1, arg2] = expr.arguments;
const dep = new URLDependency(
request,
@ -113,7 +204,8 @@ class URLPlugin {
/** @type {Range} */ (arg2.range)[1]
],
/** @type {Range} */ (expr.range),
relative
relative,
preloadOptions
);
dep.loc = /** @type {DependencyLocation} */ (expr.loc);
parser.state.current.addDependency(dep);

View File

@ -464,6 +464,9 @@ module.exports = mergeExports(fn, {
prefetch: {
get ChunkPrefetchPreloadPlugin() {
return require("./prefetch/ChunkPrefetchPreloadPlugin");
},
get URLPreloadPlugin() {
return require("./prefetch/URLPreloadPlugin");
}
},

View File

@ -0,0 +1,35 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const RuntimeGlobals = require("../RuntimeGlobals");
const URLPreloadRuntimeModule = require("./URLPreloadRuntimeModule");
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGroup").RawChunkGroupOptions} RawChunkGroupOptions */
/** @typedef {import("../Compiler")} Compiler */
const PLUGIN_NAME = "URLPreloadPlugin";
class URLPreloadPlugin {
/**
* @param {Compiler} compiler the compiler
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hasPreloadUrl)
.tap(PLUGIN_NAME, (chunk, set) => {
set.add(RuntimeGlobals.hasOwnProperty);
compilation.addRuntimeModule(chunk, new URLPreloadRuntimeModule(set));
return true;
});
});
}
}
module.exports = URLPreloadPlugin;

View File

@ -0,0 +1,127 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const { SyncWaterfallHook } = require("tapable");
const Compilation = require("../Compilation");
const RuntimeGlobals = require("../RuntimeGlobals");
const RuntimeModule = require("../RuntimeModule");
const Template = require("../Template");
/** @typedef {import("../Module").ReadOnlyRuntimeRequirements} ReadOnlyRuntimeRequirements */
/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
/**
* @typedef {object} URLPreloadRuntimeModulePluginHooks
* @property {SyncWaterfallHook<[string]>} linkPreload
*/
/** @type {WeakMap<Compilation, URLPreloadRuntimeModulePluginHooks>} */
const compilationHooksMap = new WeakMap();
class URLPreloadRuntimeModule extends RuntimeModule {
/**
* @param {Compilation} compilation the compilation
* @returns {URLPreloadRuntimeModulePluginHooks} hooks
*/
static getCompilationHooks(compilation) {
if (!(compilation instanceof Compilation)) {
throw new TypeError(
"The 'compilation' argument must be an instance of Compilation"
);
}
let hooks = compilationHooksMap.get(compilation);
if (hooks === undefined) {
hooks = {
linkPreload: new SyncWaterfallHook(["source"])
};
compilationHooksMap.set(compilation, hooks);
}
return hooks;
}
/**
* @param {ReadOnlyRuntimeRequirements} runtimeRequirements runtime requirements
*/
constructor(runtimeRequirements) {
super("asset preloading", RuntimeModule.STAGE_ATTACH);
this.runtimeRequirements = runtimeRequirements;
}
/**
* @returns {string | null} runtime code
*/
generate() {
const compilation = /** @type {Compilation} */ (this.compilation);
const {
runtimeTemplate,
outputOptions: { crossOriginLoading }
} = compilation;
const { linkPreload } =
URLPreloadRuntimeModule.getCompilationHooks(compilation);
const hasBaseURI = this.runtimeRequirements.has(RuntimeGlobals.baseURI);
const hasRelativeUrl = this.runtimeRequirements.has(
RuntimeGlobals.relativeUrl
);
let urlConstructor;
// TODO improve me
if (hasBaseURI && !hasRelativeUrl) {
urlConstructor = "URL";
} else if (!hasBaseURI && hasRelativeUrl) {
urlConstructor = RuntimeGlobals.relativeUrl;
} else {
urlConstructor = `(relative ? ${RuntimeGlobals.relativeUrl} : URL)`;
}
return Template.asString([
`${RuntimeGlobals.preloadUrl} = ${runtimeTemplate.basicFunction(
`moduleId, as, fetchPriority${hasRelativeUrl ? ", relative" : ""}`,
[
`if((!${RuntimeGlobals.hasOwnProperty}(__webpack_module_cache__, moduleId))) {`,
Template.indent([
linkPreload.call(
Template.asString([
"var link = document.createElement('link');",
`if (${RuntimeGlobals.scriptNonce}) {`,
Template.indent(
`link.setAttribute("nonce", ${RuntimeGlobals.scriptNonce});`
),
"}",
"if(fetchPriority) {",
Template.indent(
'link.setAttribute("fetchpriority", fetchPriority);'
),
"}",
'link.rel = "preload";',
"link.as = as;",
`link.href = new ${urlConstructor}(${RuntimeGlobals.require}(moduleId), ${RuntimeGlobals.baseURI});`,
crossOriginLoading
? crossOriginLoading === "use-credentials"
? 'link.crossOrigin = "use-credentials";'
: Template.asString([
"if (link.href.indexOf(window.location.origin + '/') !== 0) {",
Template.indent(
`link.crossOrigin = ${JSON.stringify(
crossOriginLoading
)};`
),
"}"
])
: ""
])
),
"document.head.appendChild(link);"
]),
"}"
]
)};`
]);
}
}
module.exports = URLPreloadRuntimeModule;

8
types.d.ts vendored
View File

@ -14275,6 +14275,10 @@ declare interface TrustedTypes {
policyName?: string;
}
declare const UNDEFINED_MARKER: unique symbol;
declare class URLPreloadPlugin {
constructor();
apply(compiler: Compiler): void;
}
/**
* `URL` class is a global reference for `require('url').URL`
@ -15070,6 +15074,8 @@ declare namespace exports {
export let prefetchChunkHandlers: "__webpack_require__.F";
export let preloadChunk: "__webpack_require__.G";
export let preloadChunkHandlers: "__webpack_require__.H";
export let hasPreloadUrl: "has preload url";
export let preloadUrl: "__webpack_require__.B";
export let definePropertyGetters: "__webpack_require__.d";
export let makeNamespaceObject: "__webpack_require__.r";
export let createFakeNamespaceObject: "__webpack_require__.t";
@ -15231,7 +15237,7 @@ declare namespace exports {
export { GetChunkFilenameRuntimeModule, LoadScriptRuntimeModule };
}
export namespace prefetch {
export { ChunkPrefetchPreloadPlugin };
export { ChunkPrefetchPreloadPlugin, URLPreloadPlugin };
}
export namespace web {
export {