diff --git a/src/installer/prepare-artifacts.proj b/src/installer/prepare-artifacts.proj
index f2b108bf4217a9..3938e88029ca1d 100644
--- a/src/installer/prepare-artifacts.proj
+++ b/src/installer/prepare-artifacts.proj
@@ -248,6 +248,7 @@
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.props b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.props
index 2890daaf708283..dae0b088f16071 100644
--- a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.props
+++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.props
@@ -1,5 +1,5 @@
-
+
wasm
browser
true
diff --git a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in
index 0f9edfb14f10d3..60d993f9c1a531 100644
--- a/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in
+++ b/src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/Sdk/Sdk.targets.in
@@ -9,7 +9,7 @@
$([MSBuild]::NormalizeDirectory($(MSBuildThisFileDirectory), '..', 'WasmAppHost'))
- true
+ true
diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/Microsoft.NET.Sdk.WebAssembly.Pack.pkgproj b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/Microsoft.NET.Sdk.WebAssembly.Pack.pkgproj
new file mode 100644
index 00000000000000..32b0ded6b35fd7
--- /dev/null
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/Microsoft.NET.Sdk.WebAssembly.Pack.pkgproj
@@ -0,0 +1,14 @@
+
+
+
+
+ SDK for building and publishing WebAssembly applications.
+
+
+
+
+
+
+
+
+
diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props
new file mode 100644
index 00000000000000..dc074e761f24e1
--- /dev/null
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.props
@@ -0,0 +1,43 @@
+
+
+
+
+ false
+
+ exe
+
+ false
+
+ false
+
+
+ false
+
+
+ true
+ partial
+ false
+
+
+ /
+ Root
+ $(StaticWebAssetsAdditionalBuildPropertiesToRemove);RuntimeIdentifier;SelfContained
+ ComputeFilesToPublish;GetCurrentProjectPublishStaticWebAssetItems
+ $(StaticWebAssetsAdditionalPublishProperties);BuildProjectReferences=false;ResolveAssemblyReferencesFindRelatedSatellites=true
+ $(StaticWebAssetsAdditionalPublishPropertiesToRemove);NoBuild;RuntimeIdentifier
+
+
+
+
+
+
diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
new file mode 100644
index 00000000000000..3fe69f51c2e0ed
--- /dev/null
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
@@ -0,0 +1,493 @@
+
+
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)..\
+ <_WebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">net8.0
+ <_WebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' != 'Core'">net472
+ <_WebAssemblySdkTasksAssembly>$(WebAssemblySdkDirectoryRoot)tools\$(_WebAssemblySdkTasksTFM)\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.dll
+
+
+
+
+
+
+
+ true
+ true
+
+
+ false
+ false
+ true
+ false
+ false
+ false
+ <_AggressiveAttributeTrimming Condition="'$(_AggressiveAttributeTrimming)' == ''">true
+ false
+ true
+ false
+
+
+ false
+ false
+ false
+ false
+ true
+
+
+
+ false
+
+ false
+ _GatherWasmFilesToPublish;$(WasmNestedPublishAppDependsOn)
+ <_WasmNestedPublishAppPreTarget>ComputeFilesToPublish
+
+
+
+
+
+
+
+
+
+
+
+ $(ResolveStaticWebAssetsInputsDependsOn);
+ _AddWasmStaticWebAssets;
+
+
+
+ _GenerateBuildWasmBootJson;
+ $(StaticWebAssetsPrepareForRunDependsOn)
+
+
+
+ $(ResolvePublishStaticWebAssetsDependsOn);
+ ProcessPublishFilesForWasm;
+ ComputeWasmExtensions;
+ _AddPublishWasmBootJsonToStaticWebAssets;
+
+
+
+ $(GenerateStaticWebAssetsPublishManifestDependsOn);
+ GeneratePublishWasmBootJson;
+
+
+
+ $(AddWasmStaticWebAssetsDependsOn);
+ ResolveWasmOutputs;
+
+
+ $(GenerateBuildWasmBootJsonDependsOn);
+ ResolveStaticWebAssetsInputs;
+
+
+ $(GeneratePublishWasmBootJsonDependsOn);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_BlazorWebAssemblyLoadAllGlobalizationData Condition="'$(InvariantGlobalization)' != 'true'">$(BlazorWebAssemblyLoadAllGlobalizationData)
+ <_BlazorWebAssemblyLoadAllGlobalizationData Condition="'$(_BlazorWebAssemblyLoadAllGlobalizationData)' == ''">false
+ <_BlazorIcuDataFileName Condition="'$(InvariantGlobalization)' != 'true' AND '$(BlazorWebAssemblyLoadAllGlobalizationData)' != 'true'">$(BlazorIcuDataFileName)
+ <_LoadCustomIcuData>false
+ <_LoadCustomIcuData Condition="'$(_BlazorIcuDataFileName)' != ''">true
+
+
+
+
+
+ <_BlazorEnableTimeZoneSupport>$(BlazorEnableTimeZoneSupport)
+ <_BlazorEnableTimeZoneSupport Condition="'$(_BlazorEnableTimeZoneSupport)' == ''">true
+ <_WasmInvariantGlobalization>$(InvariantGlobalization)
+ <_WasmInvariantGlobalization Condition="'$(_WasmInvariantGlobalization)' == ''">true
+ <_WasmCopyOutputSymbolsToOutputDirectory>$(CopyOutputSymbolsToOutputDirectory)
+ <_WasmCopyOutputSymbolsToOutputDirectory Condition="'$(_WasmCopyOutputSymbolsToOutputDirectory)'==''">true
+ <_BlazorWebAssemblyStartupMemoryCache>$(BlazorWebAssemblyStartupMemoryCache)
+ <_BlazorWebAssemblyJiterpreter>$(BlazorWebAssemblyJiterpreter)
+ <_BlazorWebAssemblyRuntimeOptions>$(BlazorWebAssemblyRuntimeOptions)
+
+
+ $(OutputPath)$(PublishDirName)\
+
+
+
+
+
+
+
+ <_WasmConfigFileCandidates Include="@(StaticWebAsset)" Condition="'%(SourceType)' == 'Discovered'" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_WasmBuildBootJsonPath>$(IntermediateOutputPath)blazor.boot.json
+
+
+
+ <_BuildWasmBootJson
+ Include="$(_WasmBuildBootJsonPath)"
+ RelativePath="_framework/blazor.boot.json" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_WasmBuildBootJsonPath>$(IntermediateOutputPath)blazor.boot.json
+
+
+
+ <_WasmJsModuleCandidatesForBuild
+ Include="@(StaticWebAsset)"
+ Condition="'%(StaticWebAsset.AssetTraitName)' == 'JSModule' and '%(StaticWebAsset.AssetTraitValue)' == 'JSLibraryModule' and '%(AssetKind)' != 'Publish'" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_WasmPublishPrefilteredAssets
+ Include="@(StaticWebAsset)"
+ Condition="'%(StaticWebAsset.AssetTraitName)' == 'WasmResource' or '%(StaticWebAsset.AssetTraitName)' == 'Culture' or '%(AssetRole)' == 'Alternative'" />
+
+
+
+ <_DotNetJsItem Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.DestinationSubPath)' == 'dotnet.js' AND '%(ResolvedFileToPublish.AssetType)' == 'native'" />
+
+
+
+ <_DotNetJsVersion>%(_DotNetJsItem.NuGetPackageVersion)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_BlazorExtensionsCandidate Include="@(BlazorPublishExtension->'%(FullPath)')">
+ $(PackageId)
+ Computed
+ $(PublishDir)wwwroot
+ $(StaticWebAssetBasePath)
+ %(BlazorPublishExtension.RelativePath)
+ Publish
+ All
+ Primary
+ WasmResource
+ extension:%(BlazorPublishExtension.ExtensionName)
+ Never
+ PreserveNewest
+ %(BlazorPublishExtension.Identity)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PublishWasmBootJson
+ Include="$(IntermediateOutputPath)blazor.publish.boot.json"
+ RelativePath="_framework/blazor.boot.json" />
+
+
+
+
+
+
+
+
+
+
+ <_WasmPublishAsset
+ Include="@(StaticWebAsset)"
+ Condition="'%(AssetKind)' != 'Build' and '%(StaticWebAsset.AssetTraitValue)' != 'manifest' and ('%(StaticWebAsset.AssetTraitName)' == 'WasmResource' or '%(StaticWebAsset.AssetTraitName)' == 'Culture') and '%(StaticWebAsset.AssetTraitValue)' != 'boot'" />
+
+ <_WasmPublishConfigFile
+ Include="@(StaticWebAsset)"
+ Condition="'%(StaticWebAsset.AssetTraitName)' == 'WasmResource' and '%(StaticWebAsset.AssetTraitValue)' == 'settings'"/>
+
+ <_WasmJsModuleCandidatesForPublish
+ Include="@(StaticWebAsset)"
+ Condition="'%(StaticWebAsset.AssetTraitName)' == 'JSModule' and '%(StaticWebAsset.AssetTraitValue)' == 'JSLibraryModule' and '%(AssetKind)' != 'Build'" />
+
+
+ <_WasmPublishAsset Remove="@(_BlazorExtensionsCandidatesForPublish)" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Pack.props b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Pack.props
new file mode 100644
index 00000000000000..a41609b5c15a65
--- /dev/null
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Pack.props
@@ -0,0 +1,20 @@
+
+
+
+ browser-wasm
+
+
+ <_WebAssemblyPropsFile>$(MSBuildThisFileDirectory)\Microsoft.NET.Sdk.WebAssembly.Browser.props
+ <_WebAssemblyTargetsFile>$(MSBuildThisFileDirectory)\Microsoft.NET.Sdk.WebAssembly.Browser.targets
+
+
diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Pack.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Pack.targets
new file mode 100644
index 00000000000000..df15f880ba1b39
--- /dev/null
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Pack.targets
@@ -0,0 +1,12 @@
+
+
diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Wasm.web.config b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Wasm.web.config
new file mode 100644
index 00000000000000..586d3565ed1eb4
--- /dev/null
+++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Wasm.web.config
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in
index c9a2c7494450fd..b42b351e97ac2f 100644
--- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in
+++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WorkloadManifest.targets.in
@@ -20,6 +20,8 @@
<_BrowserWorkloadNotSupportedForTFM Condition="$([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '6.0'))">true
<_BrowserWorkloadDisabled>$(_BrowserWorkloadNotSupportedForTFM)
+ <_UsingBlazorOrWasmSdk Condition="'$(UsingMicrosoftNETSdkBlazorWebAssembly)' == 'true' or '$(UsingMicrosoftNETSdkWebAssembly)' == 'true'">true
+
@@ -39,7 +41,7 @@
<_WasmNativeWorkloadNeeded Condition="'$(RunAOTCompilation)' == 'true' or '$(WasmEnableSIMD)' == 'true' or '$(WasmBuildNative)' == 'true' or
- '$(WasmGenerateAppBundle)' == 'true' or '$(UsingMicrosoftNETSdkBlazorWebAssembly)' != 'true'" >true
+ '$(WasmGenerateAppBundle)' == 'true' or '$(_UsingBlazorOrWasmSdk)' != 'true'" >true
false
true
@@ -59,7 +61,7 @@
true
-
+
false
true
diff --git a/src/mono/nuget/mono-packages.proj b/src/mono/nuget/mono-packages.proj
index 6a2ddff782b75e..438ec97ace3e1a 100644
--- a/src/mono/nuget/mono-packages.proj
+++ b/src/mono/nuget/mono-packages.proj
@@ -8,6 +8,7 @@
+
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs
new file mode 100644
index 00000000000000..2854594ae10547
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.WebAssembly;
+
+public class AssetsComputingHelper
+{
+ public static bool ShouldFilterCandidate(
+ ITaskItem candidate,
+ bool timezoneSupport,
+ bool invariantGlobalization,
+ bool copySymbols,
+ string customIcuCandidateFilename,
+ out string reason)
+ {
+ var extension = candidate.GetMetadata("Extension");
+ var fileName = candidate.GetMetadata("FileName");
+ var assetType = candidate.GetMetadata("AssetType");
+ var fromMonoPackage = string.Equals(
+ candidate.GetMetadata("NuGetPackageId"),
+ "Microsoft.NETCore.App.Runtime.Mono.browser-wasm",
+ StringComparison.Ordinal);
+
+ reason = extension switch
+ {
+ ".a" when fromMonoPackage => "extension is .a is not supported.",
+ ".c" when fromMonoPackage => "extension is .c is not supported.",
+ ".h" when fromMonoPackage => "extension is .h is not supported.",
+ // It is safe to filter out all XML files since we are not interested in any XML file from the list
+ // of ResolvedFilesToPublish to become a static web asset. Things like this include XML doc files and
+ // so on.
+ ".xml" => "it is a documentation file",
+ ".rsp" when fromMonoPackage => "extension is .rsp is not supported.",
+ ".props" when fromMonoPackage => "extension is .props is not supported.",
+ ".blat" when !timezoneSupport => "timezone support is not enabled.",
+ ".dat" when invariantGlobalization && fileName.StartsWith("icudt") => "invariant globalization is enabled",
+ ".dat" when !string.IsNullOrEmpty(customIcuCandidateFilename) && fileName != customIcuCandidateFilename => "custom icu file will be used instead of icu from the runtime pack",
+ ".json" when fromMonoPackage && (fileName == "emcc-props" || fileName == "package") => $"{fileName}{extension} is not used by Blazor",
+ ".ts" when fromMonoPackage && fileName == "dotnet.d" => "dotnet type definition is not used by Blazor",
+ ".ts" when fromMonoPackage && fileName == "dotnet-legacy.d" => "dotnet type definition is not used by Blazor",
+ ".js" when assetType == "native" && fileName != "dotnet" => $"{fileName}{extension} is not used by Blazor",
+ ".pdb" when !copySymbols => "copying symbols is disabled",
+ ".symbols" when fromMonoPackage => "extension .symbols is not required.",
+ _ => null
+ };
+
+ return reason != null;
+ }
+
+ public static string GetCandidateRelativePath(ITaskItem candidate)
+ {
+ var destinationSubPath = candidate.GetMetadata("DestinationSubPath");
+ if (!string.IsNullOrEmpty(destinationSubPath))
+ return $"_framework/{destinationSubPath}";
+
+ var relativePath = candidate.GetMetadata("FileName") + candidate.GetMetadata("Extension");
+ return $"_framework/{relativePath}";
+ }
+
+ public static ITaskItem GetCustomIcuAsset(ITaskItem candidate)
+ {
+ var customIcuCandidate = new TaskItem(candidate);
+ var relativePath = GetCandidateRelativePath(customIcuCandidate);
+ customIcuCandidate.SetMetadata("RelativePath", relativePath);
+ customIcuCandidate.SetMetadata("AssetTraitName", "BlazorWebAssemblyResource");
+ customIcuCandidate.SetMetadata("AssetTraitValue", "native");
+ customIcuCandidate.SetMetadata("AssetType", "native");
+ return customIcuCandidate;
+ }
+
+ public static bool TryGetAssetFilename(ITaskItem candidate, out string filename)
+ {
+ bool candidateIsValid = candidate != null && !string.IsNullOrEmpty(candidate.ItemSpec);
+ filename = candidateIsValid ?
+ $"{candidate.GetMetadata("FileName")}" :
+ "";
+ return candidateIsValid;
+ }
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
new file mode 100644
index 00000000000000..282d5cf6d0a580
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs
@@ -0,0 +1,157 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary;
+
+namespace Microsoft.NET.Sdk.WebAssembly;
+
+///
+/// Defines the structure of a Blazor boot JSON file
+///
+public class BootJsonData
+{
+ ///
+ /// Gets the name of the assembly with the application entry point
+ ///
+ public string entryAssembly { get; set; }
+
+ ///
+ /// Gets the set of resources needed to boot the application. This includes the transitive
+ /// closure of .NET assemblies (including the entrypoint assembly), the dotnet.wasm file,
+ /// and any PDBs to be loaded.
+ ///
+ /// Within , dictionary keys are resource names,
+ /// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...')
+ /// as used for subresource integrity checking.
+ ///
+ public ResourcesData resources { get; set; } = new ResourcesData();
+
+ ///
+ /// Gets a value that determines whether to enable caching of the
+ /// inside a CacheStorage instance within the browser.
+ ///
+ public bool cacheBootResources { get; set; }
+
+ ///
+ /// Gets a value that determines if this is a debug build.
+ ///
+ public bool debugBuild { get; set; }
+
+ ///
+ /// Gets a value that determines if the linker is enabled.
+ ///
+ public bool linkerEnabled { get; set; }
+
+ ///
+ /// Config files for the application
+ ///
+ public List config { get; set; }
+
+ ///
+ /// Gets or sets the that determines how icu files are loaded.
+ ///
+ public ICUDataMode icuDataMode { get; set; }
+
+ ///
+ /// Gets or sets a value that determines if the caching startup memory is enabled.
+ ///
+ public bool? startupMemoryCache { get; set; }
+
+ ///
+ /// Gets a value for mono runtime options.
+ ///
+ public string[] runtimeOptions { get; set; }
+
+ ///
+ /// Gets or sets configuration extensions.
+ ///
+ public Dictionary> extensions { get; set; }
+}
+
+public class ResourcesData
+{
+ ///
+ /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc.
+ ///
+ public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary();
+
+ ///
+ /// "assembly" (.dll) resources
+ ///
+ public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary();
+
+ ///
+ /// "debug" (.pdb) resources
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public ResourceHashesByNameDictionary pdb { get; set; }
+
+ ///
+ /// localization (.satellite resx) resources
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public Dictionary satelliteResources { get; set; }
+
+ ///
+ /// Assembly (.dll) resources that are loaded lazily during runtime
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public ResourceHashesByNameDictionary lazyAssembly { get; set; }
+
+ ///
+ /// JavaScript module initializers that Blazor will be in charge of loading.
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public ResourceHashesByNameDictionary libraryInitializers { get; set; }
+
+ ///
+ /// Extensions created by users customizing the initialization process. The format of the file(s)
+ /// is up to the user.
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public Dictionary extensions { get; set; }
+
+ ///
+ /// Additional assets that the runtime consumes as part of the boot process.
+ ///
+ [DataMember(EmitDefaultValue = false)]
+ public Dictionary runtimeAssets { get; set; }
+
+}
+
+public enum ICUDataMode : int
+{
+ // Note that the numeric values are serialized and used in JS code, so don't change them without also updating the JS code
+
+ ///
+ /// Load optimized icu data file based on the user's locale
+ ///
+ Sharded = 0,
+
+ ///
+ /// Use the combined icudt.dat file
+ ///
+ All = 1,
+
+ ///
+ /// Do not load any icu data files.
+ ///
+ Invariant = 2,
+
+ ///
+ /// Load custom icu file provided by the developer.
+ ///
+ Custom = 3,
+}
+
+[DataContract]
+public class AdditionalAsset
+{
+ [DataMember(Name = "hash")]
+ public string Hash { get; set; }
+
+ [DataMember(Name = "behavior")]
+ public string Behavior { get; set; }
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs
new file mode 100644
index 00000000000000..68a563322f613e
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs
@@ -0,0 +1,266 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Microsoft.NET.Sdk.WebAssembly;
+
+namespace Microsoft.NET.Sdk.WebAssembly;
+
+// This task does the build work of processing the project inputs and producing a set of pseudo-static web assets.
+public class ComputeWasmBuildAssets : Task
+{
+ [Required]
+ public ITaskItem[] Candidates { get; set; }
+
+ public ITaskItem CustomIcuCandidate { get; set; }
+
+ [Required]
+ public ITaskItem[] ProjectAssembly { get; set; }
+
+ [Required]
+ public ITaskItem[] ProjectDebugSymbols { get; set; }
+
+ [Required]
+ public ITaskItem[] SatelliteAssemblies { get; set; }
+
+ [Required]
+ public ITaskItem[] ProjectSatelliteAssemblies { get; set; }
+
+ [Required]
+ public string OutputPath { get; set; }
+
+ [Required]
+ public bool TimeZoneSupport { get; set; }
+
+ [Required]
+ public bool InvariantGlobalization { get; set; }
+
+ [Required]
+ public bool CopySymbols { get; set; }
+
+ public bool FingerprintDotNetJs { get; set; }
+
+ [Output]
+ public ITaskItem[] AssetCandidates { get; set; }
+
+ [Output]
+ public ITaskItem[] FilesToRemove { get; set; }
+
+ public override bool Execute()
+ {
+ var filesToRemove = new List();
+ var assetCandidates = new List();
+
+ try
+ {
+ if (ProjectAssembly.Length != 1)
+ {
+ Log.LogError("Invalid number of project assemblies '{0}'", string.Join("," + Environment.NewLine, ProjectAssembly.Select(a => a.ItemSpec)));
+ return true;
+ }
+
+ if (ProjectDebugSymbols.Length > 1)
+ {
+ Log.LogError("Invalid number of symbol assemblies '{0}'", string.Join("," + Environment.NewLine, ProjectDebugSymbols.Select(a => a.ItemSpec)));
+ return true;
+ }
+
+ if (AssetsComputingHelper.TryGetAssetFilename(CustomIcuCandidate, out string customIcuCandidateFilename))
+ {
+ var customIcuCandidate = AssetsComputingHelper.GetCustomIcuAsset(CustomIcuCandidate);
+ assetCandidates.Add(customIcuCandidate);
+ }
+
+ for (int i = 0; i < Candidates.Length; i++)
+ {
+ var candidate = Candidates[i];
+ if (AssetsComputingHelper.ShouldFilterCandidate(candidate, TimeZoneSupport, InvariantGlobalization, CopySymbols, customIcuCandidateFilename, out var reason))
+ {
+ Log.LogMessage(MessageImportance.Low, "Skipping asset '{0}' because '{1}'", candidate.ItemSpec, reason);
+ filesToRemove.Add(candidate);
+ continue;
+ }
+
+ var satelliteAssembly = SatelliteAssemblies.FirstOrDefault(s => s.ItemSpec == candidate.ItemSpec);
+ if (satelliteAssembly != null)
+ {
+ var inferredCulture = satelliteAssembly.GetMetadata("DestinationSubDirectory").Trim('\\', '/');
+ Log.LogMessage(MessageImportance.Low, "Found satellite assembly '{0}' asset for candidate '{1}' with inferred culture '{2}'", satelliteAssembly.ItemSpec, candidate.ItemSpec, inferredCulture);
+
+ var assetCandidate = new TaskItem(satelliteAssembly);
+ assetCandidate.SetMetadata("AssetKind", "Build");
+ assetCandidate.SetMetadata("AssetRole", "Related");
+ assetCandidate.SetMetadata("AssetTraitName", "Culture");
+ assetCandidate.SetMetadata("AssetTraitValue", inferredCulture);
+ assetCandidate.SetMetadata("RelativePath", $"_framework/{inferredCulture}/{satelliteAssembly.GetMetadata("FileName")}{satelliteAssembly.GetMetadata("Extension")}");
+ assetCandidate.SetMetadata("RelatedAsset", Path.GetFullPath(Path.Combine(OutputPath, "wwwroot", "_framework", Path.GetFileName(assetCandidate.GetMetadata("ResolvedFrom")))));
+
+ assetCandidates.Add(assetCandidate);
+ continue;
+ }
+
+ if (candidate.GetMetadata("FileName") == "dotnet" && candidate.GetMetadata("Extension") == ".js")
+ {
+ string newDotnetJSFileName = null;
+ string newDotNetJSFullPath = null;
+ if (FingerprintDotNetJs)
+ {
+ var itemHash = FileHasher.GetFileHash(candidate.ItemSpec);
+ newDotnetJSFileName = $"dotnet.{candidate.GetMetadata("NuGetPackageVersion")}.{itemHash}.js";
+
+ var originalFileFullPath = Path.GetFullPath(candidate.ItemSpec);
+ var originalFileDirectory = Path.GetDirectoryName(originalFileFullPath);
+
+ newDotNetJSFullPath = Path.Combine(originalFileDirectory, newDotnetJSFileName);
+ }
+ else
+ {
+ newDotNetJSFullPath = candidate.ItemSpec;
+ newDotnetJSFileName = Path.GetFileName(newDotNetJSFullPath);
+ }
+
+ var newDotNetJs = new TaskItem(newDotNetJSFullPath, candidate.CloneCustomMetadata());
+ newDotNetJs.SetMetadata("OriginalItemSpec", candidate.ItemSpec);
+
+ var newRelativePath = $"_framework/{newDotnetJSFileName}";
+ newDotNetJs.SetMetadata("RelativePath", newRelativePath);
+
+ newDotNetJs.SetMetadata("AssetTraitName", "WasmResource");
+ newDotNetJs.SetMetadata("AssetTraitValue", "native");
+
+ assetCandidates.Add(newDotNetJs);
+ continue;
+ }
+ else
+ {
+ string relativePath = AssetsComputingHelper.GetCandidateRelativePath(candidate);
+ candidate.SetMetadata("RelativePath", relativePath);
+ }
+
+ // Workaround for https://github.com/dotnet/aspnetcore/issues/37574.
+ // For items added as "Reference" in project references, the OriginalItemSpec is incorrect.
+ // Ignore it, and use the FullPath instead.
+ if (candidate.GetMetadata("ReferenceSourceTarget") == "ProjectReference")
+ {
+ candidate.SetMetadata("OriginalItemSpec", candidate.ItemSpec);
+ }
+
+ var culture = candidate.GetMetadata("Culture");
+ if (!string.IsNullOrEmpty(culture))
+ {
+ candidate.SetMetadata("AssetKind", "Build");
+ candidate.SetMetadata("AssetRole", "Related");
+ candidate.SetMetadata("AssetTraitName", "Culture");
+ candidate.SetMetadata("AssetTraitValue", culture);
+ var fileName = candidate.GetMetadata("FileName");
+ var suffixIndex = fileName.Length - ".resources".Length;
+ var relatedAssetPath = Path.GetFullPath(Path.Combine(
+ OutputPath,
+ "wwwroot",
+ "_framework",
+ fileName.Substring(0, suffixIndex) + ProjectAssembly[0].GetMetadata("Extension")));
+
+ candidate.SetMetadata("RelatedAsset", relatedAssetPath);
+
+ Log.LogMessage(MessageImportance.Low, "Found satellite assembly '{0}' asset for inferred candidate '{1}' with culture '{2}'", candidate.ItemSpec, relatedAssetPath, culture);
+ }
+
+ assetCandidates.Add(candidate);
+ }
+
+ var intermediateAssembly = new TaskItem(ProjectAssembly[0]);
+ intermediateAssembly.SetMetadata("RelativePath", $"_framework/{intermediateAssembly.GetMetadata("FileName")}{intermediateAssembly.GetMetadata("Extension")}");
+ assetCandidates.Add(intermediateAssembly);
+
+ if (ProjectDebugSymbols.Length > 0)
+ {
+ var debugSymbols = new TaskItem(ProjectDebugSymbols[0]);
+ debugSymbols.SetMetadata("RelativePath", $"_framework/{debugSymbols.GetMetadata("FileName")}{debugSymbols.GetMetadata("Extension")}");
+ assetCandidates.Add(debugSymbols);
+ }
+
+ for (int i = 0; i < ProjectSatelliteAssemblies.Length; i++)
+ {
+ var projectSatelliteAssembly = ProjectSatelliteAssemblies[i];
+ var candidateCulture = projectSatelliteAssembly.GetMetadata("Culture");
+ Log.LogMessage(
+ "Found satellite assembly '{0}' asset for project '{1}' with culture '{2}'",
+ projectSatelliteAssembly.ItemSpec,
+ intermediateAssembly.ItemSpec,
+ candidateCulture);
+
+ var assetCandidate = new TaskItem(Path.GetFullPath(projectSatelliteAssembly.ItemSpec), projectSatelliteAssembly.CloneCustomMetadata());
+ var projectAssemblyAssetPath = Path.GetFullPath(Path.Combine(
+ OutputPath,
+ "wwwroot",
+ "_framework",
+ ProjectAssembly[0].GetMetadata("FileName") + ProjectAssembly[0].GetMetadata("Extension")));
+
+ var normalizedPath = assetCandidate.GetMetadata("TargetPath").Replace('\\', '/');
+
+ assetCandidate.SetMetadata("AssetKind", "Build");
+ assetCandidate.SetMetadata("AssetRole", "Related");
+ assetCandidate.SetMetadata("AssetTraitName", "Culture");
+ assetCandidate.SetMetadata("AssetTraitValue", candidateCulture);
+ assetCandidate.SetMetadata("RelativePath", Path.Combine("_framework", normalizedPath));
+ assetCandidate.SetMetadata("RelatedAsset", projectAssemblyAssetPath);
+
+ assetCandidates.Add(assetCandidate);
+ }
+
+ for (var i = 0; i < assetCandidates.Count; i++)
+ {
+ var candidate = assetCandidates[i];
+ ApplyUniqueMetadataProperties(candidate);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.LogError(ex.ToString());
+ return false;
+ }
+
+ FilesToRemove = filesToRemove.ToArray();
+ AssetCandidates = assetCandidates.ToArray();
+
+ return !Log.HasLoggedErrors;
+ }
+
+ private static void ApplyUniqueMetadataProperties(ITaskItem candidate)
+ {
+ var extension = candidate.GetMetadata("Extension");
+ var filename = candidate.GetMetadata("FileName");
+ switch (extension)
+ {
+ case ".dll":
+ if (string.IsNullOrEmpty(candidate.GetMetadata("AssetTraitName")))
+ {
+ candidate.SetMetadata("AssetTraitName", "WasmResource");
+ candidate.SetMetadata("AssetTraitValue", "runtime");
+ }
+ if (string.Equals(candidate.GetMetadata("ResolvedFrom"), "{HintPathFromItem}", StringComparison.Ordinal))
+ {
+ candidate.RemoveMetadata("OriginalItemSpec");
+ }
+ break;
+ case ".wasm":
+ case ".blat":
+ case ".dat" when filename.StartsWith("icudt"):
+ candidate.SetMetadata("AssetTraitName", "WasmResource");
+ candidate.SetMetadata("AssetTraitValue", "native");
+ break;
+ case ".pdb":
+ candidate.SetMetadata("AssetTraitName", "WasmResource");
+ candidate.SetMetadata("AssetTraitValue", "symbol");
+ candidate.RemoveMetadata("OriginalItemSpec");
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs
new file mode 100644
index 00000000000000..d622db24a31a5e
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs
@@ -0,0 +1,628 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Microsoft.NET.Sdk.WebAssembly;
+
+namespace Microsoft.NET.Sdk.WebAssembly;
+
+// This target computes the list of publish static web assets based on the changes that happen during publish and the list of build static
+// web assets.
+// In this target we need to do 2 things:
+// * Harmonize the list of dlls produced at build time with the list of resolved files to publish.
+// * We iterate over the list of existing static web assets and do as follows:
+// * If we find the assembly in the resolved files to publish and points to the original assembly (linker disabled or assembly not linked)
+// we create a new "Publish" static web asset for the assembly.
+// * If we find the assembly in the resolved files to publish and points to a new location, we assume this assembly has been updated (as part of linking)
+// and we create a new "Publish" static web asset for the asembly pointing to the new location.
+// * If we don't find the assembly on the resolved files to publish it has been linked out from the app, so we don't add any new static web asset and we
+// also avoid adding any existing related static web asset (satellite assemblies and compressed versions).
+// * We update static web assets for satellite assemblies and compressed assets accordingly.
+// * Look at the list of "native" assets and determine whether we need to create new publish assets for the current build assets or if we need to
+// update the native assets because the app was ahead of time compiled.
+public class ComputeWasmPublishAssets : Task
+{
+ [Required]
+ public ITaskItem[] ResolvedFilesToPublish { get; set; }
+
+ public ITaskItem CustomIcuCandidate { get; set; }
+
+ [Required]
+ public ITaskItem[] WasmAotAssets { get; set; }
+
+ [Required]
+ public ITaskItem[] ExistingAssets { get; set; }
+
+ [Required]
+ public bool TimeZoneSupport { get; set; }
+
+ [Required]
+ public bool InvariantGlobalization { get; set; }
+
+ [Required]
+ public bool CopySymbols { get; set; }
+
+ [Required]
+ public string PublishPath { get; set; }
+
+ [Required]
+ public string DotNetJsVersion { get; set; }
+
+ public bool FingerprintDotNetJs { get; set; }
+
+ [Output]
+ public ITaskItem[] NewCandidates { get; set; }
+
+ [Output]
+ public ITaskItem[] FilesToRemove { get; set; }
+
+ public override bool Execute()
+ {
+ var filesToRemove = new List();
+ var newAssets = new List();
+
+ try
+ {
+ // We'll do a first pass over the resolved files to publish to figure out what files need to be removed
+ // as well as categorize resolved files into different groups.
+ var resolvedFilesToPublishToRemove = new Dictionary(StringComparer.Ordinal);
+
+ // These assemblies are keyed of the assembly name "computed" based on the relative path, which must be
+ // unique.
+ var resolvedAssembliesToPublish = new Dictionary(StringComparer.Ordinal);
+ var resolvedSymbolsToPublish = new Dictionary(StringComparer.Ordinal);
+ var satelliteAssemblyToPublish = new Dictionary<(string, string), ITaskItem>(EqualityComparer<(string, string)>.Default);
+ var resolvedNativeAssetToPublish = new Dictionary(StringComparer.Ordinal);
+ GroupResolvedFilesToPublish(
+ resolvedFilesToPublishToRemove,
+ resolvedAssembliesToPublish,
+ satelliteAssemblyToPublish,
+ resolvedSymbolsToPublish,
+ resolvedNativeAssetToPublish);
+
+ // Group candidate static web assets
+ var assemblyAssets = new Dictionary();
+ var symbolAssets = new Dictionary();
+ var nativeAssets = new Dictionary();
+ var satelliteAssemblyAssets = new Dictionary();
+ var compressedRepresentations = new Dictionary();
+ GroupExistingStaticWebAssets(
+ assemblyAssets,
+ nativeAssets,
+ satelliteAssemblyAssets,
+ symbolAssets,
+ compressedRepresentations);
+
+ var newStaticWebAssets = ComputeUpdatedAssemblies(
+ satelliteAssemblyToPublish,
+ filesToRemove,
+ resolvedAssembliesToPublish,
+ assemblyAssets,
+ satelliteAssemblyAssets,
+ compressedRepresentations);
+
+ newAssets.AddRange(newStaticWebAssets);
+
+ var nativeStaticWebAssets = ProcessNativeAssets(
+ nativeAssets,
+ resolvedFilesToPublishToRemove,
+ resolvedNativeAssetToPublish,
+ compressedRepresentations,
+ filesToRemove);
+
+ newAssets.AddRange(nativeStaticWebAssets);
+
+ var symbolStaticWebAssets = ProcessSymbolAssets(
+ symbolAssets,
+ compressedRepresentations,
+ resolvedFilesToPublishToRemove,
+ resolvedSymbolsToPublish,
+ filesToRemove);
+
+ newAssets.AddRange(symbolStaticWebAssets);
+
+ foreach (var kvp in resolvedFilesToPublishToRemove)
+ {
+ var resolvedPublishFileToRemove = kvp.Value;
+ filesToRemove.Add(resolvedPublishFileToRemove);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.LogError(ex.ToString());
+ return false;
+ }
+
+ FilesToRemove = filesToRemove.ToArray();
+ NewCandidates = newAssets.ToArray();
+
+ return !Log.HasLoggedErrors;
+ }
+
+ private List ProcessNativeAssets(
+ Dictionary nativeAssets,
+ IDictionary resolvedPublishFilesToRemove,
+ Dictionary resolvedNativeAssetToPublish,
+ Dictionary compressedRepresentations,
+ List filesToRemove)
+ {
+ var nativeStaticWebAssets = new List();
+
+ // Keep track of the updated assets to determine what compressed assets we can reuse
+ var updateMap = new Dictionary();
+
+ foreach (var kvp in nativeAssets)
+ {
+ var key = kvp.Key;
+ var asset = kvp.Value;
+ var isDotNetJs = IsDotNetJs(key);
+ var isDotNetWasm = IsDotNetWasm(key);
+ if (!isDotNetJs && !isDotNetWasm)
+ {
+ if (resolvedNativeAssetToPublish.TryGetValue(Path.GetFileName(asset.GetMetadata("OriginalItemSpec")), out var existing))
+ {
+ if (!resolvedPublishFilesToRemove.TryGetValue(existing.ItemSpec, out var removed))
+ {
+ // This is a native asset like timezones.blat or similar that was not filtered and that needs to be updated
+ // to a publish asset.
+ var newAsset = new TaskItem(asset);
+ ApplyPublishProperties(newAsset);
+ nativeStaticWebAssets.Add(newAsset);
+ filesToRemove.Add(existing);
+ updateMap.Add(asset.ItemSpec, newAsset);
+ Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Removing asset '{0}'.", existing.ItemSpec);
+ // This was a file that was filtered, so just remove it, we don't need to add any publish static web asset
+ filesToRemove.Add(removed);
+
+ // Remove the file from the list to avoid double processing later when we process other files we filtered.
+ resolvedPublishFilesToRemove.Remove(existing.ItemSpec);
+ }
+ }
+
+ continue;
+ }
+
+ if (isDotNetJs)
+ {
+ var aotDotNetJs = WasmAotAssets.SingleOrDefault(a => $"{a.GetMetadata("FileName")}{a.GetMetadata("Extension")}" == "dotnet.js");
+ ITaskItem newDotNetJs = null;
+ if (aotDotNetJs != null && FingerprintDotNetJs)
+ {
+ newDotNetJs = new TaskItem(Path.GetFullPath(aotDotNetJs.ItemSpec), asset.CloneCustomMetadata());
+ newDotNetJs.SetMetadata("OriginalItemSpec", aotDotNetJs.ItemSpec);
+ newDotNetJs.SetMetadata("RelativePath", $"_framework/{$"dotnet.{DotNetJsVersion}.{FileHasher.GetFileHash(aotDotNetJs.ItemSpec)}.js"}");
+
+ updateMap.Add(asset.ItemSpec, newDotNetJs);
+ Log.LogMessage(MessageImportance.Low, "Replacing asset '{0}' with AoT version '{1}'", asset.ItemSpec, newDotNetJs.ItemSpec);
+ }
+ else
+ {
+ newDotNetJs = new TaskItem(asset);
+ Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec);
+ }
+
+ ApplyPublishProperties(newDotNetJs);
+ nativeStaticWebAssets.Add(newDotNetJs);
+ if (resolvedNativeAssetToPublish.TryGetValue("dotnet.js", out var resolved))
+ {
+ filesToRemove.Add(resolved);
+ }
+ continue;
+ }
+
+ if (isDotNetWasm)
+ {
+ var aotDotNetWasm = WasmAotAssets.SingleOrDefault(a => $"{a.GetMetadata("FileName")}{a.GetMetadata("Extension")}" == "dotnet.wasm");
+ ITaskItem newDotNetWasm = null;
+ if (aotDotNetWasm != null)
+ {
+ newDotNetWasm = new TaskItem(Path.GetFullPath(aotDotNetWasm.ItemSpec), asset.CloneCustomMetadata());
+ newDotNetWasm.SetMetadata("OriginalItemSpec", aotDotNetWasm.ItemSpec);
+ updateMap.Add(asset.ItemSpec, newDotNetWasm);
+ Log.LogMessage(MessageImportance.Low, "Replacing asset '{0}' with AoT version '{1}'", asset.ItemSpec, newDotNetWasm.ItemSpec);
+ }
+ else
+ {
+ newDotNetWasm = new TaskItem(asset);
+ Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec);
+ }
+
+ ApplyPublishProperties(newDotNetWasm);
+ nativeStaticWebAssets.Add(newDotNetWasm);
+ if (resolvedNativeAssetToPublish.TryGetValue("dotnet.wasm", out var resolved))
+ {
+ filesToRemove.Add(resolved);
+ }
+ continue;
+ }
+ }
+
+ var compressedUpdatedFiles = ProcessCompressedAssets(compressedRepresentations, nativeAssets, updateMap);
+ foreach (var f in compressedUpdatedFiles)
+ {
+ nativeStaticWebAssets.Add(f);
+ }
+
+ return nativeStaticWebAssets;
+
+ static bool IsDotNetJs(string key)
+ {
+ var fileName = Path.GetFileName(key);
+ return fileName.StartsWith("dotnet.", StringComparison.Ordinal) && fileName.EndsWith(".js", StringComparison.Ordinal) && !fileName.Contains("worker");
+ }
+
+ static bool IsDotNetWasm(string key) => string.Equals("dotnet.wasm", Path.GetFileName(key), StringComparison.Ordinal);
+ }
+
+ private List ProcessSymbolAssets(
+ Dictionary symbolAssets,
+ Dictionary compressedRepresentations,
+ Dictionary resolvedPublishFilesToRemove,
+ Dictionary resolvedSymbolAssetToPublish,
+ List filesToRemove)
+ {
+ var symbolStaticWebAssets = new List();
+ var updateMap = new Dictionary();
+
+ foreach (var kvp in symbolAssets)
+ {
+ var asset = kvp.Value;
+ if (resolvedSymbolAssetToPublish.TryGetValue(Path.GetFileName(asset.GetMetadata("OriginalItemSpec")), out var existing))
+ {
+ if (!resolvedPublishFilesToRemove.TryGetValue(existing.ItemSpec, out var removed))
+ {
+ // This is a symbol asset like classlibrary.pdb or similar that was not filtered and that needs to be updated
+ // to a publish asset.
+ var newAsset = new TaskItem(asset);
+ ApplyPublishProperties(newAsset);
+ symbolStaticWebAssets.Add(newAsset);
+ updateMap.Add(newAsset.ItemSpec, newAsset);
+ filesToRemove.Add(existing);
+ Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec);
+ }
+ else
+ {
+ // This was a file that was filtered, so just remove it, we don't need to add any publish static web asset
+ filesToRemove.Add(removed);
+
+ // Remove the file from the list to avoid double processing later when we process other files we filtered.
+ resolvedPublishFilesToRemove.Remove(existing.ItemSpec);
+ }
+ }
+ }
+
+ var compressedFiles = ProcessCompressedAssets(compressedRepresentations, symbolAssets, updateMap);
+
+ foreach (var file in compressedFiles)
+ {
+ symbolStaticWebAssets.Add(file);
+ }
+
+ return symbolStaticWebAssets;
+ }
+
+ private List ComputeUpdatedAssemblies(
+ IDictionary<(string, string assemblyName), ITaskItem> satelliteAssemblies,
+ List filesToRemove,
+ Dictionary resolvedAssembliesToPublish,
+ Dictionary assemblyAssets,
+ Dictionary satelliteAssemblyAssets,
+ Dictionary compressedRepresentations)
+ {
+ // All assemblies, satellite assemblies and gzip files are initially defined as build assets.
+ // We need to update them to publish assets when they haven't changed or when they have been linked.
+ // For satellite assemblies and compressed files, we won't include them in the list of assets to update
+ // when the original assembly they depend on has been linked out.
+ var assetsToUpdate = new Dictionary();
+ var linkedAssets = new Dictionary();
+
+ foreach (var kvp in assemblyAssets)
+ {
+ var asset = kvp.Value;
+ var fileName = Path.GetFileName(asset.GetMetadata("RelativePath"));
+ if (resolvedAssembliesToPublish.TryGetValue(fileName, out var existing))
+ {
+ // We found the assembly, so it'll have to be updated.
+ assetsToUpdate.Add(asset.ItemSpec, asset);
+ filesToRemove.Add(existing);
+ if (!string.Equals(asset.ItemSpec, existing.GetMetadata("FullPath"), StringComparison.Ordinal))
+ {
+ linkedAssets.Add(asset.ItemSpec, existing);
+ }
+ }
+ }
+
+ foreach (var kvp in satelliteAssemblyAssets)
+ {
+ var satelliteAssembly = kvp.Value;
+ var relatedAsset = satelliteAssembly.GetMetadata("RelatedAsset");
+ if (assetsToUpdate.ContainsKey(relatedAsset))
+ {
+ assetsToUpdate.Add(satelliteAssembly.ItemSpec, satelliteAssembly);
+ var culture = satelliteAssembly.GetMetadata("AssetTraitValue");
+ var fileName = Path.GetFileName(satelliteAssembly.GetMetadata("RelativePath"));
+ if (satelliteAssemblies.TryGetValue((culture, fileName), out var existing))
+ {
+ filesToRemove.Add(existing);
+ }
+ else
+ {
+ var message = $"Can't find the original satellite assembly in the list of resolved files to " +
+ $"publish for asset '{satelliteAssembly.ItemSpec}'.";
+ throw new InvalidOperationException(message);
+ }
+ }
+ }
+
+ var compressedFiles = ProcessCompressedAssets(compressedRepresentations, assetsToUpdate, linkedAssets);
+
+ foreach (var file in compressedFiles)
+ {
+ assetsToUpdate.Add(file.ItemSpec, file);
+ }
+
+ var updatedAssetsMap = new Dictionary(StringComparer.Ordinal);
+ foreach (var asset in assetsToUpdate.Select(a => a.Value).OrderBy(a => a.GetMetadata("AssetRole"), Comparer.Create(OrderByAssetRole)))
+ {
+ var assetTraitName = asset.GetMetadata("AssetTraitName");
+ switch (assetTraitName)
+ {
+ case "WasmResource":
+ ITaskItem newAsemblyAsset = null;
+ if (linkedAssets.TryGetValue(asset.ItemSpec, out var linked))
+ {
+ newAsemblyAsset = new TaskItem(linked.GetMetadata("FullPath"), asset.CloneCustomMetadata());
+ newAsemblyAsset.SetMetadata("OriginalItemSpec", linked.ItemSpec);
+ Log.LogMessage(MessageImportance.Low, "Replacing asset '{0}' with linked version '{1}'",
+ asset.ItemSpec,
+ newAsemblyAsset.ItemSpec);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Linked asset not found for asset '{0}'", asset.ItemSpec);
+ newAsemblyAsset = new TaskItem(asset);
+ }
+ ApplyPublishProperties(newAsemblyAsset);
+
+ updatedAssetsMap.Add(asset.ItemSpec, newAsemblyAsset);
+ break;
+ default:
+ // Satellite assembliess and compressed assets
+ var dependentAsset = new TaskItem(asset);
+ ApplyPublishProperties(dependentAsset);
+ UpdateRelatedAssetProperty(asset, dependentAsset, updatedAssetsMap);
+ Log.LogMessage(MessageImportance.Low, "Promoting asset '{0}' to Publish asset.", asset.ItemSpec);
+
+ updatedAssetsMap.Add(asset.ItemSpec, dependentAsset);
+ break;
+ }
+ }
+
+ return updatedAssetsMap.Values.ToList();
+ }
+
+ private List ProcessCompressedAssets(
+ Dictionary compressedRepresentations,
+ Dictionary assetsToUpdate,
+ Dictionary updatedAssets)
+ {
+ var processed = new List();
+ var runtimeAssetsToUpdate = new List();
+ foreach (var kvp in compressedRepresentations)
+ {
+ var compressedAsset = kvp.Value;
+ var relatedAsset = compressedAsset.GetMetadata("RelatedAsset");
+ if (assetsToUpdate.ContainsKey(relatedAsset))
+ {
+ if (!updatedAssets.ContainsKey(relatedAsset))
+ {
+ Log.LogMessage(MessageImportance.Low, "Related assembly for '{0}' was not updated and the compressed asset can be reused.", relatedAsset);
+ var newCompressedAsset = new TaskItem(compressedAsset);
+ ApplyPublishProperties(newCompressedAsset);
+ runtimeAssetsToUpdate.Add(newCompressedAsset);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Related assembly for '{0}' was updated and the compressed asset will be discarded.", relatedAsset);
+ }
+
+ processed.Add(kvp.Key);
+ }
+ }
+
+ // Remove all the elements we've found to avoid having to iterate over them when we process other assets.
+ foreach (var element in processed)
+ {
+ compressedRepresentations.Remove(element);
+ }
+
+ return runtimeAssetsToUpdate;
+ }
+
+ private static void UpdateRelatedAssetProperty(ITaskItem asset, TaskItem newAsset, Dictionary updatedAssetsMap)
+ {
+ if (!updatedAssetsMap.TryGetValue(asset.GetMetadata("RelatedAsset"), out var updatedRelatedAsset))
+ {
+ throw new InvalidOperationException("Related asset not found.");
+ }
+
+ newAsset.SetMetadata("RelatedAsset", updatedRelatedAsset.ItemSpec);
+ }
+
+ private int OrderByAssetRole(string left, string right)
+ {
+ var leftScore = GetScore(left);
+ var rightScore = GetScore(right);
+
+ return leftScore - rightScore;
+
+ static int GetScore(string candidate) => candidate switch
+ {
+ "Primary" => 0,
+ "Related" => 1,
+ "Alternative" => 2,
+ _ => throw new InvalidOperationException("Invalid asset role"),
+ };
+ }
+
+ private void ApplyPublishProperties(ITaskItem newAsemblyAsset)
+ {
+ newAsemblyAsset.SetMetadata("AssetKind", "Publish");
+ newAsemblyAsset.SetMetadata("ContentRoot", Path.Combine(PublishPath, "wwwroot"));
+ newAsemblyAsset.SetMetadata("CopyToOutputDirectory", "Never");
+ newAsemblyAsset.SetMetadata("CopyToPublishDirectory", "PreserveNewest");
+ }
+
+ private void GroupExistingStaticWebAssets(
+ Dictionary assemblyAssets,
+ Dictionary nativeAssets,
+ Dictionary satelliteAssemblyAssets,
+ Dictionary symbolAssets,
+ Dictionary compressedRepresentations)
+ {
+ foreach (var asset in ExistingAssets)
+ {
+ var traitName = asset.GetMetadata("AssetTraitName");
+ if (IsWebAssemblyResource(traitName))
+ {
+ var traitValue = asset.GetMetadata("AssetTraitValue");
+ if (IsRuntimeAsset(traitValue))
+ {
+ assemblyAssets.Add(asset.ItemSpec, asset);
+ }
+ else if (IsNativeAsset(traitValue))
+ {
+ nativeAssets.Add(asset.ItemSpec, asset);
+ }
+ else if (IsSymbolAsset(traitValue))
+ {
+ symbolAssets.Add(asset.ItemSpec, asset);
+ }
+ }
+ else if (IsCulture(traitName))
+ {
+ satelliteAssemblyAssets.Add(asset.ItemSpec, asset);
+ }
+ else if (IsAlternative(asset))
+ {
+ compressedRepresentations.Add(asset.ItemSpec, asset);
+ }
+ }
+ }
+
+ private void GroupResolvedFilesToPublish(
+ Dictionary resolvedFilesToPublishToRemove,
+ Dictionary resolvedAssemblyToPublish,
+ Dictionary<(string, string), ITaskItem> satelliteAssemblyToPublish,
+ Dictionary resolvedSymbolsToPublish,
+ Dictionary resolvedNativeAssetToPublish)
+ {
+ var resolvedFilesToPublish = ResolvedFilesToPublish.ToList();
+ if (AssetsComputingHelper.TryGetAssetFilename(CustomIcuCandidate, out string customIcuCandidateFilename))
+ {
+ var customIcuCandidate = AssetsComputingHelper.GetCustomIcuAsset(CustomIcuCandidate);
+ resolvedFilesToPublish.Add(customIcuCandidate);
+ }
+
+ foreach (var candidate in resolvedFilesToPublish)
+ {
+ if (AssetsComputingHelper.ShouldFilterCandidate(candidate, TimeZoneSupport, InvariantGlobalization, CopySymbols, customIcuCandidateFilename, out var reason))
+ {
+ Log.LogMessage(MessageImportance.Low, "Skipping asset '{0}' because '{1}'", candidate.ItemSpec, reason);
+ if (!resolvedFilesToPublishToRemove.ContainsKey(candidate.ItemSpec))
+ {
+ resolvedFilesToPublishToRemove.Add(candidate.ItemSpec, candidate);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Duplicate candidate '{0}' found in ResolvedFilesToPublish", candidate.ItemSpec);
+ }
+ continue;
+ }
+
+ var extension = candidate.GetMetadata("Extension");
+ if (string.Equals(extension, ".dll", StringComparison.Ordinal))
+ {
+ var culture = candidate.GetMetadata("Culture");
+ var inferredCulture = candidate.GetMetadata("DestinationSubDirectory").Replace("\\", "/").Trim('/');
+ if (!string.IsNullOrEmpty(culture) || !string.IsNullOrEmpty(inferredCulture))
+ {
+ var finalCulture = !string.IsNullOrEmpty(culture) ? culture : inferredCulture;
+ var assemblyName = Path.GetFileName(candidate.GetMetadata("RelativePath").Replace("\\", "/"));
+ if (!satelliteAssemblyToPublish.ContainsKey((finalCulture, assemblyName)))
+ {
+ satelliteAssemblyToPublish.Add((finalCulture, assemblyName), candidate);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Duplicate candidate '{0}' found in ResolvedFilesToPublish", candidate.ItemSpec);
+ }
+ continue;
+ }
+
+ var candidateName = Path.GetFileName(candidate.GetMetadata("RelativePath"));
+ if (!resolvedAssemblyToPublish.ContainsKey(candidateName))
+ {
+ resolvedAssemblyToPublish.Add(candidateName, candidate);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Duplicate candidate '{0}' found in ResolvedFilesToPublish", candidate.ItemSpec);
+ }
+
+ continue;
+ }
+
+ if (string.Equals(extension, ".pdb", StringComparison.Ordinal))
+ {
+ var candidateName = Path.GetFileName(candidate.GetMetadata("RelativePath"));
+ if (!resolvedSymbolsToPublish.ContainsKey(candidateName))
+ {
+ resolvedSymbolsToPublish.Add(candidateName, candidate);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Duplicate candidate '{0}' found in ResolvedFilesToPublish", candidate.ItemSpec);
+ }
+
+ continue;
+ }
+
+ // Capture all the native unfiltered assets since we need to process them to determine what static web assets need to get
+ // upgraded
+ if (string.Equals(candidate.GetMetadata("AssetType"), "native", StringComparison.Ordinal))
+ {
+ var candidateName = $"{candidate.GetMetadata("FileName")}{extension}";
+ if (!resolvedNativeAssetToPublish.ContainsKey(candidateName))
+ {
+ resolvedNativeAssetToPublish.Add(candidateName, candidate);
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Duplicate candidate '{0}' found in ResolvedFilesToPublish", candidate.ItemSpec);
+ }
+ continue;
+ }
+ }
+ }
+
+ private static bool IsNativeAsset(string traitValue) => string.Equals(traitValue, "native", StringComparison.Ordinal);
+
+ private static bool IsRuntimeAsset(string traitValue) => string.Equals(traitValue, "runtime", StringComparison.Ordinal);
+ private static bool IsSymbolAsset(string traitValue) => string.Equals(traitValue, "symbol", StringComparison.Ordinal);
+
+ private static bool IsAlternative(ITaskItem asset) => string.Equals(asset.GetMetadata("AssetRole"), "Alternative", StringComparison.Ordinal);
+
+ private static bool IsCulture(string traitName) => string.Equals(traitName, "Culture", StringComparison.Ordinal);
+
+ private static bool IsWebAssemblyResource(string traitName) => string.Equals(traitName, "WasmResource", StringComparison.Ordinal);
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/FileHasher.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/FileHasher.cs
new file mode 100644
index 00000000000000..c264a1b42a4617
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/FileHasher.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Numerics;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Microsoft.NET.Sdk.WebAssembly;
+
+public static class FileHasher
+{
+ public static string GetFileHash(string filePath)
+ {
+ using var hash = SHA256.Create();
+ var bytes = Encoding.UTF8.GetBytes(filePath);
+ var hashBytes = hash.ComputeHash(bytes);
+ return ToBase36(hashBytes);
+ }
+
+ private static string ToBase36(byte[] hash)
+ {
+ const string chars = "0123456789abcdefghijklmnopqrstuvwxyz";
+
+ var result = new char[10];
+ var dividend = BigInteger.Abs(new BigInteger(hash.AsSpan().Slice(0, 9).ToArray()));
+ for (var i = 0; i < 10; i++)
+ {
+ dividend = BigInteger.DivRem(dividend, 36, out var remainder);
+ result[i] = chars[(int)remainder];
+ }
+
+ return new string(result);
+ }
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
new file mode 100644
index 00000000000000..d8a5a3d2ae9e6a
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs
@@ -0,0 +1,341 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using System.Xml;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary;
+
+namespace Microsoft.NET.Sdk.WebAssembly;
+
+public class GenerateWasmBootJson : Task
+{
+ private static readonly string[] jiterpreterOptions = new[] { "jiterpreter-traces-enabled", "jiterpreter-interp-entry-enabled", "jiterpreter-jit-call-enabled" };
+
+ [Required]
+ public string AssemblyPath { get; set; }
+
+ [Required]
+ public ITaskItem[] Resources { get; set; }
+
+ [Required]
+ public bool DebugBuild { get; set; }
+
+ [Required]
+ public bool LinkerEnabled { get; set; }
+
+ [Required]
+ public bool CacheBootResources { get; set; }
+
+ public bool LoadAllICUData { get; set; }
+
+ public bool LoadCustomIcuData { get; set; }
+
+ public string InvariantGlobalization { get; set; }
+
+ public ITaskItem[] ConfigurationFiles { get; set; }
+
+ public ITaskItem[] Extensions { get; set; }
+
+ public string StartupMemoryCache { get; set; }
+
+ public string Jiterpreter { get; set; }
+
+ public string RuntimeOptions { get; set; }
+
+ [Required]
+ public string OutputPath { get; set; }
+
+ public ITaskItem[] LazyLoadedAssemblies { get; set; }
+
+ public override bool Execute()
+ {
+ using var fileStream = File.Create(OutputPath);
+ var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name;
+
+ try
+ {
+ WriteBootJson(fileStream, entryAssemblyName);
+ }
+ catch (Exception ex)
+ {
+ Log.LogError(ex.ToString());
+ }
+
+ return !Log.HasLoggedErrors;
+ }
+
+ // Internal for tests
+ public void WriteBootJson(Stream output, string entryAssemblyName)
+ {
+ var icuDataMode = ICUDataMode.Sharded;
+
+ if (string.Equals(InvariantGlobalization, "true", StringComparison.OrdinalIgnoreCase))
+ {
+ icuDataMode = ICUDataMode.Invariant;
+ }
+ else if (LoadAllICUData)
+ {
+ icuDataMode = ICUDataMode.All;
+ }
+ else if (LoadCustomIcuData)
+ {
+ icuDataMode = ICUDataMode.Custom;
+ }
+
+ var result = new BootJsonData
+ {
+ entryAssembly = entryAssemblyName,
+ cacheBootResources = CacheBootResources,
+ debugBuild = DebugBuild,
+ linkerEnabled = LinkerEnabled,
+ resources = new ResourcesData(),
+ config = new List(),
+ icuDataMode = icuDataMode,
+ startupMemoryCache = ParseOptionalBool(StartupMemoryCache),
+ };
+
+ if (!string.IsNullOrEmpty(RuntimeOptions))
+ {
+ string[] runtimeOptions = RuntimeOptions.Split(' ');
+ result.runtimeOptions = runtimeOptions;
+ }
+
+ bool? jiterpreter = ParseOptionalBool(Jiterpreter);
+ if (jiterpreter != null)
+ {
+ var runtimeOptions = result.runtimeOptions?.ToHashSet() ?? new HashSet(3);
+ foreach (var jiterpreterOption in jiterpreterOptions)
+ {
+ if (jiterpreter == true)
+ {
+ if (!runtimeOptions.Contains($"--no-{jiterpreterOption}"))
+ runtimeOptions.Add($"--{jiterpreterOption}");
+ }
+ else
+ {
+ if (!runtimeOptions.Contains($"--{jiterpreterOption}"))
+ runtimeOptions.Add($"--no-{jiterpreterOption}");
+ }
+ }
+
+ result.runtimeOptions = runtimeOptions.ToArray();
+ }
+
+ // Build a two-level dictionary of the form:
+ // - assembly:
+ // - UriPath (e.g., "System.Text.Json.dll")
+ // - ContentHash (e.g., "4548fa2e9cf52986")
+ // - runtime:
+ // - UriPath (e.g., "dotnet.js")
+ // - ContentHash (e.g., "3448f339acf512448")
+ if (Resources != null)
+ {
+ var remainingLazyLoadAssemblies = new List(LazyLoadedAssemblies ?? Array.Empty());
+ var resourceData = result.resources;
+ foreach (var resource in Resources)
+ {
+ ResourceHashesByNameDictionary resourceList = null;
+
+ string behavior = null;
+ var fileName = resource.GetMetadata("FileName");
+ var fileExtension = resource.GetMetadata("Extension");
+ var assetTraitName = resource.GetMetadata("AssetTraitName");
+ var assetTraitValue = resource.GetMetadata("AssetTraitValue");
+ var resourceName = Path.GetFileName(resource.GetMetadata("RelativePath"));
+
+ if (TryGetLazyLoadedAssembly(resourceName, out var lazyLoad))
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a lazy loaded assembly.", resource.ItemSpec);
+ remainingLazyLoadAssemblies.Remove(lazyLoad);
+ resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary();
+ resourceList = resourceData.lazyAssembly;
+ }
+ else if (string.Equals("Culture", assetTraitName, StringComparison.OrdinalIgnoreCase))
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as satellite assembly with culture '{1}'.", resource.ItemSpec, assetTraitValue);
+ resourceData.satelliteResources ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
+ resourceName = assetTraitValue + "/" + resourceName;
+
+ if (!resourceData.satelliteResources.TryGetValue(assetTraitValue, out resourceList))
+ {
+ resourceList = new ResourceHashesByNameDictionary();
+ resourceData.satelliteResources.Add(assetTraitValue, resourceList);
+ }
+ }
+ else if (string.Equals("symbol", assetTraitValue, StringComparison.OrdinalIgnoreCase))
+ {
+ if (TryGetLazyLoadedAssembly($"{fileName}.dll", out _))
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a lazy loaded symbols file.", resource.ItemSpec);
+ resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary();
+ resourceList = resourceData.lazyAssembly;
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as symbols file.", resource.ItemSpec);
+ resourceData.pdb ??= new ResourceHashesByNameDictionary();
+ resourceList = resourceData.pdb;
+ }
+ }
+ else if (string.Equals("runtime", assetTraitValue, StringComparison.OrdinalIgnoreCase))
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as an app assembly.", resource.ItemSpec);
+ resourceList = resourceData.assembly;
+ }
+ else if (string.Equals(assetTraitName, "WasmResource", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase))
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a native application resource.", resource.ItemSpec);
+ if (string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(fileExtension, ".wasm", StringComparison.OrdinalIgnoreCase))
+ {
+ behavior = "dotnetwasm";
+ }
+
+ resourceList = resourceData.runtime;
+ }
+ else if (string.Equals("JSModule", assetTraitName, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(assetTraitValue, "JSLibraryModule", StringComparison.OrdinalIgnoreCase))
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a library initializer resource.", resource.ItemSpec);
+ resourceData.libraryInitializers ??= new();
+ resourceList = resourceData.libraryInitializers;
+ var targetPath = resource.GetMetadata("TargetPath");
+ Debug.Assert(!string.IsNullOrEmpty(targetPath), "Target path for '{0}' must exist.", resource.ItemSpec);
+ AddResourceToList(resource, resourceList, targetPath);
+ continue;
+ }
+ else if (string.Equals("WasmResource", assetTraitName, StringComparison.OrdinalIgnoreCase) &&
+ assetTraitValue.StartsWith("extension:", StringComparison.OrdinalIgnoreCase))
+ {
+ Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as an extension resource '{1}'.", resource.ItemSpec, assetTraitValue);
+ var extensionName = assetTraitValue.Substring("extension:".Length);
+ resourceData.extensions ??= new();
+ if (!resourceData.extensions.TryGetValue(extensionName, out resourceList))
+ {
+ resourceList = new();
+ resourceData.extensions[extensionName] = resourceList;
+ }
+ var targetPath = resource.GetMetadata("TargetPath");
+ Debug.Assert(!string.IsNullOrEmpty(targetPath), "Target path for '{0}' must exist.", resource.ItemSpec);
+ AddResourceToList(resource, resourceList, targetPath);
+ continue;
+ }
+ else
+ {
+ Log.LogMessage(MessageImportance.Low, "Skipping resource '{0}' since it doesn't belong to a defined category.", resource.ItemSpec);
+ // This should include items such as XML doc files, which do not need to be recorded in the manifest.
+ continue;
+ }
+
+ if (resourceList != null)
+ {
+ AddResourceToList(resource, resourceList, resourceName);
+ }
+
+ if (!string.IsNullOrEmpty(behavior))
+ {
+ resourceData.runtimeAssets ??= new Dictionary();
+ AddToAdditionalResources(resource, resourceData.runtimeAssets, resourceName, behavior);
+ }
+ }
+
+ if (remainingLazyLoadAssemblies.Count > 0)
+ {
+ const string message = "Unable to find '{0}' to be lazy loaded later. Confirm that project or " +
+ "package references are included and the reference is used in the project.";
+
+ Log.LogError(
+ subcategory: null,
+ errorCode: "BLAZORSDK1001",
+ helpKeyword: null,
+ file: null,
+ lineNumber: 0,
+ columnNumber: 0,
+ endLineNumber: 0,
+ endColumnNumber: 0,
+ message: message,
+ string.Join(";", LazyLoadedAssemblies.Select(a => a.ItemSpec)));
+
+ return;
+ }
+ }
+
+ if (ConfigurationFiles != null)
+ {
+ foreach (var configFile in ConfigurationFiles)
+ {
+ result.config.Add(Path.GetFileName(configFile.ItemSpec));
+ }
+ }
+
+ if (Extensions != null && Extensions.Length > 0)
+ {
+ var configSerializer = new DataContractJsonSerializer(typeof(Dictionary), new DataContractJsonSerializerSettings
+ {
+ UseSimpleDictionaryFormat = true
+ });
+
+ result.extensions = new Dictionary> ();
+ foreach (var configExtension in Extensions)
+ {
+ var key = configExtension.GetMetadata("key");
+ var config = (Dictionary)configSerializer.ReadObject(File.OpenRead(configExtension.ItemSpec));
+ result.extensions[key] = config;
+ }
+ }
+
+ var serializer = new DataContractJsonSerializer(typeof(BootJsonData), new DataContractJsonSerializerSettings
+ {
+ UseSimpleDictionaryFormat = true
+ });
+
+ using var writer = JsonReaderWriterFactory.CreateJsonWriter(output, Encoding.UTF8, ownsStream: false, indent: true);
+ serializer.WriteObject(writer, result);
+
+ void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resourceList, string resourceKey)
+ {
+ if (!resourceList.ContainsKey(resourceKey))
+ {
+ Log.LogMessage(MessageImportance.Low, "Added resource '{0}' to the manifest.", resource.ItemSpec);
+ resourceList.Add(resourceKey, $"sha256-{resource.GetMetadata("FileHash")}");
+ }
+ }
+ }
+
+ private static bool? ParseOptionalBool(string value)
+ {
+ if (string.IsNullOrEmpty(value) || !bool.TryParse(value, out var boolValue))
+ return null;
+
+ return boolValue;
+ }
+
+ private void AddToAdditionalResources(ITaskItem resource, Dictionary additionalResources, string resourceName, string behavior)
+ {
+ if (!additionalResources.ContainsKey(resourceName))
+ {
+ Log.LogMessage(MessageImportance.Low, "Added resource '{0}' to the list of additional assets in the manifest.", resource.ItemSpec);
+ additionalResources.Add(resourceName, new AdditionalAsset
+ {
+ Hash = $"sha256-{resource.GetMetadata("FileHash")}",
+ Behavior = behavior
+ });
+ }
+ }
+
+ private bool TryGetLazyLoadedAssembly(string fileName, out ITaskItem lazyLoadedAssembly)
+ {
+ return (lazyLoadedAssembly = LazyLoadedAssemblies?.SingleOrDefault(a => a.ItemSpec == fileName)) != null;
+ }
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj
new file mode 100644
index 00000000000000..e56ee68e46a5eb
--- /dev/null
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj
@@ -0,0 +1,32 @@
+
+
+
+ $(TargetFrameworkForNETCoreTasks);$(TargetFrameworkForNETFrameworkTasks)
+ $(NoWarn),CA1050,CA1850,CA1845,CA1859,NU5128
+ Microsoft.NET.Sdk.WebAssembly
+ true
+ true
+
+
+
+
+ All
+ true
+
+
+
+
+
+
+
+
+
+
+ <_PublishFramework Remove="@(_PublishFramework)" />
+ <_PublishFramework Include="$(TargetFrameworks)" />
+
+
+
+
+
+