1010namespace Microsoft . AspNetCore . Components . WebView . Maui
1111{
1212 /// <summary>
13- /// Parses the static web assets endpoints manifest (<c>*.staticwebassets.endpoints.json</c>) that
14- /// is bundled with a hybrid app and exposes it as:
13+ /// Loads the minimal fingerprinted-asset manifest that MAUI generates at build time (from the
14+ /// static web assets endpoints) and bundles with a hybrid app, and exposes it as:
1515 /// <list type="bullet">
1616 /// <item><description>a <see cref="ResourceAssetCollection"/> so <c>@Assets["logical"]</c> resolves to the
1717 /// fingerprinted URL at render time; and</description></item>
1818 /// <item><description>a route-to-physical map so the web view can serve the physical asset for a
1919 /// fingerprinted request URL.</description></item>
2020 /// </list>
2121 /// Blazor Web Apps build this from endpoint metadata via <c>MapStaticAssets</c>; hybrid apps have no
22- /// server, so this reconstructs the same information from the bundled manifest. The manifest is
23- /// bundled outside the web root and read via the app package APIs, so it is never exposed to the
24- /// web view.
22+ /// server, so MAUI reconstructs just the fingerprint mapping at build time. The manifest is bundled
23+ /// outside the web root and read via the app package APIs, so it is never exposed to the web view.
24+ /// Its content is derived entirely from asset fingerprints and logical names (no timestamps, absolute
25+ /// paths, or runtime identifiers), so it is deterministic and identical across architectures - which
26+ /// is required for universal (multi-RID) app bundles to merge.
2527 /// </summary>
2628 internal sealed class StaticWebAssetsManifest
2729 {
@@ -41,7 +43,7 @@ private StaticWebAssetsManifest(ResourceAssetCollection assets, IReadOnlyDiction
4143 /// <summary>Gets the fingerprint-aware asset collection used to resolve <c>@Assets</c>.</summary>
4244 public ResourceAssetCollection Assets { get ; }
4345
44- /// <summary>Gets the map of request route (possibly fingerprinted) to the physical asset file.</summary>
46+ /// <summary>Gets the map of fingerprinted request route to the physical asset file under the web root .</summary>
4547 public IReadOnlyDictionary < string , string > RouteToPhysicalPath { get ; }
4648
4749 /// <summary>
@@ -84,53 +86,28 @@ internal static StaticWebAssetsManifest Parse(Stream stream)
8486 internal static StaticWebAssetsManifest FromData ( ManifestData ? data )
8587 {
8688 var resources = new List < ResourceAsset > ( ) ;
87- var seenLabels = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
8889 var routeToPhysical = new Dictionary < string , string > ( StringComparer . OrdinalIgnoreCase ) ;
8990
90- var endpoints = data ? . Endpoints ;
91- if ( endpoints is null || endpoints . Count == 0 )
91+ var assets = data ? . Assets ;
92+ if ( assets is null || assets . Count == 0 )
9293 {
9394 return new StaticWebAssetsManifest ( ResourceAssetCollection . Empty , routeToPhysical ) ;
9495 }
9596
96- foreach ( var endpoint in endpoints )
97+ foreach ( var asset in assets )
9798 {
98- var route = endpoint . Route ;
99- var assetFile = endpoint . AssetFile ;
100- if ( route is null || assetFile is null )
99+ var route = asset . Route ;
100+ var label = asset . Label ;
101+ if ( route is null || label is null )
101102 {
102103 continue ;
103104 }
104105
105- // Endpoints with selectors are alternative representations (for example gzip/brotli
106- // content negotiation). They are not distinct assets, so skip them - mirroring the
107- // framework's own ResourceCollectionResolver.
108- if ( endpoint . Selectors is { Count : > 0 } )
109- {
110- continue ;
111- }
112-
113- var isCompressed = assetFile . EndsWith ( ".gz" , StringComparison . OrdinalIgnoreCase ) ||
114- assetFile . EndsWith ( ".br" , StringComparison . OrdinalIgnoreCase ) ;
115-
116- var ( label , integrity ) = ReadProperties ( endpoint . EndpointProperties ) ;
117-
118- // @Assets resolution: map the human-readable label to the fingerprinted route. Skip
119- // compressed variants and duplicate labels to avoid collisions.
120- if ( label is not null && ! isCompressed && seenLabels . Add ( label ) )
121- {
122- var properties = integrity is null
123- ? new [ ] { new ResourceAssetProperty ( "label" , label ) }
124- : new [ ] { new ResourceAssetProperty ( "label" , label ) , new ResourceAssetProperty ( "integrity" , integrity ) } ;
125- resources . Add ( new ResourceAsset ( route , properties ) ) ;
126- }
106+ // @Assets resolution: the fingerprinted route is exposed under its logical label.
107+ resources . Add ( new ResourceAsset ( route , new [ ] { new ResourceAssetProperty ( "label" , label ) } ) ) ;
127108
128- // Serving: map the (possibly fingerprinted) route to the physical file on disk. Prefer
129- // the uncompressed asset and keep the first mapping for a given route.
130- if ( ! isCompressed )
131- {
132- routeToPhysical . TryAdd ( NormalizePath ( route ) , NormalizePath ( assetFile ) ) ;
133- }
109+ // Serving: the physical file under the web root is the logical (non-fingerprinted) label.
110+ routeToPhysical . TryAdd ( NormalizePath ( route ) , NormalizePath ( label ) ) ;
134111 }
135112
136113 return new StaticWebAssetsManifest ( new ResourceAssetCollection ( resources ) , routeToPhysical ) ;
@@ -157,72 +134,25 @@ public bool TryResolvePhysicalPath(string requestedPath, out string physicalPath
157134 return false ;
158135 }
159136
160- private static ( string ? Label , string ? Integrity ) ReadProperties ( List < EndpointProperty > ? properties )
161- {
162- string ? label = null ;
163- string ? integrity = null ;
164-
165- if ( properties is not null )
166- {
167- foreach ( var property in properties )
168- {
169- if ( property . Name is null )
170- {
171- continue ;
172- }
173-
174- if ( label is null && property . Name . Equals ( "label" , StringComparison . OrdinalIgnoreCase ) )
175- {
176- label = property . Value ;
177- }
178- else if ( integrity is null && property . Name . Equals ( "integrity" , StringComparison . OrdinalIgnoreCase ) )
179- {
180- integrity = property . Value ;
181- }
182- }
183- }
184-
185- return ( label , integrity ) ;
186- }
187-
188137 private static string NormalizePath ( string path ) =>
189138 ( path ?? string . Empty ) . Replace ( '\\ ' , '/' ) . TrimStart ( '/' ) ;
190139
191- /// <summary>The subset of the endpoints manifest that hybrid asset resolution needs .</summary>
140+ /// <summary>The minimal fingerprint manifest MAUI generates at build time .</summary>
192141 internal sealed class ManifestData
193142 {
194- [ JsonPropertyName ( "Endpoints " ) ]
195- public List < Endpoint > ? Endpoints { get ; set ; }
143+ [ JsonPropertyName ( "Assets " ) ]
144+ public List < AssetEntry > ? Assets { get ; set ; }
196145 }
197146
198- internal sealed class Endpoint
147+ internal sealed class AssetEntry
199148 {
149+ /// <summary>The fingerprinted, served route (for example <c>_content/Pkg/app.abc123.css</c>).</summary>
200150 [ JsonPropertyName ( "Route" ) ]
201151 public string ? Route { get ; set ; }
202152
203- [ JsonPropertyName ( "AssetFile" ) ]
204- public string ? AssetFile { get ; set ; }
205-
206- [ JsonPropertyName ( "Selectors" ) ]
207- public List < EndpointSelector > ? Selectors { get ; set ; }
208-
209- [ JsonPropertyName ( "EndpointProperties" ) ]
210- public List < EndpointProperty > ? EndpointProperties { get ; set ; }
211- }
212-
213- internal sealed class EndpointSelector
214- {
215- [ JsonPropertyName ( "Name" ) ]
216- public string ? Name { get ; set ; }
217- }
218-
219- internal sealed class EndpointProperty
220- {
221- [ JsonPropertyName ( "Name" ) ]
222- public string ? Name { get ; set ; }
223-
224- [ JsonPropertyName ( "Value" ) ]
225- public string ? Value { get ; set ; }
153+ /// <summary>The logical, non-fingerprinted path used by <c>@Assets</c> and stored on disk under the web root.</summary>
154+ [ JsonPropertyName ( "Label" ) ]
155+ public string ? Label { get ; set ; }
226156 }
227157 }
228158
0 commit comments