Skip to content

Commit 3088e0b

Browse files
authored
Allow to fingerprint Blazor.js (#46988)
1 parent 61583eb commit 3088e0b

File tree

2 files changed

+97
-16
lines changed

2 files changed

+97
-16
lines changed

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Copyright (c) .NET Foundation. All rights reserved.
4949
<HttpActivityPropagationSupport Condition="'$(HttpActivityPropagationSupport)' == ''">false</HttpActivityPropagationSupport>
5050
<DebuggerSupport Condition="'$(DebuggerSupport)' == '' and '$(Configuration)' != 'Debug'">false</DebuggerSupport>
5151
<BlazorCacheBootResources Condition="'$(BlazorCacheBootResources)' == ''">true</BlazorCacheBootResources>
52+
<BlazorFingerprintBlazorJs Condition="'$(BlazorFingerprintBlazorJs)' == '' and '$(WriteImportMapToHtml)' == 'true'">true</BlazorFingerprintBlazorJs>
5253

5354
<_TargetingNETBefore80>$([MSBuild]::VersionLessThan('$(TargetFrameworkVersion)', '8.0'))</_TargetingNETBefore80>
5455
<_TargetingNET80OrLater>$([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '8.0'))</_TargetingNET80OrLater>
@@ -83,6 +84,15 @@ Copyright (c) .NET Foundation. All rights reserved.
8384
GenerateBuildBlazorBootExtensionJson;
8485
</GenerateBuildWasmBootJsonDependsOn>
8586

87+
<ResolvePublishRelatedStaticWebAssetsDependsOn>
88+
$(ResolvePublishRelatedStaticWebAssetsDependsOn);
89+
_ReplaceFingerprintedBlazorJsForPublish
90+
</ResolvePublishRelatedStaticWebAssetsDependsOn>
91+
<ResolveCompressedFilesForPublishDependsOn>
92+
$(ResolveCompressedFilesForPublishDependsOn);
93+
_ReplaceFingerprintedBlazorJsForPublish
94+
</ResolveCompressedFilesForPublishDependsOn>
95+
8696
<GeneratePublishWasmBootJsonDependsOn>
8797
$(GeneratePublishWasmBootJsonDependsOn);
8898
GeneratePublishBlazorBootExtensionJson;
@@ -104,6 +114,7 @@ Copyright (c) .NET Foundation. All rights reserved.
104114
<_BlazorJsFile>
105115
<RelativePath>_framework/%(Filename)%(Extension)</RelativePath>
106116
</_BlazorJsFile>
117+
<_BlazorJSFingerprintPattern Include="Js" Pattern="*.js" Expression="#[.{fingerprint}]!" />
107118

108119
<!-- A missing blazor.webassembly.js is our packaging error. Produce an error so it's discovered early. -->
109120
<Error
@@ -114,6 +125,8 @@ Copyright (c) .NET Foundation. All rights reserved.
114125

115126
<DefineStaticWebAssets
116127
CandidateAssets="@(_BlazorJSFile)"
128+
FingerprintCandidates="$(BlazorFingerprintBlazorJs)"
129+
FingerprintPatterns="@(_BlazorJSFingerprintPattern)"
117130
SourceId="$(PackageId)"
118131
SourceType="Computed"
119132
AssetKind="All"
@@ -143,6 +156,61 @@ Copyright (c) .NET Foundation. All rights reserved.
143156
</ItemGroup>
144157
</Target>
145158

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

test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssetsFingerprintingTest.cs

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,18 @@ public void Build_FingerprintsContent_WhenEnabled()
4646
AssertBuildAssets(manifest1, outputPath, intermediateOutputPath);
4747
}
4848

49-
public static TheoryData<string, bool> WriteImportMapToHtmlData => new TheoryData<string, bool>
49+
public static TheoryData<string, string, string> WriteImportMapToHtmlData => new TheoryData<string, string, string>
5050
{
51-
{ "VanillaWasm", true },
52-
{ "BlazorWasmMinimal", false }
51+
{ "VanillaWasm", "main.js", null },
52+
{ "BlazorWasmMinimal", "_framework/blazor.webassembly.js", "_framework/blazor.webassembly#[.{fingerprint}].js" }
5353
};
5454

5555
[Theory]
5656
[MemberData(nameof(WriteImportMapToHtmlData))]
57-
public void Build_WriteImportMapToHtml(string testAsset, bool assetMainJs)
57+
public void Build_WriteImportMapToHtml(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern)
5858
{
5959
ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
60+
ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern);
6061

6162
var build = CreateBuildCommand(ProjectDirectory);
6263
ExecuteCommand(build, "-p:WriteImportMapToHtml=true").Should().Pass();
@@ -65,44 +66,56 @@ public void Build_WriteImportMapToHtml(string testAsset, bool assetMainJs)
6566
var indexHtmlPath = Directory.EnumerateFiles(Path.Combine(intermediateOutputPath, "staticwebassets", "importmaphtml", "build"), "*.html").Single();
6667
var endpointsManifestPath = Path.Combine(intermediateOutputPath, $"staticwebassets.build.endpoints.json");
6768

68-
AssertImportMapInHtml(indexHtmlPath, endpointsManifestPath, assetMainJs);
69+
AssertImportMapInHtml(indexHtmlPath, endpointsManifestPath, scriptPath);
6970
}
7071

7172
[Theory]
7273
[MemberData(nameof(WriteImportMapToHtmlData))]
73-
public void Publish_WriteImportMapToHtml(string testAsset, bool assetMainJs)
74+
public void Publish_WriteImportMapToHtml(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern)
7475
{
7576
ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
77+
ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern);
7678

7779
var projectName = Path.GetFileNameWithoutExtension(Directory.EnumerateFiles(ProjectDirectory.TestRoot, "*.csproj").Single());
7880

7981
var publish = CreatePublishCommand(ProjectDirectory);
8082
ExecuteCommand(publish, "-p:WriteImportMapToHtml=true").Should().Pass();
8183

8284
var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
83-
var indexHtmlPath = Path.Combine(outputPath, "wwwroot", "index.html");
85+
var indexHtmlOutputPath = Path.Combine(outputPath, "wwwroot", "index.html");
8486
var endpointsManifestPath = Path.Combine(outputPath, $"{projectName}.staticwebassets.endpoints.json");
8587

86-
AssertImportMapInHtml(indexHtmlPath, endpointsManifestPath, assetMainJs);
88+
AssertImportMapInHtml(indexHtmlOutputPath, endpointsManifestPath, scriptPath);
8789
}
8890

89-
private void AssertImportMapInHtml(string indexHtmlPath, string endpointsManifestPath, bool assetMainJs)
91+
private void ReplaceStringInIndexHtml(TestAsset testAsset, string scriptPath, string scriptPathWithFingerprintPattern)
92+
{
93+
if (scriptPathWithFingerprintPattern != null)
94+
{
95+
var indexHtmlPath = Path.Combine(testAsset.TestRoot, "wwwroot", "index.html");
96+
var indexHtmlContent = File.ReadAllText(indexHtmlPath);
97+
var newIndexHtmlContent = indexHtmlContent.Replace(scriptPath, scriptPathWithFingerprintPattern);
98+
if (indexHtmlContent == newIndexHtmlContent)
99+
throw new Exception($"Script replacement '{scriptPath}' for '{scriptPathWithFingerprintPattern}' didn't produce any change in '{indexHtmlPath}'");
100+
101+
File.WriteAllText(indexHtmlPath, newIndexHtmlContent);
102+
}
103+
}
104+
105+
private void AssertImportMapInHtml(string indexHtmlPath, string endpointsManifestPath, string scriptPath)
90106
{
91107
var indexHtmlContent = File.ReadAllText(indexHtmlPath);
92108
var endpoints = JsonSerializer.Deserialize<StaticWebAssetEndpointsManifest>(File.ReadAllText(endpointsManifestPath));
93109

94-
if (assetMainJs)
95-
{
96-
var mainJs = GetFingerprintedPath("main.js");
97-
Assert.DoesNotContain("src=\"main.js\"", indexHtmlContent);
98-
Assert.Contains($"src=\"{mainJs}\"", indexHtmlContent);
99-
}
110+
var fingerprintedScriptPath = GetFingerprintedPath(scriptPath);
111+
Assert.DoesNotContain($"src=\"{scriptPath}\"", indexHtmlContent);
112+
Assert.Contains($"src=\"{fingerprintedScriptPath}\"", indexHtmlContent);
100113

101114
Assert.Contains(GetFingerprintedPath("_framework/dotnet.js"), indexHtmlContent);
102115
Assert.Contains(GetFingerprintedPath("_framework/dotnet.native.js"), indexHtmlContent);
103116
Assert.Contains(GetFingerprintedPath("_framework/dotnet.runtime.js"), indexHtmlContent);
104117

105118
string GetFingerprintedPath(string route)
106-
=> endpoints.Endpoints.FirstOrDefault(e => e.Route == route && e.Selectors.Length == 0)?.AssetFile ?? throw new Exception($"Missing endpoint for file '{route}'");
119+
=> endpoints.Endpoints.FirstOrDefault(e => e.Route == route && e.Selectors.Length == 0)?.AssetFile ?? throw new Exception($"Missing endpoint for file '{route}' in '{endpointsManifestPath}'");
107120
}
108121
}

0 commit comments

Comments
 (0)