From ebf020f1afdd03197941c4757cec8c18224c905e Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 13:02:09 +0100 Subject: [PATCH 01/11] Detect pre-compressed assets --- ...ET.Sdk.StaticWebAssets.Compression.targets | 21 +++ .../DiscoverPrecompressedAssets.cs | 131 ++++++++++++++++++ .../DiscoverPrecompressedAssetsTest.cs | 109 +++++++++++++++ .../ResolveCompressedAssetsTest.cs | 70 ++++++++++ 4 files changed, 331 insertions(+) create mode 100644 src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs create mode 100644 test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets index d14cea18509d..423ba4e72fd2 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets @@ -276,6 +276,27 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + + + + + + + + + (); + + // This is a loop inside of a loop (n^2), however, we hope the first condition is a good filter to + // avoid the inner loop in most cases. + foreach (var candidate in candidates) + { + if (HasCompressionExtension(candidate.RelativePath) && + // We only care about assets that are not already considered compressed + !IsCompressedAsset(candidate) && + // The candidate doesn't already have a related asset + string.IsNullOrEmpty(candidate.RelatedAsset)) + { + Log.LogMessage( + MessageImportance.Low, + "The asset '{0}' was detected as compressed but it didn't specify a related asset.", + candidate.Identity); + var relatedAsset = FindRelatedAsset(candidate, candidates); + if (relatedAsset is null) + { + Log.LogMessage( + MessageImportance.Low, + "The asset '{0}' was detected as compressed but the related asset with relative path '{1}' was not found.", + candidate.Identity, + Path.GetFileNameWithoutExtension(candidate.RelativePath)); + continue; + } + + Log.LogMessage( + "The asset '{0}' was detected as compressed and the related asset '{1}' was found.", + candidate.Identity, + relatedAsset.Identity); + UpdateCompressedAsset(candidate, relatedAsset); + assetsToUpdate.Add(candidate.ToTaskItem()); + } + } + + DiscoveredCompressedAssets = [.. assetsToUpdate]; + + return !Log.HasLoggedErrors; + } + + private StaticWebAsset FindRelatedAsset(StaticWebAsset candidate, StaticWebAsset[] candidates) + { + // The only pattern that we support is a related asset that lives in the same directory, with the same name, + // but without the compression extension. In any other case we are not going to consider the assets related + // and an error will occur. + var identityWithoutExtension = candidate.Identity.Substring(0, candidate.Identity.Length - 3); // We take advantage we know the extension is .br or .gz. + for (int i = 0; i < candidates.Length; i++) + { + var asset = candidates[i]; + if (string.Equals(asset.Identity, identityWithoutExtension, OSPath.PathComparison)) + { + return asset; + } + } + + return null; + } + + private bool HasCompressionExtension(string relativePath) + { + return relativePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) || + relativePath.EndsWith(".br", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsCompressedAsset(StaticWebAsset asset) + => string.Equals("Content-Encoding", asset.AssetTraitName, StringComparison.Ordinal); + + private void UpdateCompressedAsset(StaticWebAsset asset, StaticWebAsset relatedAsset) + { + string fileExtension; + string assetTraitValue; + + if (!asset.RelativePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) + { + fileExtension = ".br"; + assetTraitValue = BrotliAssetTraitValue; + } + else + { + fileExtension = ".gz"; + assetTraitValue = GzipAssetTraitValue; + } + + var originalItemSpec = asset.OriginalItemSpec; + var relativePath = relatedAsset.EmbedTokens(relatedAsset.RelativePath); + + asset.RelativePath = $"{relativePath}{fileExtension}"; + asset.OriginalItemSpec = relatedAsset.Identity; + asset.RelatedAsset = relatedAsset.Identity; + asset.AssetRole = "Alternative"; + asset.AssetTraitName = "Content-Encoding"; + asset.AssetTraitValue = assetTraitValue; + } +} diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs new file mode 100644 index 000000000000..a575ff6ce6da --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.Build.Framework; +using Moq; + +namespace Microsoft.NET.Sdk.Razor.Tests; + +public class DiscoverPrecompressedAssetsTest +{ + public string ItemSpec { get; } + + public string OriginalItemSpec { get; } + + public string OutputBasePath { get; } + + public DiscoverPrecompressedAssetsTest() + { + OutputBasePath = Path.Combine(TestContext.Current.TestExecutionDirectory, nameof(ResolveCompressedAssetsTest)); + ItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); + OriginalItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); + } + + [Fact] + public void DiscoversPrecompressedAssetsCorrectly() + { + var errorMessages = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); + + var uncompressedCandidate = new StaticWebAsset + { + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js"), + RelativePath = "js/site#[.{fingerprint}]?.js", + BasePath = "_content/Test", + AssetMode = StaticWebAsset.AssetModes.All, + AssetKind = StaticWebAsset.AssetKinds.All, + AssetMergeSource = string.Empty, + SourceId = "Test", + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + Fingerprint = "uncompressed", + RelatedAsset = string.Empty, + ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"), + SourceType = StaticWebAsset.SourceTypes.Discovered, + Integrity = "uncompressed-integrity", + AssetRole = StaticWebAsset.AssetRoles.Primary, + AssetMergeBehavior = string.Empty, + AssetTraitValue = string.Empty, + AssetTraitName = string.Empty, + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js"), + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest + }; + + var compressedCandidate = new StaticWebAsset + { + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js.gz"), + RelativePath = "js/site.js#[.{fingerprint}]?.gz", + BasePath = "_content/Test", + AssetMode = StaticWebAsset.AssetModes.All, + AssetKind = StaticWebAsset.AssetKinds.All, + AssetMergeSource = string.Empty, + SourceId = "Test", + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + Fingerprint = "compressed", + RelatedAsset = string.Empty, + ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"), + SourceType = StaticWebAsset.SourceTypes.Discovered, + Integrity = "compressed-integrity", + AssetRole = StaticWebAsset.AssetRoles.Primary, + AssetMergeBehavior = string.Empty, + AssetTraitValue = string.Empty, + AssetTraitName = string.Empty, + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js.gz"), + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest + }; + + var task = new DiscoverPrecompressedAssets + { + CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()], + BuildEngine = buildEngine.Object + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.DiscoveredCompressedAssets.Should().ContainSingle(); + var asset = task.DiscoveredCompressedAssets[0]; + asset.ItemSpec.Should().Be(compressedCandidate.Identity); + asset.GetMetadata("RelatedAsset").Should().Be(uncompressedCandidate.Identity); + asset.GetMetadata("OriginalItemSpec").Should().Be(uncompressedCandidate.Identity); + asset.GetMetadata("RelativePath").Should().Be("js/site#[.{fingerprint=uncompressed}]?.js.gz"); + asset.GetMetadata("AssetRole").Should().Be("Alternative"); + asset.GetMetadata("AssetTraitName").Should().Be("Content-Encoding"); + asset.GetMetadata("AssetTraitValue").Should().Be("gzip"); + asset.GetMetadata("Fingerprint").Should().Be("compressed"); + asset.GetMetadata("Integrity").Should().Be("compressed-integrity"); + asset.GetMetadata("CopyToPublishDirectory").Should().Be("PreserveNewest"); + asset.GetMetadata("CopyToOutputDirectory").Should().Be("Never"); + asset.GetMetadata("AssetMergeSource").Should().Be(string.Empty); + asset.GetMetadata("AssetMergeBehavior").Should().Be(string.Empty); + asset.GetMetadata("AssetKind").Should().Be("All"); + asset.GetMetadata("AssetMode").Should().Be("All"); + asset.GetMetadata("SourceId").Should().Be("Test"); + asset.GetMetadata("SourceType").Should().Be("Discovered"); + asset.GetMetadata("ContentRoot").Should().Be(Path.Combine(Environment.CurrentDirectory, "wwwroot\\")); + } +} diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs index bd164de346d6..67878b28dce1 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs @@ -1,11 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.Metrics; using Microsoft.AspNetCore.StaticWebAssets.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Moq; using NuGet.ContentModel; +using NuGet.Packaging.Core; namespace Microsoft.NET.Sdk.Razor.Tests; @@ -70,6 +72,74 @@ public void ResolvesExplicitlyProvidedAssets() task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); } + [Fact] + public void InfersPreCompressedAssetsCorrectly() + { + var errorMessages = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); + + var uncompressedCandidate = new StaticWebAsset + { + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js"), + RelativePath = "js/site#[.{fingerprint}]?.js", + BasePath = "_content/Test", + AssetMode = StaticWebAsset.AssetModes.All, + AssetKind = StaticWebAsset.AssetKinds.All, + AssetMergeSource = string.Empty, + SourceId = "Test", + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + Fingerprint = "xtxxf3hu2r", + RelatedAsset = string.Empty, + ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"), + SourceType = StaticWebAsset.SourceTypes.Discovered, + Integrity = "hRQyftXiu1lLX2P9Ly9xa4gHJgLeR1uGN5qegUobtGo=", + AssetRole = StaticWebAsset.AssetRoles.Primary, + AssetMergeBehavior = string.Empty, + AssetTraitValue = string.Empty, + AssetTraitName = string.Empty, + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js"), + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest + }; + + var compressedCandidate = new StaticWebAsset + { + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js.gz"), + RelativePath = "js/site.js#[.{fingerprint}]?.gz", + BasePath = "_content/Test", + AssetMode = StaticWebAsset.AssetModes.All, + AssetKind = StaticWebAsset.AssetKinds.All, + AssetMergeSource = string.Empty, + SourceId = "Test", + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + Fingerprint = "es13vhk42b", + RelatedAsset = string.Empty, + ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"), + SourceType = StaticWebAsset.SourceTypes.Discovered, + Integrity = "zs5Fd3XI6+g9f4N1SFLVdgghuiqdvq+nETAjTbvVxx4=", + AssetRole = StaticWebAsset.AssetRoles.Primary, + AssetMergeBehavior = string.Empty, + AssetTraitValue = string.Empty, + AssetTraitName = string.Empty, + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js.gz"), + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest + }; + + var task = new ResolveCompressedAssets + { + OutputPath = OutputBasePath, + CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()], + Formats = "gzip", + BuildEngine = buildEngine.Object + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.AssetsToCompress.Should().HaveCount(0); + } + [Fact] public void ResolvesAssetsMatchingIncludePattern() { From 1f2461ad0fa77cc9d9fe9a621ebf967c3ffc03ce Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 13:20:04 +0100 Subject: [PATCH 02/11] E2E test --- ...ET.Sdk.StaticWebAssets.Compression.targets | 1 + .../AspNetSdkBaselineTest.cs | 4 +- ...aticWebAssetsCompressionIntegrationTest.cs | 61 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets index 423ba4e72fd2..83a3a083c551 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets @@ -14,6 +14,7 @@ Copyright (c) .NET Foundation. All rights reserved. + diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs index fb183f3b1acf..08edb9a72f59 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs @@ -20,10 +20,10 @@ public class AspNetSdkBaselineTest : AspNetSdkTest #if GENERATE_SWA_BASELINES public static bool GenerateBaselines = true; #else - public static bool GenerateBaselines = bool.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_TEST_BASELINES"), out var result) && result; + public static bool GenerateBaselines = true || bool.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_TEST_BASELINES"), out var result) && result; #endif - private bool _generateBaselines = GenerateBaselines; + private bool _generateBaselines = true || GenerateBaselines; public AspNetSdkBaselineTest(ITestOutputHelper log) : base(log) { diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs new file mode 100644 index 000000000000..868d2b004a7c --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.IO.Compression; +using System.Reflection; +using System.Runtime.Intrinsics.Arm; +using Microsoft.AspNetCore.StaticWebAssets.Tasks; + +namespace Microsoft.NET.Sdk.Razor.Tests +{ + public class StaticWebAssetsCompressionIntegrationTest : AspNetSdkBaselineTest + { + public StaticWebAssetsCompressionIntegrationTest(ITestOutputHelper log) : base(log, GenerateBaselines) { } + + [Fact] + public void Build_Detects_PrecompressedAssets() + { + var expectedManifest = LoadBuildManifest(); + var testAsset = "RazorAppWithP2PReference"; + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + var file = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js"); + var gzipFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.gz"); + var brotliFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.br"); + + // Compress file into gzip and brotli + using (var gzipStream = new GZipStream(File.Create(gzipFile), CompressionLevel.NoCompression)) + { + using var stream = File.OpenRead(file); + stream.CopyTo(gzipStream); + } + + using (var brotliStream = new BrotliStream(File.Create(brotliFile), CompressionLevel.NoCompression)) + { + using var stream = File.OpenRead(file); + stream.CopyTo(brotliStream); + } + + var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); + ExecuteCommand(build).Should().Pass(); + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + // GenerateStaticWebAssetsManifest should generate the manifest file. + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + new FileInfo(path).Should().Exist(); + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + AssertManifest(manifest, expectedManifest); + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + new FileInfo(finalPath).Should().Exist(); + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + AssertManifest(manifest1, expectedManifest); + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + } + } +} From 286be418f9d3405ed6a96cdb8a4f55d1eeac8d3d Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 14:14:10 +0100 Subject: [PATCH 03/11] Make sure endpoints are re-generated --- .../Microsoft.NET.Sdk.StaticWebAssets.Compression.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets index 83a3a083c551..1042c438908f 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets @@ -226,7 +226,7 @@ Copyright (c) .NET Foundation. All rights reserved. From 376733555c8bb3bae86cb933f05f9c0abec20da5 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 14:31:32 +0100 Subject: [PATCH 04/11] E2E test --- ...tects_PrecompressedAssets.Build.files.json | 4 + ...ompressedAssets.Build.staticwebassets.json | 1209 +++++++++++++++++ ...aticWebAssetsCompressionIntegrationTest.cs | 5 +- 3 files changed, 1214 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.files.json create mode 100644 test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.staticwebassets.json diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.files.json b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.files.json new file mode 100644 index 000000000000..817adc62eec6 --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.files.json @@ -0,0 +1,4 @@ +[ + "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css" +] \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.staticwebassets.json b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.staticwebassets.json new file mode 100644 index 000000000000..d228706df30c --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/Build_Detects_PrecompressedAssets.Build.staticwebassets.json @@ -0,0 +1,1209 @@ +{ + "Version": 1, + "Hash": "__hash__", + "Source": "AppWithP2PReference", + "BasePath": "_content/AppWithP2PReference", + "Mode": "Default", + "ManifestType": "Build", + "ReferencedProjectsConfiguration": [ + { + "Identity": "${ProjectPath}\\ClassLibrary\\ClassLibrary.csproj", + "Version": 2, + "Source": "ClassLibrary", + "GetPublishAssetsTargets": "ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems", + "AdditionalPublishProperties": "", + "AdditionalPublishPropertiesToRemove": "WebPublishProfileFile;TargetFramework;RuntimeIdentifier;SelfContained", + "GetBuildAssetsTargets": "GetCurrentProjectBuildStaticWebAssetItems", + "AdditionalBuildProperties": "", + "AdditionalBuildPropertiesToRemove": "WebPublishProfileFile;TargetFramework;RuntimeIdentifier;SelfContained" + } + ], + "DiscoveryPatterns": [ + { + "Name": "ClassLibrary\\wwwroot", + "Source": "ClassLibrary", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "Pattern": "**" + } + ], + "Assets": [ + { + "Identity": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "SourceId": "AppWithP2PReference", + "SourceType": "Computed", + "ContentRoot": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\", + "BasePath": "_content/AppWithP2PReference", + "RelativePath": "AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "AssetKind": "All", + "AssetMode": "CurrentProject", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz" + }, + { + "Identity": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "SourceId": "AppWithP2PReference", + "SourceType": "Computed", + "ContentRoot": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\", + "BasePath": "_content/AppWithP2PReference", + "RelativePath": "AppWithP2PReference#[.{fingerprint}]?.styles.css", + "AssetKind": "All", + "AssetMode": "CurrentProject", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "ScopedCss", + "AssetTraitValue": "ApplicationBundle", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "AssetKind": "All", + "AssetMode": "Reference", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.v4.js.gz", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "ClassLibrary#[.{fingerprint}]!.bundle.scp.css", + "AssetKind": "All", + "AssetMode": "Reference", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "ScopedCss", + "AssetTraitValue": "ProjectBundle", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.js.br", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "br", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.js.gz", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.js", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "", + "AssetTraitValue": "", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.v4.js", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "", + "AssetTraitValue": "", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js" + } + ], + "Endpoints": [ + { + "Route": "AppWithP2PReference.styles.css.gz", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "AppWithP2PReference.__fingerprint__.styles.css.gz", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "AppWithP2PReference.styles.css.gz" + } + ] + }, + { + "Route": "AppWithP2PReference.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "AppWithP2PReference.__fingerprint__.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "AppWithP2PReference.styles.css" + } + ] + }, + { + "Route": "AppWithP2PReference.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "AppWithP2PReference.__fingerprint__.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "AppWithP2PReference.styles.css" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.bundle.scp.css.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "_content/ClassLibrary/ClassLibrary.bundle.scp.css.gz" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "_content/ClassLibrary/ClassLibrary.bundle.scp.css" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.v4.js.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.v4.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "_content/ClassLibrary/ClassLibrary.bundle.scp.css" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js.br", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "br" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "br", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "br" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.v4.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs index 868d2b004a7c..cbbf94779ede 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.IO.Compression; -using System.Reflection; -using System.Runtime.Intrinsics.Arm; using Microsoft.AspNetCore.StaticWebAssets.Tasks; namespace Microsoft.NET.Sdk.Razor.Tests @@ -50,7 +47,7 @@ public void Build_Detects_PrecompressedAssets() AssertManifest(manifest, expectedManifest); // GenerateStaticWebAssetsManifest should copy the file to the output folder. - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + var finalPath = Path.Combine(outputPath, "AppWithP2PReference.staticwebassets.runtime.json"); new FileInfo(finalPath).Should().Exist(); var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); From cc4f402f4386f3a33cedafa05b7842de890f46ef Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 14:32:08 +0100 Subject: [PATCH 05/11] Clean baselines --- test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs index 08edb9a72f59..fb183f3b1acf 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/AspNetSdkBaselineTest.cs @@ -20,10 +20,10 @@ public class AspNetSdkBaselineTest : AspNetSdkTest #if GENERATE_SWA_BASELINES public static bool GenerateBaselines = true; #else - public static bool GenerateBaselines = true || bool.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_TEST_BASELINES"), out var result) && result; + public static bool GenerateBaselines = bool.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_TEST_BASELINES"), out var result) && result; #endif - private bool _generateBaselines = true || GenerateBaselines; + private bool _generateBaselines = GenerateBaselines; public AspNetSdkBaselineTest(ITestOutputHelper log) : base(log) { From 78331a9142fe39267ca4806708a7438ed976d561 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 15:37:41 +0100 Subject: [PATCH 06/11] Improved E2E test --- ...aticWebAssetsCompressionIntegrationTest.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs index cbbf94779ede..5875706d1853 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Compression; +using System.Net.Http.Headers; using Microsoft.AspNetCore.StaticWebAssets.Tasks; namespace Microsoft.NET.Sdk.Razor.Tests @@ -53,6 +54,49 @@ public void Build_Detects_PrecompressedAssets() var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); AssertManifest(manifest1, expectedManifest); AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + var manifest2 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + var standardEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, file, StringComparison.Ordinal)).ToArray(); + var gzipEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, gzipFile, StringComparison.Ordinal)).ToArray(); + var brotliEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, brotliFile, StringComparison.Ordinal)).ToArray(); + + var gzipAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, gzipFile, StringComparison.Ordinal)); + var brotliAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, brotliFile, StringComparison.Ordinal)); + + standardEndpoints.Should().HaveCount(1); + gzipEndpoints.Should().HaveCount(2); + brotliEndpoints.Should().HaveCount(2); + + var expectedWeakEndpointEtag = new EntityTagHeaderValue( + EntityTagHeaderValue.Parse(standardEndpoints.First().ResponseHeaders.Single(h => h.Name == "ETag").Value).Tag, + isWeak: true); + + foreach (var endpoint in gzipEndpoints) + { + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("gzip"); + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + etags.Where(e=> !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{gzipAsset.Integrity}\""); + if (endpoint.Route.EndsWith(".gz")) + { + continue; + } + etags.Should().Contain(expectedWeakEndpointEtag); + } + + foreach (var endpoint in brotliEndpoints) + { + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("br"); + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{brotliAsset.Integrity}\""); + if (endpoint.Route.EndsWith(".br")) + { + continue; + } + etags.Should().Contain(expectedWeakEndpointEtag); + } } } } From 179c57d4d29b3318f14c67999034ff1249ae0a33 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 15:41:19 +0100 Subject: [PATCH 07/11] Add publish test and improve the assertions --- ..._With_PrecompressedAssets.Build.files.json | 18 + ...ompressedAssets.Build.staticwebassets.json | 1209 +++++++++++++++++ ...aticWebAssetsCompressionIntegrationTest.cs | 85 ++ 3 files changed, 1312 insertions(+) create mode 100644 test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.files.json create mode 100644 test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.staticwebassets.json diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.files.json b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.files.json new file mode 100644 index 000000000000..4658dfcbf916 --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.files.json @@ -0,0 +1,18 @@ +[ + "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "${OutputPath}\\wwwroot\\AppWithP2PReference.styles.css", + "${OutputPath}\\wwwroot\\AppWithP2PReference.styles.css.br", + "${OutputPath}\\wwwroot\\AppWithP2PReference.styles.css.gz", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\ClassLibrary.__fingerprint__.bundle.scp.css", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\ClassLibrary.__fingerprint__.bundle.scp.css.br", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\ClassLibrary.__fingerprint__.bundle.scp.css.gz", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\js\\project-transitive-dep.js", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.br", + "${OutputPath}\\wwwroot\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz", + "${OutputPath}\\wwwroot\\js\\project-transitive-dep.js.br", + "${OutputPath}\\wwwroot\\js\\project-transitive-dep.js.gz" +] \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.staticwebassets.json b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.staticwebassets.json new file mode 100644 index 000000000000..d228706df30c --- /dev/null +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsBaselines/PublishWorks_With_PrecompressedAssets.Build.staticwebassets.json @@ -0,0 +1,1209 @@ +{ + "Version": 1, + "Hash": "__hash__", + "Source": "AppWithP2PReference", + "BasePath": "_content/AppWithP2PReference", + "Mode": "Default", + "ManifestType": "Build", + "ReferencedProjectsConfiguration": [ + { + "Identity": "${ProjectPath}\\ClassLibrary\\ClassLibrary.csproj", + "Version": 2, + "Source": "ClassLibrary", + "GetPublishAssetsTargets": "ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems", + "AdditionalPublishProperties": "", + "AdditionalPublishPropertiesToRemove": "WebPublishProfileFile;TargetFramework;RuntimeIdentifier;SelfContained", + "GetBuildAssetsTargets": "GetCurrentProjectBuildStaticWebAssetItems", + "AdditionalBuildProperties": "", + "AdditionalBuildPropertiesToRemove": "WebPublishProfileFile;TargetFramework;RuntimeIdentifier;SelfContained" + } + ], + "DiscoveryPatterns": [ + { + "Name": "ClassLibrary\\wwwroot", + "Source": "ClassLibrary", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "Pattern": "**" + } + ], + "Assets": [ + { + "Identity": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "SourceId": "AppWithP2PReference", + "SourceType": "Computed", + "ContentRoot": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\", + "BasePath": "_content/AppWithP2PReference", + "RelativePath": "AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "AssetKind": "All", + "AssetMode": "CurrentProject", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz" + }, + { + "Identity": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "SourceId": "AppWithP2PReference", + "SourceType": "Computed", + "ContentRoot": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\", + "BasePath": "_content/AppWithP2PReference", + "RelativePath": "AppWithP2PReference#[.{fingerprint}]?.styles.css", + "AssetKind": "All", + "AssetMode": "CurrentProject", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "ScopedCss", + "AssetTraitValue": "ApplicationBundle", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "AssetKind": "All", + "AssetMode": "Reference", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.v4.js.gz", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "ClassLibrary#[.{fingerprint}]!.bundle.scp.css", + "AssetKind": "All", + "AssetMode": "Reference", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "ScopedCss", + "AssetTraitValue": "ProjectBundle", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.js.br", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "br", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.js.gz", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Alternative", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "AssetTraitName": "Content-Encoding", + "AssetTraitValue": "gzip", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.js", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "", + "AssetTraitValue": "", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js" + }, + { + "Identity": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js", + "SourceId": "ClassLibrary", + "SourceType": "Project", + "ContentRoot": "${ProjectPath}\\ClassLibrary\\wwwroot\\", + "BasePath": "_content/ClassLibrary", + "RelativePath": "js/project-transitive-dep.v4.js", + "AssetKind": "All", + "AssetMode": "All", + "AssetRole": "Primary", + "AssetMergeBehavior": "", + "AssetMergeSource": "", + "RelatedAsset": "", + "AssetTraitName": "", + "AssetTraitValue": "", + "Fingerprint": "__fingerprint__", + "Integrity": "__integrity__", + "CopyToOutputDirectory": "Never", + "CopyToPublishDirectory": "PreserveNewest", + "OriginalItemSpec": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js" + } + ], + "Endpoints": [ + { + "Route": "AppWithP2PReference.styles.css.gz", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "AppWithP2PReference.__fingerprint__.styles.css.gz", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "AppWithP2PReference.styles.css.gz" + } + ] + }, + { + "Route": "AppWithP2PReference.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "AppWithP2PReference.__fingerprint__.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\compressed\\_content\\AppWithP2PReference\\AppWithP2PReference#[.{fingerprint=__fingerprint__}]?.styles.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "AppWithP2PReference.styles.css" + } + ] + }, + { + "Route": "AppWithP2PReference.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "AppWithP2PReference.__fingerprint__.styles.css", + "AssetFile": "${ProjectPath}\\AppWithP2PReference\\obj\\Debug\\${Tfm}\\scopedcss\\bundle\\AppWithP2PReference.styles.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Link", + "Value": "\u003C_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css\u003E; rel=\u0022preload\u0022; as=\u0022style\u0022" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "AppWithP2PReference.styles.css" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.bundle.scp.css.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "_content/ClassLibrary/ClassLibrary.bundle.scp.css.gz" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\ClassLibrary#[.{fingerprint=__fingerprint__}]!.bundle.scp.css.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "_content/ClassLibrary/ClassLibrary.bundle.scp.css" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.v4.js.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.v4.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\compressed\\_content\\ClassLibrary\\js\\project-transitive-dep.v4.js.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/ClassLibrary.__fingerprint__.bundle.scp.css", + "AssetFile": "${ProjectPath}\\ClassLibrary\\obj\\Debug\\${Tfm}\\scopedcss\\projectbundle\\ClassLibrary.bundle.scp.css", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "max-age=31536000, immutable" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/css" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "fingerprint", + "Value": "__fingerprint__" + }, + { + "Name": "integrity", + "Value": "__integrity__" + }, + { + "Name": "label", + "Value": "_content/ClassLibrary/ClassLibrary.bundle.scp.css" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js.br", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "br" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.br", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "br", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "br" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js.gz", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\_content\\ClassLibrary\\js\\project-transitive-dep.js.gz", + "Selectors": [ + { + "Name": "Content-Encoding", + "Value": "gzip", + "Quality": "__quality__" + } + ], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Encoding", + "Value": "gzip" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + }, + { + "Name": "Vary", + "Value": "Content-Encoding" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.js", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + }, + { + "Route": "_content/ClassLibrary/js/project-transitive-dep.v4.js", + "AssetFile": "${ProjectPath}\\ClassLibrary\\wwwroot\\js\\project-transitive-dep.v4.js", + "Selectors": [], + "ResponseHeaders": [ + { + "Name": "Accept-Ranges", + "Value": "bytes" + }, + { + "Name": "Cache-Control", + "Value": "no-cache" + }, + { + "Name": "Content-Length", + "Value": "__content-length__" + }, + { + "Name": "Content-Type", + "Value": "text/javascript" + }, + { + "Name": "ETag", + "Value": "__etag__" + }, + { + "Name": "Last-Modified", + "Value": "__last-modified__" + } + ], + "EndpointProperties": [ + { + "Name": "integrity", + "Value": "__integrity__" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs index 5875706d1853..94de9a5784dc 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsCompressionIntegrationTest.cs @@ -98,5 +98,90 @@ public void Build_Detects_PrecompressedAssets() etags.Should().Contain(expectedWeakEndpointEtag); } } + + [Fact] + public void PublishWorks_With_PrecompressedAssets() + { + var expectedManifest = LoadBuildManifest(); + var testAsset = "RazorAppWithP2PReference"; + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + var file = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js"); + var gzipFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.gz"); + var brotliFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.br"); + + // Compress file into gzip and brotli + using (var gzipStream = new GZipStream(File.Create(gzipFile), CompressionLevel.NoCompression)) + { + using var stream = File.OpenRead(file); + stream.CopyTo(gzipStream); + } + + using (var brotliStream = new BrotliStream(File.Create(brotliFile), CompressionLevel.NoCompression)) + { + using var stream = File.OpenRead(file); + stream.CopyTo(brotliStream); + } + + var build = CreatePublishCommand(ProjectDirectory, "AppWithP2PReference"); + ExecuteCommand(build).Should().Pass(); + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + // GenerateStaticWebAssetsManifest should generate the manifest file. + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + new FileInfo(path).Should().Exist(); + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + AssertManifest(manifest, expectedManifest); + + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + AssertManifest(manifest1, expectedManifest); + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + var manifest2 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"))); + + var standardEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, file, StringComparison.Ordinal)).ToArray(); + var gzipEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, gzipFile, StringComparison.Ordinal)).ToArray(); + var brotliEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, brotliFile, StringComparison.Ordinal)).ToArray(); + + var gzipAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, gzipFile, StringComparison.Ordinal)); + var brotliAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, brotliFile, StringComparison.Ordinal)); + + standardEndpoints.Should().HaveCount(1); + gzipEndpoints.Should().HaveCount(2); + brotliEndpoints.Should().HaveCount(2); + + var expectedWeakEndpointEtag = new EntityTagHeaderValue( + EntityTagHeaderValue.Parse(standardEndpoints.First().ResponseHeaders.Single(h => h.Name == "ETag").Value).Tag, + isWeak: true); + + foreach (var endpoint in gzipEndpoints) + { + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("gzip"); + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{gzipAsset.Integrity}\""); + if (endpoint.Route.EndsWith(".gz")) + { + continue; + } + etags.Should().Contain(expectedWeakEndpointEtag); + } + + foreach (var endpoint in brotliEndpoints) + { + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("br"); + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{brotliAsset.Integrity}\""); + if (endpoint.Route.EndsWith(".br")) + { + continue; + } + etags.Should().Contain(expectedWeakEndpointEtag); + } + } } } From 1b958c8b6e623b336e8c784bd87fcf2fd3548b91 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 15:43:27 +0100 Subject: [PATCH 08/11] Fix test --- .../StaticWebAssets/DiscoverPrecompressedAssetsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs index a575ff6ce6da..e4019c5b1da9 100644 --- a/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs @@ -104,6 +104,6 @@ public void DiscoversPrecompressedAssetsCorrectly() asset.GetMetadata("AssetMode").Should().Be("All"); asset.GetMetadata("SourceId").Should().Be("Test"); asset.GetMetadata("SourceType").Should().Be("Discovered"); - asset.GetMetadata("ContentRoot").Should().Be(Path.Combine(Environment.CurrentDirectory, "wwwroot\\")); + asset.GetMetadata("ContentRoot").Should().Be(Path.Combine(Environment.CurrentDirectory, $"wwwroot{Path.DirectorySeparatorChar}")); } } From 638635a9e278b9694eb470d0cb4a5f660cf2ef19 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 20:33:56 +0100 Subject: [PATCH 09/11] Switch to use a Dictionary --- .../Compression/DiscoverPrecompressedAssets.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs index 08948393da75..069b1e1f9a74 100644 --- a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs @@ -36,6 +36,8 @@ public override bool Execute() var candidates = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToArray(); var assetsToUpdate = new List(); + var candidatesByIdentity = candidates.ToDictionary(asset => asset.Identity, OSPath.PathComparer); + // This is a loop inside of a loop (n^2), however, we hope the first condition is a good filter to // avoid the inner loop in most cases. foreach (var candidate in candidates) @@ -50,7 +52,7 @@ public override bool Execute() MessageImportance.Low, "The asset '{0}' was detected as compressed but it didn't specify a related asset.", candidate.Identity); - var relatedAsset = FindRelatedAsset(candidate, candidates); + var relatedAsset = FindRelatedAsset(candidate, candidatesByIdentity); if (relatedAsset is null) { Log.LogMessage( @@ -75,22 +77,13 @@ public override bool Execute() return !Log.HasLoggedErrors; } - private StaticWebAsset FindRelatedAsset(StaticWebAsset candidate, StaticWebAsset[] candidates) + private StaticWebAsset FindRelatedAsset(StaticWebAsset candidate, IDictionary candidates) { // The only pattern that we support is a related asset that lives in the same directory, with the same name, // but without the compression extension. In any other case we are not going to consider the assets related // and an error will occur. var identityWithoutExtension = candidate.Identity.Substring(0, candidate.Identity.Length - 3); // We take advantage we know the extension is .br or .gz. - for (int i = 0; i < candidates.Length; i++) - { - var asset = candidates[i]; - if (string.Equals(asset.Identity, identityWithoutExtension, OSPath.PathComparison)) - { - return asset; - } - } - - return null; + return candidates.TryGetValue(identityWithoutExtension, out var relatedAsset) ? relatedAsset : null; } private bool HasCompressionExtension(string relativePath) From ff4758b7e4313a06584087bb674675de567b8f58 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 20:35:44 +0100 Subject: [PATCH 10/11] Cleanup --- .../Tasks/Compression/DiscoverPrecompressedAssets.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs index 069b1e1f9a74..bbad09d2700a 100644 --- a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs @@ -1,22 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Security.Cryptography; using Microsoft.Build.Framework; -using Microsoft.Extensions.FileSystemGlobbing; namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; public class DiscoverPrecompressedAssets : Task { - private static readonly char[] PatternSeparator = [';']; - private const string GzipAssetTraitValue = "gzip"; private const string BrotliAssetTraitValue = "br"; - private const string GzipFormatName = "gzip"; - private const string BrotliFormatName = "brotli"; - public ITaskItem[] CandidateAssets { get; set; } [Output] From b7d241678c672ff33d63c58cfd4d71eb67c11ea3 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 20 Nov 2024 20:46:29 +0100 Subject: [PATCH 11/11] Remove unnecessary comment --- .../Tasks/Compression/DiscoverPrecompressedAssets.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs index bbad09d2700a..a4a7357fa0c6 100644 --- a/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/Compression/DiscoverPrecompressedAssets.cs @@ -31,8 +31,6 @@ public override bool Execute() var candidatesByIdentity = candidates.ToDictionary(asset => asset.Identity, OSPath.PathComparer); - // This is a loop inside of a loop (n^2), however, we hope the first condition is a good filter to - // avoid the inner loop in most cases. foreach (var candidate in candidates) { if (HasCompressionExtension(candidate.RelativePath) &&