Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ Copyright (c) .NET Foundation. All rights reserved.
Assets="@(_ReferencedProjectPublishStaticWebAssetsUpdateCandidates)"
Endpoints="@(_ReferencedProjectPublishStaticWebAssetEndpointsUpdateCandidates)"
FingerprintInferenceExpressions="@(StaticWebAssetFingerprintInferenceExpression)"
IntermediateOutputPath="$(_StaticWebAssetsIntermediateOutputPath)"
ProjectPackageId="$(PackageId)"
ProjectBasePath="$(StaticWebAssetBasePath)"
>
<Output TaskParameter="UpdatedAssets" ItemName="_ReferencedProjectPublishStaticWebAssets" />
<Output TaskParameter="UpdatedEndpoints" ItemName="_ReferencedProjectPublishStaticWebAssetEndpoints" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ Copyright (c) .NET Foundation. All rights reserved.
Endpoints="@(_ReferencedProjectBuildStaticWebAssetEndpointsUpdateCandidates)"
FingerprintInferenceExpressions="@(StaticWebAssetFingerprintInferenceExpression)"
StaticWebAssetGroups="@(StaticWebAssetGroup)"
IntermediateOutputPath="$(_StaticWebAssetsIntermediateOutputPath)"
ProjectPackageId="$(PackageId)"
ProjectBasePath="$(StaticWebAssetBasePath)"
>
<Output TaskParameter="UpdatedAssets" ItemName="_ResolvedReferencedProjectBuildStaticWebAssets" />
<Output TaskParameter="UpdatedEndpoints" ItemName="_ResolvedReferencedProjectBuildStaticWebAssetEndpoints" />
Expand Down Expand Up @@ -184,6 +187,7 @@ Copyright (c) .NET Foundation. All rights reserved.
AssetKind="Build"
MakeReferencedAssetOriginalItemSpecAbsolute="$(StaticWebAssetMakeReferencedAssetOriginalItemSpecAbsolute)"
Source="$(PackageId)"
FrameworkPattern="$(StaticWebAssetFrameworkPattern)"
>
<Output TaskParameter="StaticWebAssets" ItemName="_CachedBuildReferencedStaticWebAsset" />
<Output TaskParameter="DiscoveryPatterns" ItemName="_CachedBuildReferencedStaticWebAssetDiscoveryPatterns" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<Output TaskParameter="UpdatedAssets" ItemName="_UpdatedPackageAssets" />
<Output TaskParameter="OriginalAssets" ItemName="_OriginalPackageAssets" />
<Output TaskParameter="RemappedEndpoints" ItemName="_RemappedFrameworkEndpoints" />
<Output TaskParameter="OriginalFrameworkEndpoints" ItemName="_OriginalFrameworkEndpoints" />
</UpdatePackageStaticWebAssets>

<ItemGroup>
Expand All @@ -857,8 +858,8 @@ Copyright (c) .NET Foundation. All rights reserved.
<StaticWebAsset Remove="@(_OriginalPackageAssets)"/>
<StaticWebAsset Include="@(_UpdatedPackageAssets)" />
<StaticWebAssetEndpoint Include="@(_UpdatedPackageAssetsEndpoint)" />
<!-- Remap endpoints for materialized framework assets -->
<StaticWebAssetEndpoint Remove="@(_RemappedFrameworkEndpoints)" />
<!-- Remove original framework endpoints (with old routes) and add remapped ones (with new routes) -->
<StaticWebAssetEndpoint Remove="@(_OriginalFrameworkEndpoints)" />
<StaticWebAssetEndpoint Include="@(_RemappedFrameworkEndpoints)" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ public class ComputeReferenceStaticWebAssetItems : Task

public bool MakeReferencedAssetOriginalItemSpecAbsolute { get; set; }

/// <summary>
/// Semicolon-separated glob patterns (e.g. <c>**/*.js;**/*.wasm</c>).
/// Assets whose <c>RelativePath</c> matches any pattern will have their
/// <c>SourceType</c> set to <c>Framework</c>.
/// </summary>
public string FrameworkPattern { get; set; }

[Output]
public ITaskItem[] StaticWebAssets { get; set; }

Expand All @@ -42,6 +49,10 @@ public override bool Execute()

var resultAssets = new List<StaticWebAsset>(existingAssets.Count);
var groupSet = new HashSet<string>(StringComparer.Ordinal);

var frameworkMatcher = CreateFrameworkMatcher();
var matchContext = frameworkMatcher != null ? StaticWebAssetGlobMatcher.CreateMatchContext() : default;

foreach (var kvp in existingAssets)
{
var targetPath = kvp.Key;
Expand All @@ -55,6 +66,7 @@ public override bool Execute()
{
if (ShouldIncludeAssetAsReference(groupedAsset, out var groupReason))
{
ApplyFrameworkPattern(groupedAsset, frameworkMatcher, ref matchContext);
if (UpdateSourceType && !StaticWebAsset.SourceTypes.IsFramework(groupedAsset.SourceType))
{
groupedAsset.SourceType = StaticWebAsset.SourceTypes.Project;
Expand All @@ -78,6 +90,7 @@ public override bool Execute()

if (ShouldIncludeAssetAsReference(selected, out var reason))
{
ApplyFrameworkPattern(selected, frameworkMatcher, ref matchContext);
if (UpdateSourceType && !StaticWebAsset.SourceTypes.IsFramework(selected.SourceType))
{
selected.SourceType = StaticWebAsset.SourceTypes.Project;
Expand Down Expand Up @@ -123,6 +136,53 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}

private StaticWebAssetGlobMatcher CreateFrameworkMatcher()
{
if (string.IsNullOrEmpty(FrameworkPattern))
{
return null;
}

var patterns = FrameworkPattern
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.ToArray();

if (patterns.Length == 0)
{
return null;
}

return new StaticWebAssetGlobMatcherBuilder()
.AddIncludePatterns(patterns)
.Build();
}

private void ApplyFrameworkPattern(
StaticWebAsset asset,
StaticWebAssetGlobMatcher matcher,
ref StaticWebAssetGlobMatcher.MatchContext matchContext)
{
if (matcher == null || !asset.IsDiscovered())
{
return;
}

var relativePath = StaticWebAssetPathPattern.PathWithoutTokens(asset.RelativePath);
matchContext.SetPathAndReinitialize(relativePath.AsSpan());
var match = matcher.Match(matchContext);
if (match.IsMatch)
{
asset.SourceType = StaticWebAsset.SourceTypes.Framework;
Log.LogMessage(
MessageImportance.Low,
"Asset '{0}' with relative path '{1}' matched framework pattern. Updating SourceType to Framework.",
asset.Identity,
relativePath);
}
}

private bool ShouldIncludeAssetAsReference(StaticWebAsset candidate, out string reason)
{
if (!StaticWebAssetsManifest.ManifestModes.ShouldIncludeAssetAsReference(candidate, ProjectMode))
Expand Down
57 changes: 57 additions & 0 deletions src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Security.Cryptography;
using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;

Expand Down Expand Up @@ -1136,6 +1137,62 @@ public bool ShouldCopyToPublishDirectory()
public bool HasContentRoot(string path) =>
string.Equals(ContentRoot, NormalizeContentRootPath(path), StringComparison.Ordinal);

/// <summary>
/// Materializes a framework asset by copying it to the consuming project's intermediate directory
/// and updating its metadata (Identity, ContentRoot, SourceId, BasePath, SourceType, AssetMode).
/// Used by both the P2P and NuGet package paths.
/// Returns the old identity and old base path so callers can remap endpoints and related assets.
/// </summary>
public static (StaticWebAsset Asset, string OldIdentity, string OldBasePath) MaterializeFrameworkAsset(
StaticWebAsset asset,
string intermediateOutputPath,
string projectPackageId,
string projectBasePath,
TaskLoggingHelper log)
{
var originalSourceId = asset.SourceId;
var oldBasePath = asset.BasePath;
var relativePath = asset.RelativePath;
var oldIdentity = asset.Identity;

var fxDir = Path.Combine(intermediateOutputPath, "fx", originalSourceId);
var fileSystemRelativePath = asset.ComputePathWithoutTokens(relativePath);
var destPath = Path.Combine(fxDir, Normalize(fileSystemRelativePath));
destPath = Path.GetFullPath(destPath);

var sourceFile = asset.Identity;
if (!File.Exists(sourceFile))
{
log.LogError("Source file '{0}' does not exist for framework asset materialization.", sourceFile);
return (null, null, null);
}

var destDir = Path.GetDirectoryName(destPath);
Directory.CreateDirectory(destDir);

if (!File.Exists(destPath) || File.GetLastWriteTimeUtc(sourceFile) > File.GetLastWriteTimeUtc(destPath))
{
File.Copy(sourceFile, destPath, overwrite: true);
log.LogMessage(MessageImportance.Low, "Materialized framework asset '{0}' to '{1}'.", sourceFile, destPath);
}
else
{
log.LogMessage(MessageImportance.Low, "Framework asset '{0}' already up to date at '{1}'.", sourceFile, destPath);
}

asset.Identity = destPath;
asset.OriginalItemSpec = destPath;
asset.ContentRoot = NormalizeContentRootPath(fxDir);
asset.SourceType = SourceTypes.Discovered;
asset.SourceId = projectPackageId;
asset.BasePath = projectBasePath;
asset.AssetMode = AssetModes.CurrentProject;
asset.AssetGroups = "";
asset.Normalize();

return (asset, oldIdentity, oldBasePath);
}

public static string Normalize(string path, bool allowEmpyPath = false)
{
var normalizedPath = path.Replace('\\', '/').Trim('/');
Expand Down
64 changes: 64 additions & 0 deletions src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -605,4 +605,68 @@ public static bool RouteHasPathPrefix(

return true;
}

/// <summary>
/// Remaps the Route (and label endpoint property) on an endpoint by stripping the old base path
/// prefix and prepending the new base path. Used when materializing framework assets to update
/// endpoint routes from the library's base path to the consuming project's base path.
/// </summary>
public static void RemapEndpointRoute(
StaticWebAssetEndpoint endpoint,
string oldBasePath,
string newBasePath,
List<PathTokenizer.Segment> routeSegments,
List<PathTokenizer.Segment> basePathSegments)
{
var normalizedOldBase = oldBasePath is null or "/" ? "" : StaticWebAsset.Normalize(oldBasePath);

if (!string.IsNullOrEmpty(normalizedOldBase) &&
RouteHasPathPrefix(endpoint.Route, normalizedOldBase, routeSegments, basePathSegments))
{
var remaining = endpoint.Route.Length > normalizedOldBase.Length
? endpoint.Route.Substring(normalizedOldBase.Length).TrimStart('/')
: "";
endpoint.Route = StaticWebAsset.CombineNormalizedPaths("", newBasePath, remaining, '/');

// Also remap the label endpoint property (used by fingerprinting/HTML asset placeholders).
RemapLabelProperty(endpoint, normalizedOldBase, newBasePath, routeSegments, basePathSegments);
}
else if (string.IsNullOrEmpty(normalizedOldBase))
{
// Old base path was empty/root — prepend the new base path to the existing route.
endpoint.Route = StaticWebAsset.CombineNormalizedPaths("", newBasePath, endpoint.Route, '/');

for (var j = 0; j < endpoint.EndpointProperties.Length; j++)
{
ref var property = ref endpoint.EndpointProperties[j];
if (string.Equals(property.Name, "label", StringComparison.OrdinalIgnoreCase))
{
property.Value = StaticWebAsset.CombineNormalizedPaths("", newBasePath, property.Value, '/');
endpoint.MarkProperiesAsModified();
}
}
}
}

private static void RemapLabelProperty(
StaticWebAssetEndpoint endpoint,
string normalizedOldBase,
string newBasePath,
List<PathTokenizer.Segment> routeSegments,
List<PathTokenizer.Segment> basePathSegments)
{
for (var j = 0; j < endpoint.EndpointProperties.Length; j++)
{
ref var property = ref endpoint.EndpointProperties[j];
if (string.Equals(property.Name, "label", StringComparison.OrdinalIgnoreCase) &&
RouteHasPathPrefix(property.Value, normalizedOldBase, routeSegments, basePathSegments))
{
var labelRemaining = property.Value.Length > normalizedOldBase.Length
? property.Value.Substring(normalizedOldBase.Length).TrimStart('/')
: "";
property.Value = StaticWebAsset.CombineNormalizedPaths("", newBasePath, labelRemaining, '/');
endpoint.MarkProperiesAsModified();
}
}
}
}
Loading
Loading