From 5a9b471c5d0580b508d045be93f2380a4b85c639 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 10 Apr 2026 23:29:09 +0200 Subject: [PATCH 1/2] Remove Identity-without-backing-file quirk from Static Web Assets - Simplify ComputeCandidateIdentity to always use the real file path as identity, instead of synthesizing a path under the content root when the file is not under it - Remove CopyCandidates output from DefineStaticWebAssets and all supporting infrastructure (CopyCandidate class, cache tracking) - Simplify ResolveFile to only use Identity (remove OriginalItemSpec fallback) - Simplify CollectStaticWebAssetsToCopy to always use asset.Identity as source (remove IsComputed branch with file-existence checks) - Simplify ServiceWorker.targets to always use Identity (remove Exists('%(Identity)') / OriginalItemSpec fallback pattern) - Simplify AssetToCompress to only use RelatedAsset (remove RelatedAssetOriginalItemSpec fallback) - Remove RelatedAssetOriginalItemSpec metadata from ResolveCompressedAssets - Fix _ComputeScopedCssStaticWebAssets to depend on _ProcessScopedCssFiles ensuring scoped CSS output files exist before asset definition - Update test for new identity behavior --- ....NET.Sdk.StaticWebAssets.ScopedCss.targets | 2 +- ....Sdk.StaticWebAssets.ServiceWorker.targets | 3 +- .../Tasks/CollectStaticWebAssetsToCopy.cs | 25 +---- .../Compression/ResolveCompressedAssets.cs | 1 - .../Tasks/Data/StaticWebAsset.cs | 23 ++--- .../Tasks/DefineStaticWebAssets.Cache.cs | 32 +------ .../Tasks/DefineStaticWebAssets.cs | 91 +++---------------- .../Tasks/Utils/AssetToCompress.cs | 17 +--- .../DiscoverStaticWebAssetsTest.cs | 23 ++--- 9 files changed, 36 insertions(+), 181 deletions(-) diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets index e04fa4a14ec3..36e8342381f2 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ScopedCss.targets @@ -371,7 +371,7 @@ Integration with static web assets: - + <_ScopedCssCandidateFile Include="%(_ScopedCss.OutputFile)" Condition="@(_ScopedCss) != ''"> %(_ScopedCss.RelativePath) diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ServiceWorker.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ServiceWorker.targets index a42ad8f5c8f0..a1356b2cee59 100644 --- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ServiceWorker.targets +++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.ServiceWorker.targets @@ -188,8 +188,7 @@ Copyright (c) .NET Foundation. All rights reserved. - <_BuildAssetsForManifestCandidate Include="@(StaticWebAsset)" Condition="'%(AssetRole)' != 'Alternative' and Exists('%(Identity)')" /> - <_BuildAssetsForManifestCandidate Include="@(StaticWebAsset->'%(OriginalItemSpec)')" Condition="'%(AssetRole)' != 'Alternative' and !Exists('%(Identity)')" /> + <_BuildAssetsForManifestCandidate Include="@(StaticWebAsset)" Condition="'%(AssetRole)' != 'Alternative'" /> diff --git a/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs b/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs index 9b3f235bb69f..77caed951631 100644 --- a/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs +++ b/src/StaticWebAssetsSdk/Tasks/CollectStaticWebAssetsToCopy.cs @@ -43,31 +43,8 @@ public override bool Execute() { // We have an asset we want to copy to the output folder. fileOutputPath = Path.Combine(normalizedOutputPath, asset.ComputeTargetPath("", Path.DirectorySeparatorChar, StaticWebAssetTokenResolver.Instance)); - string source = null; - if (asset.IsComputed()) - { - if (asset.Identity.StartsWith(normalizedOutputPath, StringComparison.Ordinal)) - { - Log.LogMessage(MessageImportance.Low, "Source for asset '{0}' is '{1}' since the identity points to the output path.", asset.Identity, asset.OriginalItemSpec); - source = asset.OriginalItemSpec; - } - else if (File.Exists(asset.Identity)) - { - Log.LogMessage(MessageImportance.Low, "Source for asset '{0}' is '{0}' since the asset exists.", asset.Identity); - source = asset.Identity; - } - else - { - Log.LogMessage(MessageImportance.Low, "Source for asset '{0}' is '{1}' since the asset does not exist.", asset.Identity, asset.OriginalItemSpec); - source = asset.OriginalItemSpec; - } - } - else - { - source = asset.Identity; - } - copyToOutputFolder.Add(new TaskItem(source, new Dictionary + copyToOutputFolder.Add(new TaskItem(asset.Identity, new Dictionary { ["OriginalItemSpec"] = asset.Identity, ["TargetPath"] = fileOutputPath, diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs index c7e38a3b8d76..71c09571faeb 100644 --- a/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs @@ -156,7 +156,6 @@ public override bool Execute() out var compressedAsset)) { var result = compressedAsset.ToTaskItem(); - result.SetMetadata("RelatedAssetOriginalItemSpec", asset.OriginalItemSpec); assetsToCompress[assetCounter++] = result; existingFormats.Add(format); diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs index c03a4c3796fa..ea3e546014e1 100644 --- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs +++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs @@ -830,7 +830,7 @@ public void ApplyDefaults() AssetRole = string.IsNullOrEmpty(AssetRole) ? AssetRoles.Primary : AssetRole; if (string.IsNullOrEmpty(Fingerprint) || string.IsNullOrEmpty(Integrity) || FileLength == -1 || LastWriteTime == DateTimeOffset.MinValue) { - var file = ResolveFile(Identity, OriginalItemSpec); + var file = ResolveFile(); (Fingerprint, Integrity) = string.IsNullOrEmpty(Fingerprint) || string.IsNullOrEmpty(Integrity) ? ComputeFingerprintAndIntegrityIfNeeded(file) : (Fingerprint, Integrity); FileLength = FileLength == -1 ? file.Length : FileLength; @@ -859,9 +859,9 @@ internal static (string fingerprint, string integrity) ComputeFingerprintAndInte return (FileHasher.ToBase36(hash), Convert.ToBase64String(hash)); } - internal static string ComputeIntegrity(string identity, string originalItemSpec) + internal static string ComputeIntegrity(string identity) { - var fileInfo = ResolveFile(identity, originalItemSpec); + var fileInfo = ResolveFile(identity); return ComputeIntegrity(fileInfo); } @@ -1540,22 +1540,11 @@ internal string EmbedTokens(string relativePath) return pattern.RawPattern.ToString(); } - internal FileInfo ResolveFile() => ResolveFile(Identity, OriginalItemSpec); + internal FileInfo ResolveFile() => new FileInfo(Identity); - internal static FileInfo ResolveFile(string identity, string originalItemSpec) + internal static FileInfo ResolveFile(string identity) { - var fileInfo = new FileInfo(identity); - if (fileInfo.Exists) - { - return fileInfo; - } - fileInfo = new FileInfo(originalItemSpec); - if (fileInfo.Exists) - { - return fileInfo; - } - - throw new InvalidOperationException($"No file exists for the asset at either location '{identity}' or '{originalItemSpec}'."); + return new FileInfo(identity); } internal static Dictionary ToAssetDictionary(ITaskItem[] candidateAssets, bool validate = false) diff --git a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs index f7a0c53ae1e1..a833ec4f886e 100644 --- a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs +++ b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs @@ -70,7 +70,6 @@ private DefineStaticWebAssetsCache GetOrCreateAssetsCache() internal class DefineStaticWebAssetsCache { private readonly List _assets = []; - private readonly List _copyCandidates = []; private string? _manifestPath; private IDictionary? _inputByHash; private ITaskItem[]? _noCacheCandidates; @@ -90,7 +89,6 @@ internal DefineStaticWebAssetsCache(TaskLoggingHelper log, string? manifestPath) // Outputs for the cache public Dictionary CachedAssets { get; set; } = []; - public Dictionary CachedCopyCandidates { get; set; } = []; internal static DefineStaticWebAssetsCache ReadOrCreateCache(TaskLoggingHelper log, string manifestPath) { @@ -131,16 +129,6 @@ internal void AppendAsset(string hash, StaticWebAsset asset, ITaskItem item) } } - internal void AppendCopyCandidate(string hash, string identity, string targetPath) - { - var copyCandidate = new CopyCandidate(identity, targetPath); - _copyCandidates.Add(copyCandidate.ToTaskItem()); - if (!string.IsNullOrEmpty(hash)) - { - CachedCopyCandidates[hash] = copyCandidate; - } - } - internal void Update( byte[] propertiesHash, byte[] fingerprintPatternsHash, @@ -166,7 +154,6 @@ private void TotalUpdate(byte[] propertiesHash, byte[] fingerprintPatternsHash, FingerprintPatternsHash = fingerprintPatternsHash; PropertyOverridesHash = propertyOverridesHash; CachedAssets.Clear(); - CachedCopyCandidates.Clear(); InputHashes = [.. inputsByHash.Keys]; _inputByHash = inputsByHash; } @@ -189,10 +176,6 @@ private void PartialUpdate(Dictionary inputHashes) { _assets.Add(cachedAsset.Value.ToTaskItem()); } - foreach (var cachedCopyCandidate in CachedCopyCandidates) - { - _copyCandidates.Add(cachedCopyCandidate.Value.ToTaskItem()); - } _cacheUpToDate = true; _log?.LogMessage(MessageImportance.Low, "Cache is fully up to date."); @@ -212,10 +195,6 @@ private void PartialUpdate(Dictionary inputHashes) { _log?.LogMessage(MessageImportance.Low, "Asset {0} is up to date", candidate.ItemSpec); _assets.Add(asset.ToTaskItem()); - if (CachedCopyCandidates.TryGetValue(hash, out var copyCandidate)) - { - _copyCandidates.Add(copyCandidate.ToTaskItem()); - } } } @@ -225,7 +204,6 @@ private void PartialUpdate(Dictionary inputHashes) foreach (var hash in assetsToRemove) { CachedAssets.Remove(hash); - CachedCopyCandidates.Remove(hash); } _inputByHash = remainingCandidates; @@ -233,7 +211,7 @@ private void PartialUpdate(Dictionary inputHashes) internal void SetPathAndLogger(string? manifestPath, TaskLoggingHelper log) => (_manifestPath, _log) = (manifestPath, log); - public (IList CopyCandidates, IList Assets) GetComputedOutputs() => (_copyCandidates, _assets); + public IList GetComputedOutputs() => _assets; internal void NoCache(ITaskItem[] candidateAssets) { @@ -264,14 +242,6 @@ IEnumerable> EnumerateNoCache() internal bool IsUpToDate() => _cacheUpToDate; } - internal class CopyCandidate(string identity, string targetPath) - { - public string Identity { get; set; } = identity; - public string TargetPath { get; set; } = targetPath; - - internal ITaskItem ToTaskItem() => new TaskItem(Identity, new Dictionary { ["TargetPath"] = TargetPath }); - } - [JsonSerializable(typeof(DefineStaticWebAssetsCache))] [JsonSourceGenerationOptions( GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization, diff --git a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs index 924d6764755a..563ea6b285cf 100644 --- a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs +++ b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs @@ -72,9 +72,6 @@ public partial class DefineStaticWebAssets : Task [Output] public ITaskItem[] Assets { get; set; } - [Output] - public ITaskItem[] CopyCandidates { get; set; } - public Func TestResolveFileDetails { get; set; } private HashSet _overrides; @@ -99,9 +96,7 @@ public override bool Execute() if (assetsCache.IsUpToDate()) { - var outputs = assetsCache.GetComputedOutputs(); - Assets = [.. outputs.Assets]; - CopyCandidates = [.. outputs.CopyCandidates]; + Assets = [.. assetsCache.GetComputedOutputs()]; } else { @@ -266,23 +261,7 @@ public override bool Execute() { // We ignore the content root for publish only assets since it doesn't matter. var contentRootPrefix = StaticWebAsset.AssetKinds.IsPublish(assetKind) ? null : contentRoot; - (identity, var computed) = ComputeCandidateIdentity(candidate, contentRootPrefix, relativePathCandidate, matcher, matchContext); - - if (computed) - { - // If we synthesized identity and there is a fingerprint placeholder pattern in the file name - // expand it to the concrete fingerprinted file name while keeping RelativePath pattern form. - if (FingerprintCandidates && !string.IsNullOrEmpty(fingerprint)) - { - var fileNamePattern = Path.GetFileName(identity); - if (fileNamePattern.Contains("#[")) - { - var expanded = StaticWebAssetPathPattern.ExpandIdentityFileNameForFingerprint(fileNamePattern, fingerprint); - identity = Path.Combine(Path.GetDirectoryName(identity) ?? string.Empty, expanded); - } - } - assetsCache.AppendCopyCandidate(hash, candidate.ItemSpec, identity); - } + identity = ComputeCandidateIdentity(candidate, contentRootPrefix); } var asset = StaticWebAsset.FromProperties( @@ -340,13 +319,9 @@ public override bool Execute() assetsCache.AppendAsset(hash, asset, item); } - var outputs = assetsCache.GetComputedOutputs(); - var results = outputs.Assets; - assetsCache.WriteCacheManifest(); - Assets = [.. outputs.Assets]; - CopyCandidates = [.. outputs.CopyCandidates]; + Assets = [.. assetsCache.GetComputedOutputs()]; } catch (Exception ex) { @@ -365,75 +340,39 @@ public override bool Execute() { return TestResolveFileDetails(identity, originalItemSpec); } - var file = StaticWebAsset.ResolveFile(identity, originalItemSpec); + var file = new FileInfo(identity); + if (!file.Exists) + { + throw new InvalidOperationException($"No file exists for the asset at location '{identity}'."); + } var fileLength = file.Length; var lastWriteTimeUtc = file.LastWriteTimeUtc; return (file, fileLength, lastWriteTimeUtc); } - private (string identity, bool computed) ComputeCandidateIdentity( + private string ComputeCandidateIdentity( ITaskItem candidate, - string contentRoot, - string relativePath, - StaticWebAssetGlobMatcher matcher, - StaticWebAssetGlobMatcher.MatchContext matchContext) + string contentRoot) { var candidateFullPath = Path.GetFullPath(candidate.GetMetadata("FullPath")); if (contentRoot == null) { Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because content root is not defined.", candidate.ItemSpec, candidateFullPath); - return (candidateFullPath, false); + return candidateFullPath; } var normalizedContentRoot = StaticWebAsset.NormalizeContentRootPath(contentRoot); if (candidateFullPath.StartsWith(normalizedContentRoot)) { Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because it starts with content root '{2}'.", candidate.ItemSpec, candidateFullPath, normalizedContentRoot); - return (candidateFullPath, false); } else { - // We want to support assets that are part of the source codebase but that might get transformed during the build or - // publish processes, so we want to allow defining these assets by setting up a different content root path from their - // original location in the project. For example the asset can be wwwroot\my-prod-asset.js, the content root can be - // obj\transform and the final asset identity can be <>\obj\transform\my-prod-asset.js - GlobMatch matchResult = default; - if (matcher != null) - { - matchContext.SetPathAndReinitialize(StaticWebAssetPathPattern.PathWithoutTokens(candidate.ItemSpec)); - matchResult = matcher.Match(matchContext); - } - if (matcher == null) - { - // If no relative path pattern was specified, we are going to suggest that the identity is `%(ContentRoot)\RelativePath\OriginalFileName` - // We don't want to use the relative path file name since multiple assets might map to that and conflicts might arise. - // Alternatively, we could be explicit here and support ContentRootSubPath to indicate where it needs to go. - var identitySubPath = Path.GetDirectoryName(relativePath); - var itemSpecFileName = Path.GetFileName(candidateFullPath); - var relativeFileName = Path.GetFileName(relativePath); - // If the relative path filename has been modified (e.g. fingerprint pattern appended) use it when synthesizing identity. - if (!string.IsNullOrEmpty(relativeFileName) && !string.Equals(relativeFileName, itemSpecFileName, StringComparison.OrdinalIgnoreCase)) - { - itemSpecFileName = relativeFileName; - } - var finalIdentity = Path.Combine(normalizedContentRoot, identitySubPath ?? string.Empty, itemSpecFileName); - Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because it did not start with the content root '{2}'", candidate.ItemSpec, finalIdentity, normalizedContentRoot); - return (finalIdentity, true); - } - else if (!matchResult.IsMatch) - { - Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because it didn't match the relative path pattern", candidate.ItemSpec, candidateFullPath); - return (candidateFullPath, false); - } - else - { - var stem = matchResult.Stem; - var assetIdentity = Path.GetFullPath(Path.Combine(normalizedContentRoot, stem)); - Log.LogMessage(MessageImportance.Low, "Computed identity '{0}' for candidate '{1}'", assetIdentity, candidate.ItemSpec); - - return (assetIdentity, true); - } + // The asset is not under the content root. Use the candidate's real file path as the identity. + Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because it did not start with the content root '{2}'", candidate.ItemSpec, candidateFullPath, normalizedContentRoot); } + + return candidateFullPath; } private string ComputePropertyValue(ITaskItem element, string metadataName, string propertyValue, bool isRequired = true) diff --git a/src/StaticWebAssetsSdk/Tasks/Utils/AssetToCompress.cs b/src/StaticWebAssetsSdk/Tasks/Utils/AssetToCompress.cs index 537feb3e48f2..7326db5d7c18 100644 --- a/src/StaticWebAssetsSdk/Tasks/Utils/AssetToCompress.cs +++ b/src/StaticWebAssetsSdk/Tasks/Utils/AssetToCompress.cs @@ -12,8 +12,6 @@ internal static class AssetToCompress { public static bool TryFindInputFilePath(ITaskItem assetToCompress, TaskLoggingHelper log, out string fullPath) { - // Check RelatedAsset first (the asset's Identity path) as it's more reliable. - // RelatedAssetOriginalItemSpec may point to a project file (e.g., .esproj) rather than the actual asset. var relatedAsset = assetToCompress.GetMetadata("RelatedAsset"); if (File.Exists(relatedAsset)) { @@ -24,20 +22,9 @@ public static bool TryFindInputFilePath(ITaskItem assetToCompress, TaskLoggingHe return true; } - var relatedAssetOriginalItemSpec = assetToCompress.GetMetadata("RelatedAssetOriginalItemSpec"); - if (File.Exists(relatedAssetOriginalItemSpec)) - { - log.LogMessage(MessageImportance.Low, "Asset '{0}' found at original item spec '{1}'.", - assetToCompress.ItemSpec, - relatedAssetOriginalItemSpec); - fullPath = relatedAssetOriginalItemSpec; - return true; - } - - log.LogError("The asset '{0}' can not be found at any of the searched locations '{1}' and '{2}'.", + log.LogError("The asset '{0}' can not be found at the searched location '{1}'.", assetToCompress.ItemSpec, - relatedAsset, - relatedAssetOriginalItemSpec); + relatedAsset); fullPath = null; return false; } diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs index f252022d1338..f5b4faa25cc6 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs @@ -219,7 +219,7 @@ public void FingerprintsContentUsingPatternsWhenMoreThanOneExtension(string file [Fact] [Trait("Category", "FingerprintIdentity")] - public void ComputesIdentity_UsingFingerprintPattern_ForComputedAssets_WhenIdentityNeedsComputation() + public void ComputesIdentity_UsingRealFilePath_ForComputedAssets_WhenFileIsOutsideContentRoot() { // Arrange: simulate a packaged asset (outside content root) with a RelativePath inside the app var errorMessages = new List(); @@ -227,7 +227,7 @@ public void ComputesIdentity_UsingFingerprintPattern_ForComputedAssets_WhenIdent buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) .Callback(args => errorMessages.Add(args.Message)); - // Create a physical file to allow fingerprint computation (tests override ResolveFileDetails returning null file otherwise) + // Create a physical file to allow fingerprint computation var tempRoot = Path.Combine(Path.GetTempPath(), "swafp_identity_test"); var nugetPackagePath = Path.Combine(tempRoot, "microsoft.aspnetcore.components.webassembly", "10.0.0-rc.1.25451.107", "build", "net10.0"); Directory.CreateDirectory(nugetPackagePath); @@ -250,7 +250,6 @@ public void ComputesIdentity_UsingFingerprintPattern_ForComputedAssets_WhenIdent ["RelativePath"] = relativePath }) ], - // No RelativePathPattern, we trigger the branch that synthesizes identity under content root. FingerprintPatterns = [ new TaskItem("Js", new Dictionary{{"Pattern","*.js"},{"Expression","#[.{fingerprint}]!"}})], FingerprintCandidates = true, SourceType = "Computed", @@ -270,14 +269,11 @@ public void ComputesIdentity_UsingFingerprintPattern_ForComputedAssets_WhenIdent task.Assets.Length.Should().Be(1); var asset = task.Assets[0]; - // RelativePath should still contain the hard fingerprint pattern placeholder (not expanded yet) + // RelativePath should still contain the fingerprint pattern placeholder asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("_framework/blazor.webassembly#[.{fingerprint}]!.js"); - // Identity must contain the ACTUAL fingerprint value in the file name (placeholder expanded) - var actualFingerprint = asset.GetMetadata(nameof(StaticWebAsset.Fingerprint)); - actualFingerprint.Should().NotBeNullOrEmpty(); - var expectedIdentity = Path.GetFullPath(Path.Combine(contentRoot, "_framework", $"blazor.webassembly.{actualFingerprint}.js")); - asset.ItemSpec.Should().Be(expectedIdentity); + // Identity should be the real file path (no synthesized identity under content root) + asset.ItemSpec.Should().Be(Path.GetFullPath(assetFullPath)); } [Fact] @@ -669,7 +665,6 @@ public void DefineStaticWebAssetsCache_Recomputes_All_WhenPropertiesChange(Updat Assert.False(cache.IsUpToDate()); Assert.Same(inputHashes, cache.OutOfDateInputs()); Assert.Empty(cache.CachedAssets); - Assert.Empty(cache.CachedCopyCandidates); } [Fact] @@ -687,7 +682,7 @@ public void DefineStaticWebAssetsCache_PartialUpdate_WhenOnlySome_InputsChange() Assert.NotSame(inputHashes, cache.OutOfDateInputs()); var input1 = Assert.Single(cache.OutOfDateInputs()); var ouput = cache.GetComputedOutputs(); - var input2 = Assert.Single(ouput.Assets); + var input2 = Assert.Single(ouput); } [Fact] @@ -709,9 +704,9 @@ public void DefineStaticWebAssetsCache_PartialUpdate_NewAssetsCanBeAddedToTheCac Assert.Contains("input1", cache.CachedAssets.Keys); var ouput = cache.GetComputedOutputs(); - Assert.Equal(2, ouput.Assets.Count); - Assert.Equal("input2", ouput.Assets[0].ItemSpec); - Assert.Equal("input1", ouput.Assets[1].ItemSpec); + Assert.Equal(2, ouput.Count); + Assert.Equal("input2", ouput[0].ItemSpec); + Assert.Equal("input1", ouput[1].ItemSpec); } [Fact] From 347a2e1cd6273b61d02c14f3578b8d4d254a3d2b Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Sat, 11 Apr 2026 10:25:44 +0200 Subject: [PATCH 2/2] Fix AssetToCompress tests after removing RelatedAssetOriginalItemSpec fallback Remove tests that validated the removed RelatedAssetOriginalItemSpec fallback behavior and update remaining tests to match the simplified TryFindInputFilePath that only uses RelatedAsset. --- .../StaticWebAssets/AssetToCompressTest.cs | 73 +++---------------- 1 file changed, 11 insertions(+), 62 deletions(-) diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs index c32b7b8dd3f7..e47f8d5b856c 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs @@ -65,31 +65,12 @@ public void TryFindInputFilePath_UsesRelatedAsset_WhenFileExists() } [Fact] - public void TryFindInputFilePath_FallsBackToRelatedAssetOriginalItemSpec_WhenRelatedAssetDoesNotExist() + public void TryFindInputFilePath_ReturnsError_WhenRelatedAssetDoesNotExist() { // Arrange + var nonExistentPath = Path.Combine(_testDirectory, "non-existent.js"); var assetToCompress = new TaskItem("test.js.gz"); - assetToCompress.SetMetadata("RelatedAsset", Path.Combine(_testDirectory, "non-existent.js")); - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", _testFilePath); - - // Act - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - // Assert - result.Should().BeTrue(); - fullPath.Should().Be(_testFilePath); - _errorMessages.Should().BeEmpty(); - } - - [Fact] - public void TryFindInputFilePath_ReturnsError_WhenNeitherPathExists() - { - // Arrange - var nonExistentPath1 = Path.Combine(_testDirectory, "non-existent1.js"); - var nonExistentPath2 = Path.Combine(_testDirectory, "non-existent2.js"); - var assetToCompress = new TaskItem("test.js.gz"); - assetToCompress.SetMetadata("RelatedAsset", nonExistentPath1); - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", nonExistentPath2); + assetToCompress.SetMetadata("RelatedAsset", nonExistentPath); // Act var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); @@ -99,75 +80,43 @@ public void TryFindInputFilePath_ReturnsError_WhenNeitherPathExists() fullPath.Should().BeNull(); _errorMessages.Should().ContainSingle(); _errorMessages[0].Should().Contain("can not be found"); - _errorMessages[0].Should().Contain(nonExistentPath1); - _errorMessages[0].Should().Contain(nonExistentPath2); - } - - [Fact] - public void TryFindInputFilePath_PrefersRelatedAsset_OverRelatedAssetOriginalItemSpec_WhenBothExist() - { - // Arrange - create two files to simulate the scenario where both metadata values point to existing files - var relatedAssetPath = Path.Combine(_testDirectory, "correct-asset.js"); - var originalItemSpecPath = Path.Combine(_testDirectory, "project-file.esproj"); - File.WriteAllText(relatedAssetPath, "// correct JavaScript content"); - File.WriteAllText(originalItemSpecPath, ""); - - var assetToCompress = new TaskItem("test.js.gz"); - assetToCompress.SetMetadata("RelatedAsset", relatedAssetPath); - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", originalItemSpecPath); - - // Act - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - // Assert - should prefer RelatedAsset (the actual JavaScript file) over RelatedAssetOriginalItemSpec (the esproj file) - result.Should().BeTrue(); - fullPath.Should().Be(relatedAssetPath); - fullPath.Should().NotBe(originalItemSpecPath); - _errorMessages.Should().BeEmpty(); + _errorMessages[0].Should().Contain(nonExistentPath); } [Fact] - public void TryFindInputFilePath_HandlesEmptyRelatedAsset_AndUsesRelatedAssetOriginalItemSpec() + public void TryFindInputFilePath_ReturnsError_WhenRelatedAssetIsEmpty() { // Arrange var assetToCompress = new TaskItem("test.js.gz"); assetToCompress.SetMetadata("RelatedAsset", ""); - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", _testFilePath); // Act var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); // Assert - result.Should().BeTrue(); - fullPath.Should().Be(_testFilePath); - _errorMessages.Should().BeEmpty(); + result.Should().BeFalse(); + fullPath.Should().BeNull(); + _errorMessages.Should().ContainSingle(); } [Fact] - public void TryFindInputFilePath_HandlesEsprojScenario_WhereOriginalItemSpecPointsToProjectFile() + public void TryFindInputFilePath_HandlesEsprojScenario_WhereRelatedAssetPointsToActualFile() { - // Arrange - simulate the esproj bug scenario where RelatedAssetOriginalItemSpec - // incorrectly points to the .esproj project file instead of the actual JS asset - var esprojFile = Path.Combine(_testDirectory, "MyProject.esproj"); + // Arrange - simulate the esproj scenario where RelatedAsset points to the actual JS file var actualJsFile = Path.Combine(_testDirectory, "dist", "app.min.js"); Directory.CreateDirectory(Path.GetDirectoryName(actualJsFile)); - File.WriteAllText(esprojFile, ""); File.WriteAllText(actualJsFile, "// actual JavaScript content"); var assetToCompress = new TaskItem(Path.Combine(_testDirectory, "compressed", "app.min.js.gz")); - // RelatedAsset should contain the correct path to the actual JS file assetToCompress.SetMetadata("RelatedAsset", actualJsFile); - // RelatedAssetOriginalItemSpec may incorrectly point to .esproj due to esproj SDK bug - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", esprojFile); // Act var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - // Assert - should use RelatedAsset (correct JS file) not RelatedAssetOriginalItemSpec (esproj file) + // Assert - should use RelatedAsset (the actual JavaScript file) result.Should().BeTrue(); fullPath.Should().Be(actualJsFile); - fullPath.Should().NotBe(esprojFile); _errorMessages.Should().BeEmpty(); } }