Add some extra publish integration tests, plus fix publish-from-VS-with-RID (#20410)
* Add detailed integration tests for publishing service workers, assets manifests, and blazor.boot.json * Fix publishing from VS with non-portable RID
This commit is contained in:
parent
a8bd551d99
commit
ae569e2b48
|
|
@ -210,6 +210,10 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Note that the VS-specific condition below is a workaround for https://github.com/dotnet/aspnetcore/issues/19822
|
||||||
|
For more details, see https://github.com/dotnet/aspnetcore/issues/20413
|
||||||
|
-->
|
||||||
<UsingTask TaskName="BlazorILLink" AssemblyFile="$(_BlazorTasksPath)" />
|
<UsingTask TaskName="BlazorILLink" AssemblyFile="$(_BlazorTasksPath)" />
|
||||||
<Target
|
<Target
|
||||||
Name="_LinkBlazorApplication"
|
Name="_LinkBlazorApplication"
|
||||||
|
|
@ -217,7 +221,8 @@
|
||||||
@(_BlazorManagedRuntimeAssembly);
|
@(_BlazorManagedRuntimeAssembly);
|
||||||
@(BlazorLinkerDescriptor);
|
@(BlazorLinkerDescriptor);
|
||||||
$(MSBuildAllProjects)"
|
$(MSBuildAllProjects)"
|
||||||
Outputs="$(_BlazorLinkerOutputCache)">
|
Outputs="$(_BlazorLinkerOutputCache)"
|
||||||
|
Condition="'$(BuildingInsideVisualStudio)' != 'true' OR '$(DeployOnBuild)' != 'true'">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions)</_BlazorLinkerAdditionalOptions>
|
<_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions)</_BlazorLinkerAdditionalOptions>
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="_OmitServiceWorkerContent" BeforeTargets="AssignTargetPaths">
|
<Target Name="_OmitServiceWorkerContent" BeforeTargets="AssignTargetPaths; ResolveCurrentProjectStaticWebAssetsInputs">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Don't emit the service worker source files to the output -->
|
<!-- Don't emit the service worker source files to the output -->
|
||||||
<Content Remove="@(ServiceWorker)" />
|
<Content Remove="@(ServiceWorker)" />
|
||||||
|
|
@ -113,6 +113,7 @@
|
||||||
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
|
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
|
||||||
<TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath>
|
<TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath>
|
||||||
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot\'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
|
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot\'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
|
||||||
|
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot/'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
|
||||||
</_ServiceWorkerIntermediateFile>
|
</_ServiceWorkerIntermediateFile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection.Metadata;
|
using System.Reflection.Metadata;
|
||||||
using System.Reflection.PortableExecutable;
|
using System.Reflection.PortableExecutable;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
|
@ -238,6 +239,23 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void FileHashEquals(MSBuildResult result, string filePath, string expectedSha256Base64)
|
||||||
|
{
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = Path.Combine(result.Project.DirectoryPath, filePath);
|
||||||
|
FileExists(result, filePath);
|
||||||
|
|
||||||
|
var actual = File.ReadAllBytes(filePath);
|
||||||
|
using var algorithm = SHA256.Create();
|
||||||
|
var actualSha256 = algorithm.ComputeHash(actual);
|
||||||
|
var actualSha256Base64 = Convert.ToBase64String(actualSha256);
|
||||||
|
Assert.Equal(expectedSha256Base64, actualSha256Base64);
|
||||||
|
}
|
||||||
|
|
||||||
public static void FileEquals(MSBuildResult result, string expected, string actual)
|
public static void FileEquals(MSBuildResult result, string expected, string actual)
|
||||||
{
|
{
|
||||||
if (result == null)
|
if (result == null)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
// 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.
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Build;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
|
||||||
using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage;
|
using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
@ -44,6 +50,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
||||||
// Verify web.config
|
// Verify web.config
|
||||||
Assert.FileExists(result, publishDirectory, "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]
|
[Fact]
|
||||||
|
|
@ -81,6 +93,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
||||||
// Verify web.config
|
// Verify web.config
|
||||||
Assert.FileExists(result, publishDirectory, "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]
|
[Fact]
|
||||||
|
|
@ -116,6 +134,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
||||||
// Verify web.config
|
// Verify web.config
|
||||||
Assert.FileExists(result, publishDirectory, "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]
|
[Fact]
|
||||||
|
|
@ -145,6 +169,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
var bootJsonPath = Path.Combine(blazorPublishDirectory, "_framework", "blazor.boot.json");
|
var bootJsonPath = Path.Combine(blazorPublishDirectory, "_framework", "blazor.boot.json");
|
||||||
Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
|
Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
|
||||||
Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
|
Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
|
||||||
|
|
||||||
|
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]
|
[Fact]
|
||||||
|
|
@ -178,6 +208,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
||||||
// Verify web.config
|
// Verify web.config
|
||||||
Assert.FileExists(result, publishDirectory, "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]
|
[Fact]
|
||||||
|
|
@ -226,6 +262,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
// Verify web.config
|
// Verify web.config
|
||||||
Assert.FileExists(result, publishDirectory, "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");
|
||||||
|
|
||||||
void AddSiblingProjectFileContent(string content)
|
void AddSiblingProjectFileContent(string content)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(project.SolutionPath, "standalone", "standalone.csproj");
|
var path = Path.Combine(project.SolutionPath, "standalone", "standalone.csproj");
|
||||||
|
|
@ -269,6 +311,109 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
||||||
// Verify web.config
|
// Verify web.config
|
||||||
Assert.FileExists(result, publishDirectory, "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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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<GenerateBlazorBootJson.BootJsonData>(bootManifestJson);
|
||||||
|
|
||||||
|
VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.assembly, r => $"_framework/_bin/{r}");
|
||||||
|
VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.runtime, r => $"_framework/wasm/{r}");
|
||||||
|
|
||||||
|
if (bootManifest.resources.pdb != null)
|
||||||
|
{
|
||||||
|
VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.pdb, r => $"_framework/_bin/{r}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bootManifest.resources.satelliteResources != null)
|
||||||
|
{
|
||||||
|
foreach (var resourcesForCulture in bootManifest.resources.satelliteResources.Values)
|
||||||
|
{
|
||||||
|
VerifyBootManifestHashes(result, blazorPublishDirectory, resourcesForCulture, r => $"_framework/_bin/{r}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifyBootManifestHashes(MSBuildResult result, string blazorPublishDirectory, ResourceHashesByNameDictionary resources, Func<string, string> relativePathFunc)
|
||||||
|
{
|
||||||
|
foreach (var (name, hash) in resources)
|
||||||
|
{
|
||||||
|
var relativePath = Path.Combine(blazorPublishDirectory, relativePathFunc(name));
|
||||||
|
Assert.FileHashEquals(result, relativePath, ParseWebFormattedHash(hash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void VerifyServiceWorkerFiles(MSBuildResult result, string blazorPublishDirectory, string serviceWorkerPath, string serviceWorkerContent, string assetsManifestPath)
|
||||||
|
{
|
||||||
|
// Check the expected files are there
|
||||||
|
var serviceWorkerResolvedPath = Assert.FileExists(result, blazorPublishDirectory, serviceWorkerPath);
|
||||||
|
var assetsManifestResolvedPath = Assert.FileExists(result, blazorPublishDirectory, assetsManifestPath);
|
||||||
|
|
||||||
|
// Check the service worker contains the expected content (which comes from the PublishedContent file)
|
||||||
|
Assert.FileContains(result, serviceWorkerResolvedPath, serviceWorkerContent);
|
||||||
|
|
||||||
|
// Check the assets manifest version was added to the published service worker
|
||||||
|
var assetsManifest = ReadServiceWorkerAssetsManifest(assetsManifestResolvedPath);
|
||||||
|
Assert.FileContains(result, serviceWorkerResolvedPath, $"/* Manifest version: {assetsManifest.version} */");
|
||||||
|
|
||||||
|
// Check the assets manifest contains correct entries for all static content we're publishing
|
||||||
|
var resolvedPublishDirectory = Path.Combine(result.Project.DirectoryPath, blazorPublishDirectory);
|
||||||
|
var publishedStaticFiles = Directory.GetFiles(resolvedPublishDirectory, "*", new EnumerationOptions { RecurseSubdirectories = true });
|
||||||
|
var assetsManifestHashesByUrl = (IReadOnlyDictionary<string, string>)assetsManifest.assets.ToDictionary(x => x.url, x => x.hash);
|
||||||
|
foreach (var publishedFilePath in publishedStaticFiles)
|
||||||
|
{
|
||||||
|
var publishedFileRelativePath = Path.GetRelativePath(resolvedPublishDirectory, publishedFilePath);
|
||||||
|
|
||||||
|
// We don't list compressed files in the SWAM, as these are transparent to the client,
|
||||||
|
// nor do we list the service worker itself or its assets manifest, as these don't need to be fetched in the same way
|
||||||
|
if (IsCompressedFile(publishedFileRelativePath)
|
||||||
|
|| string.Equals(publishedFileRelativePath, serviceWorkerPath, StringComparison.Ordinal)
|
||||||
|
|| string.Equals(publishedFileRelativePath, assetsManifestPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify hash
|
||||||
|
var publishedFileUrl = publishedFileRelativePath.Replace('\\', '/');
|
||||||
|
var expectedHash = ParseWebFormattedHash(assetsManifestHashesByUrl[publishedFileUrl]);
|
||||||
|
Assert.Contains(publishedFileUrl, assetsManifestHashesByUrl);
|
||||||
|
Assert.FileHashEquals(result, publishedFilePath, expectedHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ParseWebFormattedHash(string webFormattedHash)
|
||||||
|
{
|
||||||
|
Assert.StartsWith("sha256-", webFormattedHash);
|
||||||
|
return webFormattedHash.Substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCompressedFile(string path)
|
||||||
|
{
|
||||||
|
switch (Path.GetExtension(path))
|
||||||
|
{
|
||||||
|
case ".br":
|
||||||
|
case ".gz":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerateServiceWorkerAssetsManifest.AssetsManifestFile ReadServiceWorkerAssetsManifest(string assetsManifestResolvedPath)
|
||||||
|
{
|
||||||
|
var jsContents = File.ReadAllText(assetsManifestResolvedPath);
|
||||||
|
var jsonStart = jsContents.IndexOf("{");
|
||||||
|
var jsonLength = jsContents.LastIndexOf("}") - jsonStart + 1;
|
||||||
|
var json = jsContents.Substring(jsonStart, jsonLength);
|
||||||
|
return JsonSerializer.Deserialize<GenerateServiceWorkerAssetsManifest.AssetsManifestFile>(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
<Import Project="$(ReferenceBlazorBuildFromSourceProps)" />
|
<Import Project="$(ReferenceBlazorBuildFromSourceProps)" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.1</TargetFramework>
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
<RazorLangVersion>3.0</RazorLangVersion>
|
<RazorLangVersion>3.0</RazorLangVersion>
|
||||||
|
<ServiceWorkerAssetsManifest>custom-service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Test Placeholder -->
|
<!-- Test Placeholder -->
|
||||||
|
|
@ -32,6 +33,8 @@
|
||||||
<!-- This asset should not be treated as a static web asset as it is being linked out of the wwwroot folder. -->
|
<!-- This asset should not be treated as a static web asset as it is being linked out of the wwwroot folder. -->
|
||||||
<Content Update="wwwroot\Fake-License.txt" Link="Excluded-Static-Web-Assets\Fake-License.txt" />
|
<Content Update="wwwroot\Fake-License.txt" Link="Excluded-Static-Web-Assets\Fake-License.txt" />
|
||||||
|
|
||||||
|
<!-- The content from my-prod-service-worker.js should be published under the name my-service-worker.js -->
|
||||||
|
<ServiceWorker Include="wwwroot\serviceworkers\my-service-worker.js" PublishedContent="wwwroot\serviceworkers\my-prod-service-worker.js" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// This is the production service worker
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
// This is the development service worker
|
||||||
Loading…
Reference in New Issue