Skip to content

Commit 79c270e

Browse files
authored
[StaticWebAssets] Fix subpath issue on SWA dev manifest (dotnet#42227)
Fixes an issue where the subpath was being computed incorrectly for assets that will be copied out to the wwwroot folder during the build. The fix is to check if the file exists to only use the suffix from the identity if the file exists on disk and the content root is a prefix of the file Identity. In any other case we expect the file to be at the contentroot + relative path location.
1 parent 18985b1 commit 79c270e

File tree

2 files changed

+193
-15
lines changed

2 files changed

+193
-15
lines changed

src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ private StaticWebAssetsDevelopmentManifest CreateManifest(
151151
}
152152
var matchingAsset = new StaticWebAssetMatch
153153
{
154-
SubPath = asset.Identity.StartsWith(asset.ContentRoot) ?
155-
StaticWebAsset.Normalize(asset.Identity.Substring(asset.ContentRoot.Length)) :
156-
asset.RelativePath,
154+
SubPath = ResolveSubPath(asset),
157155
ContentRootIndex = index
158156
};
159157
currentNode.Children ??= new Dictionary<string, StaticWebAssetNode>(StringComparer.Ordinal);
@@ -271,6 +269,33 @@ private StaticWebAssetsDevelopmentManifest CreateManifest(
271269
ContentRoots = contentRootIndex.OrderBy(kvp => kvp.Value).Select(kvp => kvp.Key).ToArray(),
272270
Root = root
273271
};
272+
273+
static string ResolveSubPath(StaticWebAsset asset)
274+
{
275+
if (File.Exists(asset.Identity))
276+
{
277+
if (asset.Identity.StartsWith(asset.ContentRoot, OSPath.PathComparison))
278+
{
279+
// We need an extra check that the file exist to avoid pointing out to a non-existing file. This can happen
280+
// when the asset is defined with an identity that doesn't exist yet, but that will be materialized later
281+
// when the asset is copied to the wwwroot folder.
282+
return StaticWebAsset.Normalize(asset.Identity.Substring(asset.ContentRoot.Length));
283+
}
284+
else
285+
{
286+
// This is a content root that we don't know about, so we can't resolve the subpath based on the identity, and
287+
// we need to rely on the assumption that the file will be available at contentRoot + relativePath.
288+
return asset.ReplaceTokens(asset.RelativePath, StaticWebAssetTokenResolver.Instance);
289+
}
290+
}
291+
else
292+
{
293+
// In any other case where the file doesn't exist, we expect the file to end up at the correct final location
294+
// which is defined by contentRoot + relativePath, and since the file will be copied there, the tokens will be
295+
// replaced as needed so that the file can be found.
296+
return asset.ReplaceTokens(asset.RelativePath, StaticWebAssetTokenResolver.Instance);
297+
}
298+
}
274299
}
275300

276301
public class StaticWebAssetsDevelopmentManifest

test/Microsoft.NET.Sdk.Razor.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void ComputeDevelopmentManifest_ReplacesAssetTokens(string fingerprintExp
7777

7878
var expectedManifest = CreateExpectedManifest(
7979
CreateIntermediateNode(
80-
(path, CreateMatchNode(0, fileName))),
80+
(path, CreateMatchNode(0, path))),
8181
Environment.CurrentDirectory);
8282

8383
var task = new GenerateStaticWebAssetsDevelopmentManifest()
@@ -95,6 +95,133 @@ public void ComputeDevelopmentManifest_ReplacesAssetTokens(string fingerprintExp
9595
manifest.Should().BeEquivalentTo(expectedManifest);
9696
}
9797

98+
[Theory]
99+
[InlineData("#[.{fingerprint}]?", "index.html", "optional.html")]
100+
[InlineData("#[.{fingerprint}]!", "index.fingerprint.html", "preferred.html")]
101+
[InlineData("#[.{fingerprint}]", "index.fingerprint.html", "required.html")]
102+
public void ComputeDevelopmentManifest_ReplacesAssetTokens_FileExists(string fingerprintExpression, string path, string subPath)
103+
{
104+
// Arrange
105+
var messages = new List<string>();
106+
var buildEngine = new Mock<IBuildEngine>();
107+
buildEngine.Setup(e => e.LogMessageEvent(It.IsAny<BuildMessageEventArgs>()))
108+
.Callback<BuildMessageEventArgs>(args => messages.Add(args.Message));
109+
110+
var expectedManifest = CreateExpectedManifest(
111+
CreateIntermediateNode(
112+
(path, CreateMatchNode(0, subPath))),
113+
Environment.CurrentDirectory);
114+
115+
var task = new GenerateStaticWebAssetsDevelopmentManifest()
116+
{
117+
BuildEngine = buildEngine.Object,
118+
};
119+
120+
var assets = new[] { CreateAsset(subPath, $"index{fingerprintExpression}.html", assetKind: StaticWebAsset.AssetKinds.All) };
121+
var patterns = Array.Empty<StaticWebAssetsDiscoveryPattern>();
122+
123+
var fileName = Path.Combine(Environment.CurrentDirectory, subPath);
124+
try
125+
{
126+
File.WriteAllText(fileName, "content");
127+
// Act
128+
var manifest = task.ComputeDevelopmentManifest(assets, patterns);
129+
130+
// Assert
131+
manifest.Should().BeEquivalentTo(expectedManifest);
132+
}
133+
finally
134+
{
135+
if (File.Exists(fileName))
136+
{
137+
File.Delete(fileName);
138+
}
139+
}
140+
}
141+
142+
[Fact]
143+
public void ComputeDevelopmentManifest_UsesIdentitySubpath_WhenFileExists_AndContentRoot_IsPrefix()
144+
{
145+
// Arrange
146+
var messages = new List<string>();
147+
var buildEngine = new Mock<IBuildEngine>();
148+
buildEngine.Setup(e => e.LogMessageEvent(It.IsAny<BuildMessageEventArgs>()))
149+
.Callback<BuildMessageEventArgs>(args => messages.Add(args.Message));
150+
151+
var expectedManifest = CreateExpectedManifest(
152+
CreateIntermediateNode(
153+
("_framework",
154+
CreateIntermediateNode(
155+
("dotnet.native.fingerprint.js.gz", CreateMatchNode(0, "blob-hash.gz"))))),
156+
Environment.CurrentDirectory);
157+
158+
var task = new GenerateStaticWebAssetsDevelopmentManifest()
159+
{
160+
BuildEngine = buildEngine.Object,
161+
};
162+
163+
var fileName = Path.Combine(Environment.CurrentDirectory, "blob-hash.gz");
164+
try
165+
{
166+
File.WriteAllText(fileName, "content");
167+
var assets = new[] { CreateAsset(
168+
fileName,
169+
$$"""_framework/dotnet.native#[.{fingerprint}]!.js.gz""",
170+
contentRoot: Environment.CurrentDirectory,
171+
assetKind: StaticWebAsset.AssetKinds.All) };
172+
var patterns = Array.Empty<StaticWebAssetsDiscoveryPattern>();
173+
174+
// Act
175+
var manifest = task.ComputeDevelopmentManifest(assets, patterns);
176+
177+
// Assert
178+
manifest.Should().BeEquivalentTo(expectedManifest);
179+
}
180+
finally
181+
{
182+
if (File.Exists(fileName))
183+
{
184+
File.Delete(fileName);
185+
}
186+
}
187+
}
188+
189+
[Fact]
190+
public void ComputeDevelopmentManifest_UsesRelativePath_ReplacesAssetTokens_WhenFileDoesNotExist_AtIdentity()
191+
{
192+
// Arrange
193+
var messages = new List<string>();
194+
var buildEngine = new Mock<IBuildEngine>();
195+
buildEngine.Setup(e => e.LogMessageEvent(It.IsAny<BuildMessageEventArgs>()))
196+
.Callback<BuildMessageEventArgs>(args => messages.Add(args.Message));
197+
198+
var expectedManifest = CreateExpectedManifest(
199+
CreateIntermediateNode(
200+
("_framework",
201+
CreateIntermediateNode(
202+
("dotnet.native.fingerprint.js", CreateMatchNode(0, "_framework/dotnet.native.fingerprint.js"))))),
203+
Environment.CurrentDirectory);
204+
205+
var task = new GenerateStaticWebAssetsDevelopmentManifest()
206+
{
207+
BuildEngine = buildEngine.Object,
208+
};
209+
210+
var fileName = Path.Combine(Environment.CurrentDirectory, "dotnet.native.js");
211+
var assets = new[] { CreateAsset(
212+
fileName,
213+
$$"""_framework/dotnet.native#[.{fingerprint}]!.js""",
214+
contentRoot: Environment.CurrentDirectory,
215+
assetKind: StaticWebAsset.AssetKinds.All) };
216+
var patterns = Array.Empty<StaticWebAssetsDiscoveryPattern>();
217+
218+
// Act
219+
var manifest = task.ComputeDevelopmentManifest(assets, patterns);
220+
221+
// Assert
222+
manifest.Should().BeEquivalentTo(expectedManifest);
223+
}
224+
98225
[Fact]
99226
public void ComputeDevelopmentManifest_IncludesAllAssets()
100227
{
@@ -205,11 +332,23 @@ public void ComputeDevelopmentManifest_PrefersBuildAssetsOverAllAssets()
205332
};
206333
var patterns = Array.Empty<StaticWebAssetsDiscoveryPattern>();
207334

208-
// Act
209-
var manifest = task.ComputeDevelopmentManifest(assets, patterns);
210-
211-
// Assert
212-
manifest.Should().BeEquivalentTo(expectedManifest);
335+
var fileName = Path.Combine(Environment.CurrentDirectory, "index.build.html");
336+
try
337+
{
338+
File.WriteAllText(fileName, "content");
339+
// Act
340+
var manifest = task.ComputeDevelopmentManifest(assets, patterns);
341+
342+
// Assert
343+
manifest.Should().BeEquivalentTo(expectedManifest);
344+
}
345+
finally
346+
{
347+
if (File.Exists(fileName))
348+
{
349+
File.Delete(fileName);
350+
}
351+
}
213352
}
214353

215354
[Fact]
@@ -221,9 +360,10 @@ public void ComputeDevelopmentManifest_UsesIdentityWhenContentRootStartsByIdenti
221360
buildEngine.Setup(e => e.LogMessageEvent(It.IsAny<BuildMessageEventArgs>()))
222361
.Callback<BuildMessageEventArgs>(args => messages.Add(args.Message));
223362

363+
var filePath = Path.Combine("some", "subfolder", "index.build.html");
224364
var expectedManifest = CreateExpectedManifest(
225365
CreateIntermediateNode(
226-
("index.html", CreateMatchNode(0, StaticWebAsset.Normalize(Path.Combine("some", "subfolder", "index.build.html"))))),
366+
("index.html", CreateMatchNode(0, StaticWebAsset.Normalize(filePath)))),
227367
Environment.CurrentDirectory);
228368

229369
var task = new GenerateStaticWebAssetsDevelopmentManifest()
@@ -233,15 +373,28 @@ public void ComputeDevelopmentManifest_UsesIdentityWhenContentRootStartsByIdenti
233373
};
234374

235375
var assets = new[] {
236-
CreateAsset(Path.Combine("some", "subfolder", "index.build.html"), "index.html"),
376+
CreateAsset(filePath, "index.html"),
237377
};
238378
var patterns = Array.Empty<StaticWebAssetsDiscoveryPattern>();
239379

240-
// Act
241-
var manifest = task.ComputeDevelopmentManifest(assets, patterns);
380+
try
381+
{
382+
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
383+
File.WriteAllText(filePath, "content");
242384

243-
// Assert
244-
manifest.Should().BeEquivalentTo(expectedManifest);
385+
// Act
386+
var manifest = task.ComputeDevelopmentManifest(assets, patterns);
387+
388+
// Assert
389+
manifest.Should().BeEquivalentTo(expectedManifest);
390+
}
391+
finally
392+
{
393+
if (File.Exists(filePath))
394+
{
395+
File.Delete(filePath);
396+
}
397+
}
245398
}
246399

247400
[Fact]

0 commit comments

Comments
 (0)