22// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
44using System ;
5+ using System . Collections . Generic ;
56using System . IO ;
6- using System . Linq ;
77using System . Reflection ;
88using System . Runtime . Serialization . Json ;
9+ using System . Text ;
910using Microsoft . Build . Framework ;
1011using Microsoft . Build . Utilities ;
12+ using ResourceHashesByNameDictionary = System . Collections . Generic . Dictionary < string , string > ;
1113
1214namespace Microsoft . AspNetCore . Blazor . Build
1315{
@@ -17,53 +19,107 @@ public class GenerateBlazorBootJson : Task
1719 public string AssemblyPath { get ; set ; }
1820
1921 [ Required ]
20- public ITaskItem [ ] References { get ; set ; }
22+ public ITaskItem [ ] Resources { get ; set ; }
23+
24+ [ Required ]
25+ public bool DebugBuild { get ; set ; }
2126
2227 [ Required ]
2328 public bool LinkerEnabled { get ; set ; }
2429
30+ [ Required ]
31+ public bool CacheBootResources { get ; set ; }
32+
2533 [ Required ]
2634 public string OutputPath { get ; set ; }
2735
2836 public override bool Execute ( )
2937 {
38+ using var fileStream = File . Create ( OutputPath ) ;
3039 var entryAssemblyName = AssemblyName . GetAssemblyName ( AssemblyPath ) . Name ;
31- var assemblies = References . Select ( GetUriPath ) . OrderBy ( c => c , StringComparer . Ordinal ) . ToArray ( ) ;
3240
33- using var fileStream = File . Create ( OutputPath ) ;
34- WriteBootJson ( fileStream , entryAssemblyName , assemblies , LinkerEnabled ) ;
41+ try
42+ {
43+ WriteBootJson ( fileStream , entryAssemblyName ) ;
44+ }
45+ catch ( Exception ex )
46+ {
47+ Log . LogErrorFromException ( ex ) ;
48+ }
3549
36- return true ;
50+ return ! Log . HasLoggedErrors ;
51+ }
3752
38- static string GetUriPath ( ITaskItem item )
53+ // Internal for tests
54+ internal void WriteBootJson ( Stream output , string entryAssemblyName )
55+ {
56+ var result = new BootJsonData
3957 {
40- var outputPath = item . GetMetadata ( "RelativeOutputPath" ) ;
41- if ( string . IsNullOrEmpty ( outputPath ) )
58+ entryAssembly = entryAssemblyName ,
59+ cacheBootResources = CacheBootResources ,
60+ debugBuild = DebugBuild ,
61+ linkerEnabled = LinkerEnabled ,
62+ resources = new Dictionary < ResourceType , ResourceHashesByNameDictionary > ( )
63+ } ;
64+
65+ // Build a two-level dictionary of the form:
66+ // - BootResourceType (e.g., "assembly")
67+ // - UriPath (e.g., "System.Text.Json.dll")
68+ // - ContentHash (e.g., "4548fa2e9cf52986")
69+ if ( Resources != null )
70+ {
71+ foreach ( var resource in Resources )
4272 {
43- outputPath = Path . GetFileName ( item . ItemSpec ) ;
44- }
73+ var resourceTypeMetadata = resource . GetMetadata ( "BootResourceType" ) ;
74+ if ( ! Enum . TryParse < ResourceType > ( resourceTypeMetadata , out var resourceType ) )
75+ {
76+ throw new NotSupportedException ( $ "Unsupported BootResourceType metadata value: { resourceTypeMetadata } ") ;
77+ }
4578
46- return outputPath . Replace ( '\\ ' , '/' ) ;
79+ if ( ! result . resources . TryGetValue ( resourceType , out var resourceList ) )
80+ {
81+ resourceList = new ResourceHashesByNameDictionary ( ) ;
82+ result . resources . Add ( resourceType , resourceList ) ;
83+ }
84+
85+ var resourceFileRelativePath = GetResourceFileRelativePath ( resource ) ;
86+ if ( ! resourceList . ContainsKey ( resourceFileRelativePath ) )
87+ {
88+ resourceList . Add ( resourceFileRelativePath , $ "sha256-{ resource . GetMetadata ( "FileHash" ) } ") ;
89+ }
90+ }
4791 }
92+
93+ var serializer = new DataContractJsonSerializer ( typeof ( BootJsonData ) , new DataContractJsonSerializerSettings
94+ {
95+ UseSimpleDictionaryFormat = true
96+ } ) ;
97+
98+ using var writer = JsonReaderWriterFactory . CreateJsonWriter ( output , Encoding . UTF8 , ownsStream : false , indent : true ) ;
99+ serializer . WriteObject ( writer , result ) ;
48100 }
49101
50- internal static void WriteBootJson ( Stream stream , string entryAssemblyName , string [ ] assemblies , bool linkerEnabled )
102+ private static string GetResourceFileRelativePath ( ITaskItem item )
51103 {
52- var data = new BootJsonData
104+ // The build targets use RelativeOutputPath in the case of satellite assemblies, which
105+ // will have relative paths like "fr\\SomeAssembly.resources.dll". If RelativeOutputPath
106+ // is specified, we want to use all of it.
107+ var outputPath = item . GetMetadata ( "RelativeOutputPath" ) ;
108+
109+ if ( string . IsNullOrEmpty ( outputPath ) )
53110 {
54- entryAssembly = entryAssemblyName ,
55- assemblies = assemblies ,
56- linkerEnabled = linkerEnabled ,
57- } ;
111+ // If RelativeOutputPath was not specified, we assume the item will be placed at the
112+ // root of whatever directory is used for its resource type (e.g., assemblies go in _bin)
113+ outputPath = Path . GetFileName ( item . ItemSpec ) ;
114+ }
58115
59- var serializer = new DataContractJsonSerializer ( typeof ( BootJsonData ) ) ;
60- serializer . WriteObject ( stream , data ) ;
116+ return outputPath . Replace ( '\\ ' , '/' ) ;
61117 }
62118
119+ #pragma warning disable IDE1006 // Naming Styles
63120 /// <summary>
64121 /// Defines the structure of a Blazor boot JSON file
65122 /// </summary>
66- #pragma warning disable IDE1006 // Naming Styles
67123 public class BootJsonData
68124 {
69125 /// <summary>
@@ -72,15 +128,39 @@ public class BootJsonData
72128 public string entryAssembly { get ; set ; }
73129
74130 /// <summary>
75- /// Gets the closure of assemblies to be loaded by Blazor WASM. This includes the application entry assembly.
131+ /// Gets the set of resources needed to boot the application. This includes the transitive
132+ /// closure of .NET assemblies (including the entrypoint assembly), the dotnet.wasm file,
133+ /// and any PDBs to be loaded.
134+ ///
135+ /// Within <see cref="ResourceHashesByNameDictionary"/>, dictionary keys are resource names,
136+ /// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...')
137+ /// as used for subresource integrity checking.
76138 /// </summary>
77- public string [ ] assemblies { get ; set ; }
139+ public Dictionary < ResourceType , ResourceHashesByNameDictionary > resources { get ; set ; }
140+
141+ /// <summary>
142+ /// Gets a value that determines whether to enable caching of the <see cref="resources"/>
143+ /// inside a CacheStorage instance within the browser.
144+ /// </summary>
145+ public bool cacheBootResources { get ; set ; }
146+
147+ /// <summary>
148+ /// Gets a value that determines if this is a debug build.
149+ /// </summary>
150+ public bool debugBuild { get ; set ; }
78151
79152 /// <summary>
80153 /// Gets a value that determines if the linker is enabled.
81154 /// </summary>
82155 public bool linkerEnabled { get ; set ; }
83156 }
157+
158+ public enum ResourceType
159+ {
160+ assembly ,
161+ pdb ,
162+ wasm
163+ }
84164#pragma warning restore IDE1006 // Naming Styles
85165 }
86166}
0 commit comments