[Fixes #19644][Blazor] Support brotli compression for framework files (#20363)

* [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:
Javier Calvarro Nelson 2020-04-06 17:29:24 +02:00 committed by GitHub
parent fd9c786165
commit 8232c6a4d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1261 additions and 202 deletions

View File

@ -47,7 +47,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAssembly.JSInterop", "WebAssembly.JSInterop", "{37FA056D-A7B3-4F72-A8B9-8D3C175E831E}"
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
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{7920B09F-8016-49CF-A229-E72D0CECDD17}"
EndProject
@ -105,7 +105,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Authentication.We
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebugProxy", "DebugProxy", "{96DE9B14-D81F-422E-A33A-728BFB9C153A}"
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
Global
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -591,6 +621,9 @@ Global
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43} = {E4D756A7-A934-4D7F-BC6E-7B95FE4098AB}
{96DE9B14-D81F-422E-A33A-728BFB9C153A} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
{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
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net46</TargetFrameworks>
@ -70,4 +70,36 @@
</Copy>
</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>

View File

@ -10,6 +10,8 @@
$CommonFileElements$
<file src="..\..\..\THIRD-PARTY-NOTICES.txt" />
<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="$taskskDir$\**" target="tools/" />
<file src="..\..\..\Web.JS\dist\$configuration$\blazor.webassembly.js" target="tools/blazor" />

View File

@ -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}\"";
}
}

View File

@ -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;
}
}
}

View File

@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
var resourceName = GetResourceName(resource);
if (!resourceList.ContainsKey(resourceName))
{
resourceList.Add(resourceName, $"sha256-{resource.GetMetadata("FileHash")}");
resourceList.Add(resourceName, $"sha256-{resource.GetMetadata("Integrity")}");
}
}
}

View File

@ -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; }
}
}
}

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
assets = AssetsWithHashes.Select(item => new AssetsManifestFileEntry
{
url = item.GetMetadata("AssetUrl"),
hash = $"sha256-{item.GetMetadata("FileHash")}",
hash = $"sha256-{item.GetMetadata("Integrity")}",
}).ToArray()
};

View File

@ -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; }
}
}
}

View File

@ -6,7 +6,7 @@
<Target
Name="_PrepareBlazorOutputs"
DependsOnTargets="_ResolveBlazorInputs;_ResolveBlazorOutputs;_GenerateBlazorBootJson">
DependsOnTargets="_ResolveBlazorInputs;_ResolveBlazorOutputs;_GenerateBlazorBootJson;_GenerateBlazorBootJsonIntegrity">
</Target>
<Target Name="_ResolveBlazorInputs" DependsOnTargets="ResolveReferences;ResolveRuntimePackAssets">
@ -33,6 +33,16 @@
<_BlazorApplicationAssembliesCacheFile>$(_BlazorIntermediateOutputPath)unlinked.output</_BlazorApplicationAssembliesCacheFile>
</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>
<_WebAssemblyBCLFolder Include="
$(ComponentsWebAssemblyBaseClassLibraryPath);
@ -123,6 +133,7 @@
<_BlazorOutputWithTargetPath Include="@(_BlazorJSFile)">
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</_BlazorOutputWithTargetPath>
</ItemGroup>
<!--
@ -133,6 +144,33 @@
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
<_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
</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>
<!--
@ -233,16 +271,6 @@
<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
ILLinkPath="$(ComponentsWebAssemblyLinkerPath)"
AssemblyPaths="@(_BlazorAssemblyToLink)"
@ -360,23 +388,41 @@
<_BlazorBootResource Include="@(_BlazorOutputWithTargetPath->HasMetadata('BootManifestResourceType'))" />
</ItemGroup>
<GetFileHash Files="@(_BlazorBootResource)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_BlazorBootResourceWithHash" />
</GetFileHash>
<GenerateBlazorBootJson
AssemblyPath="@(IntermediateAssembly)"
Resources="@(_BlazorBootResourceWithHash)"
Resources="@(_BlazorBootResource)"
DebugBuild="$(_IsDebugBuild)"
LinkerEnabled="$(BlazorWebAssemblyEnableLinking)"
CacheBootResources="$(BlazorCacheBootResources)"
OutputPath="$(_BlazorBootJsonIntermediateOutputPath)"
ConfigurationFiles="@(_BlazorConfigFile)" />
</Target>
<Target Name="_GenerateBlazorBootJsonIntegrity">
<GetFileHash Files="$(_BlazorBootJsonIntermediateOutputPath)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_BlazorBootJsonWithHash" />
</GetFileHash>
<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="%(_BlazorBootJsonWithIntegrity.IntegrityFile)" />
</ItemGroup>
<WriteLinesToFile Lines="%(_BlazorBootJsonWithIntegrity.Integrity)" File="%(_BlazorBootJsonWithIntegrity.IntegrityFile)" WriteOnlyWhenDifferent="true" Overwrite="true" />
</Target>
</Project>

View File

@ -1,9 +1,10 @@
<Project>
<PropertyGroup>
<_BlazorBrotliPath>$(_BlazorToolsDir)compression\blazor-brotli.dll</_BlazorBrotliPath>
<ResolveCurrentProjectStaticWebAssetsDependsOn>
$(ResolveCurrentProjectStaticWebAssetsDependsOn);
_CompressBlazorApplicationFiles;
_ResolveBlazorFilesToCompress;
</ResolveCurrentProjectStaticWebAssetsDependsOn>
</PropertyGroup>
@ -12,16 +13,41 @@
<PropertyGroup>
<_BlazorFilesIntermediateOutputPath>$(IntermediateOutputPath)compressed\</_BlazorFilesIntermediateOutputPath>
<_GzipCompressionBlazorApplicationFilesManifestPath>$(IntermediateOutputPath)compressed\gzip.manifest.json</_GzipCompressionBlazorApplicationFilesManifestPath>
<_BrotliCompressionBlazorApplicationFilesManifestPath>$(IntermediateOutputPath)compressed\brotli.manifest.json</_BrotliCompressionBlazorApplicationFilesManifestPath>
</PropertyGroup>
<MakeDir Directories="$(_BlazorFilesIntermediateOutputPath)" />
<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>
<TargetOutputPath>%(RelativePath).gz</TargetOutputPath>
<RelativePath>%(RelativePath).gz</RelativePath>
<InputSource>$([MSBuild]::NormalizePath('$(_BlazorFilesIntermediateOutputPath)%(RelativePath).hash'))</InputSource>
</_BlazorFileToCompress>
</_GzipBlazorFileToCompress>
<_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" />
@ -30,28 +56,53 @@
</ItemGroup>
<GetFileHash Files="@(_BlazorFileToCompress)">
<Output TaskParameter="Items" ItemName="_LinkerOutputHashes" />
</GetFileHash>
</Target>
<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>
<FileWrites Include="%(_LinkerOutputHashes.InputSource)" />
</ItemGroup>
<GzipCompressBlazorApplicationFiles ManifestPath="$(_GzipCompressionBlazorApplicationFilesManifestPath)" />
</Target>
<UsingTask TaskName="CompressBlazorApplicationFiles" AssemblyFile="$(_BlazorTasksPath)" />
<Target
Name="_GenerateGzipCompressionBlazorApplicationFilesManifest"
BeforeTargets="_GzipCompressBlazorApplicationFiles"
Inputs="@(_GzipBlazorFileToCompress->'%(InputSource)')"
Outputs="$(_GzipCompressionBlazorApplicationFilesManifestPath)">
<GenerateBlazorCompressionManifest FilesToCompress="@(_GzipBlazorFileToCompress)" ManifestPath="$(_GzipCompressionBlazorApplicationFilesManifestPath)" />
</Target>
<Target
Name="_CompressBlazorApplicationFiles"
AfterTargets="_ResolveBlazorFilesToCompress"
Inputs="%(_BlazorFileToCompress.InputSource)"
Outputs="%(_BlazorFileToCompress.TargetCompressionPath)">
Name="_BrotliCompressBlazorApplicationFiles"
BeforeTargets="GetCopyToPublishDirectoryItems;_CopyResolvedFilesToPublishPreserveNewest"
DependsOnTargets="ResolveStaticWebAssetsInputs"
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>
</Project>

View File

@ -1,17 +1,33 @@
<Project>
<Target Name="_ComputeServiceWorkerAssetsManifestInputs"
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
BeforeTargets="_ResolveBlazorOutputs">
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
BeforeTargets="_ResolveBlazorOutputs;_ResolveBlazorFilesToCompress">
<PropertyGroup>
<_ServiceWorkerAssetsManifestIntermediateOutputPath>$(_BlazorIntermediateOutputPath)serviceworkerassets.js</_ServiceWorkerAssetsManifestIntermediateOutputPath>
<_ServiceWorkerAssetsManifestIntermediateOutputPath>$(_BlazorIntermediateOutputPath)$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath>
<_ServiceWorkerAssetsManifestFullPath>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(_ServiceWorkerAssetsManifestIntermediateOutputPath)'))</_ServiceWorkerAssetsManifestFullPath>
</PropertyGroup>
<ItemGroup>
<_BlazorOutputWithTargetPath Condition="'$(ServiceWorkerAssetsManifest)' != ''"
Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
TargetOutputPath="$(_BaseBlazorDistPath)$(ServiceWorkerAssetsManifest)" />
<_BlazorOutputWithTargetPath
Include="$(_ServiceWorkerAssetsManifestFullPath)"
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>
</Target>
@ -19,11 +35,11 @@
<UsingTask TaskName="GenerateServiceWorkerAssetsManifest" AssemblyFile="$(_BlazorTasksPath)" />
<Target Name="_WriteServiceWorkerAssetsManifest"
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
Inputs="@(ServiceWorkerAssetsManifestItem)"
Outputs="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
BeforeTargets="_BlazorStaticWebAssetsCopyGeneratedFilesToOutputDirectory"
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes; _ComputeDefaultServiceWorkerAssetsManifestVersion; _GenerateServiceWorkerIntermediateFiles">
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
Inputs="@(ServiceWorkerAssetsManifestItem)"
Outputs="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
BeforeTargets="_ComputeManifestIntegrity"
DependsOnTargets="ResolveStaticWebAssetsInputs;_GenerateServiceWorkerIntermediateFiles">
<GenerateServiceWorkerAssetsManifest
Version="$(ServiceWorkerAssetsManifestVersion)"
@ -36,7 +52,52 @@
</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>
<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 -->
<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>
<GetFileHash Files="@(ServiceWorkerAssetsManifestItem)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestItemWithHash" />
<GetFileHash Files="@(_StaticWebAssetsWithoutHash)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_StaticWebAssetHash" />
</GetFileHash>
<ItemGroup>
<_StaticWebAssetIntegrity Include="%(_StaticWebAssetHash.Identity)">
<Integrity>%(_StaticWebAssetHash.FileHash)</Integrity>
</_StaticWebAssetIntegrity>
</ItemGroup>
</Target>
<!--
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.
-->
<Target Name="_ComputeDefaultServiceWorkerAssetsManifestVersion"
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes"
Condition="'$(ServiceWorkerAssetsManifest)' != ''">
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes">
<PropertyGroup>
<_CombinedHashIntermediatePath>$(_BlazorIntermediateOutputPath)serviceworkerhashes.txt</_CombinedHashIntermediatePath>
</PropertyGroup>
@ -89,7 +176,10 @@
</PropertyGroup>
</Target>
<Target Name="_OmitServiceWorkerContent" BeforeTargets="AssignTargetPaths; ResolveCurrentProjectStaticWebAssetsInputs">
<Target Name="_OmitServiceWorkerContent"
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
BeforeTargets="AssignTargetPaths">
<ItemGroup>
<!-- Don't emit the service worker source files to the output -->
<Content Remove="@(ServiceWorker)" />
@ -98,30 +188,45 @@
</Target>
<Target Name="_ResolveServiceWorkerOutputs"
BeforeTargets="_ResolveBlazorOutputs"
DependsOnTargets="_ComputeServiceWorkerOutputs">
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
BeforeTargets="_ResolveBlazorOutputs"
DependsOnTargets="_ComputeServiceWorkerOutputs">
<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>
</Target>
<Target Name="_ComputeServiceWorkerOutputs">
<Target Name="_ComputeServiceWorkerOutputs"
Condition="'$(ServiceWorkerAssetsManifest)' != ''">
<ItemGroup>
<!-- 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)')">
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
<TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath>
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot\'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot/'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').Replace('\','/').StartsWith('wwwroot/'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
</_ServiceWorkerIntermediateFile>
</ItemGroup>
</Target>
<Target Name="_GenerateServiceWorkerIntermediateFiles"
Inputs="@(_ServiceWorkerIntermediateFile->'%(ContentSourcePath)'); $(_CombinedHashIntermediatePath)"
Outputs="@(_ServiceWorkerIntermediateFile)"
DependsOnTargets="_ComputeDefaultServiceWorkerAssetsManifestVersion">
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
Inputs="@(_ServiceWorkerIntermediateFile->'%(ContentSourcePath)'); $(_CombinedHashIntermediatePath)"
Outputs="@(_ServiceWorkerIntermediateFile)"
DependsOnTargets="_ComputeDefaultServiceWorkerAssetsManifestVersion">
<Copy SourceFiles="%(_ServiceWorkerIntermediateFile.ContentSourcePath)" DestinationFiles="%(_ServiceWorkerIntermediateFile.Identity)" />
<WriteLinesToFile
File="%(_ServiceWorkerIntermediateFile.Identity)"

View File

@ -1,18 +1,29 @@
<Project>
<PropertyGroup>
<_BlazorCurrentProjectWWWroot>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)\wwwroot\'))</_BlazorCurrentProjectWWWroot>
</PropertyGroup>
<Target Name="_ResolveBlazorGeneratedAssets" DependsOnTargets="_PrepareBlazorOutputs">
<ItemGroup>
<StaticWebAsset Include="@(_BlazorOutputWithTargetPath->'%(FullPath)')" RemoveMetadata="TargetOutputPath">
<_BlazorOutputCandidateAsset Include="@(_BlazorOutputWithTargetPath->'%(FullPath)')">
<SourceType></SourceType>
<SourceId>$(PackageId)</SourceId>
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
<BasePath>$(StaticWebAssetBasePath)</BasePath>
<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'" />
<!-- 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.
We only need to do this for the standalone case, for hosted scenarios this works just fine as the assets are considered
external. -->
@ -39,12 +50,11 @@
AfterTargets="CopyFilesToOutputDirectory"
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
<PropertyGroup>
<_BlazorCurrentProjectWWWroot>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)\wwwroot\'))</_BlazorCurrentProjectWWWroot>
</PropertyGroup>
<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>
<!-- Copy the blazor output files -->

View File

@ -113,6 +113,18 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.Equal(frameworkFiles.Length, compressedFiles.Length);
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]
@ -134,5 +146,112 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
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);
}
}
}

View File

@ -211,5 +211,7 @@ $@"<Project>
.FirstOrDefault(f => f.Key == key)
?.Value;
}
public override string ToString() => DirectoryPath;
}
}

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Blazor.Build;
using Xunit;
using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage;
using System.IO.Compression;
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
{
@ -99,6 +100,20 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
serviceWorkerContent: "// This is the production service worker",
assetsManifestPath: "custom-service-worker-assets.js");
VerifyCompression(result, blazorPublishDirectory);
}
private static void VerifyCompression(MSBuildResult result, string blazorPublishDirectory)
{
var original = Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
var compressed = Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json.br");
using var brotliStream = new BrotliStream(File.OpenRead(compressed), CompressionMode.Decompress);
using var textReader = new StreamReader(brotliStream);
var uncompressedText = textReader.ReadToEnd();
var originalText = File.ReadAllText(original);
Assert.Equal(originalText, uncompressedText);
}
[Fact]

View File

@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
var mock = new Mock<ITaskItem>();
mock.Setup(m => m.GetMetadata("BootManifestResourceType")).Returns(type);
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)
{

View File

@ -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>

View File

@ -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; }
}
}
}

View File

@ -0,0 +1,3 @@
{
"rollForwardOnNoCandidateFx": 2
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Mime;
using Microsoft.AspNetCore.Components.WebAssembly.Server;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
@ -21,15 +22,6 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
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>
/// 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>
@ -55,11 +47,11 @@ namespace Microsoft.AspNetCore.Builder
{
context.Response.Headers.Append("Blazor-Environment", webHostEnvironment.EnvironmentName);
// This will invoke the static files middleware plugged-in below.
NegotiateEncoding(context, webHostEnvironment);
await next();
});
subBuilder.UseMiddleware<ContentEncodingNegotiator>();
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
// release builds unless BlazorEnableDebugging is explicitly set to true.
AddMapping(contentTypeProvider, ".pdb", MediaTypeNames.Application.Octet);
AddMapping(contentTypeProvider, ".br", MediaTypeNames.Application.Octet);
options.ContentTypeProvider = contentTypeProvider;
@ -96,11 +89,12 @@ namespace Microsoft.AspNetCore.Builder
fileContext.Context.Response.Headers.Append(HeaderNames.CacheControl, "no-cache");
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
// on the accept encoding and replace the path with <<original>>.gz if we can serve gzip
// content.
// on the accept encoding and replace the path with <<original>>.gz|br if we can serve gzip or brotli content
// respectively.
// Here we simply calculate the original content type by removing the extension and apply it
// again.
// 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);
}
}
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;
}
}

View File

@ -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;
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@ -12,6 +12,10 @@
<NoWarn>$(NoWarn);NU5100</NoWarn>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.WebAssembly.Server.Tests" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
<Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" Link="Shared\DotNetMuxer.cs" />

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -5,6 +5,8 @@ using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
@ -84,6 +86,7 @@ namespace Templates.Test
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", serverProject, aspNetProcess.Process));
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
await AssertCompressionFormat(aspNetProcess, "br");
if (BrowserFixture.IsHostAutomationSupported())
{
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]
public async Task BlazorWasmStandalonePwaTemplate_Works()
{
@ -389,6 +408,8 @@ namespace Templates.Test
ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process));
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
// We only do brotli precompression for published apps
await AssertCompressionFormat(aspNetProcess, "gzip");
if (BrowserFixture.IsHostAutomationSupported())
{
aspNetProcess.VisitInBrowser(Browser);

View File

@ -240,9 +240,16 @@ namespace Templates.Test.Helpers
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)
{
var response = await RequestWithRetries(client => {
var response = await RequestWithRetries(client =>
{
var request = new HttpRequestMessage(
HttpMethod.Get,
new Uri(ListeningUri, requestUrl));

View File

@ -120,7 +120,7 @@ namespace Templates.Test.Helpers
await effectiveLock.WaitAsync();
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;
CaptureBinLogOnFailure(result);
return result;
@ -515,10 +515,20 @@ namespace Templates.Test.Helpers
{
if (result.ExitCode != 0 && !string.IsNullOrEmpty(ArtifactsLogDir))
{
var sourceFile = Path.Combine(TemplateOutputDir, "msbuild.binlog");
Assert.True(File.Exists(sourceFile), $"Log for '{ProjectName}' not found in '{sourceFile}'.");
var destination = Path.Combine(ArtifactsLogDir, ProjectName + ".binlog");
File.Move(sourceFile, destination);
var build = Path.Combine(TemplateOutputDir, "msbuild.binlog");
var publish = Path.Combine(TemplateOutputDir, "publish.binlog");
Assert.True(File.Exists(build) || File.Exists(publish), $"Log for '{ProjectName}' not found in '{build}'.");
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);
}
}
}