diff --git a/cspell.json b/cspell.json index 81ff64131..10583e63a 100644 --- a/cspell.json +++ b/cspell.json @@ -90,6 +90,7 @@ "externref", "fetchpriority", "filebase", + "flac", "fileoverview", "filepath", "finalizer", diff --git a/lib/RuntimeGlobals.js b/lib/RuntimeGlobals.js index 0cbcc02f9..9ab25b6ef 100644 --- a/lib/RuntimeGlobals.js +++ b/lib/RuntimeGlobals.js @@ -312,6 +312,11 @@ module.exports.nodeModuleDecorator = "__webpack_require__.nmd"; */ module.exports.onChunksLoaded = "__webpack_require__.O"; +/** + * the asset prefetch function + */ +module.exports.prefetchAsset = "__webpack_require__.PA"; + /** * the chunk prefetch function */ @@ -322,6 +327,11 @@ module.exports.prefetchChunk = "__webpack_require__.E"; */ module.exports.prefetchChunkHandlers = "__webpack_require__.F"; +/** + * the asset preload function + */ +module.exports.preloadAsset = "__webpack_require__.LA"; + /** * the chunk preload function */ diff --git a/lib/WebpackOptionsApply.js b/lib/WebpackOptionsApply.js index f9efab230..0d6e5f754 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"); @@ -200,6 +202,21 @@ class WebpackOptionsApply extends OptionsApply { new ChunkPrefetchPreloadPlugin().apply(compiler); + // Apply AssetResourcePrefetchPlugin only for web targets or universal targets + // Check if we're targeting web environment + const externalsPresets = options.externalsPresets || {}; + const isTargetingWeb = Boolean( + externalsPresets.web || + externalsPresets.webAsync || + externalsPresets.electronRenderer + ); + + // Apply the plugin if we're targeting web environment + // For universal targets (["web", "node"]), the runtime module will handle platform detection using isNeutralPlatform + if (isTargetingWeb || !externalsPresets.node) { + new AssetResourcePrefetchPlugin().apply(compiler); + } + if (typeof options.output.chunkFormat === "string") { switch (options.output.chunkFormat) { case "array-push": { diff --git a/lib/asset/AssetResourcePrefetchPlugin.js b/lib/asset/AssetResourcePrefetchPlugin.js new file mode 100644 index 000000000..17cf65a70 --- /dev/null +++ b/lib/asset/AssetResourcePrefetchPlugin.js @@ -0,0 +1,55 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const ResourcePrefetchRuntimeModule = require("../prefetch/ResourcePrefetchRuntimeModule"); + +/** @typedef {import("../Compiler")} Compiler */ + +const PLUGIN_NAME = "AssetResourcePrefetchPlugin"; + +class AssetResourcePrefetchPlugin { + /** + * @param {Compiler} compiler the compiler + * @returns {void} + */ + apply(compiler) { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + // prefetchAsset + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.prefetchAsset) + .tap(PLUGIN_NAME, (chunk, set) => { + set.add(RuntimeGlobals.publicPath); + set.add(RuntimeGlobals.require); + set.add(RuntimeGlobals.baseURI); + set.add(RuntimeGlobals.relativeUrl); + compilation.addRuntimeModule( + chunk, + new ResourcePrefetchRuntimeModule("prefetch") + ); + return true; + }); + + // preloadAsset + compilation.hooks.runtimeRequirementInTree + .for(RuntimeGlobals.preloadAsset) + .tap(PLUGIN_NAME, (chunk, set) => { + set.add(RuntimeGlobals.publicPath); + set.add(RuntimeGlobals.require); + set.add(RuntimeGlobals.baseURI); + set.add(RuntimeGlobals.relativeUrl); + compilation.addRuntimeModule( + chunk, + new ResourcePrefetchRuntimeModule("preload") + ); + return true; + }); + }); + } +} + +module.exports = AssetResourcePrefetchPlugin; diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index d1b03f74c..1e7d69d5e 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 { @@ -43,6 +44,15 @@ class URLDependency extends ModuleDependency { this.relative = relative || false; /** @type {UsedByExports | undefined} */ this.usedByExports = undefined; + this.prefetch = undefined; + this.preload = undefined; + this.fetchPriority = undefined; + /** @type {string|undefined} */ + this.preloadAs = undefined; + /** @type {string|undefined} */ + this.preloadType = undefined; + /** @type {string|undefined} */ + this.preloadMedia = undefined; } get type() { @@ -81,6 +91,12 @@ class URLDependency extends ModuleDependency { write(this.outerRange); write(this.relative); write(this.usedByExports); + write(this.prefetch); + write(this.preload); + write(this.fetchPriority); + write(this.preloadAs); + write(this.preloadType); + write(this.preloadMedia); super.serialize(context); } @@ -92,6 +108,12 @@ class URLDependency extends ModuleDependency { this.outerRange = read(); this.relative = read(); this.usedByExports = read(); + this.prefetch = read(); + this.preload = read(); + this.fetchPriority = read(); + this.preloadAs = read(); + this.preloadType = read(); + this.preloadMedia = read(); super.deserialize(context); } } @@ -99,6 +121,32 @@ class URLDependency extends ModuleDependency { URLDependency.Template = class URLDependencyTemplate extends ( ModuleDependency.Template ) { + /** + * Determines the 'as' attribute value for prefetch/preload based on file extension + * https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload#what_types_of_content_can_be_preloaded + * @param {string} request module request string or filename + * @returns {string} asset type for link element 'as' attribute + */ + static _getAssetType(request) { + if (/\.(png|jpe?g|gif|svg|webp|avif|bmp|ico|tiff?)$/i.test(request)) { + return "image"; + } else if (/\.(woff2?|ttf|otf|eot)$/i.test(request)) { + return "font"; + } else if (/\.(js|mjs|jsx|ts|tsx)$/i.test(request)) { + return "script"; + } else if (/\.css$/i.test(request)) { + return "style"; + } else if (/\.vtt$/i.test(request)) { + return "track"; + } else if ( + /\.(mp4|webm|ogg|mp3|wav|flac|aac|m4a|avi|mov|wmv|mkv)$/i.test(request) + ) { + // Audio/video files use 'fetch' as browser support varies + return "fetch"; + } + return "fetch"; + } + /** * @param {Dependency} dependency the dependency for which the template should be applied * @param {ReplaceSource} source the current replace source which can be modified @@ -111,9 +159,12 @@ URLDependency.Template = class URLDependencyTemplate extends ( moduleGraph, runtimeRequirements, runtimeTemplate, - runtime + runtime, + initFragments } = templateContext; const dep = /** @type {URLDependency} */ (dependency); + + const module = moduleGraph.getModule(dep); const connection = moduleGraph.getConnection(dep); // Skip rendering depending when dependency is conditional if (connection && !connection.isTargetActive(runtime)) { @@ -125,38 +176,87 @@ URLDependency.Template = class URLDependencyTemplate extends ( return; } - runtimeRequirements.add(RuntimeGlobals.require); - + // Standard URL generation if (dep.relative) { runtimeRequirements.add(RuntimeGlobals.relativeUrl); source.replace( dep.outerRange[0], dep.outerRange[1] - 1, - `/* asset import */ new ${ - RuntimeGlobals.relativeUrl - }(${runtimeTemplate.moduleRaw({ - chunkGraph, - module: moduleGraph.getModule(dep), - request: dep.request, - runtimeRequirements, - weak: false - })})` + `/* asset import */ new ${RuntimeGlobals.relativeUrl}(${runtimeTemplate.moduleRaw( + { + chunkGraph, + module, + request: dep.request, + runtimeRequirements, + weak: false + } + )})` ); } else { runtimeRequirements.add(RuntimeGlobals.baseURI); - source.replace( dep.range[0], dep.range[1] - 1, `/* asset import */ ${runtimeTemplate.moduleRaw({ chunkGraph, - module: moduleGraph.getModule(dep), + module, request: dep.request, runtimeRequirements, weak: false })}, ${RuntimeGlobals.baseURI}` ); } + + // Prefetch/Preload via InitFragment + if ((dep.prefetch || dep.preload) && module) { + const request = dep.request; + const detectedAssetType = URLDependencyTemplate._getAssetType(request); + const id = chunkGraph.getModuleId(module); + if (id !== null) { + const moduleId = runtimeTemplate.moduleId({ + module, + chunkGraph, + request: dep.request, + weak: false + }); + + if (dep.preload) { + runtimeRequirements.add(RuntimeGlobals.preloadAsset); + const asArg = JSON.stringify(dep.preloadAs || detectedAssetType); + const fetchPriorityArg = dep.fetchPriority + ? JSON.stringify(dep.fetchPriority) + : "undefined"; + const typeArg = dep.preloadType + ? JSON.stringify(dep.preloadType) + : "undefined"; + const mediaArg = dep.preloadMedia + ? JSON.stringify(dep.preloadMedia) + : "undefined"; + initFragments.push( + new InitFragment( + `${RuntimeGlobals.preloadAsset}(${moduleId}, ${asArg}, ${fetchPriorityArg}, ${typeArg}, ${mediaArg}, ${dep.relative});\n`, + InitFragment.STAGE_CONSTANTS, + -10, + `asset_preload_${moduleId}` + ) + ); + } else if (dep.prefetch) { + runtimeRequirements.add(RuntimeGlobals.prefetchAsset); + const asArg = JSON.stringify(detectedAssetType); + const fetchPriorityArg = dep.fetchPriority + ? JSON.stringify(dep.fetchPriority) + : "undefined"; + initFragments.push( + new InitFragment( + `${RuntimeGlobals.prefetchAsset}(${moduleId}, ${asArg}, ${fetchPriorityArg}, undefined, undefined, ${dep.relative});\n`, + InitFragment.STAGE_CONSTANTS, + -5, + `asset_prefetch_${moduleId}` + ) + ); + } + } + } } }; diff --git a/lib/dependencies/WorkerPlugin.js b/lib/dependencies/WorkerPlugin.js index 8cb86a9d8..8f63eade5 100644 --- a/lib/dependencies/WorkerPlugin.js +++ b/lib/dependencies/WorkerPlugin.js @@ -38,6 +38,7 @@ const WorkerDependency = require("./WorkerDependency"); /** @typedef {import("../../declarations/WebpackOptions").WasmLoading} WasmLoading */ /** @typedef {import("../../declarations/WebpackOptions").WorkerPublicPath} WorkerPublicPath */ /** @typedef {import("../Compiler")} Compiler */ +/** @typedef {import("../ChunkGroup").RawChunkGroupOptions} RawChunkGroupOptions */ /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */ /** @typedef {import("../Entrypoint").EntryOptions} EntryOptions */ /** @typedef {import("../NormalModule")} NormalModule */ @@ -223,9 +224,12 @@ class WorkerPlugin { } } const insertType = expr.properties.length > 0 ? "comma" : "single"; - const insertLocation = /** @type {Range} */ ( - expr.properties[expr.properties.length - 1].range - )[1]; + const insertLocation = + expr.properties.length > 0 + ? /** @type {Range} */ ( + expr.properties[expr.properties.length - 1].range + )[1] + : /** @type {Range} */ (expr.range)[0] + 1; return { expressions, otherElements, @@ -299,6 +303,10 @@ class WorkerPlugin { ? /** @type {Range} */ (arg2.range) : /** @type {Range} */ (arg1.range)[1] }; + + /** @type {RawChunkGroupOptions} */ + const groupOptions = {}; + const { options: importOptions, errors: commentErrors } = parser.parseCommentOptions(/** @type {Range} */ (expr.range)); @@ -360,6 +368,60 @@ class WorkerPlugin { entryOptions.name = importOptions.webpackChunkName; } } + + // Support webpackPrefetch (true | number) + if (importOptions.webpackPrefetch !== undefined) { + if (importOptions.webpackPrefetch === true) { + groupOptions.prefetchOrder = 0; + } else if (typeof importOptions.webpackPrefetch === "number") { + groupOptions.prefetchOrder = importOptions.webpackPrefetch; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` expected true or a number, but received: ${importOptions.webpackPrefetch}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // Support webpackPreload (true | number) + if (importOptions.webpackPreload !== undefined) { + if (importOptions.webpackPreload === true) { + groupOptions.preloadOrder = 0; + } else if (typeof importOptions.webpackPreload === "number") { + groupOptions.preloadOrder = importOptions.webpackPreload; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` expected true or a number, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // Support webpackFetchPriority ("high" | "low" | "auto") + if (importOptions.webpackFetchPriority !== undefined) { + if ( + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes( + importOptions.webpackFetchPriority + ) + ) { + groupOptions.fetchPriority = + /** @type {"auto" | "high" | "low"} */ ( + importOptions.webpackFetchPriority + ); + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } } if ( @@ -388,6 +450,7 @@ class WorkerPlugin { } const block = new AsyncDependenciesBlock({ + ...groupOptions, name: entryOptions.name, entryOptions: { chunkLoading: this._chunkLoading, diff --git a/lib/prefetch/ResourcePrefetchRuntimeModule.js b/lib/prefetch/ResourcePrefetchRuntimeModule.js new file mode 100644 index 000000000..cd9603aec --- /dev/null +++ b/lib/prefetch/ResourcePrefetchRuntimeModule.js @@ -0,0 +1,105 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); +const RuntimeModule = require("../RuntimeModule"); +const Template = require("../Template"); + +/** @typedef {import("../Compilation")} Compilation */ + +class ResourcePrefetchRuntimeModule extends RuntimeModule { + /** + * @param {string} type "prefetch" or "preload" + */ + constructor(type) { + super(`asset ${type}`, RuntimeModule.STAGE_ATTACH); + this._type = type; + } + + /** + * @returns {string | null} runtime code + */ + generate() { + const { compilation } = this; + if (!compilation) return null; + + const { runtimeTemplate, outputOptions } = compilation; + const fnName = + this._type === "prefetch" + ? RuntimeGlobals.prefetchAsset + : RuntimeGlobals.preloadAsset; + + const crossOriginLoading = outputOptions.crossOriginLoading; + const isNeutralPlatform = runtimeTemplate.isNeutralPlatform(); + + // For neutral platform (universal targets), generate code that checks for document at runtime + const code = [ + "var url;", + "if (relative) {", + Template.indent([ + `url = new ${RuntimeGlobals.relativeUrl}(${RuntimeGlobals.require}(moduleId));` + ]), + "} else {", + Template.indent([ + `url = new URL(${RuntimeGlobals.require}(moduleId), ${RuntimeGlobals.baseURI});` + ]), + "}", + "", + "var link = document.createElement('link');", + `link.rel = '${this._type}';`, + "if (as) link.as = as;", + "link.href = url.href;", + "", + "if (fetchPriority) {", + Template.indent([ + "link.fetchPriority = fetchPriority;", + "link.setAttribute('fetchpriority', fetchPriority);" + ]), + "}", + "", + "if (type) link.type = type;", + "if (media) link.media = media;", + "", + crossOriginLoading + ? Template.asString([ + "if (link.href.indexOf(window.location.origin + '/') !== 0) {", + Template.indent([ + `link.crossOrigin = ${JSON.stringify(crossOriginLoading)};` + ]), + "}" + ]) + : "", + "", + "document.head.appendChild(link);" + ]; + + // For neutral platform, wrap the code to check for document availability + if (isNeutralPlatform) { + return Template.asString([ + `${fnName} = ${runtimeTemplate.basicFunction( + "moduleId, as, fetchPriority, type, media, relative", + [ + "// Only execute in browser environment", + "if (typeof document !== 'undefined') {", + Template.indent(code), + "}" + ] + )};` + ]); + } + + // For browser-only targets, generate code without the check + return Template.asString([ + `${fnName} = ${runtimeTemplate.basicFunction( + "moduleId, as, fetchPriority, type, media, relative", + code + )};` + ]); + } +} + +module.exports = ResourcePrefetchRuntimeModule; diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 4140567cb..373bb4804 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -182,6 +182,91 @@ class URLParserPlugin { relative ); dep.loc = /** @type {DependencyLocation} */ (expr.loc); + // Parse magic comments with simplified rules + if (importOptions) { + // Accept only boolean true for webpackPrefetch + if (importOptions.webpackPrefetch === true) { + dep.prefetch = true; + } else if (importOptions.webpackPrefetch !== undefined) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPrefetch\` expected true, but received: ${importOptions.webpackPrefetch}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + + // Accept only boolean true for webpackPreload + if (importOptions.webpackPreload === true) { + dep.preload = true; + } else if (importOptions.webpackPreload !== undefined) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreload\` expected true, but received: ${importOptions.webpackPreload}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + + // webpackFetchPriority: "high" | "low" | "auto" + if ( + typeof importOptions.webpackFetchPriority === "string" && + ["high", "low", "auto"].includes(importOptions.webpackFetchPriority) + ) { + dep.fetchPriority = importOptions.webpackFetchPriority; + } else if (importOptions.webpackFetchPriority !== undefined) { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackFetchPriority\` expected "low", "high" or "auto", but received: ${importOptions.webpackFetchPriority}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + + // webpackPreloadAs: allow override of the "as" attribute for preload + if (importOptions.webpackPreloadAs !== undefined) { + if (typeof importOptions.webpackPreloadAs === "string") { + dep.preloadAs = importOptions.webpackPreloadAs; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadAs\` expected a string, but received: ${importOptions.webpackPreloadAs}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // webpackPreloadType: set link.type when provided + if (importOptions.webpackPreloadType !== undefined) { + if (typeof importOptions.webpackPreloadType === "string") { + dep.preloadType = importOptions.webpackPreloadType; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadType\` expected a string, but received: ${importOptions.webpackPreloadType}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + + // webpackPreloadMedia: set link.media when provided + if (importOptions.webpackPreloadMedia !== undefined) { + if (typeof importOptions.webpackPreloadMedia === "string") { + dep.preloadMedia = importOptions.webpackPreloadMedia; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadMedia\` expected a string, but received: ${importOptions.webpackPreloadMedia}.`, + /** @type {DependencyLocation} */ (expr.loc) + ) + ); + } + } + } + + // Register the dependency parser.state.current.addDependency(dep); InnerGraph.onUsage(parser.state, (e) => (dep.usedByExports = e)); return true; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/fonts/test.woff2 b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/fonts/test.woff2 new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/both-hints.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/both-hints.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/override.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/override.png new file mode 100644 index 000000000..8d1c8b69c --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/override.png @@ -0,0 +1 @@ + diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-high.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-high.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-invalid.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/priority-invalid.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/test.png b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/images/test.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/scripts/worker.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/scripts/worker.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/invalid-type.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/invalid-type.css new file mode 100644 index 000000000..6fab1b6a8 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/invalid-type.css @@ -0,0 +1,3 @@ +body { + background-color: #f0f0f0; +} \ No newline at end of file diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/priority-low.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/priority-low.css new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/test.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/test.css new file mode 100644 index 000000000..e69de29bb diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/typed.css b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/typed.css new file mode 100644 index 000000000..287e8e44a --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/assets/styles/typed.css @@ -0,0 +1,4 @@ +.typed-element { + color: #333; + font-size: 16px; +} \ No newline at end of file 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 new file mode 100644 index 000000000..e19df05d1 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/generate-warnings.js @@ -0,0 +1,30 @@ +"use strict"; + +// This file is used to generate expected warnings during compilation + +// Invalid fetchPriority value - should generate warning +const invalidPriorityUrl = new URL(/* webpackPrefetch: true */ /* webpackFetchPriority: "invalid" */ "./assets/images/priority-invalid.png", import.meta.url); +// Invalid preloadAs (non-string) - should generate warning +const invalidPreloadAs = new URL( + /* webpackPreload: true */ + /* webpackPreloadAs: 123 */ + "./assets/images/priority-invalid.png", + import.meta.url +); +// Invalid preloadType (non-string) - should generate warning +const invalidPreloadType = new URL( + /* webpackPreload: true */ + /* webpackPreloadType: 123 */ + "./assets/images/priority-invalid.png", + import.meta.url +); + +// Invalid preloadMedia (non-string) - should generate warning +const invalidPreloadMedia = new URL( + /* webpackPreload: true */ + /* webpackPreloadMedia: 456 */ + "./assets/images/priority-invalid.png", + import.meta.url +); + +export default {}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js new file mode 100644 index 000000000..2ad97d3c8 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/index.js @@ -0,0 +1,178 @@ +"use strict"; + +function verifyLink(link, expectations) { + expect(link._type).toBe("link"); + expect(link.rel).toBe(expectations.rel); + + if (expectations.as) { + expect(link.as).toBe(expectations.as); + } + + if (expectations.type !== undefined) { + if (expectations.type) { + expect(link.type).toBe(expectations.type); + } else { + expect(link.type).toBeUndefined(); + } + } + + if (expectations.media !== undefined) { + if (expectations.media) { + expect(link.media).toBe(expectations.media); + } else { + expect(link.media).toBeUndefined(); + } + } + + if (expectations.fetchPriority !== undefined) { + if (expectations.fetchPriority) { + expect(link._attributes.fetchpriority).toBe(expectations.fetchPriority); + expect(link.fetchPriority).toBe(expectations.fetchPriority); + } else { + expect(link._attributes.fetchpriority).toBeUndefined(); + expect(link.fetchPriority).toBeUndefined(); + } + } + + + if (expectations.href) { + expect(link.href.toString()).toMatch(expectations.href); + } +} + +it("should generate all prefetch and preload links", () => { + const urls = { + prefetchHigh: new URL( + /* webpackPrefetch: true */ /* webpackFetchPriority: "high" */ + "./assets/images/priority-high.png", + import.meta.url + ), + preloadLow: new URL( + /* webpackPreload: true */ /* webpackFetchPriority: "low" */ + "./assets/styles/priority-low.css", + import.meta.url + ), + prefetchAuto: new URL( + /* webpackPrefetch: true */ /* webpackFetchPriority: "auto" */ + "./priority-auto.js", + import.meta.url + ), + bothHints: new URL( + /* webpackPrefetch: true */ /* webpackPreload: true */ /* webpackFetchPriority: "high" */ + "./assets/images/both-hints.png", + import.meta.url + ), + noPriority: new URL( + /* webpackPrefetch: true */ + "./assets/images/test.png", + import.meta.url + ), + preloadFont: new URL( + /* webpackPreload: true */ + "./assets/fonts/test.woff2", + import.meta.url + ) + }; + + const prefetchHighLink = document.head._children.find( + link => link.href.includes("priority-high.png") && link.rel === "prefetch" + ); + expect(prefetchHighLink).toBeTruthy(); + verifyLink(prefetchHighLink, { + rel: "prefetch", + as: "image", + fetchPriority: "high", + href: /priority-high\.png$/ + }); + + const preloadLowLink = document.head._children.find( + link => link.href.includes("priority-low.css") && link.rel === "preload" + ); + expect(preloadLowLink).toBeTruthy(); + verifyLink(preloadLowLink, { + rel: "preload", + as: "style", + fetchPriority: "low", + href: /priority-low\.css$/ + }); + + const prefetchAutoLink = document.head._children.find( + link => link.href.includes("priority-auto.js") && link.rel === "prefetch" + ); + expect(prefetchAutoLink).toBeTruthy(); + verifyLink(prefetchAutoLink, { + rel: "prefetch", + as: "script", + fetchPriority: "auto" + }); + + const bothHintsLink = document.head._children.find( + link => link.href.includes("both-hints.png") + ); + expect(bothHintsLink).toBeTruthy(); + expect(bothHintsLink.rel).toBe("preload"); + expect(bothHintsLink._attributes.fetchpriority).toBe("high"); + + const noPriorityLink = document.head._children.find( + link => link.href.includes("test.png") && link.rel === "prefetch" && + !link._attributes.fetchpriority + ); + expect(noPriorityLink).toBeTruthy(); + verifyLink(noPriorityLink, { + rel: "prefetch", + as: "image", + fetchPriority: undefined + }); + + const fontPreloadLink = document.head._children.find( + link => link.href.includes("test.woff2") && link.rel === "preload" + ); + expect(fontPreloadLink).toBeTruthy(); + verifyLink(fontPreloadLink, { + rel: "preload", + as: "font", + href: /test\.woff2$/ + }); +}); + +it("should allow overriding as/type/media via magic comments", () => { + const override = new URL( + /* webpackPreload: true */ + /* webpackPreloadAs: "font" */ + /* webpackPreloadType: "font/woff2" */ + /* webpackPreloadMedia: "(max-width: 600px)" */ + "./assets/images/override.png", + import.meta.url + ); + + const link = document.head._children.find( + l => l.href.includes("override.png") && l.rel === "preload" + ); + expect(link).toBeTruthy(); + verifyLink(link, { + rel: "preload", + as: "font", + type: "font/woff2", + media: "(max-width: 600px)", + href: /override\.png$/ + }); +}); + +it("should accept additional as tokens from Fetch Standard (e.g., sharedworker)", () => { + const u = new URL( + /* webpackPreload: true */ + /* webpackPreloadAs: "sharedworker" */ + "./priority-auto.js", + import.meta.url + ); + + const link = document.head._children.find( + l => l.href.includes("priority-auto.js") && l.rel === "preload" + ); + expect(link).toBeTruthy(); + verifyLink(link, { + rel: "preload", + as: "sharedworker", + href: /priority-auto\.js$/ + }); +}); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js new file mode 100644 index 000000000..88088be87 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/priority-auto.js @@ -0,0 +1,4 @@ +"use strict"; + +// Test asset file +console.log("priority-auto.js loaded"); 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 new file mode 100644 index 000000000..d7e9b5768 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.config.js @@ -0,0 +1,66 @@ +"use strict"; + +// Mock document.head structure for testing +const mockCreateElement = (tagName) => { + const element = { + _type: tagName, + _attributes: {}, + setAttribute(name, value) { + this._attributes[name] = value; + // Also set as property for fetchPriority + if (name === "fetchpriority") { + this.fetchPriority = value; + } + }, + getAttribute(name) { + return this._attributes[name]; + } + }; + + // Set properties based on tag type + if (tagName === "link") { + element.rel = ""; + element.as = ""; + element.href = ""; + element.type = undefined; + element.media = undefined; + element.fetchPriority = undefined; + } else if (tagName === "script") { + element.src = ""; + element.async = true; + element.fetchPriority = undefined; + } + + return element; +}; + +module.exports = { + beforeExecute: () => { + // Mock document for browser environment + global.document = { + head: { + _children: [], + appendChild(element) { + this._children.push(element); + } + }, + createElement: mockCreateElement + }; + + // Mock window for import.meta.url + global.window = { + location: { + href: "https://test.example.com/" + } + }; + }, + + findBundle() { + return ["main.js"]; + }, + + moduleScope(scope) { + // Make document available in the module scope + scope.document = global.document; + } +}; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js new file mode 100644 index 000000000..0cca6613b --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/test.js @@ -0,0 +1,4 @@ +"use strict"; + +// Test JavaScript file +console.log("test.js loaded"); diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js new file mode 100644 index 000000000..5923056d0 --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/warnings.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = [ + // Invalid fetchPriority value warning + [ + /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ + ], + // Invalid preloadAs (non-string) + [/`webpackPreloadAs` expected a string, but received: 123\./], + // Invalid preloadType (non-string) + [/`webpackPreloadType` expected a string, but received: 123\./], + // Invalid preloadMedia (non-string) + [/`webpackPreloadMedia` expected a string, but received: 456\./] +]; diff --git a/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js new file mode 100644 index 000000000..c43fa438f --- /dev/null +++ b/test/configCases/asset-modules/url-prefetch-preload-fetchpriority/webpack.config.js @@ -0,0 +1,24 @@ +"use strict"; + +/** @type {import("../../../../types").Configuration} */ +module.exports = { + mode: "development", + entry: { + main: "./index.js", + warnings: "./generate-warnings.js" + }, + output: { + filename: "[name].js", + assetModuleFilename: "[name][ext]", + publicPath: "/public/" + }, + target: "web", + module: { + rules: [ + { + test: /\.(png|jpg|css|woff2)$/, + type: "asset/resource" + } + ] + } +}; diff --git a/test/configCases/parsing/url-prefetch-preload/both-hints-image.png b/test/configCases/parsing/url-prefetch-preload/both-hints-image.png new file mode 100644 index 000000000..5c169832e Binary files /dev/null and b/test/configCases/parsing/url-prefetch-preload/both-hints-image.png differ diff --git a/test/configCases/parsing/url-prefetch-preload/index.js b/test/configCases/parsing/url-prefetch-preload/index.js new file mode 100644 index 000000000..db6c3207d --- /dev/null +++ b/test/configCases/parsing/url-prefetch-preload/index.js @@ -0,0 +1,49 @@ +// Test cases for new URL() prefetch/preload support + +it("should prefetch an image asset", () => { + const url = new URL( + /* webpackPrefetch: true */ + "./prefetch-image.png", + import.meta.url + ); + expect(url.href).toMatch(/prefetch-image\.png$/); +}); + +it("should preload an image asset", () => { + const url = new URL( + /* webpackPreload: true */ + "./preload-image.png", + import.meta.url + ); + expect(url.href).toMatch(/preload-image\.png$/); +}); + +it("should preload with fetch priority", () => { + const url = new URL( + /* webpackPreload: true */ + /* webpackFetchPriority: "high" */ + "./priority-image.png", + import.meta.url + ); + expect(url.href).toMatch(/priority-image\.png$/); +}); + +it("should handle invalid fetch priority", () => { + const url2 = new URL( + /* webpackPreload: true */ + /* webpackFetchPriority: "invalid" */ + "./invalid-priority-image.png", + import.meta.url + ); + expect(url2.href).toMatch(/invalid-priority-image\.png$/); +}); + +it("should handle both prefetch and preload", () => { + const url3 = new URL( + /* webpackPrefetch: true */ + /* webpackPreload: true */ + "./both-hints-image.png", + import.meta.url + ); + expect(url3.href).toMatch(/both-hints-image\.png$/); +}); \ No newline at end of file diff --git a/test/configCases/parsing/url-prefetch-preload/invalid-priority-image.png b/test/configCases/parsing/url-prefetch-preload/invalid-priority-image.png new file mode 100644 index 000000000..5c169832e Binary files /dev/null and b/test/configCases/parsing/url-prefetch-preload/invalid-priority-image.png differ diff --git a/test/configCases/parsing/url-prefetch-preload/prefetch-image.png b/test/configCases/parsing/url-prefetch-preload/prefetch-image.png new file mode 100644 index 000000000..5c169832e Binary files /dev/null and b/test/configCases/parsing/url-prefetch-preload/prefetch-image.png differ diff --git a/test/configCases/parsing/url-prefetch-preload/preload-image.png b/test/configCases/parsing/url-prefetch-preload/preload-image.png new file mode 100644 index 000000000..5c169832e Binary files /dev/null and b/test/configCases/parsing/url-prefetch-preload/preload-image.png differ diff --git a/test/configCases/parsing/url-prefetch-preload/priority-image.png b/test/configCases/parsing/url-prefetch-preload/priority-image.png new file mode 100644 index 000000000..5c169832e Binary files /dev/null and b/test/configCases/parsing/url-prefetch-preload/priority-image.png differ diff --git a/test/configCases/parsing/url-prefetch-preload/test.config.js b/test/configCases/parsing/url-prefetch-preload/test.config.js new file mode 100644 index 000000000..a8755bed9 --- /dev/null +++ b/test/configCases/parsing/url-prefetch-preload/test.config.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + findBundle() { + return ["main.js"]; + } +}; diff --git a/test/configCases/parsing/url-prefetch-preload/test.filter.js b/test/configCases/parsing/url-prefetch-preload/test.filter.js new file mode 100644 index 000000000..fc9b5e2ce --- /dev/null +++ b/test/configCases/parsing/url-prefetch-preload/test.filter.js @@ -0,0 +1,5 @@ +"use strict"; + +const supportsWorker = require("../../../helpers/supportsWorker"); + +module.exports = () => supportsWorker(); diff --git a/test/configCases/parsing/url-prefetch-preload/warnings.js b/test/configCases/parsing/url-prefetch-preload/warnings.js new file mode 100644 index 000000000..eea9c4171 --- /dev/null +++ b/test/configCases/parsing/url-prefetch-preload/warnings.js @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = [ + // Invalid fetch priority + [ + /`webpackFetchPriority` expected "low", "high" or "auto", but received: invalid\./ + ] +]; diff --git a/test/configCases/parsing/url-prefetch-preload/webpack.config.js b/test/configCases/parsing/url-prefetch-preload/webpack.config.js new file mode 100644 index 000000000..0e5fe3076 --- /dev/null +++ b/test/configCases/parsing/url-prefetch-preload/webpack.config.js @@ -0,0 +1,18 @@ +"use strict"; + +/** @type {import("../../../../types").Configuration} */ +module.exports = { + output: { + filename: "[name].js", + assetModuleFilename: "[name][ext]" + }, + target: "web", + module: { + rules: [ + { + test: /\.png$/, + type: "asset/resource" + } + ] + } +}; diff --git a/types.d.ts b/types.d.ts index 09672fb10..bcf200609 100644 --- a/types.d.ts +++ b/types.d.ts @@ -18448,8 +18448,10 @@ declare namespace exports { export let moduleLoaded: "module.loaded"; export let nodeModuleDecorator: "__webpack_require__.nmd"; export let onChunksLoaded: "__webpack_require__.O"; + export let prefetchAsset: "__webpack_require__.PA"; export let prefetchChunk: "__webpack_require__.E"; export let prefetchChunkHandlers: "__webpack_require__.F"; + export let preloadAsset: "__webpack_require__.LA"; export let preloadChunk: "__webpack_require__.G"; export let preloadChunkHandlers: "__webpack_require__.H"; export let publicPath: "__webpack_require__.p";