From 733c1db68ea3336c554caa905fb4e7aa71e08fff Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 19:04:34 +0000
Subject: [PATCH 1/4] Initial plan
From 6966c3678b5f662a36e62e369941477b0b50c659 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 19:32:16 +0000
Subject: [PATCH 2/4] Add AdditionalStaticWebAssetsBasePath property for
external content roots
- Add ParseAdditionalStaticWebAssetsBasePaths task to parse and discover files from additional content roots
- Add ResolveAdditionalStaticWebAssets target that runs as part of ResolveCoreStaticWebAssets
- Add UsingTask registration for the new task
- Add E2E tests for build and publish scenarios with additional content roots
- Support semicolon-separated list of content-root,base-path pairs
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../Microsoft.NET.Sdk.StaticWebAssets.targets | 51 ++++++
...ParseAdditionalStaticWebAssetsBasePaths.cs | 136 ++++++++++++++++
.../StaticWebAssetsIntegrationTest.cs | 147 ++++++++++++++++++
3 files changed, 334 insertions(+)
create mode 100644 src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
index 0fb3afa2fbc2..419e315cf425 100644
--- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
@@ -122,6 +122,11 @@ Copyright (c) .NET Foundation. All rights reserved.
AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)"
Condition="'$(StaticWebAssetsSdkBuildTasksAssembly)' != ''" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(PackageId)
+ %(_ParsedAdditionalContentRoot.BasePath)
+ %(_ParsedAdditionalContentRoot.ContentRoot)
+ **
+
+
+
+
+
diff --git a/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs
new file mode 100644
index 000000000000..8c309351b8b1
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+///
+/// Parses the AdditionalStaticWebAssetsBasePath property which is a semicolon-separated list of
+/// content-root,base-path pairs and discovers the files in each content root.
+///
+public class ParseAdditionalStaticWebAssetsBasePaths : Task
+{
+ ///
+ /// The semicolon-separated list of content-root,base-path pairs.
+ /// Format: content-root-1,base-path-1;content-root-2,base-path-2
+ ///
+ [Required]
+ public string AdditionalStaticWebAssetsBasePaths { get; set; }
+
+ ///
+ /// The parsed content root and base path pairs as items with ContentRoot and BasePath metadata.
+ ///
+ [Output]
+ public ITaskItem[] ParsedBasePaths { get; set; }
+
+ ///
+ /// The discovered files from all content roots with appropriate metadata for DefineStaticWebAssets.
+ ///
+ [Output]
+ public ITaskItem[] DiscoveredFiles { get; set; }
+
+ public override bool Execute()
+ {
+ var parsedPaths = new List();
+ var discoveredFiles = new List();
+
+ if (string.IsNullOrEmpty(AdditionalStaticWebAssetsBasePaths))
+ {
+ ParsedBasePaths = [];
+ DiscoveredFiles = [];
+ return true;
+ }
+
+ var pairs = AdditionalStaticWebAssetsBasePaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var pair in pairs)
+ {
+ var parts = pair.Split(new[] { ',' }, 2, StringSplitOptions.None);
+
+ if (parts.Length < 2)
+ {
+ Log.LogError(
+ "Invalid format for AdditionalStaticWebAssetsBasePath entry '{0}'. Expected format: 'content-root,base-path'.",
+ pair);
+ continue;
+ }
+
+ var contentRoot = parts[0].Trim();
+ var basePath = parts[1].Trim();
+
+ if (string.IsNullOrEmpty(contentRoot))
+ {
+ Log.LogError(
+ "Content root is empty in AdditionalStaticWebAssetsBasePath entry '{0}'.",
+ pair);
+ continue;
+ }
+
+ // Normalize content root path to end with directory separator
+ if (!contentRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
+ !contentRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
+ {
+ contentRoot += Path.DirectorySeparatorChar;
+ }
+
+ // Make content root path absolute if it's relative
+ if (!Path.IsPathRooted(contentRoot))
+ {
+ contentRoot = Path.GetFullPath(contentRoot);
+ }
+
+ var pathItem = new TaskItem(parsedPaths.Count.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ pathItem.SetMetadata("ContentRoot", contentRoot);
+ pathItem.SetMetadata("BasePath", basePath);
+
+ Log.LogMessage(MessageImportance.Low,
+ "Parsed additional static web asset base path: ContentRoot='{0}', BasePath='{1}'",
+ contentRoot,
+ basePath);
+
+ parsedPaths.Add(pathItem);
+
+ // Discover files from this content root
+ if (Directory.Exists(contentRoot))
+ {
+ var files = Directory.GetFiles(contentRoot, "*", SearchOption.AllDirectories);
+ foreach (var file in files)
+ {
+ var fullPath = Path.GetFullPath(file);
+ var relativePath = fullPath.Substring(contentRoot.Length);
+
+ // Normalize path separators
+ relativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/');
+
+ var fileItem = new TaskItem(fullPath);
+ fileItem.SetMetadata("ContentRoot", contentRoot);
+ fileItem.SetMetadata("BasePath", basePath);
+ fileItem.SetMetadata("RelativePath", relativePath);
+
+ Log.LogMessage(MessageImportance.Low,
+ "Discovered file '{0}' with relative path '{1}' in content root '{2}'",
+ fullPath,
+ relativePath,
+ contentRoot);
+
+ discoveredFiles.Add(fileItem);
+ }
+ }
+ else
+ {
+ Log.LogWarning(
+ "Content root directory '{0}' does not exist.",
+ contentRoot);
+ }
+ }
+
+ ParsedBasePaths = [.. parsedPaths];
+ DiscoveredFiles = [.. discoveredFiles];
+
+ return !Log.HasLoggedErrors;
+ }
+}
diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs
index c13c8b2d5c6b..45a33ccbd683 100644
--- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs
+++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs
@@ -617,6 +617,153 @@ public void Build_DoesNotFailToCompress_TwoAssetsWith_TheSameContent()
AssertManifest(manifest1, expectedManifest);
AssertBuildAssets(manifest1, outputPath, intermediateOutputPath);
}
+
+ [Fact]
+ public void Build_AdditionalStaticWebAssetsBasePath_IncludesExternalContentRoots()
+ {
+ var testAsset = "RazorComponentApp";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ // Create an additional folder outside wwwroot to serve as additional content root
+ var additionalContentRoot = Path.Combine(ProjectDirectory.Path, "AdditionalAssets");
+ Directory.CreateDirectory(additionalContentRoot);
+ File.WriteAllText(Path.Combine(additionalContentRoot, "extra.js"), "console.log('extra');");
+ File.WriteAllText(Path.Combine(additionalContentRoot, "extra.css"), ".extra { color: red; }");
+
+ // Create a subdirectory with more assets
+ var subDir = Path.Combine(additionalContentRoot, "sub");
+ Directory.CreateDirectory(subDir);
+ File.WriteAllText(Path.Combine(subDir, "nested.txt"), "nested content");
+
+ var build = CreateBuildCommand(ProjectDirectory);
+
+ // Pass the AdditionalStaticWebAssetsBasePath property with format: content-root,base-path
+ // Quote the entire value to prevent MSBuild from interpreting the comma as a separator
+ var additionalAssetsProperty = $"\"{additionalContentRoot},_content/AdditionalAssets\"";
+ ExecuteCommand(build, $"/p:AdditionalStaticWebAssetsBasePath={additionalAssetsProperty}").Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+ var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify the manifest file exists
+ var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
+ new FileInfo(path).Should().Exist();
+
+ var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path));
+
+ // Verify the additional assets are in the manifest (only count Primary assets, not compressed alternatives)
+ var additionalAssets = manifest.Assets.Where(a =>
+ a.AssetRole == "Primary" &&
+ (a.RelativePath.Contains("extra.js") ||
+ a.RelativePath.Contains("extra.css") ||
+ a.RelativePath.Contains("nested.txt"))).ToArray();
+
+ additionalAssets.Should().HaveCount(3, "All additional assets should be included in the manifest");
+
+ // Verify the base path is correct
+ foreach (var asset in additionalAssets)
+ {
+ asset.BasePath.Should().Be("_content/AdditionalAssets");
+ }
+
+ // Verify discovery patterns include the additional content root
+ var discoveryPattern = manifest.DiscoveryPatterns.FirstOrDefault(p =>
+ p.BasePath == "_content/AdditionalAssets");
+ discoveryPattern.Should().NotBeNull("Discovery pattern for additional content root should exist");
+ }
+
+ [Fact]
+ public void Build_AdditionalStaticWebAssetsBasePath_MultipleContentRoots()
+ {
+ var testAsset = "RazorComponentApp";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ // Create first additional folder
+ var additionalContentRoot1 = Path.Combine(ProjectDirectory.Path, "AdditionalAssets1");
+ Directory.CreateDirectory(additionalContentRoot1);
+ File.WriteAllText(Path.Combine(additionalContentRoot1, "file1.js"), "console.log('file1');");
+
+ // Create second additional folder
+ var additionalContentRoot2 = Path.Combine(ProjectDirectory.Path, "AdditionalAssets2");
+ Directory.CreateDirectory(additionalContentRoot2);
+ File.WriteAllText(Path.Combine(additionalContentRoot2, "file2.js"), "console.log('file2');");
+
+ var build = CreateBuildCommand(ProjectDirectory);
+
+ // Pass multiple content roots separated by semicolons
+ // Quote the entire value to prevent MSBuild from interpreting commas/semicolons as separators
+ var additionalAssetsProperty = $"\"{additionalContentRoot1},_content/Assets1;{additionalContentRoot2},_content/Assets2\"";
+ ExecuteCommand(build, $"/p:AdditionalStaticWebAssetsBasePath={additionalAssetsProperty}").Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
+ new FileInfo(path).Should().Exist();
+
+ var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path));
+
+ // Verify assets from both content roots are in the manifest (only count Primary assets)
+ var asset1 = manifest.Assets.FirstOrDefault(a => a.AssetRole == "Primary" && a.RelativePath.Contains("file1.js"));
+ var asset2 = manifest.Assets.FirstOrDefault(a => a.AssetRole == "Primary" && a.RelativePath.Contains("file2.js"));
+
+ asset1.Should().NotBeNull("Asset from first additional content root should exist");
+ asset1!.BasePath.Should().Be("_content/Assets1");
+
+ asset2.Should().NotBeNull("Asset from second additional content root should exist");
+ asset2!.BasePath.Should().Be("_content/Assets2");
+
+ // Verify discovery patterns for both content roots
+ manifest.DiscoveryPatterns.Should().Contain(p => p.BasePath == "_content/Assets1");
+ manifest.DiscoveryPatterns.Should().Contain(p => p.BasePath == "_content/Assets2");
+ }
+
+ [Fact]
+ public void Publish_AdditionalStaticWebAssetsBasePath_IncludesExternalContentRoots()
+ {
+ var testAsset = "RazorComponentApp";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ // Create an additional folder outside wwwroot to serve as additional content root
+ var additionalContentRoot = Path.Combine(ProjectDirectory.Path, "AdditionalAssets");
+ Directory.CreateDirectory(additionalContentRoot);
+ File.WriteAllText(Path.Combine(additionalContentRoot, "publish-extra.js"), "console.log('publish-extra');");
+
+ var publish = CreatePublishCommand(ProjectDirectory);
+
+ // Pass the AdditionalStaticWebAssetsBasePath property
+ // Quote the entire value to prevent MSBuild from interpreting the comma as a separator
+ var additionalAssetsProperty = $"\"{additionalContentRoot},_content/AdditionalAssets\"";
+ ExecuteCommand(publish, $"/p:AdditionalStaticWebAssetsBasePath={additionalAssetsProperty}").Should().Pass();
+
+ var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+ var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify the build manifest file exists and includes the additional assets
+ var buildPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
+ new FileInfo(buildPath).Should().Exist();
+
+ var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(buildPath));
+
+ // Verify the additional asset is in the build manifest (only count Primary assets)
+ var additionalAsset = buildManifest.Assets.FirstOrDefault(a =>
+ a.AssetRole == "Primary" && a.RelativePath.Contains("publish-extra.js"));
+
+ additionalAsset.Should().NotBeNull("Additional asset should be included in the build manifest");
+ additionalAsset!.BasePath.Should().Be("_content/AdditionalAssets");
+
+ // Verify the publish manifest file exists
+ var publishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json");
+ new FileInfo(publishManifestPath).Should().Exist();
+
+ var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(publishManifestPath));
+
+ // Verify the additional asset is in the publish manifest
+ var publishAsset = publishManifest.Assets.FirstOrDefault(a =>
+ a.AssetRole == "Primary" && a.RelativePath.Contains("publish-extra.js"));
+
+ publishAsset.Should().NotBeNull("Additional asset should be included in the publish manifest");
+ publishAsset!.BasePath.Should().Be("_content/AdditionalAssets");
+ }
}
public class StaticWebAssetsAppWithPackagesIntegrationTest(ITestOutputHelper log)
From 7900320dc2aceb2894d7f06642acec7de9c46d21 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 19:38:10 +0000
Subject: [PATCH 3/4] Address code review comments for cross-framework
compatibility
- Use conditional compilation for EndsWith(char) and Path.GetRelativePath
- Add fallback implementations for .NET Framework 4.7.2
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../ParseAdditionalStaticWebAssetsBasePaths.cs | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs
index 8c309351b8b1..8f6616b9562c 100644
--- a/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs
@@ -71,8 +71,13 @@ public override bool Execute()
}
// Normalize content root path to end with directory separator
+#if NET
+ if (!contentRoot.EndsWith(Path.DirectorySeparatorChar) &&
+ !contentRoot.EndsWith(Path.AltDirectorySeparatorChar))
+#else
if (!contentRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
!contentRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
+#endif
{
contentRoot += Path.DirectorySeparatorChar;
}
@@ -101,7 +106,14 @@ public override bool Execute()
foreach (var file in files)
{
var fullPath = Path.GetFullPath(file);
- var relativePath = fullPath.Substring(contentRoot.Length);
+#if NET
+ var relativePath = Path.GetRelativePath(contentRoot, fullPath);
+#else
+ // For .NET Framework, manually compute relative path
+ var relativePath = fullPath.StartsWith(contentRoot, StringComparison.OrdinalIgnoreCase)
+ ? fullPath.Substring(contentRoot.Length)
+ : fullPath;
+#endif
// Normalize path separators
relativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/');
From 76775fbe600dcf80b4859dd5c0a9fd307e8dd13f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Dec 2025 19:52:58 +0000
Subject: [PATCH 4/4] Replace custom task with MSBuild ItemGroup transforms
- Remove ParseAdditionalStaticWebAssetsBasePaths custom task
- Use MSBuild property functions to parse content-root,base-path pairs
- Use ItemGroup transforms to split and discover files from content roots
Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com>
---
.../Microsoft.NET.Sdk.StaticWebAssets.targets | 27 ++--
...ParseAdditionalStaticWebAssetsBasePaths.cs | 148 ------------------
2 files changed, 16 insertions(+), 159 deletions(-)
delete mode 100644 src/StaticWebAssetsSdk/Tasks/ParseAdditionalStaticWebAssetsBasePaths.cs
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
index 419e315cf425..8de4f6806131 100644
--- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
@@ -122,11 +122,6 @@ Copyright (c) .NET Foundation. All rights reserved.
AssemblyFile="$(StaticWebAssetsSdkBuildTasksAssembly)"
Condition="'$(StaticWebAssetsSdkBuildTasksAssembly)' != ''" />
-
-
+
+ <_AdditionalStaticWebAssetsBasePathEntry Include="$(AdditionalStaticWebAssetsBasePath)" />
+ <_ParsedAdditionalContentRoot Include="@(_AdditionalStaticWebAssetsBasePathEntry)">
+ $([System.String]::Copy('%(Identity)').Split(',')[0].Trim())$([System.IO.Path]::DirectorySeparatorChar)
+ $([System.String]::Copy('%(Identity)').Split(',')[1].Trim())
+
+
+
+
+
+ <_AdditionalContentRootCandidates Include="%(_ParsedAdditionalContentRoot.ContentRoot)**">
+ %(_ParsedAdditionalContentRoot.ContentRoot)
+ %(_ParsedAdditionalContentRoot.BasePath)
+
+
-/// Parses the AdditionalStaticWebAssetsBasePath property which is a semicolon-separated list of
-/// content-root,base-path pairs and discovers the files in each content root.
-///
-public class ParseAdditionalStaticWebAssetsBasePaths : Task
-{
- ///
- /// The semicolon-separated list of content-root,base-path pairs.
- /// Format: content-root-1,base-path-1;content-root-2,base-path-2
- ///
- [Required]
- public string AdditionalStaticWebAssetsBasePaths { get; set; }
-
- ///
- /// The parsed content root and base path pairs as items with ContentRoot and BasePath metadata.
- ///
- [Output]
- public ITaskItem[] ParsedBasePaths { get; set; }
-
- ///
- /// The discovered files from all content roots with appropriate metadata for DefineStaticWebAssets.
- ///
- [Output]
- public ITaskItem[] DiscoveredFiles { get; set; }
-
- public override bool Execute()
- {
- var parsedPaths = new List();
- var discoveredFiles = new List();
-
- if (string.IsNullOrEmpty(AdditionalStaticWebAssetsBasePaths))
- {
- ParsedBasePaths = [];
- DiscoveredFiles = [];
- return true;
- }
-
- var pairs = AdditionalStaticWebAssetsBasePaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
-
- foreach (var pair in pairs)
- {
- var parts = pair.Split(new[] { ',' }, 2, StringSplitOptions.None);
-
- if (parts.Length < 2)
- {
- Log.LogError(
- "Invalid format for AdditionalStaticWebAssetsBasePath entry '{0}'. Expected format: 'content-root,base-path'.",
- pair);
- continue;
- }
-
- var contentRoot = parts[0].Trim();
- var basePath = parts[1].Trim();
-
- if (string.IsNullOrEmpty(contentRoot))
- {
- Log.LogError(
- "Content root is empty in AdditionalStaticWebAssetsBasePath entry '{0}'.",
- pair);
- continue;
- }
-
- // Normalize content root path to end with directory separator
-#if NET
- if (!contentRoot.EndsWith(Path.DirectorySeparatorChar) &&
- !contentRoot.EndsWith(Path.AltDirectorySeparatorChar))
-#else
- if (!contentRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
- !contentRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
-#endif
- {
- contentRoot += Path.DirectorySeparatorChar;
- }
-
- // Make content root path absolute if it's relative
- if (!Path.IsPathRooted(contentRoot))
- {
- contentRoot = Path.GetFullPath(contentRoot);
- }
-
- var pathItem = new TaskItem(parsedPaths.Count.ToString(System.Globalization.CultureInfo.InvariantCulture));
- pathItem.SetMetadata("ContentRoot", contentRoot);
- pathItem.SetMetadata("BasePath", basePath);
-
- Log.LogMessage(MessageImportance.Low,
- "Parsed additional static web asset base path: ContentRoot='{0}', BasePath='{1}'",
- contentRoot,
- basePath);
-
- parsedPaths.Add(pathItem);
-
- // Discover files from this content root
- if (Directory.Exists(contentRoot))
- {
- var files = Directory.GetFiles(contentRoot, "*", SearchOption.AllDirectories);
- foreach (var file in files)
- {
- var fullPath = Path.GetFullPath(file);
-#if NET
- var relativePath = Path.GetRelativePath(contentRoot, fullPath);
-#else
- // For .NET Framework, manually compute relative path
- var relativePath = fullPath.StartsWith(contentRoot, StringComparison.OrdinalIgnoreCase)
- ? fullPath.Substring(contentRoot.Length)
- : fullPath;
-#endif
-
- // Normalize path separators
- relativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/');
-
- var fileItem = new TaskItem(fullPath);
- fileItem.SetMetadata("ContentRoot", contentRoot);
- fileItem.SetMetadata("BasePath", basePath);
- fileItem.SetMetadata("RelativePath", relativePath);
-
- Log.LogMessage(MessageImportance.Low,
- "Discovered file '{0}' with relative path '{1}' in content root '{2}'",
- fullPath,
- relativePath,
- contentRoot);
-
- discoveredFiles.Add(fileItem);
- }
- }
- else
- {
- Log.LogWarning(
- "Content root directory '{0}' does not exist.",
- contentRoot);
- }
- }
-
- ParsedBasePaths = [.. parsedPaths];
- DiscoveredFiles = [.. discoveredFiles];
-
- return !Log.HasLoggedErrors;
- }
-}