Skip to content

Commit e6f25ab

Browse files
authored
[Blazor] Remove unnecessary update to the Blazor webassembly js file (#50949)
Hosted webassembly apps failed to publish due to a bug in how we were trying to update the blazor.webassembly.js asset definition. We realized that we did not have to do so, but that there was a bug precluding us from finding the asset during the publish process. The fix corrects the identity of the blazor.webassembly.js to account for the fingerprint for the file we introduced in 10.0, which wasn't being taken into account.
1 parent 2da7939 commit e6f25ab

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)