diff --git a/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets b/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets index 08a0bffb9073..92b52ae0da22 100644 --- a/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets +++ b/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets @@ -84,8 +84,6 @@ Copyright (c) .NET Foundation. All rights reserved. - - diff --git a/src/BlazorWasmSdk/Tasks/AssetsManifestFile.cs b/src/BlazorWasmSdk/Tasks/AssetsManifestFile.cs index aed2f0f44be1..e8686acba161 100644 --- a/src/BlazorWasmSdk/Tasks/AssetsManifestFile.cs +++ b/src/BlazorWasmSdk/Tasks/AssetsManifestFile.cs @@ -4,6 +4,7 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly { #pragma warning disable IDE1006 // Naming Styles + // Only used in legacy builds (5.0 and earlier) public class AssetsManifestFile { /// diff --git a/src/BlazorWasmSdk/Tasks/GenerateServiceWorkerAssetsManifest.cs b/src/BlazorWasmSdk/Tasks/GenerateServiceWorkerAssetsManifest.cs index d3c39c0b17da..0227f239ca9c 100644 --- a/src/BlazorWasmSdk/Tasks/GenerateServiceWorkerAssetsManifest.cs +++ b/src/BlazorWasmSdk/Tasks/GenerateServiceWorkerAssetsManifest.cs @@ -7,6 +7,7 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly { + // Only used in legacy builds (5.0 and earlier) public partial class GenerateServiceWorkerAssetsManifest : Task { [Required] diff --git a/src/RazorSdk/Razor.slnf b/src/RazorSdk/Razor.slnf index c29dba3946f8..45c9fb6d2e58 100644 --- a/src/RazorSdk/Razor.slnf +++ b/src/RazorSdk/Razor.slnf @@ -10,11 +10,12 @@ "src\\RazorSdk\\Tool\\Microsoft.NET.Sdk.Razor.Tool.csproj", "src\\Resolvers\\Microsoft.DotNet.NativeWrapper\\Microsoft.DotNet.NativeWrapper.csproj", "src\\StaticWebAssetsSdk\\Tasks\\Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj", + "src\\WasmSdk\\Tasks\\Microsoft.NET.Sdk.WebAssembly.Tasks.csproj", + "test\\Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests\\Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj", "test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj", "test\\Microsoft.NET.Sdk.Razor.Tests\\Microsoft.NET.Sdk.Razor.Tests.csproj", "test\\Microsoft.NET.Sdk.Razor.Tool.Tests\\Microsoft.NET.Sdk.Razor.Tool.Tests.csproj", - "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj", - "src\\WasmSdk\\Tasks\\Microsoft.NET.Sdk.WebAssembly.Tasks.csproj" + "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj" ] } } \ No newline at end of file diff --git a/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ServiceWorkerAssetsManifest.targets similarity index 98% rename from src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets rename to src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ServiceWorkerAssetsManifest.targets index 369a14a5307b..3c1995ed3a54 100644 --- a/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ServiceWorkerAssetsManifest.targets @@ -11,7 +11,11 @@ Copyright (c) .NET Foundation. All rights reserved. --> - + + $(ResolveStaticWebAssetsInputsDependsOn);_AddServiceWorkerAssets diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets index dcf4a1e61781..9fceacee75fe 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets @@ -550,4 +550,6 @@ Copyright (c) .NET Foundation. All rights reserved. + + diff --git a/src/StaticWebAssetsSdk/Tasks/ServiceWorker/AssetsManifestFile.cs b/src/StaticWebAssetsSdk/Tasks/ServiceWorker/AssetsManifestFile.cs new file mode 100644 index 000000000000..9f3872a8c177 --- /dev/null +++ b/src/StaticWebAssetsSdk/Tasks/ServiceWorker/AssetsManifestFile.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.StaticWebAssets.Tasks +{ +#pragma warning disable IDE1006 // Naming Styles + public class AssetsManifestFile + { + /// + /// Gets or sets a version string. + /// + public string version { get; set; } + + /// + /// Gets or sets the assets. Keys are URLs; values are base-64-formatted SHA256 content hashes. + /// + public AssetsManifestFileEntry[] assets { get; set; } + } + + public class AssetsManifestFileEntry + { + /// + /// Gets or sets the asset URL. Normally this will be relative to the application's base href. + /// + public string url { get; set; } + + /// + /// Gets or sets the file content hash. This should be the base-64-formatted SHA256 value. + /// + public string hash { get; set; } + } +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/src/StaticWebAssetsSdk/Tasks/ServiceWorker/GenerateServiceWorkerAssetsManifest.cs b/src/StaticWebAssetsSdk/Tasks/ServiceWorker/GenerateServiceWorkerAssetsManifest.cs new file mode 100644 index 000000000000..2b99ea83c3a4 --- /dev/null +++ b/src/StaticWebAssetsSdk/Tasks/ServiceWorker/GenerateServiceWorkerAssetsManifest.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Serialization.Json; +using System.Security.Cryptography; +using Microsoft.Build.Framework; + +namespace Microsoft.AspNetCore.StaticWebAssets.Tasks +{ + 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]; + 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/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj b/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj index 5fd78d717a58..386f91daa2a7 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj @@ -46,6 +46,7 @@ + diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/ServiceWorkerAssert.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/ServiceWorkerAssert.cs index bafa5e939f22..04c838fad66e 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/ServiceWorkerAssert.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/ServiceWorkerAssert.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json; +using Microsoft.AspNetCore.StaticWebAssets.Tasks; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs index f0eb2148c85c..e24bb1db272d 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs @@ -18,11 +18,21 @@ public void Build_ServiceWorkerAssetsManifest_Works() // Arrange var expectedExtensions = new[] { ".pdb", ".js", ".wasm" }; var testAppName = "BlazorWasmWithLibrary"; - var testInstance = CreateAspNetSdkTestAsset(testAppName); + var testInstance = CreateAspNetSdkTestAsset(testAppName) + .WithProjectChanges((p, doc) => + { + if (Path.GetFileName(p) == "blazorwasm.csproj") + { + var itemGroup = new XElement("PropertyGroup"); + var serviceWorkerAssetsManifest = new XElement("ServiceWorkerAssetsManifest", "service-worker-assets.js"); + itemGroup.Add(serviceWorkerAssetsManifest); + doc.Root.Add(itemGroup); + } + }); var buildCommand = new BuildCommand(testInstance, "blazorwasm"); buildCommand.WithWorkingDirectory(testInstance.TestRoot); - buildCommand.Execute("/p:ServiceWorkerAssetsManifest=service-worker-assets.js") + buildCommand.Execute() .Should().Pass(); var buildOutputDirectory = buildCommand.GetOutputDirectory(DefaultTfm).ToString(); @@ -136,10 +146,20 @@ public void Publish_UpdatesServiceWorkerVersionHash_WhenSourcesChange() { // Arrange var testAppName = "BlazorWasmWithLibrary"; - var testInstance = CreateAspNetSdkTestAsset(testAppName); + var testInstance = CreateAspNetSdkTestAsset(testAppName) + .WithProjectChanges((p, doc) => + { + if (Path.GetFileName(p) == "blazorwasm.csproj") + { + var itemGroup = new XElement("PropertyGroup"); + var serviceWorkerAssetsManifest = new XElement("ServiceWorkerAssetsManifest", "service-worker-assets.js"); + itemGroup.Add(serviceWorkerAssetsManifest); + doc.Root.Add(itemGroup); + } + }); var publishCommand = new PublishCommand(testInstance, "blazorwasm"); - publishCommand.Execute("/p:ServiceWorkerAssetsManifest=service-worker-assets.js").Should().Pass(); + publishCommand.Execute().Should().Pass(); var publishOutputDirectory = publishCommand.GetOutputDirectory(DefaultTfm).ToString(); @@ -158,7 +178,7 @@ public void Publish_UpdatesServiceWorkerVersionHash_WhenSourcesChange() // Assert publishCommand = new PublishCommand(testInstance, "blazorwasm"); - publishCommand.Execute("/p:ServiceWorkerAssetsManifest=service-worker-assets.js").Should().Pass(); + publishCommand.Execute().Should().Pass(); var updatedVersion = File.ReadAllLines(serviceWorkerFile).Last(); var updatedMatch = Regex.Match(updatedVersion, "\\/\\* Manifest version: (.{8}) \\*\\/"); @@ -176,10 +196,20 @@ public void Publish_DeterministicAcrossBuilds_WhenNoSourcesChange() { // Arrange var testAppName = "BlazorWasmWithLibrary"; - var testInstance = CreateAspNetSdkTestAsset(testAppName); + var testInstance = CreateAspNetSdkTestAsset(testAppName) + .WithProjectChanges((p, doc) => + { + if (Path.GetFileName(p) == "blazorwasm.csproj") + { + var itemGroup = new XElement("PropertyGroup"); + var serviceWorkerAssetsManifest = new XElement("ServiceWorkerAssetsManifest", "service-worker-assets.js"); + itemGroup.Add(serviceWorkerAssetsManifest); + doc.Root.Add(itemGroup); + } + }); var publishCommand = new PublishCommand(testInstance, "blazorwasm"); - publishCommand.Execute("/p:ServiceWorkerAssetsManifest=service-worker-assets.js").Should().Pass(); + publishCommand.Execute().Should().Pass(); var publishOutputDirectory = publishCommand.GetOutputDirectory(DefaultTfm).ToString(); @@ -194,7 +224,7 @@ public void Publish_DeterministicAcrossBuilds_WhenNoSourcesChange() // Act && Assert publishCommand = new PublishCommand(Log, Path.Combine(testInstance.TestRoot, "blazorwasm")); - publishCommand.Execute("/p:ServiceWorkerAssetsManifest=service-worker-assets.js").Should().Pass(); + publishCommand.Execute().Should().Pass(); var updatedVersion = File.ReadAllLines(serviceWorkerFile).Last(); var updatedMatch = Regex.Match(updatedVersion, "\\/\\* Manifest version: (.{8}) \\*\\/");