* [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
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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
|
||||
{
|
||||
url = item.GetMetadata("AssetUrl"),
|
||||
hash = $"sha256-{item.GetMetadata("FileHash")}",
|
||||
hash = $"sha256-{item.GetMetadata("Integrity")}",
|
||||
}).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
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,5 +211,7 @@ $@"<Project>
|
|||
.FirstOrDefault(f => f.Key == key)
|
||||
?.Value;
|
||||
}
|
||||
|
||||
public override string ToString() => DirectoryPath;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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" />
|
||||
|
|
|
|||
|
|
@ -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.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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue