* [Blazor] Support brotli compression for framework files * Adds a new tool to the Blazor.Build package to perform brotli compression. * Performs brotli compression at publish time * Centralizes hashing computation in one place and creates hash files for performing incremental compilations
This commit is contained in:
parent
fd9c786165
commit
8232c6a4d8
|
|
@ -47,7 +47,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAssembly.JSInterop", "WebAssembly.JSInterop", "{37FA056D-A7B3-4F72-A8B9-8D3C175E831E}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAssembly.JSInterop", "WebAssembly.JSInterop", "{37FA056D-A7B3-4F72-A8B9-8D3C175E831E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAssembly.JSInterop", "WebAssembly\JSInterop\src\Microsoft.JSInterop.WebAssembly.csproj", "{FBD7C733-200E-4BED-8B31-2610C2263F72}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.WebAssembly", "WebAssembly\JSInterop\src\Microsoft.JSInterop.WebAssembly.csproj", "{FBD7C733-200E-4BED-8B31-2610C2263F72}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{7920B09F-8016-49CF-A229-E72D0CECDD17}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{7920B09F-8016-49CF-A229-E72D0CECDD17}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
@ -105,7 +105,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Authentication.We
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebugProxy", "DebugProxy", "{96DE9B14-D81F-422E-A33A-728BFB9C153A}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebugProxy", "DebugProxy", "{96DE9B14-D81F-422E-A33A-728BFB9C153A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebAssembly.DebugProxy", "WebAssembly\DebugProxy\src\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj", "{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebAssembly.DebugProxy", "WebAssembly\DebugProxy\src\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj", "{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compression", "Compression", "{710765DD-C2AD-4DBB-A114-14E62F31F463}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebAssembly.Build.BrotliCompression", "WebAssembly\Compression\src\Microsoft.AspNetCore.Components.WebAssembly.Build.BrotliCompression.csproj", "{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebAssembly.Server.Tests", "WebAssembly\Server\test\Microsoft.AspNetCore.Components.WebAssembly.Server.Tests.csproj", "{72D3D00C-5281-455F-9E19-646EE766009A}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
@ -537,6 +543,30 @@ Global
|
||||||
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x64.Build.0 = Release|Any CPU
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x86.ActiveCfg = Release|Any CPU
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x86.Build.0 = Release|Any CPU
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -591,6 +621,9 @@ Global
|
||||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43} = {E4D756A7-A934-4D7F-BC6E-7B95FE4098AB}
|
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43} = {E4D756A7-A934-4D7F-BC6E-7B95FE4098AB}
|
||||||
{96DE9B14-D81F-422E-A33A-728BFB9C153A} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
{96DE9B14-D81F-422E-A33A-728BFB9C153A} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
||||||
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C} = {96DE9B14-D81F-422E-A33A-728BFB9C153A}
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C} = {96DE9B14-D81F-422E-A33A-728BFB9C153A}
|
||||||
|
{710765DD-C2AD-4DBB-A114-14E62F31F463} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
||||||
|
{DB1DC77D-122E-49E8-AB16-1AC8AEEFEEFF} = {710765DD-C2AD-4DBB-A114-14E62F31F463}
|
||||||
|
{72D3D00C-5281-455F-9E19-646EE766009A} = {7920B09F-8016-49CF-A229-E72D0CECDD17}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}
|
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net46</TargetFrameworks>
|
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net46</TargetFrameworks>
|
||||||
|
|
@ -70,4 +70,36 @@
|
||||||
</Copy>
|
</Copy>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_BrotliToolPathInput Include="..\..\Compression\src\Microsoft.AspNetCore.Components.WebAssembly.Build.BrotliCompression.csproj" />
|
||||||
|
<_BrotliToolPathInput Include="..\..\Compression\src\*.cs" />
|
||||||
|
<_BrotliToolPathInput Include="..\..\Compression\src\*.runtimeconfig.json" />
|
||||||
|
<_BrotliToolPathOutput Include="$(MSBuildThisFileDirectory)bin\$(Configuration)\tools\compression\blazor-brotli.dll" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target
|
||||||
|
Name="GetBrotliTools"
|
||||||
|
BeforeTargets="Build;GenerateNuspec"
|
||||||
|
Inputs="@(_BrotliToolPathInput)"
|
||||||
|
Outputs="@(_BrotliToolPathOutput)">
|
||||||
|
<ItemGroup>
|
||||||
|
<_BrotliToolsPath Include="$(MSBuildThisFileDirectory)bin\$(Configuration)\tools\compression\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<_BrotliToolsOutputPath>@(_BrotliToolsPath->'%(FullPath)')</_BrotliToolsOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<NuspecProperty Include="brotliDir=$(_BrotliToolsOutputPath)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<MSBuild
|
||||||
|
Projects="..\..\Compression\src\Microsoft.AspNetCore.Components.WebAssembly.Build.BrotliCompression.csproj"
|
||||||
|
Targets="Publish"
|
||||||
|
Properties="Configuration=$(Configuration);TargetFramework=netcoreapp3.1;PublishDir=$(_BrotliToolsOutputPath)">
|
||||||
|
</MSBuild>
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
$CommonFileElements$
|
$CommonFileElements$
|
||||||
<file src="..\..\..\THIRD-PARTY-NOTICES.txt" />
|
<file src="..\..\..\THIRD-PARTY-NOTICES.txt" />
|
||||||
<file src="build\**" target="build" />
|
<file src="build\**" target="build" />
|
||||||
|
<file src="$brotliDir$blazor-brotli.dll" target="tools/compression" />
|
||||||
|
<file src="$brotliDir$blazor-brotli.runtimeconfig.json" target="tools/compression" />
|
||||||
<file src="targets\**" target="targets" />
|
<file src="targets\**" target="targets" />
|
||||||
<file src="$taskskDir$\**" target="tools/" />
|
<file src="$taskskDir$\**" target="tools/" />
|
||||||
<file src="..\..\..\Web.JS\dist\$configuration$\blazor.webassembly.js" target="tools/blazor" />
|
<file src="..\..\..\Web.JS\dist\$configuration$\blazor.webassembly.js" target="tools/blazor" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
// 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.Text;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
using Microsoft.Build.Utilities;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
{
|
||||||
|
public class BrotliCompressBlazorApplicationFiles : ToolTask
|
||||||
|
{
|
||||||
|
private const string DotNetHostPathEnvironmentName = "DOTNET_HOST_PATH";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string ManifestPath { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string BlazorBrotliPath { get; set; }
|
||||||
|
|
||||||
|
private string _dotnetPath;
|
||||||
|
|
||||||
|
private string DotNetPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_dotnetPath))
|
||||||
|
{
|
||||||
|
return _dotnetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dotnetPath = Environment.GetEnvironmentVariable(DotNetHostPathEnvironmentName);
|
||||||
|
if (string.IsNullOrEmpty(_dotnetPath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{DotNetHostPathEnvironmentName} is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _dotnetPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High;
|
||||||
|
|
||||||
|
protected override string ToolName => Path.GetFileName(DotNetPath);
|
||||||
|
|
||||||
|
protected override string GenerateFullPathToTool() => DotNetPath;
|
||||||
|
|
||||||
|
protected override string GenerateCommandLineCommands() =>
|
||||||
|
$"\"{BlazorBrotliPath}\" \"{ManifestPath}\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
// 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 Microsoft.Build.Framework;
|
|
||||||
using Microsoft.Build.Utilities;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|
||||||
{
|
|
||||||
public class CompressBlazorApplicationFiles : Task
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public ITaskItem StaticWebAsset { get; set; }
|
|
||||||
|
|
||||||
public override bool Execute()
|
|
||||||
{
|
|
||||||
var targetCompressionPath = StaticWebAsset.GetMetadata("TargetCompressionPath");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(targetCompressionPath));
|
|
||||||
|
|
||||||
using var sourceStream = File.OpenRead(StaticWebAsset.GetMetadata("FullPath"));
|
|
||||||
using var fileStream = new FileStream(targetCompressionPath, FileMode.Create);
|
|
||||||
using var stream = new GZipStream(fileStream, CompressionLevel.Optimal);
|
|
||||||
|
|
||||||
sourceStream.CopyTo(stream);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
var resourceName = GetResourceName(resource);
|
var resourceName = GetResourceName(resource);
|
||||||
if (!resourceList.ContainsKey(resourceName))
|
if (!resourceList.ContainsKey(resourceName))
|
||||||
{
|
{
|
||||||
resourceList.Add(resourceName, $"sha256-{resource.GetMetadata("FileHash")}");
|
resourceList.Add(resourceName, $"sha256-{resource.GetMetadata("Integrity")}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
// 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.Runtime.Serialization;
|
||||||
|
using System.Runtime.Serialization.Json;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
using Microsoft.Build.Utilities;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
{
|
||||||
|
public class GenerateBlazorCompressionManifest : Task
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public ITaskItem[] FilesToCompress { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string ManifestPath { get; set; }
|
||||||
|
|
||||||
|
public override bool Execute()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
WriteCompressionManifest();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.LogErrorFromException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !Log.HasLoggedErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteCompressionManifest()
|
||||||
|
{
|
||||||
|
var tempFilePath = Path.GetTempFileName();
|
||||||
|
|
||||||
|
var manifest = new ManifestData();
|
||||||
|
var filesToCompress = new List<CompressedFile>();
|
||||||
|
|
||||||
|
foreach (var file in FilesToCompress)
|
||||||
|
{
|
||||||
|
filesToCompress.Add(new CompressedFile
|
||||||
|
{
|
||||||
|
Source = file.GetMetadata("FullPath"),
|
||||||
|
InputSource = file.GetMetadata("InputSource"),
|
||||||
|
Target = file.GetMetadata("TargetCompressionPath"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest.FilesToCompress = filesToCompress.ToArray();
|
||||||
|
|
||||||
|
var serializer = new DataContractJsonSerializer(typeof(ManifestData));
|
||||||
|
|
||||||
|
using (var tempFile = File.OpenWrite(tempFilePath))
|
||||||
|
{
|
||||||
|
using (var writer = JsonReaderWriterFactory.CreateJsonWriter(tempFile, Encoding.UTF8, ownsStream: false, indent: true))
|
||||||
|
{
|
||||||
|
serializer.WriteObject(writer, manifest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(ManifestPath))
|
||||||
|
{
|
||||||
|
File.Move(tempFilePath, ManifestPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalText = File.ReadAllText(ManifestPath);
|
||||||
|
var newManifest = File.ReadAllText(tempFilePath);
|
||||||
|
if (!string.Equals(originalText, newManifest, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// OnlyWriteWhenDifferent
|
||||||
|
File.Delete(ManifestPath);
|
||||||
|
File.Move(tempFilePath, ManifestPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
private class ManifestData
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public CompressedFile[] FilesToCompress { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
private class CompressedFile
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string Source { get; set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public string InputSource { get; set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public string Target { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
|
||||||
assets = AssetsWithHashes.Select(item => new AssetsManifestFileEntry
|
assets = AssetsWithHashes.Select(item => new AssetsManifestFileEntry
|
||||||
{
|
{
|
||||||
url = item.GetMetadata("AssetUrl"),
|
url = item.GetMetadata("AssetUrl"),
|
||||||
hash = $"sha256-{item.GetMetadata("FileHash")}",
|
hash = $"sha256-{item.GetMetadata("Integrity")}",
|
||||||
}).ToArray()
|
}).ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// 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.Runtime.Serialization;
|
||||||
|
using System.Runtime.Serialization.Json;
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
using Microsoft.Build.Utilities;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
{
|
||||||
|
public class GzipCompressBlazorApplicationFiles : Task
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string ManifestPath { get; set; }
|
||||||
|
|
||||||
|
public override bool Execute()
|
||||||
|
{
|
||||||
|
var serializer = new DataContractJsonSerializer(typeof(ManifestData));
|
||||||
|
|
||||||
|
ManifestData manifest = null;
|
||||||
|
using (var tempFile = File.OpenRead(ManifestPath))
|
||||||
|
{
|
||||||
|
manifest = (ManifestData)serializer.ReadObject(tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Threading.Tasks.Parallel.ForEach(manifest.FilesToCompress, (file) =>
|
||||||
|
{
|
||||||
|
var inputPath = file.Source;
|
||||||
|
var inputSource = file.InputSource;
|
||||||
|
var targetCompressionPath = file.Target;
|
||||||
|
|
||||||
|
if (!File.Exists(inputSource) ||
|
||||||
|
(File.Exists(targetCompressionPath) && File.GetLastWriteTime(inputSource) > File.GetLastWriteTime(targetCompressionPath)))
|
||||||
|
{
|
||||||
|
// Incrementalism. If input source doesn't exist or it exists and is not newer than the expected output, do nothing.
|
||||||
|
if (!File.Exists(inputSource))
|
||||||
|
{
|
||||||
|
Log.LogMessage($"Skipping '{inputPath}' because '{inputSource}' does not exist.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.LogMessage($"Skipping '{inputPath}' because '{inputSource}' is newer than '{targetCompressionPath}'.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(targetCompressionPath));
|
||||||
|
|
||||||
|
using var sourceStream = File.OpenRead(inputPath);
|
||||||
|
using var fileStream = new FileStream(targetCompressionPath, FileMode.Create);
|
||||||
|
using var stream = new GZipStream(fileStream, CompressionLevel.Optimal);
|
||||||
|
|
||||||
|
sourceStream.CopyTo(stream);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.LogErrorFromException(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return !Log.HasLoggedErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
private class ManifestData
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public CompressedFile[] FilesToCompress { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataContract]
|
||||||
|
private class CompressedFile
|
||||||
|
{
|
||||||
|
[DataMember]
|
||||||
|
public string Source { get; set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public string InputSource { get; set; }
|
||||||
|
|
||||||
|
[DataMember]
|
||||||
|
public string Target { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<Target
|
<Target
|
||||||
Name="_PrepareBlazorOutputs"
|
Name="_PrepareBlazorOutputs"
|
||||||
DependsOnTargets="_ResolveBlazorInputs;_ResolveBlazorOutputs;_GenerateBlazorBootJson">
|
DependsOnTargets="_ResolveBlazorInputs;_ResolveBlazorOutputs;_GenerateBlazorBootJson;_GenerateBlazorBootJsonIntegrity">
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="_ResolveBlazorInputs" DependsOnTargets="ResolveReferences;ResolveRuntimePackAssets">
|
<Target Name="_ResolveBlazorInputs" DependsOnTargets="ResolveReferences;ResolveRuntimePackAssets">
|
||||||
|
|
@ -33,6 +33,16 @@
|
||||||
<_BlazorApplicationAssembliesCacheFile>$(_BlazorIntermediateOutputPath)unlinked.output</_BlazorApplicationAssembliesCacheFile>
|
<_BlazorApplicationAssembliesCacheFile>$(_BlazorIntermediateOutputPath)unlinked.output</_BlazorApplicationAssembliesCacheFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
When running from Desktop MSBuild, DOTNET_HOST_PATH is not set.
|
||||||
|
In this case, explicitly specify the path to the dotnet host.
|
||||||
|
-->
|
||||||
|
<PropertyGroup Condition=" '$(DOTNET_HOST_PATH)' == '' ">
|
||||||
|
<_DotNetHostDirectory>$(NetCoreRoot)</_DotNetHostDirectory>
|
||||||
|
<_DotNetHostFileName>dotnet</_DotNetHostFileName>
|
||||||
|
<_DotNetHostFileName Condition=" '$(OS)' == 'Windows_NT' ">dotnet.exe</_DotNetHostFileName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_WebAssemblyBCLFolder Include="
|
<_WebAssemblyBCLFolder Include="
|
||||||
$(ComponentsWebAssemblyBaseClassLibraryPath);
|
$(ComponentsWebAssemblyBaseClassLibraryPath);
|
||||||
|
|
@ -123,6 +133,7 @@
|
||||||
<_BlazorOutputWithTargetPath Include="@(_BlazorJSFile)">
|
<_BlazorOutputWithTargetPath Include="@(_BlazorJSFile)">
|
||||||
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
|
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
|
||||||
</_BlazorOutputWithTargetPath>
|
</_BlazorOutputWithTargetPath>
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
@ -133,6 +144,33 @@
|
||||||
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
|
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
|
||||||
<_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
|
<_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_ExistingBlazorOutputWithTargetPath Include="@(_BlazorOutputWithTargetPath)" Condition="Exists('%(FullPath)')" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<GetFileHash Files="@(_ExistingBlazorOutputWithTargetPath)" Algorithm="SHA256" HashEncoding="base64">
|
||||||
|
<Output TaskParameter="Items" ItemName="_BlazorOutputWithHash" />
|
||||||
|
</GetFileHash>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_BlazorOutputWithIntegrity Include="@(_BlazorOutputWithHash)">
|
||||||
|
<Integrity>%(_BlazorOutputWithHash.FileHash)</Integrity>
|
||||||
|
<IntegrityFile>$(IntermediateOutputPath)integrity\$([System.String]::Copy('%(FileHash)').Replace('/','-').Replace('+','_')).hash'</IntegrityFile>
|
||||||
|
</_BlazorOutputWithIntegrity>
|
||||||
|
|
||||||
|
<_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithIntegrity)" />
|
||||||
|
<_BlazorOutputWithTargetPath Include="@(_BlazorOutputWithIntegrity)" RemoveMetadata="FileHash;FileHashAlgorithm" />
|
||||||
|
|
||||||
|
<MakeDir Directories="$(IntermediateOutputPath)integrity" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<WriteLinesToFile Lines="%(_BlazorOutputWithIntegrity.Integrity)" File="%(_BlazorOutputWithIntegrity.IntegrityFile)" WriteOnlyWhenDifferent="true" Overwrite="true" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FileWrites Include="%(_BlazorOutputWithIntegrity.IntegrityFile)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
@ -233,16 +271,6 @@
|
||||||
|
|
||||||
<Delete Files="@(_OldLinkedFile)" />
|
<Delete Files="@(_OldLinkedFile)" />
|
||||||
|
|
||||||
<!--
|
|
||||||
When running from Desktop MSBuild, DOTNET_HOST_PATH is not set.
|
|
||||||
In this case, explicitly specify the path to the dotnet host.
|
|
||||||
-->
|
|
||||||
<PropertyGroup Condition=" '$(DOTNET_HOST_PATH)' == '' ">
|
|
||||||
<_DotNetHostDirectory>$(NetCoreRoot)</_DotNetHostDirectory>
|
|
||||||
<_DotNetHostFileName>dotnet</_DotNetHostFileName>
|
|
||||||
<_DotNetHostFileName Condition=" '$(OS)' == 'Windows_NT' ">dotnet.exe</_DotNetHostFileName>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<BlazorILLink
|
<BlazorILLink
|
||||||
ILLinkPath="$(ComponentsWebAssemblyLinkerPath)"
|
ILLinkPath="$(ComponentsWebAssemblyLinkerPath)"
|
||||||
AssemblyPaths="@(_BlazorAssemblyToLink)"
|
AssemblyPaths="@(_BlazorAssemblyToLink)"
|
||||||
|
|
@ -360,23 +388,41 @@
|
||||||
<_BlazorBootResource Include="@(_BlazorOutputWithTargetPath->HasMetadata('BootManifestResourceType'))" />
|
<_BlazorBootResource Include="@(_BlazorOutputWithTargetPath->HasMetadata('BootManifestResourceType'))" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<GetFileHash Files="@(_BlazorBootResource)" Algorithm="SHA256" HashEncoding="base64">
|
|
||||||
<Output TaskParameter="Items" ItemName="_BlazorBootResourceWithHash" />
|
|
||||||
</GetFileHash>
|
|
||||||
|
|
||||||
<GenerateBlazorBootJson
|
<GenerateBlazorBootJson
|
||||||
AssemblyPath="@(IntermediateAssembly)"
|
AssemblyPath="@(IntermediateAssembly)"
|
||||||
Resources="@(_BlazorBootResourceWithHash)"
|
Resources="@(_BlazorBootResource)"
|
||||||
DebugBuild="$(_IsDebugBuild)"
|
DebugBuild="$(_IsDebugBuild)"
|
||||||
LinkerEnabled="$(BlazorWebAssemblyEnableLinking)"
|
LinkerEnabled="$(BlazorWebAssemblyEnableLinking)"
|
||||||
CacheBootResources="$(BlazorCacheBootResources)"
|
CacheBootResources="$(BlazorCacheBootResources)"
|
||||||
OutputPath="$(_BlazorBootJsonIntermediateOutputPath)"
|
OutputPath="$(_BlazorBootJsonIntermediateOutputPath)"
|
||||||
ConfigurationFiles="@(_BlazorConfigFile)" />
|
ConfigurationFiles="@(_BlazorConfigFile)" />
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="_GenerateBlazorBootJsonIntegrity">
|
||||||
|
|
||||||
|
<GetFileHash Files="$(_BlazorBootJsonIntermediateOutputPath)" Algorithm="SHA256" HashEncoding="base64">
|
||||||
|
<Output TaskParameter="Items" ItemName="_BlazorBootJsonWithHash" />
|
||||||
|
</GetFileHash>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_BlazorOutputWithTargetPath Include="$(_BlazorBootJsonIntermediateOutputPath)" TargetOutputPath="$(_BaseBlazorRuntimeOutputPath)$(_BlazorBootJsonName)" />
|
|
||||||
|
<_BlazorBootJsonWithIntegrity Include="@(_BlazorBootJsonWithHash)">
|
||||||
|
<Integrity>%(FileHash)</Integrity>
|
||||||
|
<IntegrityFile>$(IntermediateOutputPath)integrity\$([System.String]::Copy('%(FileHash)').Replace('/','-').Replace('+','_')).hash'</IntegrityFile>
|
||||||
|
</_BlazorBootJsonWithIntegrity>
|
||||||
|
|
||||||
|
<_BlazorOutputWithTargetPath Include="@(_BlazorBootJsonWithIntegrity)" RemoveMetadata="FileHash;FileHashAlgorithm">
|
||||||
|
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)$(_BlazorBootJsonName)</TargetOutputPath>
|
||||||
|
</_BlazorOutputWithTargetPath>
|
||||||
|
|
||||||
<FileWrites Include="$(_BlazorBootJsonIntermediateOutputPath)" />
|
<FileWrites Include="$(_BlazorBootJsonIntermediateOutputPath)" />
|
||||||
|
<FileWrites Include="%(_BlazorBootJsonWithIntegrity.IntegrityFile)" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<WriteLinesToFile Lines="%(_BlazorBootJsonWithIntegrity.Integrity)" File="%(_BlazorBootJsonWithIntegrity.IntegrityFile)" WriteOnlyWhenDifferent="true" Overwrite="true" />
|
||||||
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
||||||
|
<_BlazorBrotliPath>$(_BlazorToolsDir)compression\blazor-brotli.dll</_BlazorBrotliPath>
|
||||||
<ResolveCurrentProjectStaticWebAssetsDependsOn>
|
<ResolveCurrentProjectStaticWebAssetsDependsOn>
|
||||||
$(ResolveCurrentProjectStaticWebAssetsDependsOn);
|
$(ResolveCurrentProjectStaticWebAssetsDependsOn);
|
||||||
_CompressBlazorApplicationFiles;
|
_ResolveBlazorFilesToCompress;
|
||||||
</ResolveCurrentProjectStaticWebAssetsDependsOn>
|
</ResolveCurrentProjectStaticWebAssetsDependsOn>
|
||||||
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
@ -12,16 +13,41 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_BlazorFilesIntermediateOutputPath>$(IntermediateOutputPath)compressed\</_BlazorFilesIntermediateOutputPath>
|
<_BlazorFilesIntermediateOutputPath>$(IntermediateOutputPath)compressed\</_BlazorFilesIntermediateOutputPath>
|
||||||
|
<_GzipCompressionBlazorApplicationFilesManifestPath>$(IntermediateOutputPath)compressed\gzip.manifest.json</_GzipCompressionBlazorApplicationFilesManifestPath>
|
||||||
|
<_BrotliCompressionBlazorApplicationFilesManifestPath>$(IntermediateOutputPath)compressed\brotli.manifest.json</_BrotliCompressionBlazorApplicationFilesManifestPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<MakeDir Directories="$(_BlazorFilesIntermediateOutputPath)" />
|
<MakeDir Directories="$(_BlazorFilesIntermediateOutputPath)" />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_BlazorFileToCompress Include="@(StaticWebAsset)" Condition="'%(SourceType)' == '' and $([System.String]::Copy('%(RelativePath)').Replace('\','/').StartsWith('_framework/'))" KeepDuplicates="false">
|
|
||||||
|
<_CompressionCandidate Include="@(StaticWebAsset)" Condition="'%(SourceType)' == '' and $([System.String]::Copy('%(RelativePath)').Replace('\','/').StartsWith('_framework/'))" KeepDuplicates="false" />
|
||||||
|
<_CompressionCandidateIntegrity Include="@(_BlazorOutputWithTargetPath->'%(FullPath)')" />
|
||||||
|
<_CompressionCandidateWithIntegrity Include="%(Identity)">
|
||||||
|
<SourceType>@(_CompressionCandidate->'%(SourceType)')</SourceType>
|
||||||
|
<SourceId>@(_CompressionCandidate->'%(SourceId)')</SourceId>
|
||||||
|
<ContentRoot>@(_CompressionCandidate->'%(ContentRoot)')</ContentRoot>
|
||||||
|
<BasePath>@(_CompressionCandidate->'%(BasePath)')</BasePath>
|
||||||
|
<RelativePath>@(_CompressionCandidate->'%(RelativePath)')</RelativePath>
|
||||||
|
<InputSource>@(_CompressionCandidateIntegrity->'%(IntegrityFile)')</InputSource>
|
||||||
|
</_CompressionCandidateWithIntegrity>
|
||||||
|
|
||||||
|
<_GzipBlazorFileToCompress Include="@(_CompressionCandidateWithIntegrity)">
|
||||||
<TargetCompressionPath>$(_BlazorFilesIntermediateOutputPath)%(RelativePath).gz</TargetCompressionPath>
|
<TargetCompressionPath>$(_BlazorFilesIntermediateOutputPath)%(RelativePath).gz</TargetCompressionPath>
|
||||||
<TargetOutputPath>%(RelativePath).gz</TargetOutputPath>
|
<TargetOutputPath>%(RelativePath).gz</TargetOutputPath>
|
||||||
<RelativePath>%(RelativePath).gz</RelativePath>
|
<RelativePath>%(RelativePath).gz</RelativePath>
|
||||||
<InputSource>$([MSBuild]::NormalizePath('$(_BlazorFilesIntermediateOutputPath)%(RelativePath).hash'))</InputSource>
|
</_GzipBlazorFileToCompress>
|
||||||
</_BlazorFileToCompress>
|
<_GzipBlazorFileToCompress Remove="@(_BlazorFileCompressExclusion)" />
|
||||||
|
|
||||||
|
<_BrotliBlazorFileToCompress Include="@(_CompressionCandidateWithIntegrity)">
|
||||||
|
<TargetCompressionPath>$(_BlazorFilesIntermediateOutputPath)%(RelativePath).br</TargetCompressionPath>
|
||||||
|
<TargetOutputPath>%(RelativePath).br</TargetOutputPath>
|
||||||
|
<RelativePath>%(RelativePath).br</RelativePath>
|
||||||
|
</_BrotliBlazorFileToCompress>
|
||||||
|
<_BrotliBlazorFileToCompress Remove="@(_BlazorFileCompressExclusion)" />
|
||||||
|
|
||||||
|
<_BlazorFileToCompress Include="@(_GzipBlazorFileToCompress)" />
|
||||||
|
<_BlazorFileToCompress Include="@(_BrotliBlazorFileToCompress)" />
|
||||||
|
<_BlazorFileToCompress Remove="@(_BlazorFileCompressExclusion)" />
|
||||||
|
|
||||||
<_CompressedStaticWebAsset Include="@(_BlazorFileToCompress->'%(TargetCompressionPath)')" RemoveMetadata="TargetOutputPath;TargetCompressionPath" />
|
<_CompressedStaticWebAsset Include="@(_BlazorFileToCompress->'%(TargetCompressionPath)')" RemoveMetadata="TargetOutputPath;TargetCompressionPath" />
|
||||||
|
|
||||||
|
|
@ -30,28 +56,53 @@
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<GetFileHash Files="@(_BlazorFileToCompress)">
|
</Target>
|
||||||
<Output TaskParameter="Items" ItemName="_LinkerOutputHashes" />
|
|
||||||
</GetFileHash>
|
|
||||||
|
|
||||||
<WriteLinesToFile Condition="'@(_LinkerOutputHashes)' != ''" Lines="%(_LinkerOutputHashes.FileHash)" File="%(_LinkerOutputHashes.InputSource)" WriteOnlyWhenDifferent="true" Overwrite="true" />
|
<UsingTask TaskName="GzipCompressBlazorApplicationFiles" AssemblyFile="$(_BlazorTasksPath)" />
|
||||||
|
<UsingTask TaskName="BrotliCompressBlazorApplicationFiles" AssemblyFile="$(_BlazorTasksPath)" />
|
||||||
|
<UsingTask TaskName="GenerateBlazorCompressionManifest" AssemblyFile="$(_BlazorTasksPath)" />
|
||||||
|
|
||||||
|
<Target
|
||||||
|
Name="_GzipCompressBlazorApplicationFiles"
|
||||||
|
DependsOnTargets="ResolveStaticWebAssetsInputs"
|
||||||
|
BeforeTargets="_BlazorStaticWebAssetsCopyGeneratedFilesToOutputDirectory"
|
||||||
|
Inputs="$(_GzipCompressionBlazorApplicationFilesManifestPath)"
|
||||||
|
Outputs="@(_GzipBlazorFileToCompress->'%(TargetCompressionPath)')">
|
||||||
|
|
||||||
<ItemGroup>
|
<GzipCompressBlazorApplicationFiles ManifestPath="$(_GzipCompressionBlazorApplicationFilesManifestPath)" />
|
||||||
<FileWrites Include="%(_LinkerOutputHashes.InputSource)" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<UsingTask TaskName="CompressBlazorApplicationFiles" AssemblyFile="$(_BlazorTasksPath)" />
|
<Target
|
||||||
|
Name="_GenerateGzipCompressionBlazorApplicationFilesManifest"
|
||||||
|
BeforeTargets="_GzipCompressBlazorApplicationFiles"
|
||||||
|
Inputs="@(_GzipBlazorFileToCompress->'%(InputSource)')"
|
||||||
|
Outputs="$(_GzipCompressionBlazorApplicationFilesManifestPath)">
|
||||||
|
|
||||||
|
<GenerateBlazorCompressionManifest FilesToCompress="@(_GzipBlazorFileToCompress)" ManifestPath="$(_GzipCompressionBlazorApplicationFilesManifestPath)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
<Target
|
<Target
|
||||||
Name="_CompressBlazorApplicationFiles"
|
Name="_BrotliCompressBlazorApplicationFiles"
|
||||||
AfterTargets="_ResolveBlazorFilesToCompress"
|
BeforeTargets="GetCopyToPublishDirectoryItems;_CopyResolvedFilesToPublishPreserveNewest"
|
||||||
Inputs="%(_BlazorFileToCompress.InputSource)"
|
DependsOnTargets="ResolveStaticWebAssetsInputs"
|
||||||
Outputs="%(_BlazorFileToCompress.TargetCompressionPath)">
|
Inputs="$(_BrotliCompressionBlazorApplicationFilesManifestPath)"
|
||||||
|
Outputs="@(_BrotliBlazorFileToCompress->'%(TargetCompressionPath)')">
|
||||||
|
|
||||||
<CompressBlazorApplicationFiles StaticWebAsset="@(_BlazorFileToCompress)" />
|
<BrotliCompressBlazorApplicationFiles
|
||||||
|
BlazorBrotliPath="$(_BlazorBrotliPath)"
|
||||||
|
ManifestPath="$(_BrotliCompressionBlazorApplicationFilesManifestPath)"
|
||||||
|
ToolExe="$(_DotNetHostFileName)"
|
||||||
|
ToolPath="$(_DotNetHostDirectory)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target
|
||||||
|
Name="_GenerateBrotliCompressionBlazorApplicationFilesManifest"
|
||||||
|
BeforeTargets="_GzipCompressBlazorApplicationFiles"
|
||||||
|
Inputs="@(_BrotliBlazorFileToCompress->'%(InputSource)')"
|
||||||
|
Outputs="$(_BrotliCompressionBlazorApplicationFilesManifestPath)">
|
||||||
|
|
||||||
|
<GenerateBlazorCompressionManifest FilesToCompress="@(_BrotliBlazorFileToCompress)" ManifestPath="$(_BrotliCompressionBlazorApplicationFilesManifestPath)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,33 @@
|
||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<Target Name="_ComputeServiceWorkerAssetsManifestInputs"
|
<Target Name="_ComputeServiceWorkerAssetsManifestInputs"
|
||||||
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
BeforeTargets="_ResolveBlazorOutputs">
|
BeforeTargets="_ResolveBlazorOutputs;_ResolveBlazorFilesToCompress">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_ServiceWorkerAssetsManifestIntermediateOutputPath>$(_BlazorIntermediateOutputPath)serviceworkerassets.js</_ServiceWorkerAssetsManifestIntermediateOutputPath>
|
<_ServiceWorkerAssetsManifestIntermediateOutputPath>$(_BlazorIntermediateOutputPath)$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath>
|
||||||
|
<_ServiceWorkerAssetsManifestFullPath>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(_ServiceWorkerAssetsManifestIntermediateOutputPath)'))</_ServiceWorkerAssetsManifestFullPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_BlazorOutputWithTargetPath Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
<_BlazorOutputWithTargetPath
|
||||||
Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
|
Include="$(_ServiceWorkerAssetsManifestFullPath)"
|
||||||
TargetOutputPath="$(_BaseBlazorDistPath)$(ServiceWorkerAssetsManifest)" />
|
TargetOutputPath="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
|
||||||
|
|
||||||
|
<_ManifestStaticWebAsset Include="$(_ServiceWorkerAssetsManifestFullPath)">
|
||||||
|
<SourceType></SourceType>
|
||||||
|
<SourceId>$(PackageId)</SourceId>
|
||||||
|
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
|
||||||
|
<BasePath>$(StaticWebAssetBasePath)</BasePath>
|
||||||
|
<RelativePath>$([MSBuild]::MakeRelative($([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(_BlazorIntermediateOutputPath)')), $(_ServiceWorkerAssetsManifestFullPath)))</RelativePath>
|
||||||
|
</_ManifestStaticWebAsset>
|
||||||
|
|
||||||
|
<StaticWebAsset Include="@(_ManifestStaticWebAsset)" />
|
||||||
|
<_CompressionCandidate Include="@(_ManifestStaticWebAsset)" />
|
||||||
|
<_CompressionCandidateWithIntegrity Include="@(_ManifestStaticWebAsset)">
|
||||||
|
<InputSource>$(_ServiceWorkerAssetsManifestFullPath)</InputSource>
|
||||||
|
</_CompressionCandidateWithIntegrity>
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
@ -19,11 +35,11 @@
|
||||||
<UsingTask TaskName="GenerateServiceWorkerAssetsManifest" AssemblyFile="$(_BlazorTasksPath)" />
|
<UsingTask TaskName="GenerateServiceWorkerAssetsManifest" AssemblyFile="$(_BlazorTasksPath)" />
|
||||||
|
|
||||||
<Target Name="_WriteServiceWorkerAssetsManifest"
|
<Target Name="_WriteServiceWorkerAssetsManifest"
|
||||||
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
Inputs="@(ServiceWorkerAssetsManifestItem)"
|
Inputs="@(ServiceWorkerAssetsManifestItem)"
|
||||||
Outputs="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
|
Outputs="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
|
||||||
BeforeTargets="_BlazorStaticWebAssetsCopyGeneratedFilesToOutputDirectory"
|
BeforeTargets="_ComputeManifestIntegrity"
|
||||||
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes; _ComputeDefaultServiceWorkerAssetsManifestVersion; _GenerateServiceWorkerIntermediateFiles">
|
DependsOnTargets="ResolveStaticWebAssetsInputs;_GenerateServiceWorkerIntermediateFiles">
|
||||||
|
|
||||||
<GenerateServiceWorkerAssetsManifest
|
<GenerateServiceWorkerAssetsManifest
|
||||||
Version="$(ServiceWorkerAssetsManifestVersion)"
|
Version="$(ServiceWorkerAssetsManifestVersion)"
|
||||||
|
|
@ -36,7 +52,52 @@
|
||||||
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="_ComputeServiceWorkerAssetsManifestFileHashes">
|
<Target Name="_ComputeManifestIntegrity"
|
||||||
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
|
BeforeTargets="_BlazorStaticWebAssetsCopyGeneratedFilesToOutputDirectory;_GzipCompressBlazorApplicationFiles">
|
||||||
|
|
||||||
|
<GetFileHash Files="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" Algorithm="SHA256" HashEncoding="base64">
|
||||||
|
<Output TaskParameter="Items" ItemName="_ServiceWorkerManifestWithHash" />
|
||||||
|
</GetFileHash>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
|
||||||
|
<_ServiceWorkerManifestWithIntegrity Include="@(_ServiceWorkerManifestWithHash)">
|
||||||
|
<Integrity>%(FileHash)</Integrity>
|
||||||
|
<IntegrityFile>$(IntermediateOutputPath)integrity\$([System.String]::Copy('%(FileHash)').Replace('/','-').Replace('+','_')).hash'</IntegrityFile>
|
||||||
|
</_ServiceWorkerManifestWithIntegrity>
|
||||||
|
|
||||||
|
<FileWrites Include="%(_ServiceWorkerManifestWithIntegrity.IntegrityFile)" />
|
||||||
|
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<WriteLinesToFile Lines="%(_ServiceWorkerManifestWithIntegrity.Integrity)" File="%(_ServiceWorkerManifestWithIntegrity.IntegrityFile)" WriteOnlyWhenDifferent="true" Overwrite="true" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<_ServiceWorkerManifestIntegrityFile>$(IntermediateOutputPath)integrity\$([System.String]::Copy('%(_ServiceWorkerManifestWithIntegrity.FileHash)').Replace('/','-').Replace('+','_')).hash'</_ServiceWorkerManifestIntegrityFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_GzipFileToPatch Include="@(_GzipBlazorFileToCompress)" Condition="'%(Identity)' == '$(_ServiceWorkerAssetsManifestFullPath)'" KeepDuplicates="false">
|
||||||
|
<InputSource>$(_ServiceWorkerManifestIntegrityFile)</InputSource>
|
||||||
|
</_GzipFileToPatch>
|
||||||
|
|
||||||
|
<_GzipBlazorFileToCompress Remove="@(_GzipFileToPatch)" />
|
||||||
|
<_GzipBlazorFileToCompress Include="@(_GzipFileToPatch)" />
|
||||||
|
|
||||||
|
<_BrotliFileToPatch Include="@(_BrotliBlazorFileToCompress)" Condition="'%(Identity)' == '$(_ServiceWorkerAssetsManifestFullPath)'" KeepDuplicates="false">
|
||||||
|
<InputSource>$(_ServiceWorkerManifestIntegrityFile)</InputSource>
|
||||||
|
</_BrotliFileToPatch>
|
||||||
|
|
||||||
|
<_BrotliBlazorFileToCompress Remove="@(_BrotliFileToPatch)" />
|
||||||
|
<_BrotliBlazorFileToCompress Include="@(_BrotliFileToPatch)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="_ComputeServiceWorkerAssetsManifestFileHashes"
|
||||||
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
|
DependsOnTargets="ResolveStaticWebAssetsInputs;_BlazorComputeOtherAssetsIntegrity">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ServiceWorkerAssetsManifestItem
|
<ServiceWorkerAssetsManifestItem
|
||||||
|
|
@ -50,20 +111,46 @@
|
||||||
|
|
||||||
<!-- Don't include the service worker files in the manifest, as the service worker doesn't need to fetch itself -->
|
<!-- Don't include the service worker files in the manifest, as the service worker doesn't need to fetch itself -->
|
||||||
<ServiceWorkerAssetsManifestItem Remove="%(_ServiceWorkerIntermediateFile.FullPath)" />
|
<ServiceWorkerAssetsManifestItem Remove="%(_ServiceWorkerIntermediateFile.FullPath)" />
|
||||||
|
|
||||||
|
<_ServiceWorkerExclude Include="@(_StaticWebAssetIntegrity)" />
|
||||||
|
<_ServiceWorkerItemBase Include="@(ServiceWorkerAssetsManifestItem)" />
|
||||||
|
<_ServiceWorkerItemBase Remove="@(_ServiceWorkerExclude)" />
|
||||||
|
<_ServiceWorkerItemHash Include="@(ServiceWorkerAssetsManifestItem)" />
|
||||||
|
<_ServiceWorkerItemHash Remove="@(_ServiceWorkerItemBase)" />
|
||||||
|
<_ServiceWorkerAssetsManifestItemWithHash Include="%(Identity)">
|
||||||
|
<AssetUrl>@(_ServiceWorkerItemHash->'%(AssetUrl)')</AssetUrl>
|
||||||
|
<Integrity>@(_StaticWebAssetIntegrity->'%(Integrity)')</Integrity>
|
||||||
|
</_ServiceWorkerAssetsManifestItemWithHash>
|
||||||
|
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="_BlazorComputeOtherAssetsIntegrity" Condition="'$(ServiceWorkerAssetsManifest)' != ''">
|
||||||
|
<ItemGroup>
|
||||||
|
<_StaticWebAssetsWithoutHash Include="@(StaticWebAsset)" Condition="'%(SourceType)' != '' or '%(ContentRoot)' == '$(_BlazorCurrentProjectWWWroot)'" />
|
||||||
|
<_StaticWebAssetsWithoutHash Remove="@(_StaticWebAssetIntegrity)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<GetFileHash Files="@(ServiceWorkerAssetsManifestItem)" Algorithm="SHA256" HashEncoding="base64">
|
<GetFileHash Files="@(_StaticWebAssetsWithoutHash)" Algorithm="SHA256" HashEncoding="base64">
|
||||||
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestItemWithHash" />
|
<Output TaskParameter="Items" ItemName="_StaticWebAssetHash" />
|
||||||
</GetFileHash>
|
</GetFileHash>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_StaticWebAssetIntegrity Include="%(_StaticWebAssetHash.Identity)">
|
||||||
|
<Integrity>%(_StaticWebAssetHash.FileHash)</Integrity>
|
||||||
|
</_StaticWebAssetIntegrity>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Compute a default ServiceWorkerAssetsManifestVersion value by combining all the asset hashes.
|
Compute a default ServiceWorkerAssetsManifestVersion value by combining all the asset hashes.
|
||||||
This is useful because then clients will only have to repopulate caches if the contents have changed.
|
This is useful because then clients will only have to repopulate caches if the contents have changed.
|
||||||
-->
|
-->
|
||||||
<Target Name="_ComputeDefaultServiceWorkerAssetsManifestVersion"
|
<Target Name="_ComputeDefaultServiceWorkerAssetsManifestVersion"
|
||||||
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes"
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
Condition="'$(ServiceWorkerAssetsManifest)' != ''">
|
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_CombinedHashIntermediatePath>$(_BlazorIntermediateOutputPath)serviceworkerhashes.txt</_CombinedHashIntermediatePath>
|
<_CombinedHashIntermediatePath>$(_BlazorIntermediateOutputPath)serviceworkerhashes.txt</_CombinedHashIntermediatePath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
@ -89,7 +176,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="_OmitServiceWorkerContent" BeforeTargets="AssignTargetPaths; ResolveCurrentProjectStaticWebAssetsInputs">
|
<Target Name="_OmitServiceWorkerContent"
|
||||||
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
|
BeforeTargets="AssignTargetPaths">
|
||||||
|
|
||||||
<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)" />
|
||||||
|
|
@ -98,30 +188,45 @@
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="_ResolveServiceWorkerOutputs"
|
<Target Name="_ResolveServiceWorkerOutputs"
|
||||||
BeforeTargets="_ResolveBlazorOutputs"
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
DependsOnTargets="_ComputeServiceWorkerOutputs">
|
BeforeTargets="_ResolveBlazorOutputs"
|
||||||
|
DependsOnTargets="_ComputeServiceWorkerOutputs">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_BlazorOutputWithTargetPath Include="@(_ServiceWorkerIntermediateFile)" />
|
<_BlazorFileCompressExclusion Include="@(_ServiceWorkerIntermediateFile->'%(FullPath)')" />
|
||||||
|
|
||||||
|
<_ServiceWorkerStaticWebAsset Include="@(_ServiceWorkerIntermediateFile->'%(FullPath)')">
|
||||||
|
<SourceType></SourceType>
|
||||||
|
<SourceId>$(PackageId)</SourceId>
|
||||||
|
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
|
||||||
|
<BasePath>$(StaticWebAssetBasePath)</BasePath>
|
||||||
|
<RelativePath>%(TargetOutputPath)</RelativePath>
|
||||||
|
</_ServiceWorkerStaticWebAsset>
|
||||||
|
|
||||||
|
<StaticWebAsset Include="@(_ServiceWorkerStaticWebAsset)" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="_ComputeServiceWorkerOutputs">
|
<Target Name="_ComputeServiceWorkerOutputs"
|
||||||
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Figure out where we're getting the content for each @(ServiceWorker) entry, depending on whether there's a PublishedContent value -->
|
<!-- Figure out where we're getting the content for each @(ServiceWorker) entry, depending on whether there's a PublishedContent value -->
|
||||||
<_ServiceWorkerIntermediateFile Include="@(ServiceWorker->'$(IntermediateOutputPath)blazor\serviceworkers\%(Identity)')">
|
<_ServiceWorkerIntermediateFile Include="@(ServiceWorker->'$(IntermediateOutputPath)blazor\serviceworkers\%(Identity)')">
|
||||||
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
|
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
|
||||||
<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)').Replace('\','/').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>
|
||||||
|
|
||||||
<Target Name="_GenerateServiceWorkerIntermediateFiles"
|
<Target Name="_GenerateServiceWorkerIntermediateFiles"
|
||||||
Inputs="@(_ServiceWorkerIntermediateFile->'%(ContentSourcePath)'); $(_CombinedHashIntermediatePath)"
|
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
|
||||||
Outputs="@(_ServiceWorkerIntermediateFile)"
|
Inputs="@(_ServiceWorkerIntermediateFile->'%(ContentSourcePath)'); $(_CombinedHashIntermediatePath)"
|
||||||
DependsOnTargets="_ComputeDefaultServiceWorkerAssetsManifestVersion">
|
Outputs="@(_ServiceWorkerIntermediateFile)"
|
||||||
|
DependsOnTargets="_ComputeDefaultServiceWorkerAssetsManifestVersion">
|
||||||
<Copy SourceFiles="%(_ServiceWorkerIntermediateFile.ContentSourcePath)" DestinationFiles="%(_ServiceWorkerIntermediateFile.Identity)" />
|
<Copy SourceFiles="%(_ServiceWorkerIntermediateFile.ContentSourcePath)" DestinationFiles="%(_ServiceWorkerIntermediateFile.Identity)" />
|
||||||
<WriteLinesToFile
|
<WriteLinesToFile
|
||||||
File="%(_ServiceWorkerIntermediateFile.Identity)"
|
File="%(_ServiceWorkerIntermediateFile.Identity)"
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<_BlazorCurrentProjectWWWroot>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)\wwwroot\'))</_BlazorCurrentProjectWWWroot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="_ResolveBlazorGeneratedAssets" DependsOnTargets="_PrepareBlazorOutputs">
|
<Target Name="_ResolveBlazorGeneratedAssets" DependsOnTargets="_PrepareBlazorOutputs">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<StaticWebAsset Include="@(_BlazorOutputWithTargetPath->'%(FullPath)')" RemoveMetadata="TargetOutputPath">
|
<_BlazorOutputCandidateAsset Include="@(_BlazorOutputWithTargetPath->'%(FullPath)')">
|
||||||
<SourceType></SourceType>
|
<SourceType></SourceType>
|
||||||
<SourceId>$(PackageId)</SourceId>
|
<SourceId>$(PackageId)</SourceId>
|
||||||
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
|
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
|
||||||
<BasePath>$(StaticWebAssetBasePath)</BasePath>
|
<BasePath>$(StaticWebAssetBasePath)</BasePath>
|
||||||
<RelativePath>$([System.String]::Copy('%(_BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/'))</RelativePath>
|
<RelativePath>$([System.String]::Copy('%(_BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/'))</RelativePath>
|
||||||
</StaticWebAsset>
|
<Integrity>%(_BlazorOutputWithTargetPath.Integrity)</Integrity>
|
||||||
|
</_BlazorOutputCandidateAsset>
|
||||||
|
|
||||||
|
<_BlazorOutputCandidateAsset Remove="@(StaticWebAsset)" />
|
||||||
|
|
||||||
|
<_StaticWebAssetIntegrity Include="@(_BlazorOutputCandidateAsset)" KeepMetadata="Integrity" />
|
||||||
|
|
||||||
|
<StaticWebAsset Include="@(_BlazorOutputCandidateAsset)" KeepMetadata="SourceType;SourceId;ContentRoot;BasePath;RelativePath" />
|
||||||
|
|
||||||
<StaticWebAsset Remove="@(StaticWebAsset)" Condition="'$(BlazorEnableDebugging)' != 'true' and '%(SourceType)' == '' and '%(Extension)' == '.pdb'" />
|
<StaticWebAsset Remove="@(StaticWebAsset)" Condition="'$(BlazorEnableDebugging)' != 'true' and '%(SourceType)' == '' and '%(Extension)' == '.pdb'" />
|
||||||
|
|
||||||
<!-- We are dependingo on a "private" property for static web assets, but this is something we can clean-up in a later release.
|
<!-- We are depending on a "private" property for static web assets, but this is something we can clean-up in a later release.
|
||||||
These files are not "external" in the "traditional" sense but it is fine for now as this is an implementation detail.
|
These files are not "external" in the "traditional" sense but it is fine for now as this is an implementation detail.
|
||||||
We only need to do this for the standalone case, for hosted scenarios this works just fine as the assets are considered
|
We only need to do this for the standalone case, for hosted scenarios this works just fine as the assets are considered
|
||||||
external. -->
|
external. -->
|
||||||
|
|
@ -39,12 +50,11 @@
|
||||||
AfterTargets="CopyFilesToOutputDirectory"
|
AfterTargets="CopyFilesToOutputDirectory"
|
||||||
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
|
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<_BlazorCurrentProjectWWWroot>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)\wwwroot\'))</_BlazorCurrentProjectWWWroot>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<_BlazorCopyLocalAssets Include="@(StaticWebAsset)" Condition="'%(SourceType)' == '' and '%(ContentRoot)' != '$(_BlazorCurrentProjectWWWroot)'" />
|
<_BlazorCopyLocalAssets
|
||||||
|
Include="@(StaticWebAsset)"
|
||||||
|
Condition="'%(SourceType)' == '' and '%(ContentRoot)' != '$(_BlazorCurrentProjectWWWroot)' and !$([System.String]::Copy('%(RelativePath)').EndsWith('.br'))" />
|
||||||
|
<_BlazorCopyLocalAssets Remove="@(_BlazorCopyLocalExclusion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Copy the blazor output files -->
|
<!-- Copy the blazor output files -->
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,18 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
||||||
Assert.Equal(frameworkFiles.Length, compressedFiles.Length);
|
Assert.Equal(frameworkFiles.Length, compressedFiles.Length);
|
||||||
Assert.Equal(frameworkFiles, compressedFiles);
|
Assert.Equal(frameworkFiles, compressedFiles);
|
||||||
|
|
||||||
|
var brotliFiles = Directory.EnumerateFiles(
|
||||||
|
compressedFilesPath,
|
||||||
|
"*",
|
||||||
|
SearchOption.AllDirectories)
|
||||||
|
.Where(f => Path.GetExtension(f) == ".br")
|
||||||
|
.Select(f => Path.GetRelativePath(compressedFilesPath, f[0..^3]))
|
||||||
|
.OrderBy(f => f)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// We don't compress things with brotli at build time
|
||||||
|
Assert.Empty(brotliFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -134,5 +146,112 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
|
|
||||||
Assert.False(Directory.Exists(compressedFilesPath));
|
Assert.False(Directory.Exists(compressedFilesPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Publish_WithLinkerAndCompression_IsIncremental()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "standalone", "razorclasslibrary" });
|
||||||
|
var result = await MSBuildProcessManager.DotnetMSBuild(project, target: "publish");
|
||||||
|
|
||||||
|
Assert.BuildPassed(result);
|
||||||
|
|
||||||
|
var buildOutputDirectory = project.BuildOutputDirectory;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var compressedFilesFolder = Path.Combine("..", "standalone", project.IntermediateOutputDirectory, "compressed");
|
||||||
|
var thumbPrint = FileThumbPrint.CreateFolderThumbprint(project, compressedFilesFolder);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
result = await MSBuildProcessManager.DotnetMSBuild(project);
|
||||||
|
Assert.BuildPassed(result);
|
||||||
|
|
||||||
|
var newThumbPrint = FileThumbPrint.CreateFolderThumbprint(project, compressedFilesFolder);
|
||||||
|
Assert.Equal(thumbPrint.Count, newThumbPrint.Count);
|
||||||
|
for (var j = 0; j < thumbPrint.Count; j++)
|
||||||
|
{
|
||||||
|
Assert.Equal(thumbPrint[j], newThumbPrint[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Publish_WithoutLinkerAndCompression_IsIncremental()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "standalone", "razorclasslibrary" });
|
||||||
|
var result = await MSBuildProcessManager.DotnetMSBuild(project, target: "publish", args: "/p:BlazorWebAssemblyEnableLinking=false");
|
||||||
|
|
||||||
|
Assert.BuildPassed(result);
|
||||||
|
|
||||||
|
var buildOutputDirectory = project.BuildOutputDirectory;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var compressedFilesFolder = Path.Combine("..", "standalone", project.IntermediateOutputDirectory, "compressed");
|
||||||
|
var thumbPrint = FileThumbPrint.CreateFolderThumbprint(project, compressedFilesFolder);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
result = await MSBuildProcessManager.DotnetMSBuild(project, args: "/p:BlazorWebAssemblyEnableLinking=false");
|
||||||
|
Assert.BuildPassed(result);
|
||||||
|
|
||||||
|
var newThumbPrint = FileThumbPrint.CreateFolderThumbprint(project, compressedFilesFolder);
|
||||||
|
Assert.Equal(thumbPrint.Count, newThumbPrint.Count);
|
||||||
|
for (var j = 0; j < thumbPrint.Count; j++)
|
||||||
|
{
|
||||||
|
Assert.Equal(thumbPrint[j], newThumbPrint[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Publish_CompressesAllFrameworkFiles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
|
||||||
|
var result = await MSBuildProcessManager.DotnetMSBuild(project, target: "publish");
|
||||||
|
|
||||||
|
Assert.BuildPassed(result);
|
||||||
|
|
||||||
|
var buildOutputDirectory = project.BuildOutputDirectory;
|
||||||
|
|
||||||
|
var extensions = new[] { ".dll", ".js", ".pdb", ".wasm", ".map", ".json" };
|
||||||
|
// Act
|
||||||
|
var compressedFilesPath = Path.Combine(
|
||||||
|
project.DirectoryPath,
|
||||||
|
"..",
|
||||||
|
"standalone",
|
||||||
|
project.IntermediateOutputDirectory,
|
||||||
|
"compressed",
|
||||||
|
"_framework");
|
||||||
|
var compressedFiles = Directory.EnumerateFiles(
|
||||||
|
compressedFilesPath,
|
||||||
|
"*",
|
||||||
|
SearchOption.AllDirectories)
|
||||||
|
.Where(f => Path.GetExtension(f) == ".br")
|
||||||
|
.Select(f => Path.GetRelativePath(compressedFilesPath, f[0..^3]))
|
||||||
|
.OrderBy(f => f)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var frameworkFilesPath = Path.Combine(
|
||||||
|
project.DirectoryPath,
|
||||||
|
project.BuildOutputDirectory,
|
||||||
|
"wwwroot",
|
||||||
|
"_framework");
|
||||||
|
var frameworkFiles = Directory.EnumerateFiles(
|
||||||
|
frameworkFilesPath,
|
||||||
|
"*",
|
||||||
|
SearchOption.AllDirectories)
|
||||||
|
.Where(f => extensions.Contains(Path.GetExtension(f)))
|
||||||
|
.Select(f => Path.GetRelativePath(frameworkFilesPath, f))
|
||||||
|
.OrderBy(f => f)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(frameworkFiles.Length, compressedFiles.Length);
|
||||||
|
Assert.Equal(frameworkFiles, compressedFiles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -211,5 +211,7 @@ $@"<Project>
|
||||||
.FirstOrDefault(f => f.Key == key)
|
.FirstOrDefault(f => f.Key == key)
|
||||||
?.Value;
|
?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => DirectoryPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Blazor.Build;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
|
using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
|
||||||
using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage;
|
using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage;
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
{
|
{
|
||||||
|
|
@ -99,6 +100,20 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
|
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
|
||||||
serviceWorkerContent: "// This is the production service worker",
|
serviceWorkerContent: "// This is the production service worker",
|
||||||
assetsManifestPath: "custom-service-worker-assets.js");
|
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]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||||
var mock = new Mock<ITaskItem>();
|
var mock = new Mock<ITaskItem>();
|
||||||
mock.Setup(m => m.GetMetadata("BootManifestResourceType")).Returns(type);
|
mock.Setup(m => m.GetMetadata("BootManifestResourceType")).Returns(type);
|
||||||
mock.Setup(m => m.GetMetadata("BootManifestResourceName")).Returns(name);
|
mock.Setup(m => m.GetMetadata("BootManifestResourceName")).Returns(name);
|
||||||
mock.Setup(m => m.GetMetadata("FileHash")).Returns(fileHash);
|
mock.Setup(m => m.GetMetadata("Integrity")).Returns(fileHash);
|
||||||
|
|
||||||
if (values != null)
|
if (values != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<AssemblyName>blazor-brotli</AssemblyName>
|
||||||
|
<IsShippingPackage>false</IsShippingPackage>
|
||||||
|
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||||
|
<MicrosoftAspNetCoreAppVersion>3.1.0</MicrosoftAspNetCoreAppVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Build.BrotliCompression
|
||||||
|
{
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
private const int _error = -1;
|
||||||
|
|
||||||
|
static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length != 1)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Invalid argument count. Usage: 'blazor-brotli <<path-to-manifest>>'");
|
||||||
|
return _error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifestPath = args[0];
|
||||||
|
if (!File.Exists(manifestPath))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Manifest '{manifestPath}' does not exist.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var manifestStream = File.OpenRead(manifestPath);
|
||||||
|
|
||||||
|
var manifest = await JsonSerializer.DeserializeAsync<ManifestData>(manifestStream);
|
||||||
|
var result = 0;
|
||||||
|
Parallel.ForEach(manifest.FilesToCompress, (file) =>
|
||||||
|
{
|
||||||
|
var inputPath = file.Source;
|
||||||
|
var inputSource = file.InputSource;
|
||||||
|
var targetCompressionPath = file.Target;
|
||||||
|
|
||||||
|
if (!File.Exists(inputSource) ||
|
||||||
|
(File.Exists(targetCompressionPath) && File.GetLastWriteTime(inputSource) > File.GetLastWriteTime(targetCompressionPath)))
|
||||||
|
{
|
||||||
|
// Incrementalism. If input source doesn't exist or it exists and is not newer than the expected output, do nothing.
|
||||||
|
if (!File.Exists(inputSource))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Skipping '{inputPath}' because '{inputSource}' does not exist.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Skipping '{inputPath}' because '{inputSource}' is newer than '{targetCompressionPath}'.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(targetCompressionPath));
|
||||||
|
|
||||||
|
using var sourceStream = File.OpenRead(inputPath);
|
||||||
|
using var fileStream = new FileStream(targetCompressionPath, FileMode.Create);
|
||||||
|
using var stream = new BrotliStream(fileStream, CompressionLevel.Optimal);
|
||||||
|
|
||||||
|
sourceStream.CopyTo(stream);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(e);
|
||||||
|
result = -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ManifestData
|
||||||
|
{
|
||||||
|
public CompressedFile[] FilesToCompress { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CompressedFile
|
||||||
|
{
|
||||||
|
public string Source { get; set; }
|
||||||
|
|
||||||
|
public string InputSource { get; set; }
|
||||||
|
|
||||||
|
public string Target { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"rollForwardOnNoCandidateFx": 2
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Server;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
|
|
@ -21,15 +22,6 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ComponentsWebAssemblyApplicationBuilderExtensions
|
public static class ComponentsWebAssemblyApplicationBuilderExtensions
|
||||||
{
|
{
|
||||||
private static readonly HashSet<StringSegment> _supportedEncodings = new HashSet<StringSegment>(StringSegmentComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
"gzip"
|
|
||||||
};
|
|
||||||
|
|
||||||
// List of encodings by preference order with their associated extension so that we can easily handle "*".
|
|
||||||
private static readonly List<(StringSegment encoding, string extension)> _preferredEncodings =
|
|
||||||
new List<(StringSegment encoding, string extension)>() { ("gzip", ".gz") };
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the application to serve Blazor WebAssembly framework files from the path <paramref name="pathPrefix"/>. This path must correspond to a referenced Blazor WebAssembly application project.
|
/// Configures the application to serve Blazor WebAssembly framework files from the path <paramref name="pathPrefix"/>. This path must correspond to a referenced Blazor WebAssembly application project.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -55,11 +47,11 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
{
|
{
|
||||||
context.Response.Headers.Append("Blazor-Environment", webHostEnvironment.EnvironmentName);
|
context.Response.Headers.Append("Blazor-Environment", webHostEnvironment.EnvironmentName);
|
||||||
|
|
||||||
// This will invoke the static files middleware plugged-in below.
|
|
||||||
NegotiateEncoding(context, webHostEnvironment);
|
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
subBuilder.UseMiddleware<ContentEncodingNegotiator>();
|
||||||
|
|
||||||
subBuilder.UseStaticFiles(options);
|
subBuilder.UseStaticFiles(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -84,6 +76,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
// We unconditionally map pdbs as there will be no pdbs in the output folder for
|
// We unconditionally map pdbs as there will be no pdbs in the output folder for
|
||||||
// release builds unless BlazorEnableDebugging is explicitly set to true.
|
// release builds unless BlazorEnableDebugging is explicitly set to true.
|
||||||
AddMapping(contentTypeProvider, ".pdb", MediaTypeNames.Application.Octet);
|
AddMapping(contentTypeProvider, ".pdb", MediaTypeNames.Application.Octet);
|
||||||
|
AddMapping(contentTypeProvider, ".br", MediaTypeNames.Application.Octet);
|
||||||
|
|
||||||
options.ContentTypeProvider = contentTypeProvider;
|
options.ContentTypeProvider = contentTypeProvider;
|
||||||
|
|
||||||
|
|
@ -96,11 +89,12 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
fileContext.Context.Response.Headers.Append(HeaderNames.CacheControl, "no-cache");
|
fileContext.Context.Response.Headers.Append(HeaderNames.CacheControl, "no-cache");
|
||||||
|
|
||||||
var requestPath = fileContext.Context.Request.Path;
|
var requestPath = fileContext.Context.Request.Path;
|
||||||
if (string.Equals(Path.GetExtension(requestPath.Value), ".gz"))
|
var fileExtension = Path.GetExtension(requestPath.Value);
|
||||||
|
if (string.Equals(fileExtension, ".gz") || string.Equals(fileExtension, ".br"))
|
||||||
{
|
{
|
||||||
// When we are serving framework files (under _framework/ we perform content negotiation
|
// When we are serving framework files (under _framework/ we perform content negotiation
|
||||||
// on the accept encoding and replace the path with <<original>>.gz if we can serve gzip
|
// on the accept encoding and replace the path with <<original>>.gz|br if we can serve gzip or brotli content
|
||||||
// content.
|
// respectively.
|
||||||
// Here we simply calculate the original content type by removing the extension and apply it
|
// Here we simply calculate the original content type by removing the extension and apply it
|
||||||
// again.
|
// again.
|
||||||
// When we revisit this, we should consider calculating the original content type and storing it
|
// When we revisit this, we should consider calculating the original content type and storing it
|
||||||
|
|
@ -123,80 +117,5 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
provider.Mappings.Add(name, mimeType);
|
provider.Mappings.Add(name, mimeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NegotiateEncoding(HttpContext context, IWebHostEnvironment webHost)
|
|
||||||
{
|
|
||||||
var accept = context.Request.Headers[HeaderNames.AcceptEncoding];
|
|
||||||
if (StringValues.IsNullOrEmpty(accept))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringWithQualityHeaderValue.TryParseList(accept, out var encodings) || encodings.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedEncoding = StringSegment.Empty;
|
|
||||||
var selectedEncodingQuality = .0;
|
|
||||||
|
|
||||||
foreach (var encoding in encodings)
|
|
||||||
{
|
|
||||||
var encodingName = encoding.Value;
|
|
||||||
var quality = encoding.Quality.GetValueOrDefault(1);
|
|
||||||
|
|
||||||
if (quality < double.Epsilon)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quality <= selectedEncodingQuality)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_supportedEncodings.Contains(encodingName))
|
|
||||||
{
|
|
||||||
selectedEncoding = encodingName;
|
|
||||||
selectedEncodingQuality = quality;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
foreach (var candidate in _preferredEncodings)
|
|
||||||
{
|
|
||||||
if (ResourceExists(context, webHost, candidate.extension))
|
|
||||||
{
|
|
||||||
selectedEncoding = candidate.encoding;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedEncodingQuality = quality;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
selectedEncoding = StringSegment.Empty;
|
|
||||||
selectedEncodingQuality = quality;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringSegment.Equals("gzip", selectedEncoding, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (ResourceExists(context, webHost, ".gz"))
|
|
||||||
{
|
|
||||||
// We only try to serve the pre-compressed file if it's actually there.
|
|
||||||
context.Request.Path = context.Request.Path + ".gz";
|
|
||||||
context.Response.Headers[HeaderNames.ContentEncoding] = "gzip";
|
|
||||||
context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.ContentEncoding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ResourceExists(HttpContext context, IWebHostEnvironment webHost, string extension) =>
|
|
||||||
webHost.WebRootFileProvider.GetFileInfo(context.Request.Path + extension).Exists;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Server
|
||||||
|
{
|
||||||
|
internal class ContentEncodingNegotiator
|
||||||
|
{
|
||||||
|
// List of encodings by preference order with their associated extension so that we can easily handle "*".
|
||||||
|
private static readonly StringSegment[] _preferredEncodings =
|
||||||
|
new StringSegment[] { "br", "gzip" };
|
||||||
|
|
||||||
|
private static readonly Dictionary<StringSegment, string> _encodingExtensionMap = new Dictionary<StringSegment, string>(StringSegmentComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["br"] = ".br",
|
||||||
|
["gzip"] = ".gz"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||||
|
|
||||||
|
public ContentEncodingNegotiator(RequestDelegate next, IWebHostEnvironment webHostEnvironment)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_webHostEnvironment = webHostEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
NegotiateEncoding(context);
|
||||||
|
return _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NegotiateEncoding(HttpContext context)
|
||||||
|
{
|
||||||
|
var accept = context.Request.Headers[HeaderNames.AcceptEncoding];
|
||||||
|
|
||||||
|
if (StringValues.IsNullOrEmpty(accept))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringWithQualityHeaderValue.TryParseList(accept, out var encodings) || encodings.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedEncoding = StringSegment.Empty;
|
||||||
|
var selectedEncodingQuality = .0;
|
||||||
|
|
||||||
|
foreach (var encoding in encodings)
|
||||||
|
{
|
||||||
|
var encodingName = encoding.Value;
|
||||||
|
var quality = encoding.Quality.GetValueOrDefault(1);
|
||||||
|
|
||||||
|
if (quality >= double.Epsilon && quality >= selectedEncodingQuality)
|
||||||
|
{
|
||||||
|
if (quality == selectedEncodingQuality)
|
||||||
|
{
|
||||||
|
selectedEncoding = PickPreferredEncoding(context, selectedEncoding, encoding);
|
||||||
|
}
|
||||||
|
else if (_encodingExtensionMap.TryGetValue(encodingName, out var encodingExtension) && ResourceExists(context, encodingExtension))
|
||||||
|
{
|
||||||
|
selectedEncoding = encodingName;
|
||||||
|
selectedEncodingQuality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// If we *, pick the first preferrent encoding for which a resource exists.
|
||||||
|
selectedEncoding = PickPreferredEncoding(context, default, encoding);
|
||||||
|
selectedEncodingQuality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
selectedEncoding = StringSegment.Empty;
|
||||||
|
selectedEncodingQuality = quality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_encodingExtensionMap.TryGetValue(selectedEncoding, out var extension))
|
||||||
|
{
|
||||||
|
context.Request.Path = context.Request.Path + extension;
|
||||||
|
context.Response.Headers[HeaderNames.ContentEncoding] = selectedEncoding.Value;
|
||||||
|
context.Response.Headers.Append(HeaderNames.Vary, HeaderNames.ContentEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
StringSegment PickPreferredEncoding(HttpContext context, StringSegment selectedEncoding, StringWithQualityHeaderValue encoding)
|
||||||
|
{
|
||||||
|
foreach (var preferredEncoding in _preferredEncodings)
|
||||||
|
{
|
||||||
|
if (preferredEncoding == selectedEncoding)
|
||||||
|
{
|
||||||
|
return selectedEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((preferredEncoding == encoding.Value || encoding.Value == "*") && ResourceExists(context, _encodingExtensionMap[preferredEncoding]))
|
||||||
|
{
|
||||||
|
return preferredEncoding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringSegment.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ResourceExists(HttpContext context, string extension) =>
|
||||||
|
_webHostEnvironment.WebRootFileProvider.GetFileInfo(context.Request.Path + extension).Exists;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
|
|
@ -12,6 +12,10 @@
|
||||||
<NoWarn>$(NoWarn);NU5100</NoWarn>
|
<NoWarn>$(NoWarn);NU5100</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.WebAssembly.Server.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
||||||
<Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" Link="Shared\DotNetMuxer.cs" />
|
<Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" Link="Shared\DotNetMuxer.cs" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
// 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.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Server.Tests
|
||||||
|
{
|
||||||
|
public class ContentEncodingNegotiatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task RespectsAcceptEncodingQuality()
|
||||||
|
{
|
||||||
|
var encoding = "gzip;q=0.5, deflate;q=0.3, br;q=0.2";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json.gz";
|
||||||
|
var expectedEncoding = "gzip";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment());
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.Equal(expectedEncoding, selectedEncoding);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
Assert.Contains(HeaderNames.ContentEncoding, varyHeader.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RespectsIdentity()
|
||||||
|
{
|
||||||
|
var encoding = "gzip;q=0.5, deflate;q=0.3, br;q=0.2, identity";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment());
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.False(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.False(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SkipsNonExistingFiles()
|
||||||
|
{
|
||||||
|
var encoding = "gzip;q=0.5, deflate;q=0.3, br";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json.gz";
|
||||||
|
var expectedEncoding = "gzip";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment(brotliExists: false));
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.Equal(expectedEncoding, selectedEncoding);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
Assert.Contains(HeaderNames.ContentEncoding, varyHeader.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UsesPreferredServerEncodingForEqualQualityValues()
|
||||||
|
{
|
||||||
|
var encoding = "gzip, deflate, br";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json.br";
|
||||||
|
var expectedEncoding = "br";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment());
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.Equal(expectedEncoding, selectedEncoding);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
Assert.Contains(HeaderNames.ContentEncoding, varyHeader.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SkipNonExistingFilesWhenSearchingForServerPreferencesPreferences()
|
||||||
|
{
|
||||||
|
var encoding = "gzip, deflate, br";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json.gz";
|
||||||
|
var expectedEncoding = "gzip";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment(brotliExists: false));
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.Equal(expectedEncoding, selectedEncoding);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
Assert.Contains(HeaderNames.ContentEncoding, varyHeader.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AnyUsesServerPreference()
|
||||||
|
{
|
||||||
|
var encoding = "*";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json.br";
|
||||||
|
var expectedEncoding = "br";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment());
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.Equal(expectedEncoding, selectedEncoding);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
Assert.Contains(HeaderNames.ContentEncoding, varyHeader.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AnySkipsNonExistingFiles()
|
||||||
|
{
|
||||||
|
var encoding = "*";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json.gz";
|
||||||
|
var expectedEncoding = "gzip";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment(brotliExists: false));
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.Equal(expectedEncoding, selectedEncoding);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
Assert.Contains(HeaderNames.ContentEncoding, varyHeader.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AnyDoesNotPickEncodingIfNoFilesFound()
|
||||||
|
{
|
||||||
|
var encoding = "*";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment(gzipExists: false, brotliExists: false));
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.False(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.False(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AnyRespectsServerPreference()
|
||||||
|
{
|
||||||
|
var encoding = "gzip;q=0.5, *;q=0.8, br;q=0.2";
|
||||||
|
var expectedPath = "/_framework/blazor.boot.json.br";
|
||||||
|
var expectedEncoding = "br";
|
||||||
|
RequestDelegate next = (ctx) => Task.CompletedTask;
|
||||||
|
|
||||||
|
var negotiator = new ContentEncodingNegotiator(next, CreateWebHostEnvironment());
|
||||||
|
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.Path = "/_framework/blazor.boot.json";
|
||||||
|
httpContext.Request.Headers.Append(HeaderNames.AcceptEncoding, encoding);
|
||||||
|
|
||||||
|
await negotiator.InvokeAsync(httpContext);
|
||||||
|
|
||||||
|
Assert.Equal(expectedPath, httpContext.Request.Path);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var selectedEncoding));
|
||||||
|
Assert.Equal(expectedEncoding, selectedEncoding);
|
||||||
|
Assert.True(httpContext.Response.Headers.TryGetValue(HeaderNames.Vary, out var varyHeader));
|
||||||
|
Assert.Contains(HeaderNames.ContentEncoding, varyHeader.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IWebHostEnvironment CreateWebHostEnvironment(bool gzipExists = true, bool brotliExists = true)
|
||||||
|
{
|
||||||
|
var gzMock = new Mock<IFileInfo>();
|
||||||
|
gzMock.Setup(m => m.Exists).Returns(gzipExists);
|
||||||
|
var brMock = new Mock<IFileInfo>();
|
||||||
|
brMock.Setup(m => m.Exists).Returns(brotliExists);
|
||||||
|
var fileProviderMock = new Mock<IFileProvider>();
|
||||||
|
fileProviderMock.Setup(f => f.GetFileInfo("/_framework/blazor.boot.json.gz")).Returns(gzMock.Object);
|
||||||
|
fileProviderMock.Setup(f => f.GetFileInfo("/_framework/blazor.boot.json.br")).Returns(brMock.Object);
|
||||||
|
|
||||||
|
var env = new Mock<IWebHostEnvironment>();
|
||||||
|
env.Setup(e => e.WebRootFileProvider).Returns(fileProviderMock.Object);
|
||||||
|
|
||||||
|
return env.Object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
|
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||||
|
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -5,6 +5,8 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -84,6 +86,7 @@ namespace Templates.Test
|
||||||
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", serverProject, aspNetProcess.Process));
|
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", serverProject, aspNetProcess.Process));
|
||||||
|
|
||||||
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
|
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
|
||||||
|
await AssertCompressionFormat(aspNetProcess, "br");
|
||||||
if (BrowserFixture.IsHostAutomationSupported())
|
if (BrowserFixture.IsHostAutomationSupported())
|
||||||
{
|
{
|
||||||
aspNetProcess.VisitInBrowser(Browser);
|
aspNetProcess.VisitInBrowser(Browser);
|
||||||
|
|
@ -95,6 +98,22 @@ namespace Templates.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task AssertCompressionFormat(AspNetProcess aspNetProcess, string expectedEncoding)
|
||||||
|
{
|
||||||
|
var response = await aspNetProcess.SendRequest(() =>
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(aspNetProcess.ListeningUri, "/_framework/blazor.boot.json"));
|
||||||
|
// These are the same as chrome
|
||||||
|
request.Headers.AcceptEncoding.Clear();
|
||||||
|
request.Headers.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("gzip"));
|
||||||
|
request.Headers.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("deflate"));
|
||||||
|
request.Headers.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("br"));
|
||||||
|
|
||||||
|
return request;
|
||||||
|
});
|
||||||
|
Assert.Equal(expectedEncoding, response.Content.Headers.ContentEncoding.Single());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BlazorWasmStandalonePwaTemplate_Works()
|
public async Task BlazorWasmStandalonePwaTemplate_Works()
|
||||||
{
|
{
|
||||||
|
|
@ -389,6 +408,8 @@ namespace Templates.Test
|
||||||
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process));
|
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process));
|
||||||
|
|
||||||
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
|
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
|
||||||
|
// We only do brotli precompression for published apps
|
||||||
|
await AssertCompressionFormat(aspNetProcess, "gzip");
|
||||||
if (BrowserFixture.IsHostAutomationSupported())
|
if (BrowserFixture.IsHostAutomationSupported())
|
||||||
{
|
{
|
||||||
aspNetProcess.VisitInBrowser(Browser);
|
aspNetProcess.VisitInBrowser(Browser);
|
||||||
|
|
|
||||||
|
|
@ -240,9 +240,16 @@ namespace Templates.Test.Helpers
|
||||||
return RequestWithRetries(client => client.GetAsync(new Uri(ListeningUri, path)), _httpClient);
|
return RequestWithRetries(client => client.GetAsync(new Uri(ListeningUri, path)), _httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal Task<HttpResponseMessage> SendRequest(Func<HttpRequestMessage> requestFactory)
|
||||||
|
{
|
||||||
|
return RequestWithRetries(client => client.SendAsync(requestFactory()), _httpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null)
|
public async Task AssertStatusCode(string requestUrl, HttpStatusCode statusCode, string acceptContentType = null)
|
||||||
{
|
{
|
||||||
var response = await RequestWithRetries(client => {
|
var response = await RequestWithRetries(client =>
|
||||||
|
{
|
||||||
var request = new HttpRequestMessage(
|
var request = new HttpRequestMessage(
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new Uri(ListeningUri, requestUrl));
|
new Uri(ListeningUri, requestUrl));
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ namespace Templates.Test.Helpers
|
||||||
await effectiveLock.WaitAsync();
|
await effectiveLock.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish -c Release /bl /nr:false {additionalArgs}", packageOptions);
|
var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish -c Release /bl:publish.binlog /nr:false {additionalArgs}", packageOptions);
|
||||||
await result.Exited;
|
await result.Exited;
|
||||||
CaptureBinLogOnFailure(result);
|
CaptureBinLogOnFailure(result);
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -515,10 +515,20 @@ namespace Templates.Test.Helpers
|
||||||
{
|
{
|
||||||
if (result.ExitCode != 0 && !string.IsNullOrEmpty(ArtifactsLogDir))
|
if (result.ExitCode != 0 && !string.IsNullOrEmpty(ArtifactsLogDir))
|
||||||
{
|
{
|
||||||
var sourceFile = Path.Combine(TemplateOutputDir, "msbuild.binlog");
|
var build = Path.Combine(TemplateOutputDir, "msbuild.binlog");
|
||||||
Assert.True(File.Exists(sourceFile), $"Log for '{ProjectName}' not found in '{sourceFile}'.");
|
var publish = Path.Combine(TemplateOutputDir, "publish.binlog");
|
||||||
var destination = Path.Combine(ArtifactsLogDir, ProjectName + ".binlog");
|
Assert.True(File.Exists(build) || File.Exists(publish), $"Log for '{ProjectName}' not found in '{build}'.");
|
||||||
File.Move(sourceFile, destination);
|
if (File.Exists(build))
|
||||||
|
{
|
||||||
|
var destination = Path.Combine(ArtifactsLogDir, ProjectName + ".binlog");
|
||||||
|
File.Move(build, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(publish))
|
||||||
|
{
|
||||||
|
var destination = Path.Combine(ArtifactsLogDir, ProjectName + ".publish.binlog");
|
||||||
|
File.Move(publish, destination);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue