diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index bb528d32e..72ddf39c0 100644 --- a/lib/WebpackOptionsApply.js +++ b/lib/WebpackOptionsApply.js @@ -34,6 +34,8 @@ const WebpackIsIncludedPlugin = require("./WebpackIsIncludedPlugin"); const AssetModulesPlugin = require("./asset/AssetModulesPlugin"); +const AssetResourcePrefetchPlugin = require("./asset/AssetResourcePrefetchPlugin"); + const InferAsyncModulesPlugin = require("./async-modules/InferAsyncModulesPlugin"); const ResolverCachePlugin = require("./cache/ResolverCachePlugin"); @@ -62,9 +64,7 @@ const JavascriptModulesPlugin = require("./javascript/JavascriptModulesPlugin"); const JsonModulesPlugin = require("./json/JsonModulesPlugin"); -const AssetPrefetchStartupPlugin = require("./prefetch/AssetPrefetchStartupPlugin"); const ChunkPrefetchPreloadPlugin = require("./prefetch/ChunkPrefetchPreloadPlugin"); -const AssetPrefetchPreloadPlugin = require("./runtime/AssetPrefetchPreloadPlugin"); const DataUriPlugin = require("./schemes/DataUriPlugin"); const FileUriPlugin = require("./schemes/FileUriPlugin"); @@ -225,8 +225,7 @@ class WebpackOptionsApply extends OptionsApply { } new ChunkPrefetchPreloadPlugin().apply(compiler); - new AssetPrefetchPreloadPlugin().apply(compiler); - new AssetPrefetchStartupPlugin().apply(compiler); + new AssetResourcePrefetchPlugin().apply(compiler); if (typeof options.output.chunkFormat === "string") { switch (options.output.chunkFormat) { diff --git a/lib/runtime/AssetPrefetchPreloadPlugin.js b/lib/asset/AssetResourcePrefetchPlugin.js similarity index 51% rename from lib/runtime/AssetPrefetchPreloadPlugin.js rename to lib/asset/AssetResourcePrefetchPlugin.js index 4403d96b9..6f44feab5 100644 --- a/lib/runtime/AssetPrefetchPreloadPlugin.js +++ b/lib/asset/AssetResourcePrefetchPlugin.js @@ -6,40 +6,50 @@ "use strict"; const RuntimeGlobals = require("../RuntimeGlobals"); -const AssetPrefetchPreloadRuntimeModule = require("./AssetPrefetchPreloadRuntimeModule"); +const AssetResourcePrefetchRuntimeModule = require("./AssetResourcePrefetchRuntimeModule"); /** @typedef {import("../Compiler")} Compiler */ -const PLUGIN_NAME = "AssetPrefetchPreloadPlugin"; +const PLUGIN_NAME = "AssetResourcePrefetchPlugin"; -class AssetPrefetchPreloadPlugin { +class AssetResourcePrefetchPlugin { /** * @param {Compiler} compiler the compiler * @returns {void} */ apply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - // Register runtime module for asset prefetch + // prefetchAsset compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.prefetchAsset) .tap(PLUGIN_NAME, (chunk, set) => { + set.add(RuntimeGlobals.publicPath); + set.add(RuntimeGlobals.require); + set.add(RuntimeGlobals.baseURI); + set.add(RuntimeGlobals.relativeUrl); compilation.addRuntimeModule( chunk, - new AssetPrefetchPreloadRuntimeModule("prefetch") + new AssetResourcePrefetchRuntimeModule("prefetch") ); + return true; }); - // Register runtime module for asset preload + // preloadAsset compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.preloadAsset) .tap(PLUGIN_NAME, (chunk, set) => { + set.add(RuntimeGlobals.publicPath); + set.add(RuntimeGlobals.require); + set.add(RuntimeGlobals.baseURI); + set.add(RuntimeGlobals.relativeUrl); compilation.addRuntimeModule( chunk, - new AssetPrefetchPreloadRuntimeModule("preload") + new AssetResourcePrefetchRuntimeModule("preload") ); + return true; }); }); } } -module.exports = AssetPrefetchPreloadPlugin; +module.exports = AssetResourcePrefetchPlugin; diff --git a/lib/asset/AssetResourcePrefetchRuntimeModule.js b/lib/asset/AssetResourcePrefetchRuntimeModule.js new file mode 100644 index 000000000..38ea4300f --- /dev/null +++ b/lib/asset/AssetResourcePrefetchRuntimeModule.js @@ -0,0 +1,82 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const RuntimeModule = require("../RuntimeModule"); +const Template = require("../Template"); + +/** @typedef {import("../Compilation")} Compilation */ + +class AssetResourcePrefetchRuntimeModule extends RuntimeModule { + /** + * @param {string} type "prefetch" or "preload" + */ + constructor(type) { + super(`asset ${type}`, RuntimeModule.STAGE_ATTACH); + this._type = type; + } + + /** + * @returns {string | null} runtime code + */ + generate() { + const { compilation } = this; + if (!compilation) return null; + + const { runtimeTemplate, outputOptions } = compilation; + const fnName = + this._type === "prefetch" + ? RuntimeGlobals.prefetchAsset + : RuntimeGlobals.preloadAsset; + + const crossOriginLoading = outputOptions.crossOriginLoading; + + return Template.asString([ + `${fnName} = ${runtimeTemplate.basicFunction( + "moduleId, as, fetchPriority, relative", + [ + "var url;", + "if (relative) {", + Template.indent([ + `url = new ${RuntimeGlobals.relativeUrl}(${RuntimeGlobals.require}(moduleId));` + ]), + "} else {", + Template.indent([ + `url = new URL(${RuntimeGlobals.require}(moduleId), ${RuntimeGlobals.baseURI});` + ]), + "}", + "", + "var link = document.createElement('link');", + `link.rel = '${this._type}';`, + "if (as) link.as = as;", + "link.href = url.href;", + "", + "if (fetchPriority) {", + Template.indent([ + "link.fetchPriority = fetchPriority;", + "link.setAttribute('fetchpriority', fetchPriority);" + ]), + "}", + "", + crossOriginLoading + ? Template.asString([ + "if (link.href.indexOf(window.location.origin + '/') !== 0) {", + Template.indent([ + `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};` + ]), + "}" + ]) + : "", + "", + "document.head.appendChild(link);" + ] + )};` + ]); + } +} + +module.exports = AssetResourcePrefetchRuntimeModule; diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 0f3369e25..a16218f62 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -5,6 +5,7 @@ "use strict"; +const InitFragment = require("../InitFragment"); const RuntimeGlobals = require("../RuntimeGlobals"); const RawDataUrlModule = require("../asset/RawDataUrlModule"); const { @@ -49,18 +50,9 @@ class URLDependency extends ModuleDependency { this.relative = relative || false; /** @type {Set | boolean | undefined} */ this.usedByExports = undefined; - /** @type {boolean | undefined} */ - this._startupPrefetch = undefined; - /** @type {boolean | undefined} */ this.prefetch = undefined; - /** @type {boolean | undefined} */ this.preload = undefined; - /** @type {string | undefined} */ this.fetchPriority = undefined; - /** @type {string | undefined} */ - this.preloadAs = undefined; - /** @type {string | undefined} */ - this.preloadType = undefined; } get type() { @@ -102,8 +94,6 @@ class URLDependency extends ModuleDependency { write(this.prefetch); write(this.preload); write(this.fetchPriority); - write(this.preloadAs); - write(this.preloadType); super.serialize(context); } @@ -118,8 +108,6 @@ class URLDependency extends ModuleDependency { this.prefetch = read(); this.preload = read(); this.fetchPriority = read(); - this.preloadAs = read(); - this.preloadType = read(); super.deserialize(context); } } @@ -139,9 +127,12 @@ URLDependency.Template = class URLDependencyTemplate extends ( moduleGraph, runtimeRequirements, runtimeTemplate, - runtime + runtime, + initFragments } = templateContext; const dep = /** @type {URLDependency} */ (dependency); + + const module = moduleGraph.getModule(dep); const connection = moduleGraph.getConnection(dep); // Skip rendering depending when dependency is conditional if (connection && !connection.isTargetActive(runtime)) { @@ -153,154 +144,81 @@ URLDependency.Template = class URLDependencyTemplate extends ( return; } - runtimeRequirements.add(RuntimeGlobals.require); - - // Determine if prefetch/preload hints are specified - const needsPrefetch = dep.prefetch !== undefined && dep.prefetch !== false; - const needsPreload = dep.preload !== undefined && dep.preload !== false; - - // Generate inline prefetch/preload code if not handled by startup module - if ((needsPrefetch || needsPreload) && !dep._startupPrefetch) { - // Resolve module to determine appropriate asset type - const module = moduleGraph.getModule(dep); - let asType = ""; - - if (module) { - const request = /** @type {string} */ ( - /** @type {{ request?: string }} */ (module).request || "" - ); - asType = getAssetType(request); - } - - // Get the module ID for runtime code generation - const moduleExpr = runtimeTemplate.moduleRaw({ - chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, - runtimeRequirements, - weak: false - }); - - // Construct prefetch/preload function calls - const hintCode = []; - // Validate fetchPriority against allowed values - const validFetchPriority = - dep.fetchPriority && ["high", "low", "auto"].includes(dep.fetchPriority) - ? dep.fetchPriority - : undefined; - const fetchPriority = validFetchPriority - ? `"${validFetchPriority}"` - : "undefined"; - const preloadType = dep.preloadType - ? `"${dep.preloadType}"` - : "undefined"; - - if (needsPrefetch && !needsPreload) { - // Generate prefetch call - runtimeRequirements.add(RuntimeGlobals.prefetchAsset); - hintCode.push( - `${RuntimeGlobals.prefetchAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` - ); - } else if (needsPreload) { - // Generate preload call (overrides prefetch if both specified) - runtimeRequirements.add(RuntimeGlobals.preloadAsset); - hintCode.push( - `${RuntimeGlobals.preloadAsset}(url, "${asType}", ${fetchPriority}, ${preloadType});` - ); - } - - // Create IIFE that generates URL and adds resource hints - if (dep.relative) { - runtimeRequirements.add(RuntimeGlobals.relativeUrl); - source.replace( - dep.outerRange[0], - dep.outerRange[1] - 1, - `/* asset import */ (function() { - var url = new ${RuntimeGlobals.relativeUrl}(${moduleExpr}); - ${hintCode.join("\n")} - return url; - })()` - ); - } else { - runtimeRequirements.add(RuntimeGlobals.baseURI); - source.replace( - dep.range[0], - dep.range[1] - 1, - `/* asset import */ (function() { - var url = new URL(${moduleExpr}, ${RuntimeGlobals.baseURI}); - ${hintCode.join("\n")} - return url; - })(), ${RuntimeGlobals.baseURI}` - ); - } - } else if ((needsPrefetch || needsPreload) && dep._startupPrefetch) { - // Generate standard URL when prefetch/preload is handled by startup module - if (dep.relative) { - runtimeRequirements.add(RuntimeGlobals.relativeUrl); - source.replace( - dep.outerRange[0], - dep.outerRange[1] - 1, - `/* asset import */ new ${ - RuntimeGlobals.relativeUrl - }(${runtimeTemplate.moduleRaw({ - chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, - runtimeRequirements, - weak: false - })})` - ); - } else { - runtimeRequirements.add(RuntimeGlobals.baseURI); - source.replace( - dep.range[0], - dep.range[1] - 1, - `/* asset import */ ${runtimeTemplate.moduleRaw({ - chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, - runtimeRequirements, - weak: false - })}, ${RuntimeGlobals.baseURI}` - ); - } - // Register runtime requirements for prefetch/preload functions - if (needsPrefetch && !needsPreload) { - runtimeRequirements.add(RuntimeGlobals.prefetchAsset); - } else if (needsPreload) { - runtimeRequirements.add(RuntimeGlobals.preloadAsset); - } - } else if (dep.relative) { - // Standard URL generation without resource hints + // Standard URL generation + if (dep.relative) { runtimeRequirements.add(RuntimeGlobals.relativeUrl); source.replace( dep.outerRange[0], dep.outerRange[1] - 1, - `/* asset import */ new ${ - RuntimeGlobals.relativeUrl - }(${runtimeTemplate.moduleRaw({ - chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, - runtimeRequirements, - weak: false - })})` + `/* asset import */ new ${RuntimeGlobals.relativeUrl}(${runtimeTemplate.moduleRaw( + { + chunkGraph, + module, + request: dep.request, + runtimeRequirements, + weak: false + } + )})` ); } else { runtimeRequirements.add(RuntimeGlobals.baseURI); - source.replace( dep.range[0], dep.range[1] - 1, `/* asset import */ ${runtimeTemplate.moduleRaw({ chunkGraph, - module: moduleGraph.getModule(dep), + module, request: dep.request, runtimeRequirements, weak: false })}, ${RuntimeGlobals.baseURI}` ); } + + // Prefetch/Preload via InitFragment + if ((dep.prefetch || dep.preload) && module) { + const request = dep.request; + const assetType = getAssetType(request); + const id = chunkGraph.getModuleId(module); + if (id !== null) { + const moduleId = runtimeTemplate.moduleId({ + module, + chunkGraph, + request: dep.request, + weak: false + }); + + if (dep.preload) { + runtimeRequirements.add(RuntimeGlobals.preloadAsset); + initFragments.push( + new InitFragment( + `${RuntimeGlobals.preloadAsset}(${moduleId}, ${JSON.stringify( + assetType + )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ + dep.relative + });\n`, + InitFragment.STAGE_CONSTANTS, + -10, + `asset_preload_${moduleId}` + ) + ); + } else if (dep.prefetch) { + runtimeRequirements.add(RuntimeGlobals.prefetchAsset); + initFragments.push( + new InitFragment( + `${RuntimeGlobals.prefetchAsset}(${moduleId}, ${JSON.stringify( + assetType + )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ + dep.relative + });\n`, + InitFragment.STAGE_CONSTANTS, + -5, + `asset_prefetch_${moduleId}` + ) + ); + } + } + } } }; diff --git a/lib/dependencies/WorkerPlugin.js b/lib/dependencies/WorkerPlugin.js index 26d7e225c..64bb80eb9 100644 --- a/lib/dependencies/WorkerPlugin.js +++ b/lib/dependencies/WorkerPlugin.js @@ -371,6 +371,60 @@ class WorkerPlugin { entryOptions.name = importOptions.webpackChunkName; } } + + // Support webpackPrefetch (true | number) + if (importOptions.webpackPrefetch !== undefined) { + if (importOptions.webpackPrefetch === true) { + groupOptions.prefetchOrder = 0; + } else if (typeof importOptions.webpackPrefetch === "number") { + groupOptions.prefetchOrder = importOptions.webpackPrefetch; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` expected true or a number, but received: ${importOptions.webpackPrefetch}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // Support webpackPreload (true | number) + if (importOptions.webpackPreload !== undefined) { + if (importOptions.webpackPreload === true) { + groupOptions.preloadOrder = 0; + } else if (typeof importOptions.webpackPreload === "number") { + groupOptions.preloadOrder = importOptions.webpackPreload; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // Support webpackFetchPriority ("high" | "low" | "auto") + if (importOptions.webpackFetchPriority !== undefined) { + if ( + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes( + importOptions.webpackFetchPriority + ) + ) { + groupOptions.fetchPriority = + /** @type {"auto" | "high" | "low"} */ ( + importOptions.webpackFetchPriority + ); + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } } if ( @@ -407,7 +461,7 @@ class WorkerPlugin { entryOptions: { chunkLoading: this._chunkLoading, wasmLoading: this._wasmLoading, - ...entryOptions + runtime: entryOptions.runtime } }); block.loc = expr.loc; diff --git a/lib/prefetch/AssetPrefetchStartupPlugin.js b/lib/prefetch/AssetPrefetchStartupPlugin.js deleted file mode 100644 index 1204c6c62..000000000 --- a/lib/prefetch/AssetPrefetchStartupPlugin.js +++ /dev/null @@ -1,178 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ - -"use strict"; - -const RuntimeGlobals = require("../RuntimeGlobals"); -const getAssetType = require("../util/assetType"); -const AssetPrefetchStartupRuntimeModule = require("./AssetPrefetchStartupRuntimeModule"); - -/** @typedef {import("../Chunk")} Chunk */ -/** @typedef {import("../Compiler")} Compiler */ -/** @typedef {import("../Module")} Module */ -/** @typedef {import("../NormalModule")} NormalModule */ -/** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */ - -/** - * @typedef {object} AssetInfo - * @property {string} url - * @property {string} as - * @property {string=} fetchPriority - * @property {string=} type - */ - -/** - * @typedef {object} AssetPrefetchInfo - * @property {AssetInfo[]} prefetch - * @property {AssetInfo[]} preload - */ - -const PLUGIN_NAME = "AssetPrefetchStartupPlugin"; - -class AssetPrefetchStartupPlugin { - /** - * @param {Compiler} compiler the compiler - * @returns {void} - */ - apply(compiler) { - compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { - const assetPrefetchMap = new WeakMap(); - const chunkAssetInfoMap = new WeakMap(); - - // Collect URLDependencies with prefetch/preload hints during module finalization - compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => { - for (const module of modules) { - if (!module.dependencies) continue; - - // Find all URL dependencies that have prefetch or preload hints - const assetDeps = []; - for (const dep of module.dependencies) { - if (dep.constructor.name === "URLDependency") { - const urlDep = - /** @type {import("../dependencies/URLDependency")} */ (dep); - if (urlDep.prefetch || urlDep.preload) { - assetDeps.push(urlDep); - } - } - } - - if (assetDeps.length > 0) { - assetPrefetchMap.set(module, assetDeps); - } - } - }); - - // Aggregate prefetch/preload assets by chunk during optimization - compilation.hooks.optimizeChunks.tap( - { name: PLUGIN_NAME, stage: 1 }, - (chunks) => { - const chunkGraph = compilation.chunkGraph; - const moduleGraph = compilation.moduleGraph; - - for (const chunk of chunks) { - const assetInfo = { - prefetch: /** @type {AssetInfo[]} */ ([]), - preload: /** @type {AssetInfo[]} */ ([]) - }; - - // Iterate through all modules in the chunk - for (const module of chunkGraph.getChunkModules(chunk)) { - const urlDeps = assetPrefetchMap.get(module); - if (!urlDeps) continue; - - for (const dep of urlDeps) { - // Flag this dependency as handled by startup module to prevent inline generation - dep._startupPrefetch = true; - - const resolvedModule = moduleGraph.getModule(dep); - if (!resolvedModule) continue; - - const request = /** @type {{ request?: string }} */ ( - resolvedModule - ).request; - if (!request) continue; - - // Extract the asset filename from module metadata - let assetUrl; - if ( - resolvedModule.buildInfo && - resolvedModule.buildInfo.filename - ) { - assetUrl = resolvedModule.buildInfo.filename; - } else { - // Fall back to filename from request path - assetUrl = request.split(/[\\/]/).pop() || request; - } - - const assetType = getAssetType(request); - const info = { - url: assetUrl, - as: assetType, - fetchPriority: dep.fetchPriority, - type: dep.preloadType - }; - - if (dep.prefetch && !dep.preload) { - assetInfo.prefetch.push(info); - } else if (dep.preload) { - assetInfo.preload.push(info); - } - } - } - - if (assetInfo.prefetch.length > 0 || assetInfo.preload.length > 0) { - const existing = chunkAssetInfoMap.get(chunk); - if (!existing) { - chunkAssetInfoMap.set(chunk, assetInfo); - } else { - existing.prefetch.push(...assetInfo.prefetch); - existing.preload.push(...assetInfo.preload); - } - } - } - } - ); - - compilation.hooks.additionalChunkRuntimeRequirements.tap( - PLUGIN_NAME, - (chunk, set) => { - const assetInfo = chunkAssetInfoMap.get(chunk); - if (!assetInfo) return; - - const { prefetch, preload } = assetInfo; - - if (prefetch.length > 0) { - set.add(RuntimeGlobals.prefetchAsset); - } - - if (preload.length > 0) { - set.add(RuntimeGlobals.preloadAsset); - } - - if (prefetch.length > 0 || preload.length > 0) { - compilation.addRuntimeModule( - chunk, - new AssetPrefetchStartupRuntimeModule(assetInfo) - ); - } - } - ); - - compilation.hooks.runtimeRequirementInTree - .for(RuntimeGlobals.prefetchAsset) - .tap(PLUGIN_NAME, (chunk, set) => { - set.add(RuntimeGlobals.publicPath); - }); - - compilation.hooks.runtimeRequirementInTree - .for(RuntimeGlobals.preloadAsset) - .tap(PLUGIN_NAME, (chunk, set) => { - set.add(RuntimeGlobals.publicPath); - }); - }); - } -} - -module.exports = AssetPrefetchStartupPlugin; diff --git a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js b/lib/prefetch/AssetPrefetchStartupRuntimeModule.js deleted file mode 100644 index 94aa06777..000000000 --- a/lib/prefetch/AssetPrefetchStartupRuntimeModule.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ - -"use strict"; - -const RuntimeGlobals = require("../RuntimeGlobals"); -const RuntimeModule = require("../RuntimeModule"); -const Template = require("../Template"); - -/** @typedef {import("../Chunk")} Chunk */ -/** @typedef {import("../Compilation")} Compilation */ - -/** - * @typedef {object} AssetInfo - * @property {string} url - * @property {string} as - * @property {string=} fetchPriority - * @property {string=} type - */ - -/** - * @typedef {object} AssetPrefetchInfo - * @property {AssetInfo[]} prefetch - * @property {AssetInfo[]} preload - */ - -class AssetPrefetchStartupRuntimeModule extends RuntimeModule { - /** - * @param {AssetPrefetchInfo} assetInfo asset prefetch/preload information - */ - constructor(assetInfo) { - super("asset prefetch", RuntimeModule.STAGE_TRIGGER); - this.assetInfo = assetInfo; - } - - /** - * @returns {string} a unique identifier of the module - */ - identifier() { - return `webpack/runtime/asset-prefetch-startup|${JSON.stringify( - this.assetInfo - )}`; - } - - /** - * @returns {string | null} runtime code - */ - generate() { - const { assetInfo } = this; - const compilation = /** @type {Compilation} */ (this.compilation); - const { runtimeTemplate } = compilation; - - const lines = []; - - /** - * @param {AssetInfo} asset asset info object - * @returns {string} serialized function arguments - */ - const serializeAsset = (asset) => { - const args = [ - `${RuntimeGlobals.publicPath} + ${JSON.stringify(asset.url)}`, - `"${asset.as}"` - ]; - - if (asset.fetchPriority) { - args.push(`"${asset.fetchPriority}"`); - } else { - args.push("undefined"); - } - - if (asset.type) { - args.push(`"${asset.type}"`); - } - - return args.join(", "); - }; - - if (assetInfo.prefetch.length > 0) { - const prefetchCode = - assetInfo.prefetch.length <= 2 - ? assetInfo.prefetch.map( - (asset) => - `${RuntimeGlobals.prefetchAsset}(${serializeAsset(asset)});` - ) - : Template.asString([ - `[${assetInfo.prefetch - .map( - (asset) => - `{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify( - asset.url - )}, as: "${asset.as}"${ - asset.fetchPriority - ? `, fetchPriority: "${asset.fetchPriority}"` - : "" - }${asset.type ? `, type: "${asset.type}"` : ""} }` - ) - .join(", ")}].forEach(${runtimeTemplate.basicFunction("asset", [ - `${RuntimeGlobals.prefetchAsset}(asset.url, asset.as, asset.fetchPriority, asset.type);` - ])});` - ]); - - if (Array.isArray(prefetchCode)) { - lines.push(...prefetchCode); - } else { - lines.push(prefetchCode); - } - } - - if (assetInfo.preload.length > 0) { - const preloadCode = - assetInfo.preload.length <= 2 - ? assetInfo.preload.map( - (asset) => - `${RuntimeGlobals.preloadAsset}(${serializeAsset(asset)});` - ) - : Template.asString([ - `[${assetInfo.preload - .map( - (asset) => - `{ url: ${RuntimeGlobals.publicPath} + ${JSON.stringify( - asset.url - )}, as: "${asset.as}"${ - asset.fetchPriority - ? `, fetchPriority: "${asset.fetchPriority}"` - : "" - }${asset.type ? `, type: "${asset.type}"` : ""} }` - ) - .join(", ")}].forEach(${runtimeTemplate.basicFunction("asset", [ - `${RuntimeGlobals.preloadAsset}(asset.url, asset.as, asset.fetchPriority, asset.type);` - ])});` - ]); - - if (Array.isArray(preloadCode)) { - lines.push(...preloadCode); - } else { - lines.push(preloadCode); - } - } - - return Template.asString(lines); - } - - /** - * @returns {boolean} true, if the runtime module should get it's own scope - */ - shouldIsolate() { - return false; - } -} - -module.exports = AssetPrefetchStartupRuntimeModule; diff --git a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js b/lib/runtime/AssetPrefetchPreloadRuntimeModule.js deleted file mode 100644 index 6eaed6275..000000000 --- a/lib/runtime/AssetPrefetchPreloadRuntimeModule.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ - -"use strict"; - -const RuntimeGlobals = require("../RuntimeGlobals"); -const RuntimeModule = require("../RuntimeModule"); -const Template = require("../Template"); - -/** @typedef {import("../Compilation")} Compilation */ - -class AssetPrefetchPreloadRuntimeModule extends RuntimeModule { - /** - * @param {string} type "prefetch" or "preload" - */ - constructor(type) { - super(`asset ${type}`); - this._type = type; - } - - /** - * @returns {string | null} runtime code - */ - generate() { - const { compilation } = this; - if (!compilation) return null; - const { runtimeTemplate } = compilation; - const fn = - this._type === "prefetch" - ? RuntimeGlobals.prefetchAsset - : RuntimeGlobals.preloadAsset; - - return Template.asString([ - `${fn} = ${runtimeTemplate.basicFunction("url, as, fetchPriority, type", [ - "var link = document.createElement('link');", - this._type === "prefetch" - ? "link.rel = 'prefetch';" - : "link.rel = 'preload';", - "if(as) link.as = as;", - "if(type) link.type = type;", - "link.href = url;", - "if(fetchPriority) {", - Template.indent([ - "link.fetchPriority = fetchPriority;", - "link.setAttribute('fetchpriority', fetchPriority);" - ]), - "}", - // Apply nonce attribute for CSP if configured - compilation.outputOptions.crossOriginLoading - ? Template.asString([ - `if(${RuntimeGlobals.scriptNonce}) {`, - Template.indent( - `link.setAttribute('nonce', ${RuntimeGlobals.scriptNonce});` - ), - "}" - ]) - : "", - "document.head.appendChild(link);" - ])};` - ]); - } -} - -module.exports = AssetPrefetchPreloadRuntimeModule; diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 7588dbaad..3576b1e0f 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -184,14 +184,12 @@ class URLParserPlugin { relative ); dep.loc = /** @type {DependencyLocation} */ (expr.loc); - - // Process magic comments for prefetch/preload hints + // Parse magic comments with simplified rules if (importOptions) { - // webpackPrefetch should be boolean true - if ( - importOptions.webpackPrefetch !== undefined && - importOptions.webpackPrefetch !== true - ) { + // Accept only boolean true for webpackPrefetch + if (importOptions.webpackPrefetch === true) { + dep.prefetch = true; + } else if (importOptions.webpackPrefetch !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackPrefetch\` expected true, but received: ${importOptions.webpackPrefetch}.`, @@ -200,11 +198,10 @@ class URLParserPlugin { ); } - // webpackPreload should be boolean true - if ( - importOptions.webpackPreload !== undefined && - importOptions.webpackPreload !== true - ) { + // Accept only boolean true for webpackPreload + if (importOptions.webpackPreload === true) { + dep.preload = true; + } else if (importOptions.webpackPreload !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackPreload\` expected true, but received: ${importOptions.webpackPreload}.`, @@ -213,14 +210,13 @@ class URLParserPlugin { ); } - // webpackFetchPriority should be one of: high, low, auto + // webpackFetchPriority: "high" | "low" | "auto" if ( - importOptions.webpackFetchPriority !== undefined && - (typeof importOptions.webpackFetchPriority !== "string" || - !["high", "low", "auto"].includes( - importOptions.webpackFetchPriority - )) + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes(importOptions.webpackFetchPriority) ) { + dep.fetchPriority = importOptions.webpackFetchPriority; + } else if (importOptions.webpackFetchPriority !== undefined) { parser.state.module.addWarning( new UnsupportedFeatureWarning( `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, @@ -228,39 +224,6 @@ class URLParserPlugin { ) ); } - - // webpackPreloadAs should be a string - if ( - importOptions.webpackPreloadAs !== undefined && - typeof importOptions.webpackPreloadAs !== "string" - ) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPreloadAs\` expected a string, but received: ${importOptions.webpackPreloadAs}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - - // webpackPreloadType should be a string - if ( - importOptions.webpackPreloadType !== undefined && - typeof importOptions.webpackPreloadType !== "string" - ) { - parser.state.module.addWarning( - new UnsupportedFeatureWarning( - `\`webpackPreloadType\` expected a string, but received: ${importOptions.webpackPreloadType}.`, - /** @type {DependencyLocation} */ (expr.loc) - ) - ); - } - - // Store magic comment values on dependency - dep.prefetch = importOptions.webpackPrefetch; - dep.preload = importOptions.webpackPreload; - dep.fetchPriority = importOptions.webpackFetchPriority; - dep.preloadAs = importOptions.webpackPreloadAs; - dep.preloadType = importOptions.webpackPreloadType; } // Register the dependency diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js index 0053ffe74..b741df1e0 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -5,7 +5,4 @@ // Invalid fetchPriority value - should generate warning const invalidPriorityUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "invalid" */ "./assets/images/priority-invalid.png", import.meta.url); -// Invalid webpackPreloadType value - should generate warning -const invalidTypeUrl = new URL(/* webpackPreload: true */ /* webpackPreloadType: 123 */ "./assets/styles/invalid-type.css", import.meta.url); - export default {}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js index 1158ad239..93e0f8760 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -3,11 +3,11 @@ function verifyLink(link, expectations) { expect(link._type).toBe("link"); expect(link.rel).toBe(expectations.rel); - + if (expectations.as) { expect(link.as).toBe(expectations.as); } - + if (expectations.fetchPriority !== undefined) { if (expectations.fetchPriority) { expect(link._attributes.fetchpriority).toBe(expectations.fetchPriority); @@ -17,11 +17,8 @@ function verifyLink(link, expectations) { expect(link.fetchPriority).toBeUndefined(); } } - - if (expectations.type) { - expect(link.type).toBe(expectations.type); - } - + + if (expectations.href) { expect(link.href.toString()).toMatch(expectations.href); } @@ -44,11 +41,6 @@ it("should generate all prefetch and preload links", () => { "./priority-auto.js", import.meta.url ), - preloadTyped: new URL( - /* webpackPreload: true */ /* webpackPreloadType: "text/css" */ - "./assets/styles/typed.css", - import.meta.url - ), bothHints: new URL( /* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackFetchPriority: "high" */ "./assets/images/both-hints.png", @@ -65,7 +57,7 @@ it("should generate all prefetch and preload links", () => { import.meta.url ) }; - + const prefetchHighLink = document.head._children.find( link => link.href.includes("priority-high.png") && link.rel === "prefetch" ); @@ -76,7 +68,7 @@ it("should generate all prefetch and preload links", () => { fetchPriority: "high", href: /priority-high\.png$/ }); - + const preloadLowLink = document.head._children.find( link => link.href.includes("priority-low.css") && link.rel === "preload" ); @@ -87,7 +79,7 @@ it("should generate all prefetch and preload links", () => { fetchPriority: "low", href: /priority-low\.css$/ }); - + const prefetchAutoLink = document.head._children.find( link => link.href.includes("priority-auto.js") && link.rel === "prefetch" ); @@ -97,28 +89,17 @@ it("should generate all prefetch and preload links", () => { as: "script", fetchPriority: "auto" }); - - const preloadTypedLink = document.head._children.find( - link => link.href.includes("typed.css") && link.rel === "preload" - ); - expect(preloadTypedLink).toBeTruthy(); - verifyLink(preloadTypedLink, { - rel: "preload", - as: "style", - type: "text/css", - href: /typed\.css$/ - }); - + const bothHintsLink = document.head._children.find( link => link.href.includes("both-hints.png") ); expect(bothHintsLink).toBeTruthy(); expect(bothHintsLink.rel).toBe("preload"); expect(bothHintsLink._attributes.fetchpriority).toBe("high"); - + const noPriorityLink = document.head._children.find( - link => link.href.includes("test.png") && link.rel === "prefetch" && - !link._attributes.fetchpriority + link => link.href.includes("test.png") && link.rel === "prefetch" && + !link._attributes.fetchpriority ); expect(noPriorityLink).toBeTruthy(); verifyLink(noPriorityLink, { @@ -126,7 +107,7 @@ it("should generate all prefetch and preload links", () => { as: "image", fetchPriority: undefined }); - + const fontPreloadLink = document.head._children.find( link => link.href.includes("test.woff2") && link.rel === "preload" ); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js index c5391ce61..80ef2b4e7 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -22,6 +22,7 @@ const mockCreateElement = (tagName) => { element.rel = ""; element.as = ""; element.href = ""; + element.type = undefined; element.fetchPriority = undefined; } else if (tagName === "script") { element.src = ""; @@ -60,33 +61,5 @@ module.exports = { moduleScope(scope) { // Make document available in the module scope scope.document = global.document; - // Inject runtime globals that would normally be provided by webpack - scope.__webpack_require__ = { - PA(url, as, fetchPriority, type) { - const link = global.document.createElement("link"); - link.rel = "prefetch"; - if (as) link.as = as; - if (type) link.type = type; - link.href = url; - if (fetchPriority) { - link.fetchPriority = fetchPriority; - link.setAttribute("fetchpriority", fetchPriority); - } - global.document.head.appendChild(link); - }, - LA(url, as, fetchPriority, type) { - const link = global.document.createElement("link"); - link.rel = "preload"; - if (as) link.as = as; - if (type) link.type = type; - link.href = url; - if (fetchPriority) { - link.fetchPriority = fetchPriority; - link.setAttribute("fetchpriority", fetchPriority); - } - global.document.head.appendChild(link); - }, - b: "https://test.example.com/" // baseURI - }; } }; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js index c41bfc5f8..eb4c2050e 100644 --- a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -4,7 +4,5 @@ module.exports = [ // Invalid fetchPriority value warning [ /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ - ], - // Invalid webpackPreloadType value warning - [/`webpackPreloadType` expected a string, but received: 123\./] + ] ];