From 3c30c24d8f9c309d0c133b2b31c6937ea67bfcb6 Mon Sep 17 00:00:00 2001 From: Ryuya Date: Mon, 15 Sep 2025 06:24:36 -0700 Subject: [PATCH] feat: add preload options for enhanced asset loading - Introduced `preloadAs`, `preloadType`, and `preloadMedia` properties to `URLDependency` for better control over asset preloading. --- cspell.json | 1 + lib/dependencies/URLDependency.js | 40 ++++++++--- lib/prefetch/ResourcePrefetchRuntimeModule.js | 7 +- lib/url/URLParserPlugin.js | 69 +++++++++++++++++++ 4 files changed, 104 insertions(+), 13 deletions(-) diff --git a/cspell.json b/cspell.json index 9ce10a6cb..2d3369659 100644 --- a/cspell.json +++ b/cspell.json @@ -231,6 +231,7 @@ "serializables", "serializer", "serializers", + "serviceworker", "shama", "skypack", "snapshotting", diff --git a/lib/dependencies/URLDependency.js b/lib/dependencies/URLDependency.js index 5de787427..ab934493a 100644 --- a/lib/dependencies/URLDependency.js +++ b/lib/dependencies/URLDependency.js @@ -52,6 +52,12 @@ class URLDependency extends ModuleDependency { 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() { @@ -93,6 +99,9 @@ class URLDependency extends ModuleDependency { write(this.prefetch); write(this.preload); write(this.fetchPriority); + write(this.preloadAs); + write(this.preloadType); + write(this.preloadMedia); super.serialize(context); } @@ -107,6 +116,9 @@ class URLDependency extends ModuleDependency { this.prefetch = read(); this.preload = read(); this.fetchPriority = read(); + this.preloadAs = read(); + this.preloadType = read(); + this.preloadMedia = read(); super.deserialize(context); } } @@ -203,7 +215,7 @@ URLDependency.Template = class URLDependencyTemplate extends ( // Prefetch/Preload via InitFragment if ((dep.prefetch || dep.preload) && module) { const request = dep.request; - const assetType = URLDependencyTemplate._getAssetType(request); + const detectedAssetType = URLDependencyTemplate._getAssetType(request); const id = chunkGraph.getModuleId(module); if (id !== null) { const moduleId = runtimeTemplate.moduleId({ @@ -215,13 +227,19 @@ URLDependency.Template = class URLDependencyTemplate extends ( 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}, ${JSON.stringify( - assetType - )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ - dep.relative - });\n`, + `${RuntimeGlobals.preloadAsset}(${moduleId}, ${asArg}, ${fetchPriorityArg}, ${typeArg}, ${mediaArg}, ${dep.relative});\n`, InitFragment.STAGE_CONSTANTS, -10, `asset_preload_${moduleId}` @@ -229,13 +247,13 @@ URLDependency.Template = class URLDependencyTemplate extends ( ); } 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}, ${JSON.stringify( - assetType - )}${dep.fetchPriority ? `, ${JSON.stringify(dep.fetchPriority)}` : ""}, ${ - dep.relative - });\n`, + `${RuntimeGlobals.prefetchAsset}(${moduleId}, ${asArg}, ${fetchPriorityArg}, undefined, undefined, ${dep.relative});\n`, InitFragment.STAGE_CONSTANTS, -5, `asset_prefetch_${moduleId}` diff --git a/lib/prefetch/ResourcePrefetchRuntimeModule.js b/lib/prefetch/ResourcePrefetchRuntimeModule.js index b9cc5cf20..cd9603aec 100644 --- a/lib/prefetch/ResourcePrefetchRuntimeModule.js +++ b/lib/prefetch/ResourcePrefetchRuntimeModule.js @@ -61,6 +61,9 @@ class ResourcePrefetchRuntimeModule extends RuntimeModule { ]), "}", "", + "if (type) link.type = type;", + "if (media) link.media = media;", + "", crossOriginLoading ? Template.asString([ "if (link.href.indexOf(window.location.origin + '/') !== 0) {", @@ -78,7 +81,7 @@ class ResourcePrefetchRuntimeModule extends RuntimeModule { if (isNeutralPlatform) { return Template.asString([ `${fnName} = ${runtimeTemplate.basicFunction( - "moduleId, as, fetchPriority, relative", + "moduleId, as, fetchPriority, type, media, relative", [ "// Only execute in browser environment", "if (typeof document !== 'undefined') {", @@ -92,7 +95,7 @@ class ResourcePrefetchRuntimeModule extends RuntimeModule { // For browser-only targets, generate code without the check return Template.asString([ `${fnName} = ${runtimeTemplate.basicFunction( - "moduleId, as, fetchPriority, relative", + "moduleId, as, fetchPriority, type, media, relative", code )};` ]); diff --git a/lib/url/URLParserPlugin.js b/lib/url/URLParserPlugin.js index 3576b1e0f..6fd7d8c01 100644 --- a/lib/url/URLParserPlugin.js +++ b/lib/url/URLParserPlugin.js @@ -224,6 +224,75 @@ class URLParserPlugin { ) ); } + + // webpackPreloadAs: allow override of the "as" attribute for preload + // Fetch Standard: Request destinations (enumerates destinations used by `as`) https://fetch.spec.whatwg.org/#concept-request-destination + if (importOptions.webpackPreloadAs !== undefined) { + const allowedAs = [ + // Per HTML LS "match-preload-type" + Fetch Standard request destinations + // See references above + "audio", + "audioworklet", + "document", + "embed", + "fetch", + "font", + "image", + "manifest", + "object", + "paintworklet", + "report", + "script", + "sharedworker", + "serviceworker", + "style", + "track", + "video", + "worker", + "xslt" + ]; + if ( + typeof importOptions.webpackPreloadAs === "string" && + allowedAs.includes(importOptions.webpackPreloadAs) + ) { + dep.preloadAs = importOptions.webpackPreloadAs; + } else { + parser.state.module.addWarning( + new UnsupportedFeatureWarning( + `\`webpackPreloadAs\` expected one of ${JSON.stringify(allowedAs)}, 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