// 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.IO.Compression; using System.Text.Json; using System.Threading.Tasks; using Microsoft.NET.Sdk.BlazorWebAssembly; using Microsoft.AspNetCore.Testing; using Xunit; using static Microsoft.AspNetCore.Razor.Design.IntegrationTests.ServiceWorkerAssert; using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { public class WasmPublishIntegrationTest { private static readonly string DotNetJsFileName = $"dotnet.{BuildVariables.MicrosoftNETCoreAppRuntimeVersion}.js"; [Fact] public async Task Publish_MinimalApp_Works() { // Arrange using var project = ProjectDirectory.Create("blazorwasm-minimal"); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm-minimal.dll"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); Assert.FileCountEquals(result, 1, publishDirectory, "*", SearchOption.TopDirectoryOnly); VerifyBootManifestHashes(result, blazorPublishDirectory); } [Fact] public async Task Publish_WithDefaultSettings_Works() { // Arrange using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary", "LinkBaseToWebRoot" }); project.Configuration = "Debug"; var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify referenced static web assets Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "styles.css"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify link item assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "js", "LinkedScript.js"); var cssFile = Assert.FileExists(result, blazorPublishDirectory, "css", "app.css"); Assert.FileContains(result, cssFile, ".publish"); Assert.FileDoesNotExist(result, "dist", "Fake-License.txt"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); Assert.FileCountEquals(result, 1, publishDirectory, "*", SearchOption.TopDirectoryOnly); VerifyBootManifestHashes(result, blazorPublishDirectory); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); VerifyTypeGranularTrimming(result, blazorPublishDirectory); } [Fact] public async Task Publish_WithScopedCss_Works() { // Arrange using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary", "LinkBaseToWebRoot" }); File.WriteAllText(Path.Combine(project.DirectoryPath, "App.razor.css"), "h1 { font-size: 16px; }"); project.Configuration = "Debug"; var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify scoped css Assert.FileExists(result, blazorPublishDirectory, "_framework", "scoped.styles.css"); // Verify referenced static web assets Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "styles.css"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify link item assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "js", "LinkedScript.js"); var cssFile = Assert.FileExists(result, blazorPublishDirectory, "css", "app.css"); Assert.FileContains(result, cssFile, ".publish"); Assert.FileDoesNotExist(result, "dist", "Fake-License.txt"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); Assert.FileCountEquals(result, 1, publishDirectory, "*", SearchOption.TopDirectoryOnly); VerifyBootManifestHashes(result, blazorPublishDirectory); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] public async Task Publish_InRelease_Works() { // Arrange using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary", "LinkBaseToWebRoot" }); project.Configuration = "Release"; var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", ""); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify referenced static web assets Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "styles.css"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify link item assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "js", "LinkedScript.js"); var cssFile = Assert.FileExists(result, blazorPublishDirectory, "css", "app.css"); Assert.FileContains(result, cssFile, ".publish"); Assert.FileDoesNotExist(result, "dist", "Fake-License.txt"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); Assert.FileCountEquals(result, 1, publishDirectory, "*", SearchOption.TopDirectoryOnly); } [Fact] public async Task Publish_WithExistingWebConfig_Works() { // Arrange using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary", "LinkBaseToWebRoot" }); project.Configuration = "Release"; var webConfigContents = "test webconfig contents"; AddFileToProject(project, "web.config", webConfigContents); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); Assert.FileContains(result, Path.Combine(publishDirectory, "web.config"), webConfigContents); } [Fact] public async Task Publish_WithNoBuild_Works() { // Arrange using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary" }); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build"); Assert.BuildPassed(result); result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:NoBuild=true"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify static web assets from referenced projects are copied. Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "styles.css"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); VerifyCompression(result, blazorPublishDirectory); } private static void VerifyCompression(MSBuildResult result, string blazorPublishDirectory) { var original = Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); var compressed = Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json.br"); using var brotliStream = new BrotliStream(File.OpenRead(compressed), CompressionMode.Decompress); using var textReader = new StreamReader(brotliStream); var uncompressedText = textReader.ReadToEnd(); var originalText = File.ReadAllText(original); Assert.Equal(originalText, uncompressedText); } [Fact] public async Task Publish_WithTrimmingdDisabled_Works() { // Arrange using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary" }); project.AddProjectFileContent( @" false "); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify compression works Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); // // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify referenced static web assets Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "styles.css"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); // Verify assemblies are not trimmed var loggingAssemblyPath = Path.Combine(blazorPublishDirectory, "_framework", "Microsoft.Extensions.Logging.Abstractions.dll"); Assert.AssemblyContainsType(result, loggingAssemblyPath, "Microsoft.Extensions.Logging.Abstractions.NullLogger"); } [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23756")] public async Task Publish_SatelliteAssemblies_AreCopiedToBuildOutput() { // Arrange using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary", "classlibrarywithsatelliteassemblies" }); project.AddProjectFileContent( @" false $(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies "); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", args: "/restore"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "Microsoft.CodeAnalysis.CSharp.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output. var bootJsonPath = Path.Combine(blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\""); Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\""); VerifyBootManifestHashes(result, blazorPublishDirectory); } [Fact] [QuarantinedTest] public async Task Publish_HostedApp_DefaultSettings_Works() { // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; // Make sure the main project exists Assert.FileExists(result, publishDirectory, "blazorhosted.dll"); // Verification for https://github.com/dotnet/aspnetcore/issues/19926. Verify binaries for projects // referenced by the Hosted project appear in the publish directory Assert.FileExists(result, publishDirectory, "RazorClassLibrary.dll"); Assert.FileExists(result, publishDirectory, "blazorwasm.dll"); var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify project references appear as static web assets Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll"); // Also verify project references to the server project appear in the publish output Assert.FileExists(result, publishDirectory, "RazorClassLibrary.dll"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify static web assets from referenced projects are copied. Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); // Verify compression works Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.gz"); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); VerifyTypeGranularTrimming(result, blazorPublishDirectory); } [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23756")] public async Task Publish_HostedApp_ProducesBootJsonDataWithExpectedContent() { // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); var wwwroot = Path.Combine(project.DirectoryPath, "..", "blazorwasm", "wwwroot"); File.WriteAllText(Path.Combine(wwwroot, "appsettings.json"), "Default settings"); File.WriteAllText(Path.Combine(wwwroot, "appsettings.development.json"), "Development settings"); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var bootJsonPath = Path.Combine(project.PublishOutputDirectory, "wwwroot", "_framework", "blazor.boot.json"); var bootJsonData = ReadBootJsonData(result, bootJsonPath); var runtime = bootJsonData.resources.runtime.Keys; Assert.Contains(DotNetJsFileName, runtime); Assert.Contains("dotnet.wasm", runtime); var assemblies = bootJsonData.resources.assembly.Keys; Assert.Contains("blazorwasm.dll", assemblies); Assert.Contains("RazorClassLibrary.dll", assemblies); Assert.Contains("System.Text.Json.dll", assemblies); // No pdbs Assert.Null(bootJsonData.resources.pdb); Assert.Null(bootJsonData.resources.satelliteResources); Assert.Contains("appsettings.json", bootJsonData.config); Assert.Contains("appsettings.development.json", bootJsonData.config); } [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23397")] public async Task Publish_HostedApp_WithSatelliteAssemblies() { // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", "classlibrarywithsatelliteassemblies", }); var wasmProject = project.GetSibling("blazorwasm"); wasmProject.AddProjectFileContent( @" false $(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies "); var resxfileInProject = Path.Combine(wasmProject.DirectoryPath, "Resources.ja.resx.txt"); File.Move(resxfileInProject, Path.Combine(wasmProject.DirectoryPath, "Resource.ja.resx")); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/restore"); Assert.BuildPassed(result); var bootJsonPath = Path.Combine(project.PublishOutputDirectory, "wwwroot", "_framework", "blazor.boot.json"); var bootJsonData = ReadBootJsonData(result, bootJsonPath); var publishOutputDirectory = project.PublishOutputDirectory; Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "blazorwasm.dll"); Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "classlibrarywithsatelliteassemblies.dll"); Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "Microsoft.CodeAnalysis.CSharp.dll"); Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output. Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\""); Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\""); } [Fact] // Regression test for https://github.com/dotnet/aspnetcore/issues/18752 public async Task Publish_HostedApp_WithoutTrimming_Works() { // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); AddWasmProjectContent(project, @" false "); // VS builds projects individually and then a publish with BuildDependencies=false, but building the main project is a close enough approximation for this test. var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build"); Assert.BuildPassed(result); // Publish result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildDependencies=false"); var publishDirectory = project.PublishOutputDirectory; // Make sure the main project exists Assert.FileExists(result, publishDirectory, "blazorhosted.dll"); // Verification for https://github.com/dotnet/aspnetcore/issues/19926. Verify binaries for projects // referenced by the Hosted project appear in the publish directory Assert.FileExists(result, publishDirectory, "RazorClassLibrary.dll"); Assert.FileExists(result, publishDirectory, "blazorwasm.dll"); var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify project references appear as static web assets Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll"); // Also verify project references to the server project appear in the publish output Assert.FileExists(result, publishDirectory, "RazorClassLibrary.dll"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify static web assets from referenced projects are copied. Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); // Verify compression works Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.gz"); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] public async Task Publish_HostedApp_WithNoBuild_Works() { // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build"); Assert.BuildPassed(result); result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:NoBuild=true"); var publishDirectory = project.PublishOutputDirectory; // Make sure the main project exists Assert.FileExists(result, publishDirectory, "blazorhosted.dll"); var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify static web assets from referenced projects are copied. Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] public async Task Publish_HostedApp_VisualStudio() { // Simulates publishing the same way VS does by setting BuildProjectReferences=false. // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); project.Configuration = "Release"; var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build", "/p:BuildInsideVisualStudio=true"); Assert.BuildPassed(result); result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildProjectReferences=false /p:BuildInsideVisualStudio=true"); var publishDirectory = project.PublishOutputDirectory; // Make sure the main project exists Assert.FileExists(result, publishDirectory, "blazorhosted.dll"); var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify static web assets from referenced projects are copied. Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); // Verify compression works Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] public async Task Publish_HostedAppWithScopedCss_VisualStudio() { // Simulates publishing the same way VS does by setting BuildProjectReferences=false. // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); File.WriteAllText(Path.Combine(project.SolutionPath, "blazorwasm", "App.razor.css"), "h1 { font-size: 16px; }"); project.Configuration = "Release"; var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build", "/p:BuildInsideVisualStudio=true"); Assert.BuildPassed(result); result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildProjectReferences=false /p:BuildInsideVisualStudio=true"); var publishDirectory = project.PublishOutputDirectory; // Make sure the main project exists Assert.FileExists(result, publishDirectory, "blazorhosted.dll"); var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify scoped css Assert.FileExists(result, blazorPublishDirectory, "_framework", "scoped.styles.css"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify static web assets from referenced projects are copied. Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); // Verify compression works Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); } // Regression test to verify satellite assemblies from the blazor app are copied to the published app's wwwroot output directory as // part of publishing in VS [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23397")] public async Task Publish_HostedApp_VisualStudio_WithSatelliteAssemblies() { // Simulates publishing the same way VS does by setting BuildProjectReferences=false. // Arrange using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", "classlibrarywithsatelliteassemblies" }); var blazorwasmProjectDirectory = Path.Combine(project.DirectoryPath, "..", "blazorwasm"); // Setup the blazorwasm app to have it's own satellite assemblies as well as transitively imported satellite assemblies. var resxfileInProject = Path.Combine(blazorwasmProjectDirectory, "Resources.ja.resx.txt"); File.Move(resxfileInProject, Path.Combine(blazorwasmProjectDirectory, "Resource.ja.resx")); var blazorwasmProjFile = Path.Combine(blazorwasmProjectDirectory, "blazorwasm.csproj"); var existing = File.ReadAllText(blazorwasmProjFile); var updatedContent = @" false $(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies "; var updated = existing.Replace("", updatedContent); File.WriteAllText(blazorwasmProjFile, updated); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build", $"/restore"); Assert.BuildPassed(result); result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildProjectReferences=false"); var publishDirectory = project.PublishOutputDirectory; // Make sure the main project exists Assert.FileExists(result, publishDirectory, "blazorhosted.dll"); var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "ja", "blazorwasm.resources.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "fr", "Microsoft.CodeAnalysis.resources.dll"); var bootJson = Path.Combine(project.DirectoryPath, blazorPublishDirectory, "_framework", "blazor.boot.json"); var bootJsonFile = JsonSerializer.Deserialize(File.ReadAllText(bootJson), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); var satelliteResources = bootJsonFile.resources.satelliteResources; Assert.Contains("es-ES", satelliteResources.Keys); Assert.Contains("es-ES/classlibrarywithsatelliteassemblies.resources.dll", satelliteResources["es-ES"].Keys); Assert.Contains("fr", satelliteResources.Keys); Assert.Contains("fr/Microsoft.CodeAnalysis.CSharp.resources.dll", satelliteResources["fr"].Keys); Assert.Contains("ja", satelliteResources.Keys); Assert.Contains("ja/blazorwasm.resources.dll", satelliteResources["ja"].Keys); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", 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() { // Arrange using var project = ProjectDirectory.Create("blazorhosted-rid", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); 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; // Make sure the main project exists Assert.FileExists(result, publishDirectory, "blazorhosted-rid.dll"); Assert.FileExists(result, publishDirectory, "libhostfxr.so"); // Verify that we're doing a self-contained deployment Assert.FileExists(result, publishDirectory, "RazorClassLibrary.dll"); Assert.FileExists(result, publishDirectory, "blazorwasm.dll"); var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm"); Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output. // Verify project references appear as static web assets Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll"); // Also verify project references to the server project appear in the publish output Assert.FileExists(result, publishDirectory, "RazorClassLibrary.dll"); // Verify static assets are in the publish directory Assert.FileExists(result, blazorPublishDirectory, "index.html"); // Verify static web assets from referenced projects are copied. Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js"); Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css"); // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); VerifyBootManifestHashes(result, blazorPublishDirectory); // Verify compression works Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.gz"); Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.gz"); VerifyServiceWorkerFiles(result, blazorPublishDirectory, serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), serviceWorkerContent: "// This is the production service worker", assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] public async Task Publish_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData() { // Arrange using var project = ProjectDirectory.Create("blazorwasm-minimal"); project.AddProjectFileContent( @" true "); var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); Assert.BuildPassed(result); var publishOutputDirectory = project.PublishOutputDirectory; var bootJsonPath = Path.Combine(publishOutputDirectory, "wwwroot", "_framework", "blazor.boot.json"); var bootJsonData = ReadBootJsonData(result, bootJsonPath); var runtime = bootJsonData.resources.runtime.Keys; Assert.Contains("dotnet.wasm", runtime); Assert.DoesNotContain("icudt.dat", runtime); Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "dotnet.wasm"); Assert.FileDoesNotExist(result, publishOutputDirectory, "wwwroot", "_framework", "icudt.dat"); } private static void AddWasmProjectContent(ProjectDirectory project, string content) { var path = Path.Combine(project.SolutionPath, "blazorwasm", "blazorwasm.csproj"); var existing = File.ReadAllText(path); var updated = existing.Replace("", content); File.WriteAllText(path, updated); } private static void AddFileToProject(ProjectDirectory project, string filename, string content) { var path = Path.Combine(project.DirectoryPath, filename); File.WriteAllText(path, content); } private static void VerifyBootManifestHashes(MSBuildResult result, string blazorPublishDirectory) { var bootManifestResolvedPath = Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); var bootManifestJson = File.ReadAllText(bootManifestResolvedPath); var bootManifest = JsonSerializer.Deserialize(bootManifestJson); VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.assembly); VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.runtime); if (bootManifest.resources.pdb != null) { VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.pdb); } if (bootManifest.resources.satelliteResources != null) { foreach (var resourcesForCulture in bootManifest.resources.satelliteResources.Values) { VerifyBootManifestHashes(result, blazorPublishDirectory, resourcesForCulture); } } static void VerifyBootManifestHashes(MSBuildResult result, string blazorPublishDirectory, ResourceHashesByNameDictionary resources) { foreach (var (name, hash) in resources) { var relativePath = Path.Combine(blazorPublishDirectory, "_framework", name); Assert.FileHashEquals(result, relativePath, ParseWebFormattedHash(hash)); } } static string ParseWebFormattedHash(string webFormattedHash) { Assert.StartsWith("sha256-", webFormattedHash); return webFormattedHash.Substring(7); } } private void VerifyTypeGranularTrimming(MSBuildResult result, string blazorPublishDirectory) { var componentsShimAssemblyPath = Path.Combine(blazorPublishDirectory, "_framework", "Microsoft.AspNetCore.Razor.Test.ComponentShim.dll"); Assert.FileExists(result, componentsShimAssemblyPath); // RouteView is referenced by the app, so we expect it to be preserved Assert.AssemblyContainsType(result, componentsShimAssemblyPath, "Microsoft.AspNetCore.Components.RouteView"); // RouteData is referenced by RouteView so we expect it to be preserved. Assert.AssemblyContainsType(result, componentsShimAssemblyPath, "Microsoft.AspNetCore.Components.RouteData"); // CascadingParameterAttribute is not referenced by the app, and should be trimmed. Assert.AssemblyDoesNotContainType(result, componentsShimAssemblyPath, "Microsoft.AspNetCore.Components.CascadingParameterAttribute"); } private static BootJsonData ReadBootJsonData(MSBuildResult result, string path) { return JsonSerializer.Deserialize( File.ReadAllText(Path.Combine(result.Project.DirectoryPath, path)), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } } }