Skip to content

Commit 5f50397

Browse files
authored
Put only hard fingerprinted files into import map during publish (#47515)
1 parent 556271b commit 5f50397

File tree

5 files changed

+96
-30
lines changed

5 files changed

+96
-30
lines changed

src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.HtmlImportMap.targets

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ Copyright (c) .NET Foundation. All rights reserved.
9797

9898
<Target Name="GenerateHtmlImportMapBuildStaticWebAssets" DependsOnTargets="$(GenerateHtmlImportMapBuildStaticWebAssetsDependsOn)">
9999
<WriteImportMapToHtml
100+
Assets="@(_EsModuleCandidate)"
100101
Endpoints="@(_EsModuleCandidateEndpoints)"
102+
IncludeOnlyHardFingerprintedModules="false"
101103
HtmlFiles="@(_HtmlStaticWebAssets)"
102104
OutputPath="$(_BuildImportMapHtmlPath)">
103105
<Output TaskParameter="HtmlCandidates" ItemName="_HtmlCandidates" />
@@ -165,7 +167,9 @@ Copyright (c) .NET Foundation. All rights reserved.
165167

166168
<Target Name="GenerateHtmlImportMapPublishStaticWebAssets" DependsOnTargets="$(GenerateHtmlImportMapPublishStaticWebAssetsDependsOn)">
167169
<WriteImportMapToHtml
170+
Assets="@(_EsModuleCandidateForPublish)"
168171
Endpoints="@(_EsModuleCandidateForPublishEndpoints)"
172+
IncludeOnlyHardFingerprintedModules="true"
169173
HtmlFiles="@(_HtmlStaticWebAssets)"
170174
OutputPath="$(_PublishImportMapHtmlPath)">
171175
<Output TaskParameter="HtmlCandidates" ItemName="_HtmlPublishCandidates" />

src/StaticWebAssetsSdk/Tasks/WriteImportMapToHtml.cs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
1313

1414
public partial class WriteImportMapToHtml : Task
1515
{
16+
[Required]
17+
public ITaskItem[] Assets { get; set; } = [];
18+
1619
[Required]
1720
public ITaskItem[] Endpoints { get; set; } = [];
1821

22+
[Required]
23+
public bool IncludeOnlyHardFingerprintedModules { get; set; }
24+
1925
[Required]
2026
public string OutputPath { get; set; } = string.Empty;
2127

@@ -73,7 +79,7 @@ public override bool Execute()
7379
outputContent = _assetsRegex.Replace(outputContent, e =>
7480
{
7581
string assetPath = e.Groups[1].Value + e.Groups[3].Value;
76-
string fingerprintedAssetPath = urlMappings.TryGetValue(assetPath, out var value) ? value.Url : assetPath;
82+
string fingerprintedAssetPath = GetFingerprintedAssetPath(urlMappings, assetPath);
7783
Log.LogMessage("Replacing asset '{0}' with fingerprinted version '{1}'", assetPath, fingerprintedAssetPath);
7884
return "\"" + fingerprintedAssetPath + "\"";
7985
});
@@ -99,25 +105,35 @@ public override bool Execute()
99105
return true;
100106
}
101107

102-
internal static List<ResourceAsset> CreateResourcesFromEndpoints(IEnumerable<StaticWebAssetEndpoint> endpoints)
108+
private string GetFingerprintedAssetPath(Dictionary<string, ResourceAsset> urlMappings, string assetPath)
109+
{
110+
if (urlMappings.TryGetValue(assetPath, out var asset) && (!IncludeOnlyHardFingerprintedModules || asset.IsHardFingerprinted))
111+
{
112+
return asset.Url;
113+
}
114+
115+
return assetPath;
116+
}
117+
118+
internal List<ResourceAsset> CreateResourcesFromEndpoints(IEnumerable<StaticWebAssetEndpoint> endpoints)
103119
{
104120
var resources = new List<ResourceAsset>();
105121

106122
// We are converting a subset of the descriptors to resources and including a subset of the properties exposed by the
107123
// descriptors that are useful for the resources in the context of Blazor. Specifically, we pass in the `label` property
108124
// which contains the human-readable identifier for fingerprinted assets, and the integrity, which can be used to apply
109125
// subresource integrity to things like images, script tags, etc.
110-
foreach (var descriptor in endpoints)
126+
foreach (var endpoint in endpoints)
111127
{
112128
string? label = null;
113129
string? integrity = null;
114130

115131
// If there's a selector this means that this is an alternative representation for a resource, so skip it.
116-
if (descriptor.Selectors?.Length == 0)
132+
if (endpoint.Selectors?.Length == 0)
117133
{
118-
for (var i = 0; i < descriptor.EndpointProperties?.Length; i++)
134+
for (var i = 0; i < endpoint.EndpointProperties?.Length; i++)
119135
{
120-
var property = descriptor.EndpointProperties[i];
136+
var property = endpoint.EndpointProperties[i];
121137
if (property.Name.Equals("label", StringComparison.OrdinalIgnoreCase))
122138
{
123139
label = property.Value;
@@ -128,27 +144,40 @@ internal static List<ResourceAsset> CreateResourcesFromEndpoints(IEnumerable<Sta
128144
}
129145
}
130146

131-
resources.Add(new ResourceAsset(descriptor.Route, label, integrity));
147+
bool isHardFingerprinted = true;
148+
var asset = Assets.FirstOrDefault(a => a.ItemSpec == endpoint.AssetFile);
149+
if (asset != null)
150+
{
151+
isHardFingerprinted = asset.GetMetadata("RelativePath").Contains("#[.{fingerprint}]!");
152+
}
153+
154+
resources.Add(new ResourceAsset(endpoint.Route, label, integrity, isHardFingerprinted));
132155
}
133156
}
134157

135158
return resources;
136159
}
137160

138-
private static ImportMap CreateImportMapFromResources(List<ResourceAsset> assets)
161+
private ImportMap CreateImportMapFromResources(List<ResourceAsset> assets)
139162
{
140163
Dictionary<string, string>? imports = new();
141-
Dictionary<string, Dictionary<string, string>>? scopes = new(); ;
164+
Dictionary<string, Dictionary<string, string>>? scopes = new();
142165
Dictionary<string, string>? integrity = new();
143166

144167
foreach (var asset in assets)
145168
{
169+
if (IncludeOnlyHardFingerprintedModules && !asset.IsHardFingerprinted)
170+
{
171+
continue;
172+
}
173+
146174
if (asset.Integrity != null)
147175
{
148176
integrity ??= [];
149177
integrity[$"./{asset.Url}"] = asset.Integrity;
150178
}
151179

180+
// Only fingerprinted assets have label
152181
if (asset.Label != null)
153182
{
154183
imports ??= [];
@@ -178,11 +207,12 @@ private static Dictionary<string, ResourceAsset> GroupResourcesByLabel(List<Reso
178207
}
179208
}
180209

181-
internal sealed class ResourceAsset(string url, string? label, string? integrity)
210+
internal sealed class ResourceAsset(string url, string? label, string? integrity, bool isHardFingerprinted)
182211
{
183212
public string Url { get; } = url;
184213
public string? Label { get; set; } = label;
185214
public string? Integrity { get; set; } = integrity;
215+
public bool IsHardFingerprinted { get; set; } = isHardFingerprinted;
186216
}
187217

188218
internal class ImportMap(Dictionary<string, string> imports, Dictionary<string, Dictionary<string, string>> scopes, Dictionary<string, string> integrity)

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

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

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

5556
[Theory]
5657
[MemberData(nameof(WriteImportMapToHtmlData))]
57-
public void Build_WriteImportMapToHtml(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern)
58+
public void Build_WriteImportMapToHtml(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern, bool fingerprintUserJavascriptAssets, bool expectFingerprintOnScript)
5859
{
5960
ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
6061
ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern);
62+
FingerprintUserJavascriptAssets(fingerprintUserJavascriptAssets);
6163

6264
var build = CreateBuildCommand(ProjectDirectory);
63-
ExecuteCommand(build, "-p:WriteImportMapToHtml=true").Should().Pass();
65+
ExecuteCommand(build, "-p:WriteImportMapToHtml=true", $"-p:FingerprintUserJavascriptAssets={fingerprintUserJavascriptAssets}").Should().Pass();
6466

6567
var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
6668
var indexHtmlPath = Directory.EnumerateFiles(Path.Combine(intermediateOutputPath, "staticwebassets", "importmaphtml", "build"), "*.html").Single();
6769
var endpointsManifestPath = Path.Combine(intermediateOutputPath, $"staticwebassets.build.endpoints.json");
6870

69-
AssertImportMapInHtml(indexHtmlPath, endpointsManifestPath, scriptPath);
71+
AssertImportMapInHtml(indexHtmlPath, endpointsManifestPath, scriptPath, expectFingerprintOnScript: expectFingerprintOnScript);
7072
}
7173

7274
[Theory]
7375
[MemberData(nameof(WriteImportMapToHtmlData))]
74-
public void Publish_WriteImportMapToHtml(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern)
76+
public void Publish_WriteImportMapToHtml(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern, bool fingerprintUserJavascriptAssets, bool expectFingerprintOnScript)
7577
{
7678
ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
7779
ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern);
80+
FingerprintUserJavascriptAssets(fingerprintUserJavascriptAssets);
7881

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

8184
var publish = CreatePublishCommand(ProjectDirectory);
82-
ExecuteCommand(publish, "-p:WriteImportMapToHtml=true").Should().Pass();
85+
ExecuteCommand(publish, "-p:WriteImportMapToHtml=true", $"-p:FingerprintUserJavascriptAssets={fingerprintUserJavascriptAssets}").Should().Pass();
8386

8487
var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
8588
var indexHtmlOutputPath = Path.Combine(outputPath, "wwwroot", "index.html");
8689
var endpointsManifestPath = Path.Combine(outputPath, $"{projectName}.staticwebassets.endpoints.json");
8790

88-
AssertImportMapInHtml(indexHtmlOutputPath, endpointsManifestPath, scriptPath);
91+
AssertImportMapInHtml(indexHtmlOutputPath, endpointsManifestPath, scriptPath, expectFingerprintOnScript: expectFingerprintOnScript);
8992
}
9093

91-
private void ReplaceStringInIndexHtml(TestAsset testAsset, string scriptPath, string scriptPathWithFingerprintPattern)
94+
private void FingerprintUserJavascriptAssets(bool fingerprintUserJavascriptAssets)
9295
{
93-
if (scriptPathWithFingerprintPattern != null)
96+
if (fingerprintUserJavascriptAssets)
97+
{
98+
ProjectDirectory.WithProjectChanges(p =>
99+
{
100+
if (p.Root != null)
101+
{
102+
var itemGroup = new XElement("ItemGroup");
103+
var pattern = new XElement("StaticWebAssetFingerprintPattern");
104+
pattern.SetAttributeValue("Include", "Js");
105+
pattern.SetAttributeValue("Pattern", "*.js");
106+
pattern.SetAttributeValue("Expression", "#[.{fingerprint}]!");
107+
itemGroup.Add(pattern);
108+
p.Root.Add(itemGroup);
109+
}
110+
});
111+
}
112+
}
113+
114+
private void ReplaceStringInIndexHtml(TestAsset testAsset, string sourceValue, string targetValue)
115+
{
116+
if (targetValue != null)
94117
{
95118
var indexHtmlPath = Path.Combine(testAsset.TestRoot, "wwwroot", "index.html");
96119
var indexHtmlContent = File.ReadAllText(indexHtmlPath);
97-
var newIndexHtmlContent = indexHtmlContent.Replace(scriptPath, scriptPathWithFingerprintPattern);
120+
var newIndexHtmlContent = indexHtmlContent.Replace(sourceValue, targetValue);
98121
if (indexHtmlContent == newIndexHtmlContent)
99-
throw new Exception($"Script replacement '{scriptPath}' for '{scriptPathWithFingerprintPattern}' didn't produce any change in '{indexHtmlPath}'");
122+
throw new Exception($"String replacement '{sourceValue}' for '{targetValue}' didn't produce any change in '{indexHtmlPath}'");
100123

101124
File.WriteAllText(indexHtmlPath, newIndexHtmlContent);
102125
}
103126
}
104127

105-
private void AssertImportMapInHtml(string indexHtmlPath, string endpointsManifestPath, string scriptPath)
128+
private void AssertImportMapInHtml(string indexHtmlPath, string endpointsManifestPath, string scriptPath, bool expectFingerprintOnScript = true)
106129
{
107130
var indexHtmlContent = File.ReadAllText(indexHtmlPath);
108131
var endpoints = JsonSerializer.Deserialize<StaticWebAssetEndpointsManifest>(File.ReadAllText(endpointsManifestPath));
109132

110133
var fingerprintedScriptPath = GetFingerprintedPath(scriptPath);
111-
Assert.DoesNotContain($"src=\"{scriptPath}\"", indexHtmlContent);
112-
Assert.Contains($"src=\"{fingerprintedScriptPath}\"", indexHtmlContent);
134+
if (expectFingerprintOnScript)
135+
{
136+
Assert.DoesNotContain($"src=\"{scriptPath}\"", indexHtmlContent);
137+
Assert.Contains($"src=\"{fingerprintedScriptPath}\"", indexHtmlContent);
138+
}
139+
else
140+
{
141+
Assert.Contains(scriptPath, indexHtmlContent);
142+
143+
if (scriptPath != fingerprintedScriptPath)
144+
{
145+
Assert.DoesNotContain(fingerprintedScriptPath, indexHtmlContent);
146+
}
147+
}
113148

114149
Assert.Contains(GetFingerprintedPath("_framework/dotnet.js"), indexHtmlContent);
115150
Assert.Contains(GetFingerprintedPath("_framework/dotnet.native.js"), indexHtmlContent);

test/TestAssets/TestProjects/VanillaWasm/VanillaWasm.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,4 @@
33
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
44
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
55
</PropertyGroup>
6-
<ItemGroup>
7-
<StaticWebAssetFingerprintPattern Include="Js" Pattern="*.js" Expression="#[.{fingerprint}]!" />
8-
</ItemGroup>
96
</Project>

test/TestAssets/TestProjects/VanillaWasm/wwwroot/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<meta charset="UTF-8">
99
<meta name="viewport" content="width=device-width, initial-scale=1.0">
1010
<script type="importmap"></script>
11-
<script type="module" src="main#[.{fingerprint}].js"></script>
11+
<script type="module" src="main.js"></script>
1212

1313
</head>
1414

0 commit comments

Comments
 (0)