| File: UpdateExternallyDefinedStaticWebAssets.cs | Web Access |
| Project: src\sdk\src\StaticWebAssetsSdk\Tasks\Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tasks) |
// 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 System.Text.RegularExpressions; using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils; using Microsoft.Build.Framework; namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; // Certain project types integrate with the static web asset protocol. As we evolve it and add new features // either they have to update their SDKs to support the new features or we need to provide a way to update // the assets from previous versions to the current version. // For example, the JavaScript Project Tools SDK integrates with the static web asset protocol for SPA applications // but it doesn't support integrity or fingerprinting, which causes issues when we reference the project and we try // to further process the assets. // Additionally, this task handles incoming framework assets from P2P references by materializing them // (copying files to an intermediate directory and updating metadata) so they become local to the consuming project. public class UpdateExternallyDefinedStaticWebAssets : Task { [Required] public ITaskItem[] Assets { get; set; } [Required] public ITaskItem[] Endpoints { get; set; } public ITaskItem[] FingerprintInferenceExpressions { get; set; } public ITaskItem[] StaticWebAssetGroups { get; set; } public string IntermediateOutputPath { get; set; } public string ProjectPackageId { get; set; } public string ProjectBasePath { get; set; } [Output] public ITaskItem[] UpdatedAssets { get; set; } [Output] public ITaskItem[] UpdatedEndpoints { get; set; } [Output] public ITaskItem[] AssetsWithoutEndpoints { get; set; } [Output] public ITaskItem[] OriginalFrameworkAssets { get; set; } public override bool Execute() { var assets = Assets.Select(StaticWebAsset.FromV1TaskItem).ToArray(); var endpoints = StaticWebAssetEndpoint.FromItemGroup(Endpoints); var groupLookup = StaticWebAssetGroup.FromItemGroup(StaticWebAssetGroups); var endpointByAsset = endpoints .GroupBy(e => e.AssetFile, OSPath.PathComparer) .ToDictionary(e => e.Key, e => e.ToArray(), OSPath.PathComparer); var fingerprintExpressions = CreateFingerprintExpressions(FingerprintInferenceExpressions); // Filter by group FIRST so that framework assets tagged with groups that the consuming // project doesn't accept are excluded before materialization. var (filteredAssets, excludedAssetFiles) = StaticWebAsset.FilterByGroup(assets, groupLookup, skipDeferred: true); // Rebuild the assets array from filtered results for subsequent processing. // Also build a set to identify which original input items survived filtering. var filteredSet = new HashSet<string>(filteredAssets.Select(a => a.Identity), OSPath.PathComparer); var assetsWithoutEndpoints = new List<StaticWebAsset>(); var originalFrameworkAssetItems = new List<ITaskItem>(); var assetMapping = new Dictionary<string, (string NewIdentity, string OldBasePath)>(OSPath.PathComparer); for (var i = 0; i < filteredAssets.Count; i++) { var asset = filteredAssets[i]; // Materialize framework assets from P2P references. if (StaticWebAsset.SourceTypes.IsFramework(asset.SourceType)) { // Find the original task item that corresponds to this filtered asset. var originalIndex = Array.FindIndex(assets, a => OSPath.PathComparer.Equals(a.Identity, asset.Identity)); if (originalIndex >= 0) { originalFrameworkAssetItems.Add(Assets[originalIndex]); } var (materialized, oldIdentity, oldBasePath) = StaticWebAsset.MaterializeFrameworkAsset( asset, IntermediateOutputPath, ProjectPackageId, ProjectBasePath, Log); if (materialized != null) { filteredAssets[i] = materialized; assetMapping[oldIdentity] = (materialized.Identity, oldBasePath); } continue; } if (!endpointByAsset.TryGetValue(asset.Identity, out var endpoint)) { Log.LogMessage($"Asset {asset.Identity} does not have an associated endpoint defined."); if (TryInferFingerprint(fingerprintExpressions, asset.RelativePath, out var fingerprint, out var newRelativePath)) { Log.LogMessage($"Inferred fingerprint {fingerprint} for asset {asset.Identity}. Relative path updated to {newRelativePath}."); asset.RelativePath = newRelativePath; asset.Fingerprint = fingerprint; } assetsWithoutEndpoints.Add(asset); } } // Remove alternative assets (e.g., compressed variants) whose primary was materialized as a // framework asset. These alternatives will be regenerated from the materialized primary by the // consuming project's compression step, so keeping the originals would cause conflicts. if (assetMapping.Count > 0) { filteredAssets.RemoveAll(asset => !string.IsNullOrEmpty(asset.RelatedAsset) && assetMapping.ContainsKey(asset.RelatedAsset)); } UpdatedAssets = StaticWebAsset.ToTaskItems(filteredAssets); // Filter endpoints using the shared helper. var endpointGroups = StaticWebAssetEndpointGroup.CreateEndpointGroups(endpoints); var (_, survivingEndpoints) = StaticWebAssetEndpointGroup.ComputeFilteredEndpoints(endpointGroups, excludedAssetFiles); // Remap endpoints for materialized framework assets — update AssetFile and Route // to reflect the new materialized path and the consuming project's base path. if (assetMapping.Count > 0) { var routeSegments = new List<PathTokenizer.Segment>(); var basePathSegments = new List<PathTokenizer.Segment>(); foreach (var ep in survivingEndpoints) { if (assetMapping.TryGetValue(ep.AssetFile, out var info)) { ep.AssetFile = info.NewIdentity; StaticWebAssetEndpoint.RemapEndpointRoute(ep, info.OldBasePath, ProjectBasePath, routeSegments, basePathSegments); } } } UpdatedEndpoints = StaticWebAssetEndpoint.ToTaskItems(survivingEndpoints); AssetsWithoutEndpoints = StaticWebAsset.ToTaskItems( assetsWithoutEndpoints.Where(a => !excludedAssetFiles.Contains(a.Identity))); OriginalFrameworkAssets = [.. originalFrameworkAssetItems]; return !Log.HasLoggedErrors; } private bool TryInferFingerprint(Regex[] fingerprintExpressions, string relativePath, out string fingerprint, out string newRelativePath) { for (var i = 0; i < fingerprintExpressions.Length; i++) { var regex = fingerprintExpressions[i]; var match = regex.Match(relativePath); if (match.Success) { var fingerprintGroup = match.Groups["fingerprint"]; if (fingerprintGroup == null) { Log.LogError($"The regular expression {regex} does not contain a 'fingerprint' group. Provide an expression in the form of (?<fingerprint>...)."); fingerprint = null; newRelativePath = null; return false; } fingerprint = fingerprintGroup.Value; newRelativePath = relativePath.Replace(fingerprintGroup.Value, "#[{fingerprint}]"); return true; } } fingerprint = null; newRelativePath = null; return false; } private static Regex[] CreateFingerprintExpressions(ITaskItem[] fingerprintInferenceExpressions) { if (fingerprintInferenceExpressions == null || fingerprintInferenceExpressions.Length == 0) { return []; } var regexOptions = (OSPath.PathComparison == StringComparison.OrdinalIgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None) | RegexOptions.Singleline | RegexOptions.CultureInvariant; var result = new Regex[fingerprintInferenceExpressions.Length]; for (var i = 0; i < fingerprintInferenceExpressions.Length; i++) { var fingerprintExpression = fingerprintInferenceExpressions[i]; var pattern = fingerprintExpression.GetMetadata("Pattern"); var regex = new Regex(pattern, regexOptions); result[i] = regex; } return result; } }