From e822f5f12dcdbe14de7c49f03256f8ea92f84efe Mon Sep 17 00:00:00 2001
From: Pranav K <prkrishn@hotmail.com>
Date: Mon, 20 Jul 2020 13:25:49 -0700
Subject: [PATCH] Create a Blazor WebAssembly SDK (#24044)

---
 AspNetCore.sln                                |  63 ++
 eng/Build.props                               |   1 +
 eng/ProjectReferences.props                   |   1 +
 src/Components/Components.slnf                |   3 +
 src/Components/ComponentsNoDeps.slnf          |   4 +
 src/Components/Directory.Build.props          |   8 -
 src/Components/Directory.Build.targets        |   2 +-
 ....BlazorWebAssembly.IntegrationTests.csproj |  78 +++
 .../integrationtests}/ServiceWorkerAssert.cs  |   2 +-
 .../WasmBuildIncrementalismTest.cs            |   2 +-
 .../WasmBuildIntegrationTest.cs               |   2 +-
 .../WasmBuildLazyLoadTest.cs                  |   2 +-
 .../integrationtests}/WasmCompressionTests.cs |   0
 .../WasmPublishIntegrationTest.cs             |  18 +-
 .../integrationtests}/WasmPwaManifestTests.cs |   0
 .../Sdk/integrationtests/xunit.runner.json    |   5 +
 .../WebAssembly/Sdk/src/AssetsManifestFile.cs |  33 +
 .../src/BlazorReadSatelliteAssemblyFile.cs    |  38 ++
 .../src/BlazorWriteSatelliteAssemblyFile.cs   |  53 ++
 .../WebAssembly/Sdk/src/BootJsonData.cs       |  85 +++
 .../WebAssembly/Sdk/src/BrotliCompress.cs     | 125 ++++
 .../CreateBlazorTrimmerRootDescriptorFile.cs  |  79 +++
 .../WebAssembly/Sdk/src/GZipCompress.cs       |  70 +++
 .../src/GenerateBlazorWebAssemblyBootJson.cs  | 155 +++++
 .../GenerateServiceWorkerAssetsManifest.cs    |  97 +++
 ...Microsoft.NET.Sdk.BlazorWebAssembly.csproj |  89 +++
 ...Microsoft.NET.Sdk.BlazorWebAssembly.nuspec |  19 +
 .../WebAssembly/Sdk/src/Sdk/Sdk.props         |  22 +
 .../WebAssembly/Sdk/src/Sdk/Sdk.targets       |  20 +
 .../WebAssembly/Sdk/src/_._}                  |   0
 .../Microsoft.NET.Sdk.BlazorWebAssembly.props |  16 +
 ...icrosoft.NET.Sdk.BlazorWebAssembly.targets |  16 +
 .../Sdk/src/targets/BlazorWasm.web.config     |  40 ++
 .../Sdk/src/targets/LinkerWorkaround.xml      |  15 +
 ...ft.NET.Sdk.BlazorWebAssembly.Current.props |  36 ++
 ....NET.Sdk.BlazorWebAssembly.Current.targets | 582 ++++++++++++++++++
 ...sembly.ServiceWorkerAssetsManifest.targets | 169 +++++
 .../BlazorReadSatelliteAssemblyFileTest.cs    |  68 ++
 .../Sdk/test/GenerateBlazorBootJsonTest.cs    | 195 ++++++
 ...oft.NET.Sdk.BlazorWebAssembly.Tests.csproj |  12 +
 .../Sdk/testassets/Directory.Build.props      |  50 ++
 .../Sdk/testassets/Directory.Build.targets    |   7 +
 .../LinkBaseToWebRoot/js/LinkedScript.js      |   0
 .../RestoreBlazorWasmTestProjects.csproj      |  12 +
 .../Sdk/testassets/blazor.webassembly.js      |   1 +
 .../testassets/blazorhosted-rid/Program.cs    |   0
 .../blazorhosted-rid/blazorhosted-rid.csproj  |   0
 .../Sdk}/testassets/blazorhosted/Program.cs   |   0
 .../blazorhosted/blazorhosted.csproj          |   0
 .../Sdk}/testassets/blazorwasm/App.razor      |   0
 .../blazorwasm/LinkToWebRoot/css/app.css      |   0
 .../testassets/blazorwasm/Pages/Index.razor   |   0
 .../Sdk}/testassets/blazorwasm/Program.cs     |   0
 .../blazorwasm/Resources.ja.resx.txt          |   0
 .../Sdk}/testassets/blazorwasm/_Imports.razor |   0
 .../testassets/blazorwasm/blazorwasm.csproj   |   8 +-
 .../blazorwasm/wwwroot/Fake-License.txt       |   0
 .../testassets/blazorwasm/wwwroot/css/app.css |   0
 .../testassets/blazorwasm/wwwroot/index.html  |   0
 .../serviceworkers/my-prod-service-worker.js  |   0
 .../serviceworkers/my-service-worker.js       |   0
 .../Class1.cs                                 |   0
 .../Resources.es-ES.resx                      |   0
 ...classlibrarywithsatelliteassemblies.csproj |   0
 .../testassets/razorclasslibrary/Class1.cs    |   0
 .../RazorClassLibrary.csproj                  |   0
 .../razorclasslibrary/wwwroot/styles.css      |   0
 .../wwwroot/wwwroot/exampleJsInterop.js       |   0
 .../WebAssembly/Sdk/tools/Application.cs      |  98 +++
 .../Sdk/tools/BrotliCompressCommand.cs        |  85 +++
 .../WebAssembly/Sdk/tools/DebugMode.cs        |  27 +
 ...oft.NET.Sdk.BlazorWebAssembly.Tools.csproj |  22 +
 .../WebAssembly/Sdk/tools/Program.cs          |  30 +
 .../Sdk/tools/runtimeconfig.template.json     |   3 +
 ...soft.NET.Sdk.Razor.IntegrationTests.csproj |  48 +-
 src/Shared/E2ETesting/E2ETesting.targets      |   9 +-
 .../MSBuild.Testing}/Assert.cs                |   0
 .../MSBuild.Testing}/BuildVariables.cs        |   6 +-
 .../MSBuild.Testing}/FIleThumbPrint.cs        |   0
 .../MSBuild.Testing/MSBuild.Testing.targets   |  53 ++
 .../MSBuild.Testing}/MSBuildProcessKind.cs    |   0
 .../MSBuild.Testing}/MSBuildProcessManager.cs |   3 +-
 .../MSBuild.Testing}/MSBuildResult.cs         |   0
 .../MSBuild.Testing}/ProjectDirectory.cs      |  10 +-
 84 files changed, 2625 insertions(+), 72 deletions(-)
 create mode 100644 src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm => Components/WebAssembly/Sdk/integrationtests}/ServiceWorkerAssert.cs (98%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm => Components/WebAssembly/Sdk/integrationtests}/WasmBuildIncrementalismTest.cs (99%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm => Components/WebAssembly/Sdk/integrationtests}/WasmBuildIntegrationTest.cs (99%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm => Components/WebAssembly/Sdk/integrationtests}/WasmBuildLazyLoadTest.cs (99%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm => Components/WebAssembly/Sdk/integrationtests}/WasmCompressionTests.cs (100%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm => Components/WebAssembly/Sdk/integrationtests}/WasmPublishIntegrationTest.cs (98%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm => Components/WebAssembly/Sdk/integrationtests}/WasmPwaManifestTests.cs (100%)
 create mode 100644 src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json
 create mode 100644 src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/BootJsonData.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/BrotliCompress.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/GZipCompress.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs
 create mode 100644 src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj
 create mode 100644 src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec
 create mode 100644 src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props
 create mode 100644 src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets
 rename src/{Razor/test/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js => Components/WebAssembly/Sdk/src/_._} (100%)
 create mode 100644 src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props
 create mode 100644 src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets
 create mode 100644 src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config
 create mode 100644 src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml
 create mode 100644 src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
 create mode 100644 src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets
 create mode 100644 src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets
 create mode 100644 src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs
 create mode 100644 src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs
 create mode 100644 src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj
 create mode 100644 src/Components/WebAssembly/Sdk/testassets/Directory.Build.props
 create mode 100644 src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/LinkBaseToWebRoot/js/LinkedScript.js (100%)
 create mode 100644 src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj
 create mode 100644 src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorhosted-rid/Program.cs (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorhosted-rid/blazorhosted-rid.csproj (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorhosted/Program.cs (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorhosted/blazorhosted.csproj (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/App.razor (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/LinkToWebRoot/css/app.css (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/Pages/Index.razor (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/Program.cs (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/Resources.ja.resx.txt (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/_Imports.razor (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/blazorwasm.csproj (88%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/wwwroot/Fake-License.txt (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/wwwroot/css/app.css (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/wwwroot/index.html (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/classlibrarywithsatelliteassemblies/Class1.cs (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/razorclasslibrary/Class1.cs (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/razorclasslibrary/RazorClassLibrary.csproj (100%)
 rename src/{Razor/test => Components/WebAssembly/Sdk}/testassets/razorclasslibrary/wwwroot/styles.css (100%)
 create mode 100644 src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js
 create mode 100644 src/Components/WebAssembly/Sdk/tools/Application.cs
 create mode 100644 src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs
 create mode 100644 src/Components/WebAssembly/Sdk/tools/DebugMode.cs
 create mode 100644 src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj
 create mode 100644 src/Components/WebAssembly/Sdk/tools/Program.cs
 create mode 100644 src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests => Shared/MSBuild.Testing}/Assert.cs (100%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests => Shared/MSBuild.Testing}/BuildVariables.cs (83%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests => Shared/MSBuild.Testing}/FIleThumbPrint.cs (100%)
 create mode 100644 src/Shared/MSBuild.Testing/MSBuild.Testing.targets
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests => Shared/MSBuild.Testing}/MSBuildProcessKind.cs (100%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests => Shared/MSBuild.Testing}/MSBuildProcessManager.cs (98%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests => Shared/MSBuild.Testing}/MSBuildResult.cs (100%)
 rename src/{Razor/Microsoft.NET.Sdk.Razor/integrationtests => Shared/MSBuild.Testing}/ProjectDirectory.cs (97%)

diff --git a/AspNetCore.sln b/AspNetCore.sln
index ca9dfcf2818..50068277d88 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1427,6 +1427,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions.Tests", "src\Components\Web.Extensions\test\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj", "{157605CB-5170-4C1A-980F-4BAE42DB60DE}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly", "src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", "{6B2734BF-C61D-4889-ABBF-456A4075D59B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "src\Components\WebAssembly\Sdk\test\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj", "{83371889-9A3E-4D16-AE77-EB4F83BC6374}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests", "src\Components\WebAssembly\Sdk\integrationtests\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj", "{525EBCB4-A870-470B-BC90-845306C337D1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -6729,6 +6739,54 @@ Global
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.Build.0 = Release|Any CPU
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.ActiveCfg = Release|Any CPU
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.Build.0 = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.Build.0 = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x86.Build.0 = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x64.ActiveCfg = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x64.Build.0 = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x86.ActiveCfg = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x86.Build.0 = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x64.Build.0 = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x86.Build.0 = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|Any CPU.Build.0 = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x64.ActiveCfg = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x64.Build.0 = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x86.ActiveCfg = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x86.Build.0 = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x64.Build.0 = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x86.Build.0 = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|Any CPU.Build.0 = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x64.ActiveCfg = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x64.Build.0 = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x86.ActiveCfg = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x86.Build.0 = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x64.Build.0 = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x86.Build.0 = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.ActiveCfg = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -7444,6 +7502,11 @@ Global
 		{F71FE795-9923-461B-9809-BB1821A276D0} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
 		{8294A74F-7DAA-4B69-BC56-7634D93C9693} = {F71FE795-9923-461B-9809-BB1821A276D0}
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE} = {F71FE795-9923-461B-9809-BB1821A276D0}
+		{FED4267E-E5E4-49C5-98DB-8B3F203596EE} = {562D5067-8CD8-4F19-BCBB-873204932C61}
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
+		{525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
diff --git a/eng/Build.props b/eng/Build.props
index 56b4917250d..3740546dd64 100644
--- a/eng/Build.props
+++ b/eng/Build.props
@@ -172,6 +172,7 @@
                           @(ProjectToBuild);
                           @(ProjectToExclude);
                           $(RepoRoot)src\Razor\test\testassets\**\*.*proj;
+                          $(RepoRoot)src\Components\WebAssembly\Sdk\testassets\**\*.*proj;
                           $(RepoRoot)**\node_modules\**\*;
                           $(RepoRoot)**\bin\**\*;
                           $(RepoRoot)**\obj\**\*;"
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index a0e89782cae..6dbfca4646b 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -145,6 +145,7 @@
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Web.Extensions" ProjectPath="$(RepoRoot)src\Components\Web.Extensions\src\Microsoft.AspNetCore.Components.Web.Extensions.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.Authentication.WebAssembly.Msal" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.JSInterop.WebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\JSInterop\src\Microsoft.JSInterop.WebAssembly.csproj"  />
+    <ProjectReferenceProvider Include="Microsoft.NET.Sdk.BlazorWebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Server" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Server\src\Microsoft.AspNetCore.Components.WebAssembly.Server.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj"  />
diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf
index c202f7c1c69..637ec1a3845 100644
--- a/src/Components/Components.slnf
+++ b/src/Components/Components.slnf
@@ -110,6 +110,9 @@
       "src\\Components\\WebAssembly\\WebAssembly.Authentication\\test\\Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests.csproj",
       "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Client\\Wasm.Authentication.Client.csproj",
       "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Shared\\Wasm.Authentication.Shared.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj",
       "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj"
     ]
   }
diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf
index 6dbf031fd31..dd08970d02a 100644
--- a/src/Components/ComponentsNoDeps.slnf
+++ b/src/Components/ComponentsNoDeps.slnf
@@ -31,6 +31,10 @@
       "src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
       "src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
       "src\\Components\\WebAssembly\\testassets\\StandaloneApp\\StandaloneApp.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\tools\\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj",
       "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj",
       "src\\Components\\Web\\test\\Microsoft.AspNetCore.Components.Web.Tests.csproj",
       "src\\Components\\benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj",
diff --git a/src/Components/Directory.Build.props b/src/Components/Directory.Build.props
index 1704a1b070a..b1ea764edd5 100644
--- a/src/Components/Directory.Build.props
+++ b/src/Components/Directory.Build.props
@@ -1,14 +1,6 @@
 <Project>
   <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
 
-  <PropertyGroup>
-    <!-- Workaround for https://github.com/dotnet/aspnetcore/issues/5486 which requires the bin and obj directory be in the project directory -->
-    <BaseIntermediateOutputPath />
-    <IntermediateOutputPath />
-    <BaseOutputPath />
-    <OutputPath />
-  </PropertyGroup>
-
   <PropertyGroup>
     <EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
   </PropertyGroup>
diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets
index 69204d61a66..f3143253af1 100644
--- a/src/Components/Directory.Build.targets
+++ b/src/Components/Directory.Build.targets
@@ -3,7 +3,7 @@
     <BlazorWebAssemblyJSPath>$(RepoRoot)src\Components\Web.JS\dist\$(Configuration)\blazor.webassembly.js</BlazorWebAssemblyJSPath>
     <BlazorWebAssemblyJSMapPath>$(BlazorWebAssemblyJSPath).map</BlazorWebAssemblyJSMapPath>
 
-    <_BlazorDevServerPath>$(RepoRoot)src/Components/WebAssembly/DevServer/src/bin/$(Configuration)/$(DefaultNetCoreTargetFramework)/blazor-devserver.dll</_BlazorDevServerPath>
+    <_BlazorDevServerPath>$(ArtifactsDir)/bin/Microsoft.AspNetCore.Components.WebAssembly.DevServer/$(Configuration)/$(DefaultNetCoreTargetFramework)/blazor-devserver.dll</_BlazorDevServerPath>
     <RunCommand>dotnet</RunCommand>
     <RunArguments>exec &quot;$(_BlazorDevServerPath)&quot; serve --applicationpath &quot;$(TargetPath)&quot; $(AdditionalRunArguments)</RunArguments>
   </PropertyGroup>
diff --git a/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj b/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj
new file mode 100644
index 00000000000..4255ce7c73e
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj
@@ -0,0 +1,78 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <!--
+      There's not much value in multi-targeting here, this doesn't run much .NET code, it tests MSBuild.
+
+      This is also a partial workaround for https://github.com/Microsoft/msbuild/issues/2661 - this project
+      has netcoreapp dependencies that need to be built first.
+    -->
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <PreserveCompilationContext>true</PreserveCompilationContext>
+
+    <!-- Tests do not work on Helix yet -->
+    <BuildHelixPayload>false</BuildHelixPayload>
+    <TestAppsRoot>$(MSBuildProjectDirectory)\..\testassets\</TestAppsRoot>
+  </PropertyGroup>
+
+  <Import Project="$(SharedSourceRoot)MSBuild.Testing\MSBuild.Testing.targets" />
+
+  <ItemGroup>
+    <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Build.Utilities.Core" />
+    <Reference Include="Microsoft.Extensions.DependencyModel" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="$(RepoRoot)src\Razor\test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj" />
+
+    <ProjectReference Include="..\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj">
+      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+      <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
+    </ProjectReference>
+
+    <ProjectReference Include="$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\Microsoft.NET.Sdk.Razor.csproj">
+      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+      <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
+    </ProjectReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\src\BootJsonData.cs" LinkBase="Wasm" />
+    <Compile Include="..\src\AssetsManifestFile.cs" LinkBase="Wasm" />
+    <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
+  </ItemGroup>
+
+  <Target Name="GenerateTestData" BeforeTargets="GetAssemblyAttributes">
+    <Exec Condition="'$(OS)' == 'Windows_NT'" Command="&quot;$(NuGetPackageRoot)vswhere\$(VSWhereVersion)\tools\vswhere.exe&quot; -latest -prerelease -property installationPath -requires Microsoft.Component.MSBuild" ConsoleToMsBuild="true" StandardErrorImportance="high">
+      <Output TaskParameter="ConsoleOutput" PropertyName="_VSInstallDir" />
+    </Exec>
+    <Error Condition="'$(OS)' == 'Windows_NT' and '$(_VSInstallDir)'=='' and '$(Test)' == 'true'" Text="Visual Studio not found on Windows." />
+
+    <PropertyGroup>
+      <_DesktopMSBuildPath Condition="'$(OS)' == 'Windows_NT' and Exists('$(_VSInstallDir)\MSBuild\Current\Bin\msbuild.exe')">$(_VSInstallDir)\MSBuild\Current\Bin\msbuild.exe</_DesktopMSBuildPath>
+      <_DesktopMSBuildPath Condition="'$(OS)' == 'Windows_NT' and Exists('$(_VSInstallDir)\MSBuild\15.0\Bin\msbuild.exe')">$(_VSInstallDir)\MSBuild\15.0\Bin\msbuild.exe</_DesktopMSBuildPath>
+    </PropertyGroup>
+
+    <Error Condition="'$(OS)' == 'Windows_NT' and '$(_DesktopMSBuildPath)'=='' and '$(Test)' == 'true'" Text="MSBuild.exe not found on Windows." />
+
+    <ItemGroup>
+      <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+        <_Parameter1>DesktopMSBuildPath</_Parameter1>
+        <_Parameter2>$(_DesktopMSBuildPath)</_Parameter2>
+      </AssemblyAttribute>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="RestoreTestProjects" BeforeTargets="Restore;Build" Condition="'$(DotNetBuildFromSource)' != 'true'">
+    <MSBuild Projects="..\testassets\RestoreBlazorWasmTestProjects\RestoreBlazorWasmTestProjects.csproj" Targets="Restore" Properties="MicrosoftNetCompilersToolsetPackageVersion=$(MicrosoftNetCompilersToolsetPackageVersion);RepoRoot=$(RepoRoot)" />
+  </Target>
+
+  <Target Name="EnsureLogFolder" AfterTargets="Build">
+    <MakeDir Directories="$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))" />
+  </Target>
+
+</Project>
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs b/src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs
similarity index 98%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs
index 7585b5b76ca..8597d104805 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs
@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Text.Json;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 
 namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
 {
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs
similarity index 99%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs
index f91dd03863e..218567fcc54 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs
@@ -4,7 +4,7 @@
 using System.IO;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs
similarity index 99%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs
index 12fbbd03804..c521c6be7e9 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs
@@ -4,7 +4,7 @@
 using System.IO;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Microsoft.AspNetCore.Testing;
 using Xunit;
 
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs
similarity index 99%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs
index e8f1a02d40f..424caeda2d8 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs
@@ -4,7 +4,7 @@
 using System.IO;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmCompressionTests.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmCompressionTests.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmCompressionTests.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmCompressionTests.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs
similarity index 98%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs
index 45c878ea5aa..1108d0bff94 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs
@@ -5,7 +5,7 @@ using System.IO;
 using System.IO.Compression;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Microsoft.AspNetCore.Testing;
 using Xunit;
 using static Microsoft.AspNetCore.Razor.Design.IntegrationTests.ServiceWorkerAssert;
@@ -608,6 +608,17 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
                 assetsManifestPath: "custom-service-worker-assets.js");
         }
 
+        [Fact]
+        public async Task Publish_HostedApp_WithRidSpecifiedInCLI_Works()
+        {
+            // Arrange
+            using var project = ProjectDirectory.Create("blazorhosted-rid", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", });
+            project.RuntimeIdentifier = "linux-x64";
+            var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:RuntimeIdentifier=linux-x64");
+
+            AssertRIDPublishOuput(project, result);
+        }
+
         [Fact]
         public async Task Publish_HostedApp_WithRid_Works()
         {
@@ -616,6 +627,11 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
             project.RuntimeIdentifier = "linux-x64";
             var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
 
+            AssertRIDPublishOuput(project, result);
+        }
+
+        private static void AssertRIDPublishOuput(ProjectDirectory project, MSBuildResult result)
+        {
             Assert.BuildPassed(result);
 
             var publishDirectory = project.PublishOutputDirectory;
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPwaManifestTests.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmPwaManifestTests.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPwaManifestTests.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmPwaManifestTests.cs
diff --git a/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json b/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json
new file mode 100644
index 00000000000..d00e4ae9074
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json
@@ -0,0 +1,5 @@
+{
+  "methodDisplay": "method",
+  "shadowCopy": false,
+  "maxParallelThreads": -1
+}
\ No newline at end of file
diff --git a/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs b/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs
new file mode 100644
index 00000000000..872b17aae22
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs
@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+#pragma warning disable IDE1006 // Naming Styles
+    public class AssetsManifestFile
+    {
+        /// <summary>
+        /// Gets or sets a version string.
+        /// </summary>
+        public string version { get; set; }
+
+        /// <summary>
+        /// Gets or sets the assets. Keys are URLs; values are base-64-formatted SHA256 content hashes.
+        /// </summary>
+        public AssetsManifestFileEntry[] assets { get; set; }
+    }
+
+    public class AssetsManifestFileEntry
+    {
+        /// <summary>
+        /// Gets or sets the asset URL. Normally this will be relative to the application's base href.
+        /// </summary>
+        public string url { get; set; }
+
+        /// <summary>
+        /// Gets or sets the file content hash. This should be the base-64-formatted SHA256 value.
+        /// </summary>
+        public string hash { get; set; }
+    }
+#pragma warning restore IDE1006 // Naming Styles
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs b/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs
new file mode 100644
index 00000000000..f6bf0c88f8d
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs
@@ -0,0 +1,38 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BlazorReadSatelliteAssemblyFile : Task
+    {
+        [Output]
+        public ITaskItem[] SatelliteAssembly { get; set; }
+
+        [Required]
+        public ITaskItem ReadFile { get; set; }
+
+        public override bool Execute()
+        {
+            var document = XDocument.Load(ReadFile.ItemSpec);
+            SatelliteAssembly = document.Root
+                .Elements()
+                .Select(e =>
+                {
+                    // <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
+
+                    var taskItem = new TaskItem(e.Attribute("Name").Value);
+                    taskItem.SetMetadata("Culture", e.Attribute("Culture").Value);
+                    taskItem.SetMetadata("DestinationSubDirectory", e.Attribute("DestinationSubDirectory").Value);
+
+                    return taskItem;
+                }).ToArray();
+
+            return true;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs b/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs
new file mode 100644
index 00000000000..92fd8d33e87
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs
@@ -0,0 +1,53 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BlazorWriteSatelliteAssemblyFile : Task
+    {
+        [Required]
+        public ITaskItem[] SatelliteAssembly { get; set; }
+
+        [Required]
+        public ITaskItem WriteFile { get; set; }
+
+        public override bool Execute()
+        {
+            using var fileStream = File.Create(WriteFile.ItemSpec);
+            WriteSatelliteAssemblyFile(fileStream);
+            return true;
+        }
+
+        internal void WriteSatelliteAssemblyFile(Stream stream)
+        {
+            var root = new XElement("SatelliteAssembly");
+
+            foreach (var item in SatelliteAssembly)
+            {
+                // <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
+
+                root.Add(new XElement("Assembly",
+                    new XAttribute("Name", item.ItemSpec),
+                    new XAttribute("Culture", item.GetMetadata("Culture")),
+                    new XAttribute("DestinationSubDirectory", item.GetMetadata("DestinationSubDirectory"))));
+            }
+
+            var xmlWriterSettings = new XmlWriterSettings
+            {
+                Indent = true,
+                OmitXmlDeclaration = true
+            };
+
+            using var writer = XmlWriter.Create(stream, xmlWriterSettings);
+            var xDocument = new XDocument(root);
+
+            xDocument.Save(writer);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BootJsonData.cs b/src/Components/WebAssembly/Sdk/src/BootJsonData.cs
new file mode 100644
index 00000000000..b3a2dbd3884
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BootJsonData.cs
@@ -0,0 +1,85 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+#pragma warning disable IDE1006 // Naming Styles
+    /// <summary>
+    /// Defines the structure of a Blazor boot JSON file
+    /// </summary>
+    public class BootJsonData
+    {
+        /// <summary>
+        /// Gets the name of the assembly with the application entry point
+        /// </summary>
+        public string entryAssembly { get; set; }
+
+        /// <summary>
+        /// 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 <see cref="ResourceHashesByNameDictionary"/>, 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.
+        /// </summary>
+        public ResourcesData resources { get; set; } = new ResourcesData();
+
+        /// <summary>
+        /// Gets a value that determines whether to enable caching of the <see cref="resources"/>
+        /// inside a CacheStorage instance within the browser.
+        /// </summary>
+        public bool cacheBootResources { get; set; }
+
+        /// <summary>
+        /// Gets a value that determines if this is a debug build.
+        /// </summary>
+        public bool debugBuild { get; set; }
+
+        /// <summary>
+        /// Gets a value that determines if the linker is enabled.
+        /// </summary>
+        public bool linkerEnabled { get; set; }
+
+        /// <summary>
+        /// Config files for the application
+        /// </summary>
+        public List<string> config { get; set; }
+    }
+
+    public class ResourcesData
+    {
+        /// <summary>
+        /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc.
+        /// </summary>
+        public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary();
+
+        /// <summary>
+        /// "assembly" (.dll) resources
+        /// </summary>
+        public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary();
+
+        /// <summary>
+        /// "debug" (.pdb) resources
+        /// </summary>
+        [DataMember(EmitDefaultValue = false)]
+        public ResourceHashesByNameDictionary pdb { get; set; }
+
+        /// <summary>
+        /// localization (.satellite resx) resources
+        /// </summary>
+        [DataMember(EmitDefaultValue = false)]
+        public Dictionary<string, ResourceHashesByNameDictionary> satelliteResources { get; set; }
+
+        /// <summary>
+        /// Assembly (.dll) resources that are loaded lazily during runtime
+        /// </summary>
+        [DataMember(EmitDefaultValue = false)]
+        public ResourceHashesByNameDictionary lazyAssembly { get; set; }
+    }
+#pragma warning restore IDE1006 // Naming Styles
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs b/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs
new file mode 100644
index 00000000000..89b3004a5ed
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs
@@ -0,0 +1,125 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BrotliCompress : ToolTask
+    {
+        private static readonly char[] InvalidPathChars = Path.GetInvalidFileNameChars();
+        private string _dotnetPath;
+
+        [Required]
+        public ITaskItem[] FilesToCompress { get; set; }
+
+        [Output]
+        public ITaskItem[] CompressedFiles { get; set; }
+
+        [Required]
+        public string OutputDirectory { get; set; }
+
+        public string CompressionLevel { get; set; }
+
+        public bool SkipIfOutputIsNewer { get; set; }
+
+        [Required]
+        public string ToolAssembly { get; set; }
+
+        protected override string ToolName => Path.GetDirectoryName(DotNetPath);
+
+        private string DotNetPath
+        {
+            get
+            {
+                if (!string.IsNullOrEmpty(_dotnetPath))
+                {
+                    return _dotnetPath;
+                }
+
+                _dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
+                if (string.IsNullOrEmpty(_dotnetPath))
+                {
+                    throw new InvalidOperationException("DOTNET_HOST_PATH is not set");
+                }
+
+                return _dotnetPath;
+            }
+        }
+
+        protected override string GenerateCommandLineCommands() => ToolAssembly;
+
+        protected override string GenerateResponseFileCommands()
+        {
+            var builder = new StringBuilder();
+
+
+            builder.AppendLine("brotli");
+
+            if (!string.IsNullOrEmpty(CompressionLevel))
+            {
+                builder.AppendLine("-c");
+                builder.AppendLine(CompressionLevel);
+            }
+
+            CompressedFiles = new ITaskItem[FilesToCompress.Length];
+
+            for (var i = 0; i < FilesToCompress.Length; i++)
+            {
+                var input = FilesToCompress[i];
+                var inputFullPath = input.GetMetadata("FullPath");
+                var relativePath = input.GetMetadata("RelativePath");
+                var outputRelativePath = Path.Combine(OutputDirectory, CalculateTargetPath(inputFullPath, ".br"));
+                var outputFullPath = Path.GetFullPath(outputRelativePath);
+
+                var outputItem = new TaskItem(outputRelativePath);
+                outputItem.SetMetadata("RelativePath", relativePath + ".br");
+                CompressedFiles[i] = outputItem;
+
+                if (SkipIfOutputIsNewer && File.Exists(outputFullPath) && File.GetLastWriteTimeUtc(inputFullPath) < File.GetLastWriteTimeUtc(outputFullPath))
+                {
+                    Log.LogMessage(MessageImportance.Low, $"Skipping compression for '{input.ItemSpec}' because '{outputRelativePath}' is newer than '{input.ItemSpec}'.");
+                    continue;
+                }
+
+                builder.AppendLine("-s");
+                builder.AppendLine(inputFullPath);
+
+                builder.AppendLine("-o");
+                builder.AppendLine(outputFullPath);
+            }
+
+            return builder.ToString();
+        }
+
+        internal static string CalculateTargetPath(string relativePath, string extension)
+        {
+            // RelativePath can be long and if used as-is to write the output, might result in long path issues on Windows.
+            // Instead we'll calculate a fixed length path by hashing the input file name. This uses SHA1 similar to the Hash task in MSBuild
+            // since it has no crytographic significance.
+            using var hash = SHA1.Create();
+            var bytes = Encoding.UTF8.GetBytes(relativePath);
+            var hashString = Convert.ToBase64String(hash.ComputeHash(bytes));
+
+            var builder = new StringBuilder();
+
+            for (var i = 0; i < 8; i++)
+            {
+                var c = hashString[i];
+                builder.Append(InvalidPathChars.Contains(c) ? '+' : c);
+            }
+
+            builder.Append(extension);
+            return builder.ToString();
+        }
+
+        protected override string GenerateFullPathToTool() => DotNetPath;
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs b/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs
new file mode 100644
index 00000000000..f14ffad2219
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs
@@ -0,0 +1,79 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    // Based on https://github.com/mono/linker/blob/3b329b9481e300bcf4fb88a2eebf8cb5ef8b323b/src/ILLink.Tasks/CreateRootDescriptorFile.cs
+    public class CreateBlazorTrimmerRootDescriptorFile : Task
+    {
+        [Required]
+        public ITaskItem[] Assemblies { get; set; }
+
+        [Required]
+        public ITaskItem TrimmerFile { get; set; }
+
+        public override bool Execute()
+        {
+            var rootDescriptor = CreateRootDescriptorContents();
+            if (File.Exists(TrimmerFile.ItemSpec))
+            {
+                var existing = File.ReadAllText(TrimmerFile.ItemSpec);
+
+                if (string.Equals(rootDescriptor, existing, StringComparison.Ordinal))
+                {
+                    Log.LogMessage(MessageImportance.Low, "Skipping write to file {0} because contents would not change.", TrimmerFile.ItemSpec);
+                    // Avoid writing if the file contents are identical. This is required for build incrementalism.
+                    return !Log.HasLoggedErrors;
+                }
+            }
+
+            File.WriteAllText(TrimmerFile.ItemSpec, rootDescriptor);
+            return !Log.HasLoggedErrors;
+        }
+
+        internal string CreateRootDescriptorContents()
+        {
+            var roots = new XElement("linker");
+            foreach (var assembly in Assemblies.OrderBy(a => a.ItemSpec))
+            {
+                // NOTE: Descriptor files don't include the file extension
+                // in the assemblyName.
+                var assemblyName = assembly.GetMetadata("FileName");
+                var typePreserved = assembly.GetMetadata("Preserve");
+                var typeRequired = assembly.GetMetadata("Required");
+
+                var attributes = new List<XAttribute>
+                {
+                    new XAttribute("fullname", "*"),
+                    new XAttribute("required", typeRequired),
+                };
+
+                if (!string.IsNullOrEmpty(typePreserved))
+                {
+                    attributes.Add(new XAttribute("preserve", typePreserved));
+                }
+
+                roots.Add(new XElement("assembly",
+                    new XAttribute("fullname", assemblyName),
+                    new XElement("type", attributes)));
+            }
+
+            var xmlWriterSettings = new XmlWriterSettings
+            {
+                Indent = true,
+                OmitXmlDeclaration = true
+            };
+
+            return new XDocument(roots).Root.ToString();
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/GZipCompress.cs b/src/Components/WebAssembly/Sdk/src/GZipCompress.cs
new file mode 100644
index 00000000000..4b8a0c542a1
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/GZipCompress.cs
@@ -0,0 +1,70 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class GZipCompress : Task
+    {
+        [Required]
+        public ITaskItem[] FilesToCompress { get; set; }
+
+        [Output]
+        public ITaskItem[] CompressedFiles { get; set; }
+
+        [Required]
+        public string OutputDirectory { get; set; }
+
+        public override bool Execute()
+        {
+            CompressedFiles = new ITaskItem[FilesToCompress.Length];
+
+            Directory.CreateDirectory(OutputDirectory);
+
+            System.Threading.Tasks.Parallel.For(0, FilesToCompress.Length, i =>
+            {
+                var file = FilesToCompress[i];
+                var inputPath = file.ItemSpec;
+                var relativePath = file.GetMetadata("RelativePath");
+                var outputRelativePath = Path.Combine(
+                    OutputDirectory,
+                    BrotliCompress.CalculateTargetPath(relativePath, ".gz"));
+
+                var outputItem = new TaskItem(outputRelativePath);
+                outputItem.SetMetadata("RelativePath", relativePath + ".gz");
+                CompressedFiles[i] = outputItem;
+
+                if (File.Exists(outputRelativePath) && File.GetLastWriteTimeUtc(inputPath) < File.GetLastWriteTimeUtc(outputRelativePath))
+                {
+                    // Incrementalism. If input source doesn't exist or it exists and is not newer than the expected output, do nothing.
+                    Log.LogMessage(MessageImportance.Low, $"Skipping '{inputPath}' because '{outputRelativePath}' is newer than '{inputPath}'.");
+                    return;
+                }
+
+                try
+                {
+                    using var sourceStream = File.OpenRead(inputPath);
+                    using var fileStream = File.Create(outputRelativePath);
+                    using var stream = new GZipStream(fileStream, CompressionLevel.Optimal);
+
+                    sourceStream.CopyTo(stream);
+                }
+                catch (Exception e)
+                {
+                    Log.LogErrorFromException(e);
+                    return;
+                }
+            });
+
+            return !Log.HasLoggedErrors;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs b/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs
new file mode 100644
index 00000000000..81cb6ecb96d
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs
@@ -0,0 +1,155 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class GenerateBlazorWebAssemblyBootJson : Task
+    {
+        [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 ITaskItem[] ConfigurationFiles { 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.LogErrorFromException(ex);
+            }
+
+            return !Log.HasLoggedErrors;
+        }
+
+        // Internal for tests
+        public void WriteBootJson(Stream output, string entryAssemblyName)
+        {
+            var result = new BootJsonData
+            {
+                entryAssembly = entryAssemblyName,
+                cacheBootResources = CacheBootResources,
+                debugBuild = DebugBuild,
+                linkerEnabled = LinkerEnabled,
+                resources = new ResourcesData(),
+                config = new List<string>(),
+            };
+
+            // 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 resourceData = result.resources;
+                foreach (var resource in Resources)
+                {
+                    ResourceHashesByNameDictionary resourceList;
+
+                    var fileName = resource.GetMetadata("FileName");
+                    var extension = resource.GetMetadata("Extension");
+                    var resourceCulture = resource.GetMetadata("Culture");
+                    var assetType = resource.GetMetadata("AssetType");
+                    var resourceName = $"{fileName}{extension}";
+
+                    if (IsLazyLoadedAssembly(fileName))
+                    {
+                        resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary();
+                        resourceList = resourceData.lazyAssembly;
+                    }
+                    else if (!string.IsNullOrEmpty(resourceCulture))
+                    {
+                        resourceData.satelliteResources ??= new Dictionary<string, ResourceHashesByNameDictionary>(StringComparer.OrdinalIgnoreCase);
+                        resourceName = resourceCulture + "/" + resourceName;
+
+                        if (!resourceData.satelliteResources.TryGetValue(resourceCulture, out resourceList))
+                        {
+                            resourceList = new ResourceHashesByNameDictionary();
+                            resourceData.satelliteResources.Add(resourceCulture, resourceList);
+                        }
+                    }
+                    else if (string.Equals(extension, ".pdb", StringComparison.OrdinalIgnoreCase))
+                    {
+                        resourceData.pdb ??= new ResourceHashesByNameDictionary();
+                        resourceList = resourceData.pdb;
+                    }
+                    else if (string.Equals(extension, ".dll", StringComparison.OrdinalIgnoreCase))
+                    {
+                        resourceList = resourceData.assembly;
+                    }
+                    else if (string.Equals(assetType, "native", StringComparison.OrdinalIgnoreCase))
+                    {
+                        resourceList = resourceData.runtime;
+                    }
+                    else
+                    {
+                        // This should include items such as XML doc files, which do not need to be recorded in the manifest.
+                        continue;
+                    }
+
+                    if (!resourceList.ContainsKey(resourceName))
+                    {
+                        resourceList.Add(resourceName, $"sha256-{resource.GetMetadata("FileHash")}");
+                    }
+                }
+            }
+
+            if (ConfigurationFiles != null)
+            {
+                foreach (var configFile in ConfigurationFiles)
+                {
+                    result.config.Add(Path.GetFileName(configFile.ItemSpec));
+                }
+            }
+
+            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);
+        }
+
+        private bool IsLazyLoadedAssembly(string fileName)
+        {
+            return LazyLoadedAssemblies != null && LazyLoadedAssemblies.Any(a => a.ItemSpec == fileName);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs b/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs
new file mode 100644
index 00000000000..08c75a927af
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs
@@ -0,0 +1,97 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization.Json;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public partial class GenerateServiceWorkerAssetsManifest : Task
+    {
+        [Required]
+        public ITaskItem[] Assets { get; set; }
+
+        public string Version { get; set; }
+
+        [Required]
+        public string OutputPath { get; set; }
+
+        [Output]
+        public string CalculatedVersion { get; set; }
+
+        public override bool Execute()
+        {
+            using var fileStream = File.Create(OutputPath);
+            CalculatedVersion = GenerateAssetManifest(fileStream);
+
+            return true;
+        }
+
+        internal string GenerateAssetManifest(Stream stream)
+        {
+            var assets = new AssetsManifestFileEntry[Assets.Length];
+            System.Threading.Tasks.Parallel.For(0, assets.Length, i =>
+            {
+                var item = Assets[i];
+                var hash = item.GetMetadata("FileHash");
+                var url = item.GetMetadata("AssetUrl");
+
+                if (string.IsNullOrEmpty(hash))
+                {
+                    // Some files that are part of the service worker manifest may not have their hashes previously
+                    // calcualted. Calculate them at this time.
+                    using var sha = SHA256.Create();
+                    using var file = File.OpenRead(item.ItemSpec);
+                    var bytes = sha.ComputeHash(file);
+
+                    hash = Convert.ToBase64String(bytes);
+                }
+
+                assets[i] = new AssetsManifestFileEntry
+                {
+                    hash = "sha256-" + hash,
+                    url = url,
+                };
+            });
+
+            var version = Version;
+            if (string.IsNullOrEmpty(version))
+            {
+                // If a version isn't specified (which is likely the most common case), construct a Version by combining
+                // the file names + hashes of all the inputs.
+
+                var combinedHash = string.Join(
+                    Environment.NewLine,
+                    assets.OrderBy(f => f.url, StringComparer.Ordinal).Select(f => f.hash));
+
+                using var sha = SHA256.Create();
+                var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combinedHash));
+                version = Convert.ToBase64String(bytes).Substring(0, 8);
+            }
+
+            var data = new AssetsManifestFile
+            {
+                version = version,
+                assets = assets,
+            };
+
+            using var streamWriter = new StreamWriter(stream, Encoding.UTF8, bufferSize: 50, leaveOpen: true);
+            streamWriter.Write("self.assetsManifest = ");
+            streamWriter.Flush();
+
+            using var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, ownsStream: false, indent: true);
+            new DataContractJsonSerializer(typeof(AssetsManifestFile)).WriteObject(jsonWriter, data);
+            jsonWriter.Flush();
+
+            streamWriter.WriteLine(";");
+
+            return version;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj
new file mode 100644
index 00000000000..adf7330a3fe
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj
@@ -0,0 +1,89 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <Description>MSBuild support for building Blazor WebAssembly apps.</Description>
+    <TargetFrameworks>$(DefaultNetCoreTargetFramework);net46</TargetFrameworks>
+
+    <TargetName>Microsoft.NET.Sdk.BlazorWebAssembly.Tasks</TargetName>
+    <NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile>
+    <Serviceable>true</Serviceable>
+    <SdkOutputPath>$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\sdk-output\</SdkOutputPath>
+
+    <!-- Allow assemblies outside of lib in the package -->
+    <NoWarn>$(NoWarn);NU5100</NoWarn>
+    <!-- Need to build this project in source build -->
+    <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Build.Framework" />
+    <Reference Include="Microsoft.Build.Utilities.Core" />
+
+    <ProjectReference
+      Include="..\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj"
+      Targets="Publish"
+      ReferenceOutputAssembly="false"
+      IsImplicityDefined="false"
+      SkipGetTargetFrameworkProperties="true"
+      UndefineProperties="TargetFramework;TargetFrameworks;RuntimeIdentifier;PublishDir" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Content Include="_._" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <Target Name="LayoutDependencies" BeforeTargets="Build"
+    Condition="'$(IsInnerBuild)' != 'true' AND '$(NoBuild)' != 'true'">
+    <!-- Layout tasks, compiler, and extensions in the sdk-output folder. The entire folder structure gets packaged as-is into the SDK -->
+
+    <PropertyGroup Condition="'$(ContinuousIntegrationBuild)' != 'true'">
+      <_ContinueOnError>true</_ContinueOnError>
+      <_Retries>1</_Retries>
+    </PropertyGroup>
+
+    <PropertyGroup Condition="'$(ContinuousIntegrationBuild)' == 'true'">
+      <_ContinueOnError>false</_ContinueOnError>
+      <_Retries>10</_Retries>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_WebAssemblyToolsOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly.Tools\$(Configuration)\$(DefaultNetCoreTargetFramework)\publish\Microsoft.*" />
+    </ItemGroup>
+
+    <Error
+      Text="WebAssembly SDK tools outputs were not found in $(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly.Tools\$(Configuration)\$(DefaultNetCoreTargetFramework)\publish"
+      Condition="'@(_WebAssemblyToolsOutput->Count())' == '0'" />
+
+    <Copy
+      SourceFiles="@(_WebAssemblyToolsOutput)"
+      DestinationFolder="$(SdkOutputPath)tools\$(DefaultNetCoreTargetFramework)\"
+      SkipUnchangedFiles="true"
+      Retries="$(_Retries)"
+      ContinueOnError="$(_ContinueOnError)" />
+
+    <ItemGroup>
+      <ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\net46*\Microsoft.NET.Sdk.BlazorWebAssembly.*" />
+      <ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\$(DefaultNetCoreTargetFramework)*\Microsoft.NET.Sdk.BlazorWebAssembly.*" />
+    </ItemGroup>
+
+    <Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" SkipUnchangedFiles="true" Retries="$(_Retries)" ContinueOnError="$(_ContinueOnError)">
+      <Output TaskParameter="CopiedFiles" ItemName="FileWrites" />
+    </Copy>
+
+    <Message Text="Blazor WebAssembly SDK output -&gt; $(SdkOutputPath)" Importance="High" />
+  </Target>
+
+  <Target Name="PopulateNuspec" BeforeTargets="InitializeStandardNuspecProperties" DependsOnTargets="LayoutDependencies">
+    <PropertyGroup>
+      <PackageTags>$(PackageTags.Replace(';',' '))</PackageTags>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <NuspecProperty Include="outputPath=$(OutputPath)\sdk-output" />
+    </ItemGroup>
+  </Target>
+
+  <!-- Workarounds to allow publishing to work when the SDK is referenced as a project.  -->
+  <Target Name="GetTargetPath" />
+  <Target Name="GetCopyToPublishDirectoryItems" />
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec
new file mode 100644
index 00000000000..6d74a1931cc
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
+  <metadata>
+    $CommonMetadataElements$
+    <dependencies>
+      <group targetFramework=".NET5.0" />
+    </dependencies>
+  </metadata>
+
+  <files>
+    $CommonFileElements$
+    <file src="Sdk\*" target="Sdk" />
+    <file src="build\**" target="build" />
+    <file src="targets\**" target="targets" />
+    <file src="_._" target="lib\net5.0\_._" />
+
+    <file src="$outputPath$\**" target="\" />
+  </files>
+</package>
diff --git a/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props
new file mode 100644
index 00000000000..3f6870441f4
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props
@@ -0,0 +1,22 @@
+<!--
+***********************************************************************************************
+Sdk.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0" TreatAsLocalProperty="RuntimeIdentifier">
+  <PropertyGroup>
+    <UsingMicrosoftNETSdkBlazorWebAssembly>true</UsingMicrosoftNETSdkBlazorWebAssembly>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <_BlazorWebAssemblyPropsFile Condition="'$(_BlazorWebAssemblyPropsFile)' == ''">$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.props</_BlazorWebAssemblyPropsFile>
+  </PropertyGroup>
+
+  <Import Project="$(_BlazorWebAssemblyPropsFile)" />
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets
new file mode 100644
index 00000000000..616c56fb8a0
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets
@@ -0,0 +1,20 @@
+<!--
+***********************************************************************************************
+Sdk.targets
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+
+  <PropertyGroup>
+    <_BlazorWebAssemblyTargetsFile Condition="'$(_BlazorWebAssemblyTargetsFile)' == ''">$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile>
+  </PropertyGroup>
+
+  <Import Project="$(_BlazorWebAssemblyTargetsFile)" />
+
+</Project>
diff --git a/src/Razor/test/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js b/src/Components/WebAssembly/Sdk/src/_._
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js
rename to src/Components/WebAssembly/Sdk/src/_._
diff --git a/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props
new file mode 100644
index 00000000000..27e3fde3e00
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props
@@ -0,0 +1,16 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+  <PropertyGroup>
+    <_BlazorWebAssemblyPropsFile>$(MSBuildThisFileDirectory)..\..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.props</_BlazorWebAssemblyPropsFile>
+  </PropertyGroup>
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets
new file mode 100644
index 00000000000..8091b3d876f
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets
@@ -0,0 +1,16 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+  <PropertyGroup>
+    <_BlazorWebAssemblyTargetsFile>$(MSBuildThisFileDirectory)..\..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile>
+  </PropertyGroup>
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config b/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config
new file mode 100644
index 00000000000..7f9995d792c
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+  <system.webServer>
+    <staticContent>
+      <remove fileExtension=".dat" />
+      <remove fileExtension=".dll" />
+      <remove fileExtension=".json" />
+      <remove fileExtension=".wasm" />
+      <remove fileExtension=".woff" />
+      <remove fileExtension=".woff2" />
+      <mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
+      <mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
+      <mimeMap fileExtension=".json" mimeType="application/json" />
+      <mimeMap fileExtension=".wasm" mimeType="application/wasm" />
+      <mimeMap fileExtension=".woff" mimeType="application/font-woff" />
+      <mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
+    </staticContent>
+    <httpCompression>
+      <dynamicTypes>
+        <add mimeType="application/octet-stream" enabled="true" />
+        <add mimeType="application/wasm" enabled="true" />
+      </dynamicTypes>
+    </httpCompression>
+    <rewrite>
+      <rules>
+        <rule name="Serve subdir">
+          <match url=".*" />
+          <action type="Rewrite" url="wwwroot\{R:0}" />
+        </rule>
+        <rule name="SPA fallback routing" stopProcessing="true">
+          <match url=".*" />
+          <conditions logicalGrouping="MatchAll">
+            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
+          </conditions>
+          <action type="Rewrite" url="wwwroot\" />
+        </rule>
+      </rules>
+    </rewrite>
+  </system.webServer>
+</configuration>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml b/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml
new file mode 100644
index 00000000000..c61bc7a3cca
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml
@@ -0,0 +1,15 @@
+<linker>
+
+  <!-- This file specifies which parts of the BCL or Blazor packages must not be stripped
+  by the IL linker even if they are not referenced by user code. The file format is
+  described at https://github.com/mono/linker/blob/master/src/linker/README.md#syntax-of-xml-descriptor -->
+
+  <assembly fullname="System">
+    <!-- Without this, [Required(typeof(bool), "true", "true", ErrorMessage = "...")] fails -->
+    <type fullname="System.ComponentModel.BooleanConverter" />
+
+    <!-- TypeConverters are only used through reflection. These are two built-in TypeConverters that are useful. -->
+    <type fullname="System.ComponentModel.GuidConverter" />
+    <type fullname="System.ComponentModel.TimeSpanConverter" />
+  </assembly>
+</linker>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
new file mode 100644
index 00000000000..03b94ad5666
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
@@ -0,0 +1,36 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0" TreatAsLocalProperty="RuntimeIdentifier">
+  <PropertyGroup>
+    <!-- Blazor WASM projects are always browser-wasm -->
+    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
+
+    <!-- Avoid having the rid show up in output paths -->
+    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
+
+    <OutputType>exe</OutputType>
+
+    <IsPackable>false</IsPackable>
+
+    <WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <!-- Determines if this Sdk is responsible for importing Microsoft.NET.Sdk.Razor. Temporary workaround until we can create a SDK. -->
+    <_RazorSdkImportsMicrosoftNetSdkRazor Condition="'$(UsingMicrosoftNETSdkRazor)' != 'true'">true</_RazorSdkImportsMicrosoftNetSdkRazor>
+  </PropertyGroup>
+
+  <Import Sdk="Microsoft.NET.Sdk.Razor" Project="Sdk.props" Condition="'$(_RazorSdkImportsMicrosoftNetSdkRazor)' == 'true'" />
+  <Import Sdk="Microsoft.NET.Sdk.Web.ProjectSystem" Project="Sdk.props" />
+  <Import Sdk="Microsoft.NET.Sdk.Publish" Project="Sdk.props" />
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets
new file mode 100644
index 00000000000..a11bc00625c
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets
@@ -0,0 +1,582 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.targets
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+
+  <PropertyGroup>
+    <EnableDefaultContentItems Condition=" '$(EnableDefaultContentItems)' == '' ">true</EnableDefaultContentItems>
+  </PropertyGroup>
+
+  <Import Sdk="Microsoft.NET.Sdk.Razor" Project="Sdk.targets" Condition="'$(_RazorSdkImportsMicrosoftNetSdkRazor)' == 'true'" />
+  <Import Sdk="Microsoft.NET.Sdk.Web.ProjectSystem" Project="Sdk.targets" />
+  <Import Sdk="Microsoft.NET.Sdk.Publish" Project="Sdk.targets" />
+
+  <!--
+    Targets supporting Razor MSBuild integration. Contain support for generating C# code using Razor
+    and including the generated code in the project lifecycle, including compiling, publishing and producing
+    nuget packages.
+  -->
+
+  <!--
+    This is a hook to import a set of targets before the Blazor targets. By default this is unused.
+  -->
+  <Import Project="$(CustomBeforeBlazorWebAssemblySdkTargets)" Condition="'$(CustomBeforeBlazorWebAssemblySdkTargets)' != '' and Exists('$(CustomBeforeBlazorWebAssemblySdkTargets)')"/>
+
+  <PropertyGroup>
+    <!-- Paths to tools, tasks, and extensions are calculated relative to the BlazorWebAssemblySdkDirectoryRoot. This can be modified to test a local build. -->
+    <BlazorWebAssemblySdkDirectoryRoot Condition="'$(BlazorWebAssemblySdkDirectoryRoot)'==''">$(MSBuildThisFileDirectory)..\..\</BlazorWebAssemblySdkDirectoryRoot>
+    <_BlazorWebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">net5.0</_BlazorWebAssemblySdkTasksTFM>
+    <_BlazorWebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' != 'Core'">net46</_BlazorWebAssemblySdkTasksTFM>
+    <_BlazorWebAssemblySdkTasksAssembly>$(BlazorWebAssemblySdkDirectoryRoot)tasks\$(_BlazorWebAssemblySdkTasksTFM)\Microsoft.NET.Sdk.BlazorWebAssembly.Tasks.dll</_BlazorWebAssemblySdkTasksAssembly>
+    <_BlazorWebAssemblySdkToolAssembly>$(BlazorWebAssemblySdkDirectoryRoot)tools\net5.0\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.dll</_BlazorWebAssemblySdkToolAssembly>
+  </PropertyGroup>
+
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GenerateBlazorWebAssemblyBootJson" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BlazorWriteSatelliteAssemblyFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BlazorReadSatelliteAssemblyFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BrotliCompress" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GzipCompress" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.CreateBlazorTrimmerRootDescriptorFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+
+  <PropertyGroup>
+    <SelfContained>true</SelfContained>
+    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+
+    <!-- Trimmer defaults -->
+    <PublishTrimmed Condition="'$(PublishTrimmed)' == ''">true</PublishTrimmed>
+    <TrimMode Condition="'$(TrimMode)' == ''">link</TrimMode>
+
+    <StaticWebAssetBasePath Condition="'$(StaticWebAssetBasePath)' == ''">/</StaticWebAssetBasePath>
+    <BlazorCacheBootResources Condition="'$(BlazorCacheBootResources)' == ''">true</BlazorCacheBootResources>
+
+    <!-- Turn off parts of the build that do not apply to WASM projects -->
+    <GenerateDependencyFile>false</GenerateDependencyFile>
+    <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
+    <PreserveCompilationContext>false</PreserveCompilationContext>
+    <PreserveCompilationReferences>false</PreserveCompilationReferences>
+    <IsWebConfigTransformDisabled>true</IsWebConfigTransformDisabled>
+
+    <!-- Internal properties -->
+    <_BlazorOutputPath>wwwroot\_framework\</_BlazorOutputPath>
+
+  </PropertyGroup>
+
+  <Import Project="Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets" />
+
+  <Target Name="_ScrambleDotnetJsFileName" AfterTargets="ResolveRuntimePackAssets">
+    <!--
+      We want the dotnet.js file output to have a version to better work with caching. We'll append the runtime version to the file name as soon as file has been discovered.
+    -->
+    <PropertyGroup>
+      <_DotNetJsVersion>$(BundledNETCoreAppPackageVersion)</_DotNetJsVersion>
+      <_DotNetJsVersion Condition="'$(RuntimeFrameworkVersion)' != ''">$(RuntimeFrameworkVersion)</_DotNetJsVersion>
+      <_BlazorDotnetJsFileName>dotnet.$(_DotNetJsVersion).js</_BlazorDotnetJsFileName>
+      <_BlazorDotNetJsFilePath>$(IntermediateOutputPath)$(_BlazorDotnetJsFileName)</_BlazorDotNetJsFilePath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_DotNetJsItem Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.DestinationSubPath)' == 'dotnet.js' AND '%(ReferenceCopyLocalPaths.AssetType)' == 'native'" />
+    </ItemGroup>
+
+    <Copy
+      SourceFiles="@(_DotNetJsItem)"
+      DestinationFiles="$(_BlazorDotNetJsFilePath)"
+      SkipUnchangedFiles="true"
+      OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" />
+
+    <ItemGroup Condition="'@(_DotNetJsItem->Count())' != '0'">
+      <ReferenceCopyLocalPaths
+        Include="$(_BlazorDotNetJsFilePath)"
+        AssetType="native"
+        CopyLocal="true"
+        DestinationSubPath="$(_BlazorDotnetJsFileName)" />
+
+      <ReferenceCopyLocalPaths Remove="@(_DotNetJsItem)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_ResolveBlazorWasmOutputs" DependsOnTargets="ResolveReferences;PrepareResourceNames;ComputeIntermediateSatelliteAssemblies">
+    <!--
+      Calculates the outputs and the paths for Blazor WASM. This target is invoked frequently and should perform minimal work.
+    -->
+
+    <PropertyGroup>
+      <_BlazorSatelliteAssemblyCacheFile>$(IntermediateOutputPath)blazor.satelliteasm.props</_BlazorSatelliteAssemblyCacheFile>
+      <!-- Workaround for https://github.com/dotnet/sdk/issues/12114-->
+      <PublishDir Condition="'$(AppendRuntimeIdentifierToOutputPath)' != 'true' AND '$(PublishDir)' == '$(OutputPath)$(RuntimeIdentifier)\$(PublishDirName)\'">$(OutputPath)$(PublishDirName)\</PublishDir>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_BlazorJSFile Include="$(BlazorWebAssemblyJSPath)" />
+      <_BlazorJSFile Include="$(BlazorWebAssemblyJSMapPath)" Condition="Exists('$(BlazorWebAssemblyJSMapPath)')" />
+
+      <_BlazorConfigFile Include="wwwroot\appsettings*.json" />
+
+      <!-- Clear out temporary build artifacts that the runtime packages -->
+      <ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.a'" />
+
+      <!--
+        ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing
+        any metadata that might allow them to be differentiated. We'll explicitly add those
+        to _BlazorOutputWithTargetPath so that satellite assemblies from packages, the current project and referenced project
+        are all treated the same.
+       -->
+
+      <_BlazorCopyLocalPath
+        Include="@(ReferenceCopyLocalPaths)"
+        Exclude="@(ReferenceSatellitePaths)"/>
+
+      <_BlazorCopyLocalPath Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
+        <DestinationSubDirectory>%(IntermediateSatelliteAssembliesWithTargetPath.Culture)\</DestinationSubDirectory>
+      </_BlazorCopyLocalPath>
+
+      <_BlazorOutputWithTargetPath Include="
+          @(_BlazorCopyLocalPath);
+          @(IntermediateAssembly);
+          @(_DebugSymbolsIntermediatePath);
+          @(_BlazorJSFile)" />
+
+      <_BlazorOutputWithTargetPath Include="@(ReferenceSatellitePaths)">
+        <Culture>$([System.String]::Copy('%(ReferenceSatellitePaths.DestinationSubDirectory)').Trim('\').Trim('/'))</Culture>
+      </_BlazorOutputWithTargetPath>
+    </ItemGroup>
+
+    <!--
+      BuildingProject=false is typically set for referenced projects when building inside VisualStudio.
+
+      When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for
+      referenced project). Satellite assemblies from packages get resolved.
+      To workaround this, we'll cache metadata during a regular build, and rehydrate from it when BuildingProject=false.
+    -->
+    <BlazorReadSatelliteAssemblyFile
+        ReadFile="$(_BlazorSatelliteAssemblyCacheFile)"
+        Condition="'$(BuildingProject)' != 'true' AND EXISTS('$(_BlazorSatelliteAssemblyCacheFile)')">
+      <Output TaskParameter="SatelliteAssembly" ItemName="_BlazorReadSatelliteAssembly" />
+    </BlazorReadSatelliteAssemblyFile>
+
+    <ItemGroup>
+      <!-- We've imported a previously Cacheed file. Let's turn in to a _BlazorOutputWithTargetPath -->
+      <_BlazorOutputWithTargetPath
+        Include="@(_BlazorReadSatelliteAssembly)"
+        Exclude="@(_BlazorOutputWithTargetPath)"
+        Condition="'@(_BlazorReadSatelliteAssembly->Count())' != '0'" />
+
+      <!-- Calculate the target path -->
+      <_BlazorOutputWithTargetPath
+        TargetPath="$(_BlazorOutputPath)%(_BlazorOutputWithTargetPath.DestinationSubDirectory)%(FileName)%(Extension)"
+        Condition="'%(__BlazorOutputWithTargetPath.TargetPath)' == ''" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_ProcessBlazorWasmOutputs" DependsOnTargets="_ResolveBlazorWasmOutputs">
+    <PropertyGroup>
+      <_BlazorBuildGZipCompressDirectory>$(IntermediateOutputPath)build-gz\</_BlazorBuildGZipCompressDirectory>
+    </PropertyGroup>
+
+    <!--
+      Compress referenced binaries using GZip during build. This skips files such as the project's assemblies
+      that change from build to build. Runtime assets contribute to the bulk of the download size. Compressing it
+      has the most benefit while avoiding any ongoing costs to the dev inner loop.
+    -->
+    <ItemGroup>
+      <_GzipFileToCompressForBuild
+        Include="@(ReferenceCopyLocalPaths)"
+        RelativePath="$(_BlazorOutputPath)%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)"
+        Condition="'%(Extension)' == '.dll' or '%(ReferenceCopyLocalPaths.AssetType)' == 'native'" />
+    </ItemGroup>
+
+    <GZipCompress
+      FilesToCompress="@(_GzipFileToCompressForBuild)"
+      OutputDirectory="$(_BlazorBuildGZipCompressDirectory)">
+
+      <Output TaskParameter="CompressedFiles" ItemName="_BlazorBuildGZipCompressedFile" />
+      <Output TaskParameter="CompressedFiles" ItemName="FileWrites" />
+    </GZipCompress>
+
+    <ItemGroup>
+      <_BlazorWriteSatelliteAssembly Include="@(_BlazorOutputWithTargetPath->HasMetadata('Culture'))" />
+
+      <!-- Retarget ReferenceCopyLocalPaths to copy to the wwwroot directory -->
+      <ReferenceCopyLocalPaths DestinationSubDirectory="$(_BlazorOutputPath)%(ReferenceCopyLocalPaths.DestinationSubDirectory)" />
+    </ItemGroup>
+
+    <!-- A missing blazor.webassembly.js is our packaging error. Produce an error so it's discovered early. -->
+    <Error
+      Text="Unable to find BlazorWebAssembly JS files. This usually indicates a packaging error."
+      Code="RAZORSDK1007"
+      Condition="'@(_BlazorJSFile->Count())' == '0'" />
+
+    <!--
+      When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for
+      referenced project). BuildingProject=false is typically set for referenced projects when building inside VisualStudio.
+      To workaround this, we'll cache metadata during a regular build, and rehydrate from it when BuildingProject=false.
+    -->
+
+    <BlazorWriteSatelliteAssemblyFile
+      SatelliteAssembly="@(_BlazorWriteSatelliteAssembly)"
+      WriteFile="$(_BlazorSatelliteAssemblyCacheFile)"
+      Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' != '0'" />
+
+    <Delete
+      Files="$(_BlazorSatelliteAssemblyCacheFile)"
+      Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' == '0' and EXISTS('$(_BlazorSatelliteAssemblyCacheFile)')" />
+
+    <ItemGroup>
+      <FileWrites Include="$(_BlazorSatelliteAssemblyCacheFile)" Condition="Exists('$(_BlazorSatelliteAssemblyCacheFile)')" />
+    </ItemGroup>
+
+    <GetFileHash Files="@(_BlazorOutputWithTargetPath)" Algorithm="SHA256" HashEncoding="base64">
+      <Output TaskParameter="Items" ItemName="_BlazorOutputWithHash" />
+    </GetFileHash>
+  </Target>
+
+  <PropertyGroup>
+    <PrepareForRunDependsOn>
+      _BlazorWasmPrepareForRun;
+      $(PrepareForRunDependsOn)
+    </PrepareForRunDependsOn>
+
+    <GetCurrentProjectStaticWebAssetsDependsOn>
+      $(GetCurrentProjectStaticWebAssetsDependsOn);
+      _BlazorWasmPrepareForRun;
+    </GetCurrentProjectStaticWebAssetsDependsOn>
+  </PropertyGroup>
+
+  <Target Name="_BlazorWasmPrepareForRun" DependsOnTargets="_ProcessBlazorWasmOutputs" BeforeTargets="_RazorPrepareForRun" AfterTargets="GetCurrentProjectStaticWebAssets">
+    <PropertyGroup>
+      <_BlazorBuildBootJsonPath>$(IntermediateOutputPath)blazor.boot.json</_BlazorBuildBootJsonPath>
+    </PropertyGroup>
+
+    <GenerateBlazorWebAssemblyBootJson
+      AssemblyPath="@(IntermediateAssembly)"
+      Resources="@(_BlazorOutputWithHash)"
+      DebugBuild="true"
+      LinkerEnabled="false"
+      CacheBootResources="$(BlazorCacheBootResources)"
+      OutputPath="$(_BlazorBuildBootJsonPath)"
+      ConfigurationFiles="@(_BlazorConfigFile)"
+      LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" />
+
+    <ItemGroup>
+      <FileWrites Include="$(OutDir)$(_BlazorOutputPath)blazor.boot.json" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <_BlazorWebAssemblyStaticWebAsset Include="$(_BlazorBuildBootJsonPath)">
+        <SourceId>$(PackageId)</SourceId>
+        <SourceType></SourceType>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>_framework/blazor.boot.json</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_BlazorWebAssemblyStaticWebAsset>
+
+      <_BlazorWebAssemblyStaticWebAsset Include="@(_BlazorOutputWithHash)">
+        <SourceId>$(PackageId)</SourceId>
+        <SourceType></SourceType>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>$([System.String]::Copy('%(_BlazorOutputWithHash.TargetPath)').Replace('\','/').Substring(8))</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_BlazorWebAssemblyStaticWebAsset>
+
+      <_BlazorWebAssemblyStaticWebAsset Include="@(_BlazorBuildGZipCompressedFile)">
+        <SourceId>$(PackageId)</SourceId>
+        <SourceType></SourceType>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>$([System.String]::Copy('%(_BlazorBuildGZipCompressedFile.RelativePath)').Replace('\','/').Substring(8))</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_BlazorWebAssemblyStaticWebAsset>
+
+      <StaticWebAsset Include="@(_BlazorWebAssemblyStaticWebAsset)" />
+      <_ExternalStaticWebAsset Include="@(_BlazorWebAssemblyStaticWebAsset)" SourceType="Generated" />
+    </ItemGroup>
+  </Target>
+
+  <!-- Mimics the behavior of CopyFilesToOutputDirectory. We simply copy relevant build outputs to the wwwroot directory -->
+  <Target Name="_BlazorCopyFilesToOutputDirectory" AfterTargets="CopyFilesToOutputDirectory">
+    <Copy
+        SourceFiles="@(IntermediateAssembly)"
+        DestinationFolder="$(OutDir)$(_BlazorOutputPath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)' != 'true'">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Message Importance="High" Text="$(MSBuildProjectName) (Blazor output) -&gt; $(TargetDir)wwwroot" Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)'!='true'" />
+
+    <Copy
+        SourceFiles="@(_DebugSymbolsIntermediatePath)"
+        DestinationFolder="$(OutDir)$(_BlazorOutputPath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="'$(_DebugSymbolsProduced)'=='true' and '$(SkipCopyingSymbolsToOutputDirectory)' != 'true' and '$(CopyOutputSymbolsToOutputDirectory)'=='true'">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Copy
+        SourceFiles="@(IntermediateSatelliteAssembliesWithTargetPath)"
+        DestinationFiles="@(IntermediateSatelliteAssembliesWithTargetPath->'$(OutDir)$(_BlazorOutputPath)%(Culture)\$(TargetName).resources.dll')"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="'@(IntermediateSatelliteAssembliesWithTargetPath)' != ''" >
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Copy
+        SourceFiles="@(_BlazorJSFile);$(_BlazorBuildBootJsonPath)"
+        DestinationFolder="$(OutDir)$(_BlazorOutputPath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Copy
+        SourceFiles="@(_BlazorBuildGZipCompressedFile)"
+        DestinationFiles="@(_BlazorBuildGZipCompressedFile->'$(OutDir)%(RelativePath)')"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+  </Target>
+
+  <Target Name="_BlazorWasmPrepareForLink" BeforeTargets="PrepareForILLink">
+    <PropertyGroup>
+      <_BlazorTypeGranularTrimmerDescriptorFile>$(IntermediateOutputPath)typegranularity.trimmerdescriptor.xml</_BlazorTypeGranularTrimmerDescriptorFile>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_BlazorTypeGranularAssembly
+          Include="@(ManagedAssemblyToLink)"
+          Condition="'%(Extension)' == '.dll' AND ($([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.')) or $([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.')))">
+        <Required>false</Required>
+        <Preserve>all</Preserve>
+      </_BlazorTypeGranularAssembly>
+
+      <ManagedAssemblyToLink
+        IsTrimmable="true"
+        Condition="'%(Extension)' == '.dll' AND ($([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.')) or $([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.')))" />
+    </ItemGroup>
+
+    <CreateBlazorTrimmerRootDescriptorFile
+      Assemblies="@(_BlazorTypeGranularAssembly)"
+      TrimmerFile="$(_BlazorTypeGranularTrimmerDescriptorFile)" />
+
+    <ItemGroup>
+      <TrimmerRootDescriptor Include="$(_BlazorTypeGranularTrimmerDescriptorFile)" />
+      <TrimmerRootDescriptor Include="$(MSBuildThisFileDirectory)LinkerWorkaround.xml" />
+
+      <FileWrites Include="$(_BlazorTypeGranularTrimmerDescriptorFile)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_ProcessPublishFilesForBlazor" DependsOnTargets="_ResolveBlazorWasmOutputs" AfterTargets="ILLink">
+
+    <!--
+      ResolvedFileToPublish.Culture is missing for satellite assemblies from project references.
+      Since we need the culture to correctly generate blazor.boot.json, we cross-reference the culture we calculate as part of _ResolveBlazorWasmOutputs
+    -->
+    <JoinItems Left="@(ResolvedFileToPublish)"
+               Right="@(_BlazorOutputWithTargetPath->HasMetadata('Culture'))"
+               LeftMetadata="*"
+               RightMetadata="Culture"
+               ItemSpecToUse="Left">
+      <Output TaskParameter="JoinResult" ItemName="_ResolvedSatelliteToPublish" />
+    </JoinItems>
+
+    <ItemGroup>
+      <ResolvedFileToPublish Remove="@(_ResolvedSatelliteToPublish)" />
+      <ResolvedFileToPublish Include="@(_ResolvedSatelliteToPublish)" />
+
+      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(Extension)' == '.a'" />
+
+      <!-- Remove dotnet.js from publish output -->
+      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.RelativePath)' == 'dotnet.js'" />
+
+      <!-- Retarget so that items are published to the wwwroot directory -->
+      <ResolvedFileToPublish
+        RelativePath="$(_BlazorOutputPath)%(ResolvedFileToPublish.RelativePath)"
+        Condition="'%(ResolvedFileToPublish.RelativePath)' != 'web.config' AND !$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))" />
+
+      <!-- Remove pdbs from the publish output -->
+      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'$(BlazorWebAssemblyEnableDebugging)' != 'true' AND '%(Extension)' == '.pdb'" />
+    </ItemGroup>
+
+    <ItemGroup Condition="'@(ResolvedFileToPublish->AnyHaveMetadataValue('RelativePath', 'web.config'))' != 'true'">
+      <ResolvedFileToPublish
+         Include="$(MSBuildThisFileDirectory)BlazorWasm.web.config"
+         ExcludeFromSingleFile="true"
+         CopyToPublishDirectory="PreserveNewest"
+         RelativePath="web.config" />
+    </ItemGroup>
+
+    <!-- Generate the publish boot json -->
+    <ItemGroup>
+      <_BlazorPublishBootResource
+        Include="@(ResolvedFileToPublish)"
+        Condition="$([System.String]::Copy('%(RelativePath)').Replace('\','/').StartsWith('wwwroot/_framework')) AND '%(Extension)' != '.a'" />
+    </ItemGroup>
+
+    <GetFileHash Files="@(_BlazorPublishBootResource)" Algorithm="SHA256" HashEncoding="base64">
+      <Output TaskParameter="Items" ItemName="_BlazorPublishBootResourceWithHash" />
+    </GetFileHash>
+
+    <GenerateBlazorWebAssemblyBootJson
+      AssemblyPath="@(IntermediateAssembly)"
+      Resources="@(_BlazorPublishBootResourceWithHash)"
+      DebugBuild="false"
+      LinkerEnabled="$(PublishTrimmed)"
+      CacheBootResources="$(BlazorCacheBootResources)"
+      OutputPath="$(IntermediateOutputPath)blazor.publish.boot.json"
+      ConfigurationFiles="@(_BlazorConfigFile)"
+      LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" />
+
+    <ItemGroup>
+      <ResolvedFileToPublish
+        Include="$(IntermediateOutputPath)blazor.publish.boot.json"
+        RelativePath="$(_BlazorOutputPath)blazor.boot.json" />
+
+      <ResolvedFileToPublish
+        Include="@(_BlazorJSFile)"
+        RelativePath="$(_BlazorOutputPath)%(FileName)%(Extension)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_BlazorCompressPublishFiles" AfterTargets="_ProcessPublishFilesForBlazor" Condition="'$(BlazorEnableCompression)' != 'false'">
+    <PropertyGroup>
+      <_CompressedFileOutputPath>$(IntermediateOutputPath)compress\</_CompressedFileOutputPath>
+      <_BlazorWebAssemblyBrotliIncremental>true</_BlazorWebAssemblyBrotliIncremental>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_FileToCompress
+        Include="@(ResolvedFileToPublish)"
+        Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))" />
+    </ItemGroup>
+
+    <Message Text="Compressing Blazor WebAssembly publish artifacts. This may take a while..." Importance="High" />
+
+    <MakeDir Directories="$(_CompressedFileOutputPath)" Condition="!Exists('$(_CompressedFileOutputPath)')" />
+
+    <PropertyGroup Condition="'$(DOTNET_HOST_PATH)' == ''">
+      <_DotNetHostDirectory>$(NetCoreRoot)</_DotNetHostDirectory>
+      <_DotNetHostFileName>dotnet</_DotNetHostFileName>
+      <_DotNetHostFileName Condition="'$(OS)' == 'Windows_NT'">dotnet.exe</_DotNetHostFileName>
+    </PropertyGroup>
+
+    <BrotliCompress
+      OutputDirectory="$(_CompressedFileOutputPath)"
+      FilesToCompress="@(_FileToCompress)"
+      CompressionLevel="$(_BlazorBrotliCompressionLevel)"
+      SkipIfOutputIsNewer="$(_BlazorWebAssemblyBrotliIncremental)"
+      ToolAssembly="$(_BlazorWebAssemblySdkToolAssembly)"
+      ToolExe="$(_DotNetHostFileName)"
+      ToolPath="$(_DotNetHostDirectory)">
+
+      <Output TaskParameter="CompressedFiles" ItemName="_BrotliCompressedFile" />
+      <Output TaskParameter="CompressedFiles" ItemName="FileWrites" />
+    </BrotliCompress>
+
+    <GZipCompress
+      OutputDirectory="$(_CompressedFileOutputPath)"
+      FilesToCompress="@(_FileToCompress)">
+
+      <Output TaskParameter="CompressedFiles" ItemName="_BlazorPublishGZipCompressedFile" />
+      <Output TaskParameter="CompressedFiles" ItemName="FileWrites" />
+    </GZipCompress>
+
+    <ItemGroup>
+      <ResolvedFileToPublish Include="@(_BrotliCompressedFile)" />
+      <ResolvedFileToPublish Include="@(_BlazorPublishGZipCompressedFile)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_SetupPublishSemaphore" BeforeTargets="PrepareForPublish">
+    <PropertyGroup>
+      <!--
+        Add marker that indicates Blazor WASM is doing a publish. This is used to identify when GetCopyToPublishDirectoryItems
+        is invoked as a result of a P2P reference.
+      -->
+      <_PublishingBlazorWasmProject>true</_PublishingBlazorWasmProject>
+    </PropertyGroup>
+  </Target>
+
+  <Target Name="_GetBlazorWasmFilesForPublishInner"
+    DependsOnTargets="_ResolveBlazorWasmOutputs;ComputeFilesToPublish"
+    Returns="@(ResolvedFileToPublish)" />
+
+  <Target Name="_GetBlazorWasmFilesForPublish" BeforeTargets="GetCopyToPublishDirectoryItems">
+    <MSBuild
+      Projects="$(MSBuildProjectFullPath)"
+      Targets="_GetBlazorWasmFilesForPublishInner"
+      Properties="BuildProjectReferences=false;ResolveAssemblyReferencesFindRelatedSatellites=true;_PublishingBlazorWasmProject=true"
+      RemoveProperties="NoBuild;RuntimeIdentifier"
+      BuildInParallel="$(BuildInParallel)"
+      Condition="'$(_PublishingBlazorWasmProject)' != 'true'">
+
+      <Output TaskParameter="TargetOutputs" ItemName="_ResolvedFileToPublish" />
+    </MSBuild>
+
+    <ItemGroup>
+      <AllPublishItemsFullPathWithTargetPath Include="@(_ResolvedFileToPublish->'%(FullPath)')">
+        <TargetPath>%(RelativePath)</TargetPath>
+        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+      </AllPublishItemsFullPathWithTargetPath>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_BlazorApplyLinkPreferencesToContent" BeforeTargets="AssignTargetPaths;ResolveCurrentProjectStaticWebAssetsInputs;ResolveStaticWebAssetsInputs" Returns="@(Content)">
+    <ItemGroup>
+      <Content
+        Condition="'%(Content.Link)' != '' AND '%(Content.CopyToPublishDirectory)' == '' AND $([System.String]::Copy('%(Content.Link)').Replace('\','/').StartsWith('wwwroot/'))"
+        CopyToPublishDirectory="PreserveNewest" />
+
+    </ItemGroup>
+  </Target>
+
+  <!--
+    This is a hook to import a set of targets after the Blazor WebAssembly targets. By default this is unused.
+  -->
+  <Import Project="$(CustomAfterBlazorWebAssemblySdkTargets)" Condition="'$(CustomAfterBlazorWebAssemblySdkTargets)' != '' and Exists('$(CustomAfterBlazorWebAssemblySdkTargets)')"/>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets
new file mode 100644
index 00000000000..5202eb338d4
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets
@@ -0,0 +1,169 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+
+<Project>
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GenerateServiceWorkerAssetsManifest" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+
+  <Target Name="_ComputeServiceWorkerAssets" BeforeTargets="ResolveStaticWebAssetsInputs">
+
+    <PropertyGroup>
+      <_ServiceWorkerAssetsManifestIntermediateOutputPath Condition="'$([System.IO.Path]::IsPathRooted($(BaseIntermediateOutputPath)))' == 'true'">obj\$(Configuration)\$(TargetFramework)\$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath>
+      <_ServiceWorkerAssetsManifestIntermediateOutputPath Condition="'$([System.IO.Path]::IsPathRooted($(BaseIntermediateOutputPath)))' != 'true'">$(IntermediateOutputPath)$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath>
+      <_ServiceWorkerAssetsManifestFullPath>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(_ServiceWorkerAssetsManifestIntermediateOutputPath)'))</_ServiceWorkerAssetsManifestFullPath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_ManifestStaticWebAsset Include="$(_ServiceWorkerAssetsManifestFullPath)">
+        <SourceType></SourceType>
+        <SourceId>$(PackageId)</SourceId>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>$(ServiceWorkerAssetsManifest)</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_ManifestStaticWebAsset>
+
+      <!-- Figure out where we're getting the content for each @(ServiceWorker) entry, depending on whether there's a PublishedContent value -->
+      <_ServiceWorkerIntermediateFile Include="@(ServiceWorker->'$(IntermediateOutputPath)serviceworkers\%(Identity)')">
+        <ContentSourcePath Condition="'%(_ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
+        <ContentSourcePath Condition="'%(_ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
+        <OriginalPath>%(ServiceWorker.Identity)</OriginalPath>
+        <TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath>
+        <TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').Replace('\','/').StartsWith('wwwroot/'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
+      </_ServiceWorkerIntermediateFile>
+
+      <_ServiceWorkerStaticWebAsset Include="%(_ServiceWorkerIntermediateFile.FullPath)">
+        <SourceType></SourceType>
+        <SourceId>$(PackageId)</SourceId>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>%(TargetOutputPath)</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_ServiceWorkerStaticWebAsset>
+
+      <StaticWebAsset Include="
+          @(_ManifestStaticWebAsset);
+          @(_ServiceWorkerStaticWebAsset)" />
+    </ItemGroup>
+
+  </Target>
+
+  <Target Name="_WriteServiceWorkerAssetsManifest"
+    DependsOnTargets="_ComputeServiceWorkerAssets;ResolveStaticWebAssetsInputs">
+
+    <ItemGroup>
+      <_ServiceWorkItem Include="@(StaticWebAsset)" Exclude="$(_ServiceWorkerAssetsManifestFullPath);@(_ServiceWorkerStaticWebAsset)">
+        <AssetUrl>$([System.String]::Copy('$([System.String]::Copy('%(StaticWebAsset.BasePath)').TrimEnd('/'))/%(StaticWebAsset.RelativePath)').Replace('\','/').TrimStart('/'))</AssetUrl>
+      </_ServiceWorkItem>
+    </ItemGroup>
+
+    <GenerateServiceWorkerAssetsManifest
+      Version="$(ServiceWorkerAssetsManifestVersion)"
+      Assets="@(_ServiceWorkItem)"
+      OutputPath="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)">
+      <Output TaskParameter="CalculatedVersion" PropertyName="_ServiceWorkerAssetsManifestVersion" />
+    </GenerateServiceWorkerAssetsManifest>
+
+    <Copy
+      SourceFiles="%(_ServiceWorkerIntermediateFile.ContentSourcePath)"
+      DestinationFiles="%(_ServiceWorkerIntermediateFile.Identity)" />
+
+    <WriteLinesToFile
+      File="%(_ServiceWorkerIntermediateFile.Identity)"
+      Lines="/* Manifest version: $(_ServiceWorkerAssetsManifestVersion) */"
+      Condition="'$(_ServiceWorkerAssetsManifestVersion)' != ''" />
+
+    <ItemGroup>
+      <FileWrites Include="@(_ServiceWorkerIntermediateFile)" />
+      <FileWrites Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
+    </ItemGroup>
+
+  </Target>
+
+  <Target Name="_BlazorStaticAssetsCopyFilesToOutputDirectory" AfterTargets="CopyFilesToOutputDirectory" DependsOnTargets="_WriteServiceWorkerAssetsManifest">
+    <Copy
+        SourceFiles="@(_ManifestStaticWebAsset);@(_ServiceWorkerStaticWebAsset)"
+        DestinationFiles="$(OutDir)wwwroot\%(RelativePath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="Exists('%(Identity)')">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+  </Target>
+
+  <Target Name="_OmitServiceWorkerContent"
+    BeforeTargets="AssignTargetPaths;ResolveCurrentProjectStaticWebAssetsInputs">
+
+    <ItemGroup>
+      <!-- Don't emit the service worker source files to the output -->
+      <Content Remove="@(ServiceWorker)" />
+      <Content Remove="@(ServiceWorker->'%(PublishedContent)')" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_GenerateServiceWorkerFileForPublish"
+    BeforeTargets="_BlazorCompressPublishFiles"
+    AfterTargets="_ProcessPublishFilesForBlazor">
+
+    <PropertyGroup>
+      <_ServiceWorkerAssetsManifestPublishIntermediateOutputPath>$(IntermediateOutputPath)publish-$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestPublishIntermediateOutputPath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_ServiceWorkerIntermediatePublishFile Include="$(IntermediateOutputPath)serviceworkers\%(FileName).publish%(Extension)">
+        <ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
+        <ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
+        <RelativePath>%(ServiceWorker.Identity)</RelativePath>
+      </_ServiceWorkerIntermediatePublishFile>
+
+      <_ServiceWorkerPublishFile Include="@(ResolvedFileToPublish)" Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))">
+        <AssetUrl>$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').TrimStart('/'))</AssetUrl>
+        <AssetUrl>$([System.String]::Copy('%(RelativePath)').Replace('\','/').Substring(8))</AssetUrl>
+      </_ServiceWorkerPublishFile>
+    </ItemGroup>
+
+    <GenerateServiceWorkerAssetsManifest
+      Version="$(ServiceWorkerAssetsManifestVersion)"
+      Assets="@(_ServiceWorkerPublishFile)"
+      OutputPath="$(_ServiceWorkerAssetsManifestPublishIntermediateOutputPath)">
+
+      <Output TaskParameter="CalculatedVersion" PropertyName="_ServiceWorkerPublishAssetsManifestVersion" />
+    </GenerateServiceWorkerAssetsManifest>
+
+    <Copy SourceFiles="%(_ServiceWorkerIntermediatePublishFile.ContentSourcePath)"
+      DestinationFiles="%(_ServiceWorkerIntermediatePublishFile.Identity)" />
+
+    <WriteLinesToFile
+      File="%(_ServiceWorkerIntermediatePublishFile.Identity)"
+      Lines="/* Manifest version: $(_ServiceWorkerPublishAssetsManifestVersion) */" />
+
+    <ItemGroup>
+      <ResolvedFileToPublish
+        Include="@(_ServiceWorkerIntermediatePublishFile)"
+        CopyToPublishDirectory="PreserveNewest"
+        RelativePath="%(_ServiceWorkerIntermediatePublishFile.RelativePath)"
+        ExcludeFromSingleFile="true" />
+
+      <ResolvedFileToPublish
+        Include="$(_ServiceWorkerAssetsManifestPublishIntermediateOutputPath)"
+        CopyToPublishDirectory="PreserveNewest"
+        RelativePath="wwwroot\$(ServiceWorkerAssetsManifest)"
+        ExcludeFromSingleFile="true" />
+    </ItemGroup>
+  </Target>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs b/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs
new file mode 100644
index 00000000000..b95a6154b66
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs
@@ -0,0 +1,68 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Moq;
+using Xunit;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BlazorReadSatelliteAssemblyFileTest
+    {
+        [Fact]
+        public void WritesAndReadsRoundTrip()
+        {
+            // Arrange/Act
+            var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+
+            var writer = new BlazorWriteSatelliteAssemblyFile
+            {
+                BuildEngine = Mock.Of<IBuildEngine>(),
+                WriteFile = new TaskItem(tempFile),
+                SatelliteAssembly = new[]
+                {
+                    new TaskItem("Resources.fr.dll", new Dictionary<string, string>
+                    {
+                        ["Culture"] = "fr",
+                        ["DestinationSubDirectory"] = "fr\\",
+                    }),
+                    new TaskItem("Resources.ja-jp.dll", new Dictionary<string, string>
+                    {
+                        ["Culture"] = "ja-jp",
+                        ["DestinationSubDirectory"] = "ja-jp\\",
+                    }),
+                },
+            };
+
+            var reader = new BlazorReadSatelliteAssemblyFile
+            {
+                BuildEngine = Mock.Of<IBuildEngine>(),
+                ReadFile = new TaskItem(tempFile),
+            };
+
+            writer.Execute();
+
+            Assert.True(File.Exists(tempFile), "Write should have succeeded.");
+
+            reader.Execute();
+
+            Assert.Collection(
+                reader.SatelliteAssembly,
+                assembly =>
+                {
+                    Assert.Equal("Resources.fr.dll", assembly.ItemSpec);
+                    Assert.Equal("fr", assembly.GetMetadata("Culture"));
+                    Assert.Equal("fr\\", assembly.GetMetadata("DestinationSubDirectory"));
+                },
+                assembly =>
+                {
+                    Assert.Equal("Resources.ja-jp.dll", assembly.ItemSpec);
+                    Assert.Equal("ja-jp", assembly.GetMetadata("Culture"));
+                    Assert.Equal("ja-jp\\", assembly.GetMetadata("DestinationSubDirectory"));
+                });
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs b/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs
new file mode 100644
index 00000000000..139f22f27fb
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs
@@ -0,0 +1,195 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization.Json;
+using Microsoft.Build.Framework;
+using Moq;
+using Xunit;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class GenerateBlazorWebAssemblyBootJsonTest
+    {
+        [Fact]
+        public void GroupsResourcesByType()
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson
+            {
+                AssemblyPath = "MyApp.Entrypoint.dll",
+                Resources = new[]
+                {
+                    CreateResourceTaskItem(
+                        ("FileName", "My.Assembly1"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "abcdefghikjlmnopqrstuvwxyz")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "My.Assembly2"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "012345678901234567890123456789")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "SomePdb"),
+                        ("Extension", ".pdb"),
+                        ("FileHash", "pdbhashpdbhashpdbhash")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "My.Assembly1"),
+                        ("Extension", ".pdb"),
+                        ("FileHash", "pdbdefghikjlmnopqrstuvwxyz")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "some-runtime-file"),
+                        ("FileHash", "runtimehashruntimehash"),
+                        ("AssetType", "native")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "satellite-assembly1"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "hashsatelliteassembly1"),
+                        ("Culture", "en-GB")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "satellite-assembly2"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "hashsatelliteassembly2"),
+                        ("Culture", "fr")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "satellite-assembly3"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "hashsatelliteassembly3"),
+                        ("Culture", "en-GB")),
+                }
+            };
+
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal("MyEntrypointAssembly", parsedContent.entryAssembly);
+
+            var resources = parsedContent.resources.assembly;
+            Assert.Equal(2, resources.Count);
+            Assert.Equal("sha256-abcdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.dll"]);
+            Assert.Equal("sha256-012345678901234567890123456789", resources["My.Assembly2.dll"]);
+
+            resources = parsedContent.resources.pdb;
+            Assert.Equal(2, resources.Count);
+            Assert.Equal("sha256-pdbhashpdbhashpdbhash", resources["SomePdb.pdb"]);
+            Assert.Equal("sha256-pdbdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.pdb"]);
+
+            resources = parsedContent.resources.runtime;
+            Assert.Single(resources);
+            Assert.Equal("sha256-runtimehashruntimehash", resources["some-runtime-file"]);
+
+            var satelliteResources = parsedContent.resources.satelliteResources;
+            Assert.Collection(
+                satelliteResources.OrderBy(kvp => kvp.Key),
+                kvp =>
+                {
+                    Assert.Equal("en-GB", kvp.Key);
+                    Assert.Collection(
+                        kvp.Value.OrderBy(item => item.Key),
+                        item =>
+                        {
+                            Assert.Equal("en-GB/satellite-assembly1.dll", item.Key);
+                            Assert.Equal("sha256-hashsatelliteassembly1", item.Value);
+                        },
+                        item =>
+                        {
+                            Assert.Equal("en-GB/satellite-assembly3.dll", item.Key);
+                            Assert.Equal("sha256-hashsatelliteassembly3", item.Value);
+                        });
+                },
+                kvp =>
+                {
+                    Assert.Equal("fr", kvp.Key);
+                    Assert.Collection(
+                        kvp.Value.OrderBy(item => item.Key),
+                        item =>
+                        {
+                            Assert.Equal("fr/satellite-assembly2.dll", item.Key);
+                            Assert.Equal("sha256-hashsatelliteassembly2", item.Value);
+                        });
+                });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void CanSpecifyCacheBootResources(bool flagValue)
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson { CacheBootResources = flagValue };
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal(flagValue, parsedContent.cacheBootResources);
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void CanSpecifyDebugBuild(bool flagValue)
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson { DebugBuild = flagValue };
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal(flagValue, parsedContent.debugBuild);
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void CanSpecifyLinkerEnabled(bool flagValue)
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson { LinkerEnabled = flagValue };
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal(flagValue, parsedContent.linkerEnabled);
+        }
+
+        private static BootJsonData ParseBootData(Stream stream)
+        {
+            stream.Position = 0;
+            var serializer = new DataContractJsonSerializer(
+                typeof(BootJsonData),
+                new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true });
+            return (BootJsonData)serializer.ReadObject(stream);
+        }
+
+        private static ITaskItem CreateResourceTaskItem(params (string key, string value)[] values)
+        {
+            var mock = new Mock<ITaskItem>();
+
+            foreach (var (key, value) in values)
+            {
+                mock.Setup(m => m.GetMetadata(key)).Returns(value);
+            }
+            return mock.Object;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj b/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj
new file mode 100644
index 00000000000..5d30b4f7814
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Build.Utilities.Core" />
+    <Reference Include="Microsoft.NET.Sdk.BlazorWebAssembly" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props
new file mode 100644
index 00000000000..6d0949542f9
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props
@@ -0,0 +1,50 @@
+<Project>
+  <Import Project="Before.Directory.Build.props" Condition="Exists('Before.Directory.Build.props')" />
+
+  <PropertyGroup>
+    <!--
+      In the case that a user is building a sample directly the MicrosoftNetCompilersToolsetPackagerVersion will not be provided.
+      We'll fall back to whatever the current SDK provides in regards to Roslyn's Microsoft.Net.Compilers.Toolset.
+    -->
+    <BuildingTestAppsIndependently>false</BuildingTestAppsIndependently>
+    <BuildingTestAppsIndependently Condition="'$(MicrosoftNetCompilersToolsetPackageVersion)' == ''">true</BuildingTestAppsIndependently>
+
+    <!-- Do not resolve Reference ItemGroup since it has a different semantic meaning in Razor test apps -->
+    <EnableCustomReferenceResolution>false</EnableCustomReferenceResolution>
+
+    <DefaultNetCoreTargetFramework>net5.0</DefaultNetCoreTargetFramework>
+
+    <RepoRoot Condition="'$(RepoRoot)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, 'AspNetCore.sln'))\</RepoRoot>
+
+    <RazorSdkCurrentVersionProps>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.props</RazorSdkCurrentVersionProps>
+    <RazorSdkCurrentVersionTargets>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets>
+    <RazorSdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.Razor\</RazorSdkArtifactsDirectory>
+    <BlazorWebAssemblySdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.BlazorWebAssembly\</BlazorWebAssemblySdkArtifactsDirectory>
+    <BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
+  </PropertyGroup>
+
+  <Import Project="$(RepoRoot)eng\Versions.props" />
+
+  <PropertyGroup>
+    <!-- Reset version prefix to 1.0.0 for test projects -->
+    <VersionPrefix>1.0.0</VersionPrefix>
+
+    <!-- Turn down the compression level for brotli -->
+    <_BlazorBrotliCompressionLevel>NoCompression</_BlazorBrotliCompressionLevel>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <!-- Have the SDK treat the MvcShim as an MVC assembly -->
+    <_MvcAssemblyName Include="Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib" />
+  </ItemGroup>
+
+  <ItemGroup Condition="$(BuildingTestAppsIndependently) == 'false'">
+    <PackageReference Include="Microsoft.Net.Compilers.Toolset"
+        Version="$(MicrosoftNetCompilersToolsetPackageVersion)"
+        PrivateAssets="all"
+        IsImplicitlyDefined="true" />
+  </ItemGroup>
+
+  <Import Project="After.Directory.Build.props" Condition="Exists('After.Directory.Build.props')" />
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets
new file mode 100644
index 00000000000..45a2ee1e565
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets
@@ -0,0 +1,7 @@
+<Project>
+  <PropertyGroup>
+    <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' ">$(MicrosoftNETCoreAppRuntimeVersion)</RuntimeFrameworkVersion>
+    <!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
+    <NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
+  </PropertyGroup>
+</Project>
diff --git a/src/Razor/test/testassets/LinkBaseToWebRoot/js/LinkedScript.js b/src/Components/WebAssembly/Sdk/testassets/LinkBaseToWebRoot/js/LinkedScript.js
similarity index 100%
rename from src/Razor/test/testassets/LinkBaseToWebRoot/js/LinkedScript.js
rename to src/Components/WebAssembly/Sdk/testassets/LinkBaseToWebRoot/js/LinkedScript.js
diff --git a/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj b/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj
new file mode 100644
index 00000000000..f4debf80880
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\blazorhosted\blazorhosted.csproj" />
+    <ProjectReference Include="..\blazorhosted-rid\blazorhosted-rid.csproj" />
+    <ProjectReference Include="..\blazorwasm\blazorwasm.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js b/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js
new file mode 100644
index 00000000000..84362ca046b
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js
@@ -0,0 +1 @@
+Test file
\ No newline at end of file
diff --git a/src/Razor/test/testassets/blazorhosted-rid/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/Program.cs
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted-rid/Program.cs
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/Program.cs
diff --git a/src/Razor/test/testassets/blazorhosted-rid/blazorhosted-rid.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/blazorhosted-rid.csproj
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted-rid/blazorhosted-rid.csproj
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/blazorhosted-rid.csproj
diff --git a/src/Razor/test/testassets/blazorhosted/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorhosted/Program.cs
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted/Program.cs
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted/Program.cs
diff --git a/src/Razor/test/testassets/blazorhosted/blazorhosted.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorhosted/blazorhosted.csproj
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted/blazorhosted.csproj
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted/blazorhosted.csproj
diff --git a/src/Razor/test/testassets/blazorwasm/App.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/App.razor
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/App.razor
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/App.razor
diff --git a/src/Razor/test/testassets/blazorwasm/LinkToWebRoot/css/app.css b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/LinkToWebRoot/css/app.css
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/LinkToWebRoot/css/app.css
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/LinkToWebRoot/css/app.css
diff --git a/src/Razor/test/testassets/blazorwasm/Pages/Index.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Pages/Index.razor
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/Pages/Index.razor
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Pages/Index.razor
diff --git a/src/Razor/test/testassets/blazorwasm/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Program.cs
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/Program.cs
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Program.cs
diff --git a/src/Razor/test/testassets/blazorwasm/Resources.ja.resx.txt b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Resources.ja.resx.txt
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/Resources.ja.resx.txt
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Resources.ja.resx.txt
diff --git a/src/Razor/test/testassets/blazorwasm/_Imports.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/_Imports.razor
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/_Imports.razor
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/_Imports.razor
diff --git a/src/Razor/test/testassets/blazorwasm/blazorwasm.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj
similarity index 88%
rename from src/Razor/test/testassets/blazorwasm/blazorwasm.csproj
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj
index 36a511e7c77..c8906b1bd29 100644
--- a/src/Razor/test/testassets/blazorwasm/blazorwasm.csproj
+++ b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj
@@ -1,10 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk.Razor">
+
+  <Import Project="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Sdk\Sdk.props" />
+
   <PropertyGroup>
     <TargetFramework>net5.0</TargetFramework>
-    <UseBlazorWebAssembly>true</UseBlazorWebAssembly>
     <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
-    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
     <RazorSdkDirectoryRoot>$(RazorSdkArtifactsDirectory)$(Configuration)\sdk-output\</RazorSdkDirectoryRoot>
+    <BlazorWebAssemblySdkDirectoryRoot>$(BlazorWebAssemblySdkArtifactsDirectory)$(Configuration)\sdk-output\</BlazorWebAssemblySdkDirectoryRoot>
     <ServiceWorkerAssetsManifest>custom-service-worker-assets.js</ServiceWorkerAssetsManifest>
   </PropertyGroup>
 
@@ -50,4 +52,6 @@
     <ServiceWorker Include="wwwroot\serviceworkers\my-service-worker.js" PublishedContent="wwwroot\serviceworkers\my-prod-service-worker.js" />
   </ItemGroup>
 
+  <Import Project="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Sdk\Sdk.targets" />
+
 </Project>
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/Fake-License.txt b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/Fake-License.txt
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/Fake-License.txt
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/Fake-License.txt
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/css/app.css b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/css/app.css
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/css/app.css
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/css/app.css
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/index.html b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/index.html
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/index.html
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/index.html
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js
diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Class1.cs b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Class1.cs
similarity index 100%
rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Class1.cs
rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Class1.cs
diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx
similarity index 100%
rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx
rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx
diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj
similarity index 100%
rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj
rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj
diff --git a/src/Razor/test/testassets/razorclasslibrary/Class1.cs b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/Class1.cs
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/Class1.cs
rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/Class1.cs
diff --git a/src/Razor/test/testassets/razorclasslibrary/RazorClassLibrary.csproj b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/RazorClassLibrary.csproj
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/RazorClassLibrary.csproj
rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/RazorClassLibrary.csproj
diff --git a/src/Razor/test/testassets/razorclasslibrary/wwwroot/styles.css b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/styles.css
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/wwwroot/styles.css
rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/styles.css
diff --git a/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/Components/WebAssembly/Sdk/tools/Application.cs b/src/Components/WebAssembly/Sdk/tools/Application.cs
new file mode 100644
index 00000000000..d9d66d10bc8
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/Application.cs
@@ -0,0 +1,98 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal class Application : CommandLineApplication
+    {
+        public Application(
+            CancellationToken cancellationToken,
+            TextWriter output = null,
+            TextWriter error = null)
+        {
+            CancellationToken = cancellationToken;
+            Out = output ?? Out;
+            Error = error ?? Error;
+
+            Name = "BlazorWebAssembly.Tools";
+            FullName = "Microsoft Blazor WebAssembly SDK tool";
+            Description = "CLI for Blazor WebAssembly operations.";
+            ShortVersionGetter = GetInformationalVersion;
+
+            HelpOption("-?|-h|--help");
+
+            Commands.Add(new BrotliCompressCommand(this));
+        }
+
+        public CancellationToken CancellationToken { get; }
+
+        public new int Execute(params string[] args)
+        {
+            try
+            {
+                return base.Execute(ExpandResponseFiles(args));
+            }
+            catch (AggregateException ex) when (ex.InnerException != null)
+            {
+                foreach (var innerException in ex.Flatten().InnerExceptions)
+                {
+                    Error.WriteLine(innerException.Message);
+                    Error.WriteLine(innerException.StackTrace);
+                }
+                return 1;
+            }
+            catch (CommandParsingException ex)
+            {
+                // Don't show a call stack when we have unneeded arguments, just print the error message.
+                // The code that throws this exception will print help, so no need to do it here.
+                Error.WriteLine(ex.Message);
+                return 1;
+            }
+            catch (OperationCanceledException)
+            {
+                // This is a cancellation, not a failure.
+                Error.WriteLine("Cancelled");
+                return 1;
+            }
+            catch (Exception ex)
+            {
+                Error.WriteLine(ex.Message);
+                Error.WriteLine(ex.StackTrace);
+                return 1;
+            }
+        }
+
+        private string GetInformationalVersion()
+        {
+            var assembly = typeof(Application).GetTypeInfo().Assembly;
+            var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
+            return attribute.InformationalVersion;
+        }
+
+        private static string[] ExpandResponseFiles(string[] args)
+        {
+            var expandedArgs = new List<string>();
+            foreach (var arg in args)
+            {
+                if (!arg.StartsWith("@", StringComparison.Ordinal))
+                {
+                    expandedArgs.Add(arg);
+                }
+                else
+                {
+                    var fileName = arg.Substring(1);
+                    expandedArgs.AddRange(File.ReadLines(fileName));
+                }
+            }
+
+            return expandedArgs.ToArray();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs b/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs
new file mode 100644
index 00000000000..136c2fc660e
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs
@@ -0,0 +1,85 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Threading.Tasks;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal class BrotliCompressCommand : CommandLineApplication
+    {
+        public BrotliCompressCommand(Application parent)
+            : base(throwOnUnexpectedArg: true)
+        {
+            base.Parent = parent;
+            Name = "brotli";
+            Sources = Option("-s", "files to compress", CommandOptionType.MultipleValue);
+            Outputs = Option("-o", "Output file path", CommandOptionType.MultipleValue);
+            CompressionLevelOption = Option("-c", "Compression level", CommandOptionType.SingleValue);
+
+            Invoke = () => Execute().GetAwaiter().GetResult();
+        }
+
+        public CommandOption Sources { get; }
+
+        public CommandOption Outputs { get; }
+
+        public CommandOption CompressionLevelOption { get; }
+
+        public CompressionLevel CompressionLevel { get; private set; } = CompressionLevel.Optimal;
+
+        private Task<int> Execute()
+        {
+            if (!ValidateArguments())
+            {
+                ShowHelp();
+                return Task.FromResult(1);
+            }
+
+            return ExecuteCoreAsync();
+        }
+
+        private bool ValidateArguments()
+        {
+            if (Sources.Values.Count != Outputs.Values.Count)
+            {
+                Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {Outputs.Description} has {Outputs.Values.Count} values.");
+                return false;
+            }
+
+            if (CompressionLevelOption.HasValue())
+            {
+                if (!Enum.TryParse<CompressionLevel>(CompressionLevelOption.Value(), out var value))
+                {
+                    Error.WriteLine($"Invalid option {CompressionLevelOption.Value()} for {CompressionLevelOption.Template}.");
+                    return false;
+                }
+
+                CompressionLevel = value;
+            }
+
+            return true;
+        }
+
+        private Task<int> ExecuteCoreAsync()
+        {
+            Parallel.For(0, Sources.Values.Count, i =>
+            {
+                var source = Sources.Values[i];
+                var output = Outputs.Values[i];
+
+                using var sourceStream = File.OpenRead(source);
+                using var fileStream = new FileStream(output, FileMode.Create);
+
+                using var stream = new BrotliStream(fileStream, CompressionLevel);
+
+                sourceStream.CopyTo(stream);
+            });
+
+            return Task.FromResult(0);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/tools/DebugMode.cs b/src/Components/WebAssembly/Sdk/tools/DebugMode.cs
new file mode 100644
index 00000000000..816bb4a7835
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/DebugMode.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal static class DebugMode
+    {
+        public static void HandleDebugSwitch(ref string[] args)
+        {
+            if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase))
+            {
+                args = args.Skip(1).ToArray();
+
+                Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id);
+                while (!Debugger.IsAttached)
+                {
+                    Thread.Sleep(TimeSpan.FromSeconds(3));
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj b/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj
new file mode 100644
index 00000000000..6ac7a26c192
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net5.0</TargetFramework>
+
+    <IsPackable>false</IsPackable>
+    <IsShipping>false</IsShipping>
+    <DisablePubternalApiCheck>true</DisablePubternalApiCheck>
+
+    <UseAppHost>false</UseAppHost>
+    <RuntimeIdentifier />
+
+    <!-- Need to build this project in source build -->
+    <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
+  </PropertyGroup>
+
+  <ItemGroup>
+     <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/tools/Program.cs b/src/Components/WebAssembly/Sdk/tools/Program.cs
new file mode 100644
index 00000000000..b00093323d0
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/Program.cs
@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal static class Program
+    {
+        public static int Main(string[] args)
+        {
+            DebugMode.HandleDebugSwitch(ref args);
+
+            var cancel = new CancellationTokenSource();
+            Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); };
+
+            var application = new Application(
+                cancel.Token,
+                Console.Out,
+                Console.Error);
+
+            application.Commands.Add(new BrotliCompressCommand(application));
+
+            return application.Execute(args);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json b/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json
new file mode 100644
index 00000000000..2c73f398906
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json
@@ -0,0 +1,3 @@
+{
+    "rollForwardOnNoCandidateFx": 2
+}
\ No newline at end of file
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj
index a1e160322eb..2832be128d1 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj
+++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <!--
@@ -13,8 +13,11 @@
 
     <!-- Tests do not work on Helix yet -->
     <BuildHelixPayload>false</BuildHelixPayload>
+    <TestAppsRoot>$(MSBuildProjectDirectory)\..\..\test\testassets\</TestAppsRoot>
   </PropertyGroup>
 
+  <Import Project="$(SharedSourceRoot)MSBuild.Testing\MSBuild.Testing.targets" />
+
   <ItemGroup>
     <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>
@@ -24,49 +27,6 @@
     <Reference Include="Microsoft.Extensions.DependencyModel" />
   </ItemGroup>
 
-  <ItemGroup>
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>Testing.AdditionalRestoreSources</_Parameter1>
-      <_Parameter2>$(MSBuildThisFileDirectory)..\testassets\PregeneratedPackages</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>ArtifactsLogDir</_Parameter1>
-      <_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>ProcDumpToolPath</_Parameter1>
-      <_Parameter2>$(ProcDumpPath)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>Testing.RepoRoot</_Parameter1>
-      <_Parameter2>$(RepoRoot)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>MicrosoftNETCoreAppRuntimeVersion</_Parameter1>
-      <_Parameter2>$(MicrosoftNETCoreAppRuntimeVersion)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>DefaultNetCoreTargetFramework</_Parameter1>
-      <_Parameter2>$(DefaultNetCoreTargetFramework)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>MicrosoftNetCompilersToolsetPackageVersion</_Parameter1>
-      <_Parameter2>$(MicrosoftNetCompilersToolsetPackageVersion)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>RazorSdkDirectoryRoot</_Parameter1>
-      <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\sdk-output\</_Parameter2>
-    </AssemblyAttribute>
-
-  </ItemGroup>
-
   <ItemGroup>
     <Reference Include="rzc" />
     <ProjectReference Include="..\..\test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj" />
diff --git a/src/Shared/E2ETesting/E2ETesting.targets b/src/Shared/E2ETesting/E2ETesting.targets
index d0fa6cb856e..76ced2cce99 100644
--- a/src/Shared/E2ETesting/E2ETesting.targets
+++ b/src/Shared/E2ETesting/E2ETesting.targets
@@ -66,14 +66,9 @@
       <_DefaultProjectRoot>$([System.IO.Path]::GetFullPath($(_DefaultProjectFilter)))</_DefaultProjectRoot>
     </PropertyGroup>
     <ItemGroup>
-      <_ContentRootProjectReferencesUnfiltered
-        Include="@(ReferencePath)"
-        Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference'" />
-      <_ContentRootProjectReferencesFilter
-        Include="@(_ContentRootProjectReferencesUnfiltered->StartsWith('$(_DefaultProjectRoot)'))" />
       <_ContentRootProjectReferences
-        Include="@(_ContentRootProjectReferencesFilter)"
-        Condition="'%(Identity)' == 'True'" />
+        Include="@(ReferencePath)"
+        Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference' AND $([System.String]::Copy(%(ReferencePath.MSBuildSourceProjectFile)).StartsWith('$(_DefaultProjectRoot)'))" />
     </ItemGroup>
   </Target>
 
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Assert.cs b/src/Shared/MSBuild.Testing/Assert.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Assert.cs
rename to src/Shared/MSBuild.Testing/Assert.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs b/src/Shared/MSBuild.Testing/BuildVariables.cs
similarity index 83%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs
rename to src/Shared/MSBuild.Testing/BuildVariables.cs
index 797d2446250..b659ba918c7 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs
+++ b/src/Shared/MSBuild.Testing/BuildVariables.cs
@@ -19,8 +19,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
 
         public static string RazorSdkDirectoryRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "RazorSdkDirectoryRoot").Value;
 
+        public static string BlazorWebAssemblySdkDirectoryRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "BlazorWebAssemblySdkDirectoryRoot").Value;
+
         public static string RepoRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "Testing.RepoRoot").Value;
-        
+
         public static string DefaultNetCoreTargetFramework => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "DefaultNetCoreTargetFramework").Value;
+
+        public static string TestAppsRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "TestAppsRoot").Value;
     }
 }
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/FIleThumbPrint.cs b/src/Shared/MSBuild.Testing/FIleThumbPrint.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/FIleThumbPrint.cs
rename to src/Shared/MSBuild.Testing/FIleThumbPrint.cs
diff --git a/src/Shared/MSBuild.Testing/MSBuild.Testing.targets b/src/Shared/MSBuild.Testing/MSBuild.Testing.targets
new file mode 100644
index 00000000000..8868a23d979
--- /dev/null
+++ b/src/Shared/MSBuild.Testing/MSBuild.Testing.targets
@@ -0,0 +1,53 @@
+<Project>
+
+<ItemGroup>
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>ArtifactsLogDir</_Parameter1>
+      <_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>ProcDumpToolPath</_Parameter1>
+      <_Parameter2>$(ProcDumpPath)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>Testing.RepoRoot</_Parameter1>
+      <_Parameter2>$(RepoRoot)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>MicrosoftNETCoreAppRuntimeVersion</_Parameter1>
+      <_Parameter2>$(MicrosoftNETCoreAppRuntimeVersion)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>DefaultNetCoreTargetFramework</_Parameter1>
+      <_Parameter2>$(DefaultNetCoreTargetFramework)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>MicrosoftNetCompilersToolsetPackageVersion</_Parameter1>
+      <_Parameter2>$(MicrosoftNetCompilersToolsetPackageVersion)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>RazorSdkDirectoryRoot</_Parameter1>
+      <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\sdk-output\</_Parameter2>
+    </AssemblyAttribute>
+
+     <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>BlazorWebAssemblySdkDirectoryRoot</_Parameter1>
+      <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\sdk-output\</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>TestAppsRoot</_Parameter1>
+      <_Parameter2>$(TestAppsRoot)</_Parameter2>
+    </AssemblyAttribute>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="$(MSBuildThisFileDirectory)*.cs" LinkBase="Infrastructure" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessKind.cs b/src/Shared/MSBuild.Testing/MSBuildProcessKind.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessKind.cs
rename to src/Shared/MSBuild.Testing/MSBuildProcessKind.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs b/src/Shared/MSBuild.Testing/MSBuildProcessManager.cs
similarity index 98%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs
rename to src/Shared/MSBuild.Testing/MSBuildProcessManager.cs
index cc79523d5bf..3d9ada158bf 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs
+++ b/src/Shared/MSBuild.Testing/MSBuildProcessManager.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -37,6 +37,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
                 $"/p:MicrosoftNETCoreAppRuntimeVersion={BuildVariables.MicrosoftNETCoreAppRuntimeVersion}",
                 $"/p:MicrosoftNetCompilersToolsetPackageVersion={BuildVariables.MicrosoftNetCompilersToolsetPackageVersion}",
                 $"/p:RazorSdkDirectoryRoot={BuildVariables.RazorSdkDirectoryRoot}",
+                $"/p:BlazorWebAssemblySdkDirectoryRoot={BuildVariables.BlazorWebAssemblySdkDirectoryRoot}",
                 $"/p:RepoRoot={BuildVariables.RepoRoot}",
                 $"/p:Configuration={project.Configuration}",
                 $"/t:{target}",
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildResult.cs b/src/Shared/MSBuild.Testing/MSBuildResult.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildResult.cs
rename to src/Shared/MSBuild.Testing/MSBuildResult.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs b/src/Shared/MSBuild.Testing/ProjectDirectory.cs
similarity index 97%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs
rename to src/Shared/MSBuild.Testing/ProjectDirectory.cs
index d0d0e571f46..0f669c31fce 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs
+++ b/src/Shared/MSBuild.Testing/ProjectDirectory.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -49,12 +49,11 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
                 }
 
                 var repositoryRoot = BuildVariables.RepoRoot;
-                var solutionRoot = Path.Combine(repositoryRoot, "src", "Razor");
                 var binariesRoot = Path.GetDirectoryName(typeof(ProjectDirectory).Assembly.Location);
+                var testAppsRoot = BuildVariables.TestAppsRoot;
 
                 foreach (var project in new string[] { originalProjectName, }.Concat(additionalProjects))
                 {
-                    var testAppsRoot = Path.Combine(solutionRoot, "test", "testassets");
                     var projectRoot = Path.Combine(testAppsRoot, project);
                     if (!Directory.Exists(projectRoot))
                     {
@@ -146,6 +145,11 @@ $@"<Project>
                     .ForEach(file =>
                     {
                         var source = Path.Combine(testAppsRoot, file);
+                        if (!File.Exists(source))
+                        {
+                            return;
+                        }
+
                         var destination = Path.Combine(projectDestination, file);
                         File.Copy(source, destination);
                     });
-- 
GitLab