Skip to content

Commit eec6c00

Browse files
[release/10.0.1xx-rc2] [Blazor] Remove unnecessary update to the Blazor webassembly js file (#50967)
Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com>
1 parent 8a23772 commit eec6c00

File tree

17 files changed

+292
-70
lines changed

17 files changed

+292
-70
lines changed

src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,9 @@ Copyright (c) .NET Foundation. All rights reserved.
8888

8989
<ResolvePublishRelatedStaticWebAssetsDependsOn>
9090
$(ResolvePublishRelatedStaticWebAssetsDependsOn);
91-
_ReplaceFingerprintedBlazorJsForPublish
9291
</ResolvePublishRelatedStaticWebAssetsDependsOn>
9392
<ResolveCompressedFilesForPublishDependsOn>
9493
$(ResolveCompressedFilesForPublishDependsOn);
95-
_ReplaceFingerprintedBlazorJsForPublish
9694
</ResolveCompressedFilesForPublishDependsOn>
9795

9896
<GeneratePublishWasmBootJsonDependsOn>
@@ -159,65 +157,6 @@ Copyright (c) .NET Foundation. All rights reserved.
159157
</ItemGroup>
160158
</Target>
161159

162-
<Target Name="_ReplaceFingerprintedBlazorJsForPublish" DependsOnTargets="ProcessPublishFilesForWasm" Condition="'$(WasmBuildingForNestedPublish)' != 'true' and '$(BlazorFingerprintBlazorJs)' == 'true'">
163-
<PropertyGroup>
164-
<_BlazorJSFileNames>;@(_BlazorJSFile->'%(FileName)');</_BlazorJSFileNames>
165-
</PropertyGroup>
166-
<ItemGroup>
167-
<_BlazorJSJSStaticWebAsset Include="@(StaticWebAsset)" Condition="$(_BlazorJSFileNames.Contains(';%(FileName);')) and '%(Extension)' == '.js'" />
168-
<_BlazorJSPublishCandidate Include="%(_BlazorJSJSStaticWebAsset.RelativeDir)%(_BlazorJSJSStaticWebAsset.FileName).%(_BlazorJSJSStaticWebAsset.Fingerprint)%(_BlazorJSJSStaticWebAsset.Extension)" />
169-
<_BlazorJSPublishCandidate Remove="@(_BlazorJSPublishCandidate)" Condition="'%(Extension)' == '.map'" />
170-
<_BlazorJSPublishCandidate>
171-
<RelativePath>_framework/$([System.IO.Path]::GetFileNameWithoutExtension('%(Filename)'))%(Extension)</RelativePath>
172-
</_BlazorJSPublishCandidate>
173-
</ItemGroup>
174-
175-
<DefineStaticWebAssets
176-
CandidateAssets="@(_BlazorJSPublishCandidate)"
177-
FingerprintCandidates="true"
178-
FingerprintPatterns="@(_BlazorJSFingerprintPattern)"
179-
SourceId="$(PackageId)"
180-
SourceType="Computed"
181-
AssetKind="All"
182-
AssetMergeSource="$(StaticWebAssetMergeTarget)"
183-
AssetRole="Primary"
184-
AssetTraitName="WasmResource"
185-
AssetTraitValue="boot"
186-
CopyToOutputDirectory="Never"
187-
CopyToPublishDirectory="PreserveNewest"
188-
ContentRoot="%(_BlazorJSJSStaticWebAsset.ContentRoot)"
189-
BasePath="%(_BlazorJSJSStaticWebAsset.BasePath)"
190-
>
191-
<Output TaskParameter="Assets" ItemName="_BlazorJSJSPublishStaticWebAssets" />
192-
</DefineStaticWebAssets>
193-
<DefineStaticWebAssetEndpoints
194-
CandidateAssets="@(_BlazorJSJSPublishStaticWebAssets)"
195-
ExistingEndpoints="@(StaticWebAssetEndpoint)"
196-
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
197-
>
198-
<Output TaskParameter="Endpoints" ItemName="_BlazorJSJSPublishStaticWebAssetsEndpoint" />
199-
</DefineStaticWebAssetEndpoints>
200-
<PropertyGroup>
201-
<_BlazorJSJSStaticWebAssetFullPath>@(_BlazorJSJSStaticWebAsset->'%(FullPath)')</_BlazorJSJSStaticWebAssetFullPath>
202-
</PropertyGroup>
203-
<ItemGroup>
204-
<_BlazorJSJSStaticWebAsset Include="@(StaticWebAsset)" Condition="'%(AssetTraitName)' == 'Content-Encoding' and '%(RelatedAsset)' == '$(_BlazorJSJSStaticWebAssetFullPath)'" />
205-
</ItemGroup>
206-
<FilterStaticWebAssetEndpoints Condition="'@(_BlazorJSJSStaticWebAsset)' != ''"
207-
Endpoints="@(StaticWebAssetEndpoint)"
208-
Assets="@(_BlazorJSJSStaticWebAsset)"
209-
Filters=""
210-
>
211-
<Output TaskParameter="FilteredEndpoints" ItemName="_BlazorJSEndpointsToRemove" />
212-
</FilterStaticWebAssetEndpoints>
213-
<ItemGroup>
214-
<StaticWebAsset Remove="@(_BlazorJSJSStaticWebAsset)" />
215-
<StaticWebAsset Include="@(_BlazorJSJSPublishStaticWebAssets)" />
216-
<StaticWebAssetEndpoint Remove="@(_BlazorJSEndpointsToRemove)" />
217-
<StaticWebAssetEndpoint Include="@(_BlazorJSJSPublishStaticWebAssetsEndpoint)" />
218-
</ItemGroup>
219-
</Target>
220-
221160
<!-- Just print a message here, static web assets takes care of all the copying -->
222161
<Target Name="_BlazorCopyFilesToOutputDirectory" AfterTargets="CopyFilesToOutputDirectory">
223162
<Message Importance="High" Text="$(MSBuildProjectName) (Blazor output) -&gt; $(TargetDir)wwwroot" Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)'!='true'" />

src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathPattern.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,71 @@ public override int GetHashCode()
505505
private static bool IsLiteralSegment(StaticWebAssetPathSegment segment) => segment.Parts.Count == 1 && segment.Parts[0].IsLiteral;
506506

507507
internal static string PathWithoutTokens(string path) => Parse(path).ComputePatternLabel();
508+
509+
internal static string ExpandIdentityFileNameForFingerprint(string fileNamePattern, string fingerprint)
510+
{
511+
var pattern = Parse(fileNamePattern);
512+
var sb = new StringBuilder();
513+
foreach (var segment in pattern.Segments)
514+
{
515+
var isLiteral = segment.Parts.Count == 1 && segment.Parts[0].IsLiteral;
516+
if (isLiteral)
517+
{
518+
sb.Append(segment.Parts[0].Name);
519+
continue;
520+
}
521+
522+
if (segment.IsOptional && !segment.IsPreferred)
523+
{
524+
continue; // skip non-preferred optional segments
525+
}
526+
527+
bool missingRequired = false;
528+
foreach (var part in segment.Parts)
529+
{
530+
if (!part.IsLiteral && part.Value.IsEmpty)
531+
{
532+
var tokenName = part.Name.ToString();
533+
if (string.Equals(tokenName, "fingerprint", StringComparison.OrdinalIgnoreCase) && string.IsNullOrEmpty(fingerprint))
534+
{
535+
missingRequired = true;
536+
break;
537+
}
538+
}
539+
}
540+
if (missingRequired)
541+
{
542+
if (!segment.IsOptional)
543+
{
544+
throw new InvalidOperationException($"Token 'fingerprint' not provided for '{fileNamePattern}'.");
545+
}
546+
continue;
547+
}
548+
549+
foreach (var part in segment.Parts)
550+
{
551+
if (part.IsLiteral)
552+
{
553+
sb.Append(part.Name);
554+
}
555+
else if (!part.Value.IsEmpty)
556+
{
557+
sb.Append(part.Value);
558+
}
559+
else
560+
{
561+
var tokenName = part.Name.ToString();
562+
if (string.Equals(tokenName, "fingerprint", StringComparison.OrdinalIgnoreCase))
563+
{
564+
sb.Append(fingerprint);
565+
}
566+
else
567+
{
568+
throw new InvalidOperationException($"Unsupported token '{tokenName}' in '{fileNamePattern}'.");
569+
}
570+
}
571+
}
572+
}
573+
return sb.ToString();
574+
}
508575
}

src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,14 @@ public override bool Execute()
238238
break;
239239
}
240240

241+
// IMPORTANT: Apply fingerprint pattern (which can change the file name) BEFORE computing identity
242+
// for non-Discovered assets so that a synthesized identity incorporates the fingerprint pattern.
243+
if (FingerprintCandidates)
244+
{
245+
matchContext.SetPathAndReinitialize(relativePathCandidate);
246+
relativePathCandidate = StaticWebAsset.Normalize(fingerprintPatternMatcher.AppendFingerprintPattern(matchContext, identity));
247+
}
248+
241249
if (!string.Equals(SourceType, StaticWebAsset.SourceTypes.Discovered, StringComparison.OrdinalIgnoreCase))
242250
{
243251
// We ignore the content root for publish only assets since it doesn't matter.
@@ -246,16 +254,21 @@ public override bool Execute()
246254

247255
if (computed)
248256
{
257+
// If we synthesized identity and there is a fingerprint placeholder pattern in the file name
258+
// expand it to the concrete fingerprinted file name while keeping RelativePath pattern form.
259+
if (FingerprintCandidates && !string.IsNullOrEmpty(fingerprint))
260+
{
261+
var fileNamePattern = Path.GetFileName(identity);
262+
if (fileNamePattern.Contains("#["))
263+
{
264+
var expanded = StaticWebAssetPathPattern.ExpandIdentityFileNameForFingerprint(fileNamePattern, fingerprint);
265+
identity = Path.Combine(Path.GetDirectoryName(identity) ?? string.Empty, expanded);
266+
}
267+
}
249268
assetsCache.AppendCopyCandidate(hash, candidate.ItemSpec, identity);
250269
}
251270
}
252271

253-
if (FingerprintCandidates)
254-
{
255-
matchContext.SetPathAndReinitialize(relativePathCandidate);
256-
relativePathCandidate = StaticWebAsset.Normalize(fingerprintPatternMatcher.AppendFingerprintPattern(matchContext, identity));
257-
}
258-
259272
var asset = StaticWebAsset.FromProperties(
260273
identity,
261274
sourceId,
@@ -357,7 +370,13 @@ public override bool Execute()
357370
// Alternatively, we could be explicit here and support ContentRootSubPath to indicate where it needs to go.
358371
var identitySubPath = Path.GetDirectoryName(relativePath);
359372
var itemSpecFileName = Path.GetFileName(candidateFullPath);
360-
var finalIdentity = Path.Combine(normalizedContentRoot, identitySubPath, itemSpecFileName);
373+
var relativeFileName = Path.GetFileName(relativePath);
374+
// If the relative path filename has been modified (e.g. fingerprint pattern appended) use it when synthesizing identity.
375+
if (!string.IsNullOrEmpty(relativeFileName) && !string.Equals(relativeFileName, itemSpecFileName, StringComparison.OrdinalIgnoreCase))
376+
{
377+
itemSpecFileName = relativeFileName;
378+
}
379+
var finalIdentity = Path.Combine(normalizedContentRoot, identitySubPath ?? string.Empty, itemSpecFileName);
361380
Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because it did not start with the content root '{2}'", candidate.ItemSpec, finalIdentity, normalizedContentRoot);
362381
return (finalIdentity, true);
363382
}
@@ -493,7 +512,7 @@ private void UpdateAssetKindIfNecessary(
493512
{
494513
case (StaticWebAsset.AssetCopyOptions.Never, StaticWebAsset.AssetCopyOptions.Never):
495514
case (not StaticWebAsset.AssetCopyOptions.Never, not StaticWebAsset.AssetCopyOptions.Never):
496-
var errorMessage = "Two assets found targeting the same path with incompatible asset kinds: " + Environment.NewLine +
515+
var errorMessage = "Two assets found targeting the same path with incompatible asset kinds:" + Environment.NewLine +
497516
"'{0}' with kind '{1}'" + Environment.NewLine +
498517
"'{2}' with kind '{3}'" + Environment.NewLine +
499518
"for path '{4}'";

test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTest.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,16 @@ public class TestReference
16121612
fileInWwwroot.Should().Exist();
16131613
}
16141614

1615+
[RequiresMSBuildVersionTheory("17.12", Reason = "Needs System.Text.Json 8.0.5")]
1616+
[InlineData("")]
1617+
[InlineData("/p:BlazorFingerprintBlazorJs=false")]
1618+
public void Publish_BlazorWasmReferencedByAspNetCoreServer(string publishArg)
1619+
{
1620+
var testInstance = CreateAspNetSdkTestAsset("BlazorWasmReferencedByAspNetCoreServer");
1621+
var publishCommand = CreatePublishCommand(testInstance, "Server");
1622+
ExecuteCommand(publishCommand, publishArg).Should().Pass();
1623+
}
1624+
16151625
private void VerifyTypeGranularTrimming(string blazorPublishDirectory)
16161626
{
16171627
VerifyAssemblyHasTypes(Path.Combine(blazorPublishDirectory, "_framework", "Microsoft.AspNetCore.Components.wasm"), new[] {

test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,69 @@ public void FingerprintsContentUsingPatternsWhenMoreThanOneExtension(string file
217217
asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", fileName));
218218
}
219219

220+
[Fact]
221+
[Trait("Category", "FingerprintIdentity")]
222+
public void ComputesIdentity_UsingFingerprintPattern_ForComputedAssets_WhenIdentityNeedsComputation()
223+
{
224+
// Arrange: simulate a packaged asset (outside content root) with a RelativePath inside the app
225+
var errorMessages = new List<string>();
226+
var buildEngine = new Mock<IBuildEngine>();
227+
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
228+
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
229+
230+
// Create a physical file to allow fingerprint computation (tests override ResolveFileDetails returning null file otherwise)
231+
var tempRoot = Path.Combine(Path.GetTempPath(), "swafp_identity_test");
232+
var nugetPackagePath = Path.Combine(tempRoot, "microsoft.aspnetcore.components.webassembly", "10.0.0-rc.1.25451.107", "build", "net10.0");
233+
Directory.CreateDirectory(nugetPackagePath);
234+
var assetFileName = "blazor.webassembly.js";
235+
var assetFullPath = Path.Combine(nugetPackagePath, assetFileName);
236+
File.WriteAllText(assetFullPath, "console.log('test');");
237+
// Relative path provided by the item (pre-fingerprinting)
238+
var relativePath = Path.Combine("_framework", assetFileName).Replace('\\', '/');
239+
var contentRoot = Path.Combine("bin", "Release", "net10.0", "wwwroot");
240+
241+
var task = new DefineStaticWebAssets
242+
{
243+
BuildEngine = buildEngine.Object,
244+
// Use default file resolution so the file we created is used for hashing.
245+
TestResolveFileDetails = null,
246+
CandidateAssets =
247+
[
248+
new TaskItem(assetFullPath, new Dictionary<string, string>
249+
{
250+
["RelativePath"] = relativePath
251+
})
252+
],
253+
// No RelativePathPattern, we trigger the branch that synthesizes identity under content root.
254+
FingerprintPatterns = [ new TaskItem("Js", new Dictionary<string,string>{{"Pattern","*.js"},{"Expression","#[.{fingerprint}]!"}})],
255+
FingerprintCandidates = true,
256+
SourceType = "Computed",
257+
SourceId = "Client",
258+
ContentRoot = contentRoot,
259+
BasePath = "/",
260+
AssetKind = StaticWebAsset.AssetKinds.All,
261+
AssetTraitName = "WasmResource",
262+
AssetTraitValue = "boot"
263+
};
264+
265+
// Act
266+
var result = task.Execute();
267+
268+
// Assert
269+
result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}");
270+
task.Assets.Length.Should().Be(1);
271+
var asset = task.Assets[0];
272+
273+
// RelativePath should still contain the hard fingerprint pattern placeholder (not expanded yet)
274+
asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("_framework/blazor.webassembly#[.{fingerprint}]!.js");
275+
276+
// Identity must contain the ACTUAL fingerprint value in the file name (placeholder expanded)
277+
var actualFingerprint = asset.GetMetadata(nameof(StaticWebAsset.Fingerprint));
278+
actualFingerprint.Should().NotBeNullOrEmpty();
279+
var expectedIdentity = Path.GetFullPath(Path.Combine(contentRoot, "_framework", $"blazor.webassembly.{actualFingerprint}.js"));
280+
asset.ItemSpec.Should().Be(expectedIdentity);
281+
}
282+
220283
[Fact]
221284
public void RespectsItemRelativePathWhenExplicitlySpecified()
222285
{
@@ -450,7 +513,7 @@ public void FailsDiscoveringAssetsWhenThereIsAConflict(
450513
// Assert
451514
result.Should().Be(false);
452515
errorMessages.Count.Should().Be(1);
453-
errorMessages[0].Should().Be($@"Two assets found targeting the same path with incompatible asset kinds:
516+
errorMessages[0].Should().Be($@"Two assets found targeting the same path with incompatible asset kinds:
454517
'{Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))}' with kind '{firstKind}'
455518
'{Path.GetFullPath(Path.Combine("wwwroot", "candidate.publish.js"))}' with kind '{secondKind}'
456519
for path 'candidate.js'");
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<Solution>
2+
<Project Path="Client/Client.csproj" />
3+
<Project Path="Server/Server.csproj" Id="edd5dc5a-a093-4efa-88a1-f4df05c2da44" />
4+
</Solution>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Router AppAssembly="@typeof(App).Assembly">
2+
<Found Context="routeData">
3+
<RouteView RouteData="@routeData" />
4+
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
5+
</Found>
6+
</Router>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
7+
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0-rc.1.25451.107" />
11+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0-rc.1.25451.107" PrivateAssets="all" />
12+
</ItemGroup>
13+
<ItemGroup>
14+
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
15+
</ItemGroup>
16+
</Project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Client;
2+
using Microsoft.AspNetCore.Components.Web;
3+
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
4+
5+
var builder = WebAssemblyHostBuilder.CreateDefault(args);
6+
builder.RootComponents.Add<App>("#app");
7+
builder.RootComponents.Add<HeadOutlet>("head::after");
8+
9+
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
10+
11+
await builder.Build().RunAsync();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@using System.Net.Http
2+
@using System.Net.Http.Json
3+
@using Microsoft.AspNetCore.Components.Forms
4+
@using Microsoft.AspNetCore.Components.Routing
5+
@using Microsoft.AspNetCore.Components.Web
6+
@using Microsoft.AspNetCore.Components.Web.Virtualization
7+
@using Microsoft.AspNetCore.Components.WebAssembly.Http
8+
@using Microsoft.JSInterop
9+
@using Client

0 commit comments

Comments
 (0)