Add Asset Groups support for static web assets#53187
Conversation
e0624be to
f2a0c0b
Compare
69adb59 to
3f32e43
Compare
f1b2f9d to
a827e4d
Compare
af7dca6 to
0d2f054
Compare
5b4128e to
dcd7410
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces Static Web Asset Groups support to the Static Web Assets pipeline, enabling libraries to ship multiple asset variants (e.g., Bootstrap V4/V5) and allowing consuming projects to declaratively select which variant is used. It also adds a new JSON-manifest + lightweight .targets packaging path (for newer TFMs) and integrates group-based filtering into build/publish endpoint generation.
Changes:
- Add group tagging/filtering support across project, package, and P2P static web assets (including deferred group support and cascading exclusion of related assets/endpoints).
- Introduce JSON package manifest generation + consumption and generate lightweight
.targetsfiles to reference the manifest. - Add new unit + integration tests and update existing baselines to reflect new manifest/path behavior.
Reviewed changes
Copilot reviewed 80 out of 80 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs | Adds group definition parsing and applies group tagging + path/content-root rewriting during asset definition. |
| src/StaticWebAssetsSdk/Tasks/FilterStaticWebAssetGroups.cs | New task to filter assets/endpoints by group declarations, supporting deferred groups. |
| src/StaticWebAssetsSdk/Tasks/ReadPackageAssetsManifest.cs | New task to read JSON package manifests, apply group filtering, and materialize framework assets. |
| src/StaticWebAssetsSdk/Tasks/GeneratePackageAssetsManifestFile.cs | New task to emit JSON package manifest (assets + endpoints) with remapped asset references. |
| src/StaticWebAssetsSdk/Tasks/GeneratePackageAssetsTargetsFile.cs | New task to generate lightweight .targets that points to the JSON manifest. |
| src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets*.targets | Wires group filtering into build/publish and adds JSON packaging + restore-time manifest reading. |
| src/StaticWebAssetsSdk/Tasks/UpdateExternallyDefinedStaticWebAssets.cs | Applies group filtering at the P2P boundary with SkipDeferred=true and filters endpoints accordingly. |
| src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathPattern.cs / StaticWebAssetPathSegment.cs | Adds ~ file-only token modifier to hide segments from endpoint routes while keeping file resolution. |
| test/Microsoft.NET.Sdk.StaticWebAssets.Tests/* | Adds unit + integration coverage for group filtering, deferred groups, and JSON packaging; updates baselines. |
| test/TestAssets/TestProjects/AssetGroupsSample/* | Adds sample producer/consumer projects and convention targets file for groups. |
| test/TestAssets/TestProjects/...AppWithPackageAndP2PReference.csproj | Updates test target ordering to ensure package-manifest reading happens before checks. |
| src/BlazorWasmSdk/Tasks/Microsoft.NET.Sdk.BlazorWebAssembly.Tasks.csproj | Updates linked StaticWebAssets task data files list (includes group data + OSPath). |
You can also share your feedback on Copilot code review. Take the survey.
| using System.Xml; | ||
| using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils; | ||
| using Microsoft.Build.Framework; | ||
|
|
||
| namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; | ||
|
|
||
| // Generates a lightweight .targets file that adds a single StaticWebAssetPackageManifest | ||
| // item pointing to the JSON manifest file. This replaces the heavyweight XML .props files | ||
| // that contained all asset/endpoint data as MSBuild items. | ||
| public class GeneratePackageAssetsTargetsFile : Task | ||
| { | ||
| [Required] | ||
| public string PackageId { get; set; } | ||
|
|
||
| [Required] | ||
| public string TargetFilePath { get; set; } | ||
|
|
||
| public string PackagePathPrefix { get; set; } = "staticwebassets"; | ||
|
|
||
| [Required] | ||
| public string ManifestFileName { get; set; } | ||
|
|
||
| public override bool Execute() | ||
| { | ||
| var normalizedPrefix = PackagePathPrefix.Replace("/", "\\").TrimStart('\\'); | ||
|
|
||
| var itemGroup = new XElement("ItemGroup"); | ||
| var manifestItem = new XElement("StaticWebAssetPackageManifest", | ||
| new XAttribute("Include", $@"$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory){ManifestFileName}'))"), | ||
| new XElement("SourceId", PackageId), | ||
| new XElement("ContentRoot", $@"$(MSBuildThisFileDirectory)..\{normalizedPrefix}\"), | ||
| new XElement("PackageRoot", @"$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..'))")); |
| [Required] | ||
| public ITaskItem[] PackageManifests { get; set; } | ||
|
|
||
| public ITaskItem[] StaticWebAssetGroups { get; set; } | ||
|
|
||
| public string IntermediateOutputPath { get; set; } | ||
|
|
||
| public string ProjectPackageId { get; set; } | ||
|
|
||
| public string ProjectBasePath { get; set; } | ||
|
|
| } | ||
| manifestEndpoints.Add(endpoint); |
| public string Name { get; } | ||
| public string Value { get; } | ||
| public string SourceId { get; } | ||
| public int Order { get; } | ||
| public StaticWebAssetGlobMatcher IncludeMatcher { get; } |
There was a problem hiding this comment.
These are required, we should throw if not provided
There was a problem hiding this comment.
The required field validation (Value, SourceId, Order, IncludePattern) is enforced in ParseGroupDefinitions via Log.LogError + return null — the idiomatic MSBuild pattern for fatal validation errors. The struct is private and only constructed from that one validation site, so constructor-level guards would be redundant and would also conflict with CA1510 (which requires using ThrowIfNull instead of explicit throws on multi-target projects). The existing validation in ParseGroupDefinitions is the appropriate place.
Unit tests covering: - AssetGroupFilteringTest: end-to-end group filtering scenarios - FilterStaticWebAssetGroupsTest: two-pass filtering logic (SkipDeferred) - GeneratePackageAssetsManifestFileTest: JSON manifest serialization - GeneratePackageAssetsTargetsFileTest: lightweight .targets generation - ReadPackageAssetsManifestTest: manifest deserialization and group filtering - ResolveCompressedAssetsTest: compression with asset groups - StaticWebAssetTest: AssetGroups property and related asset sorting
Integration tests: - AssetGroupsIntegrationTest: build/publish with asset group selection, verifies correct assets appear in output based on selected group - DeferredAssetGroupsIntegrationTest: deferred group resolution during restore and P2P reference scenarios - FrameworkAssetsIntegrationTest: framework asset materialization from JSON manifests with group filtering - StaticWebAssetsPackIntegrationTest: pack produces JSON manifest with correct asset group metadata and .targets entry point Regenerate 32 baseline files to incorporate AssetGroups metadata and new JSON manifest packaging output.
Move ParseGroupDefinitions() call from ApplyGroupDefinitions() to the beginning of Execute(). The parsed definitions list is now a local variable passed into ApplyGroupDefinitions(definitions) rather than re-parsed on every call. Validation errors (duplicate Order+SourceId) are caught before any asset processing begins.
Replace the post-loop ApplyGroupDefinitions() bulk operation with a per-asset ApplyGroupToAsset() call inside the main processing loop. Group definitions are now applied to each StaticWebAsset instance before it is serialized via ToTaskItem(), eliminating the need to re-parse ITaskItem back to StaticWebAsset after the fact. This means group-modified RelativePath and ContentRoot values are visible to UpdateAssetKindIfNecessary for dedup/conflict detection.
Group definitions can rewrite RelativePath so that assets from different groups (e.g. V4/css/site.css and V5/css/site.css) collapse to the same post-group path (css/site.css). UpdateAssetKindIfNecessary must use the original pre-group path for its conflict detection, since group variants are not true conflicts — they are resolved at runtime by FilterStaticWebAssetGroups.
…tRootSuffix conflicts
Introduce TokenResolveMode (None/Pack/Serve) to control how token expressions are resolved during path computation. The ~ modifier now means 'pack-only' — the segment appears in physical NuGet package paths (Pack mode) but is stripped from HTTP routes and dev manifests (Serve mode). - Rename IsFileOnly to IsPackOnly on StaticWebAssetPathSegment - Rename PatternFileOnly to PatternPackOnly - Replace bool applyPreferences with TokenResolveMode resolveMode on StaticWebAssetPathPattern.ReplaceTokens() - Add resolveMode parameter to ComputeTargetPath and ReplaceTokens on StaticWebAsset, defaulting to Serve - Update all pack call sites to use TokenResolveMode.Pack: GenerateStaticWebAssetsPropsFile, GenerateStaticWebAssetEndpointsPropsFile, GeneratePackageAssetsManifestFile
When multiple group definitions with different names (e.g., version and theme) match the same asset, their ContentRootSuffix values now compose sequentially instead of erroring. Definitions are sorted by Order so composition is deterministic. For example, version=v4 (suffix 'v4') + theme=light (suffix 'light') produces 'OriginalRoot/v4/light'. Updated test to verify composition behavior.
ComputeStaticWebAssetsTargetPaths was using TokenResolveMode.Serve
(the default) when computing pack target paths, which strips
pack-only (~) segments from relative paths. This caused group
token expressions like #[{BootstrapVersion}]~/ to be stripped,
resulting in nupkg entries like staticwebassets//css/site.css
instead of staticwebassets/V4/css/site.css.
Use TokenResolveMode.Pack when AdjustPathsForPack is true so
pack-only segments are resolved rather than stripped.
…deMatcher null-check Co-authored-by: javiercn <6995051+javiercn@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/sdk/sessions/c0028270-f89a-4a4f-b579-24b447712cd4
3aef97f to
876614f
Compare
Replace custom build/props/targets packaging with SDK StaticWebAssetGroupDefinition and StaticWebAssetGroup items (dotnet/sdk#53187). Assets.Internal changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata instead of custom _BlazorJSFile - Add StaticWebAssets.Groups.targets for consumer-side conditional inclusion - Remove BlazorWebAssemblyJSPath (no longer needed with framework assets) - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Identity UI changes: - Replace custom GetIdentityUIAssets target with StaticWebAssetGroupDefinition for Bootstrap V4/V5 variants - Add StaticWebAssets.Groups.targets for consumer-side group selection - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Consumer migration: - Components/Directory.Build.props: ProjectReference with PrivateAssets=All + Groups.targets import (replaces workaround properties) - Components/Directory.Build.targets: Remove BlazorWebAssemblyJSPath override - Identity samples/testassets: Import Groups.targets, remove old targets Validated: 89 E2E + functional tests pass (0 failures)
….WebAssembly Replace custom build/props/targets packaging with SDK StaticWebAssetGroupDefinition and StaticWebAssetGroup items (dotnet/sdk#53187). Assets.Internal changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata instead of custom _BlazorJSFile - Add StaticWebAssets.Groups.targets for consumer-side conditional inclusion - Remove blazor.webassembly.js (now provided by Components.WebAssembly) - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Components.WebAssembly changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata for blazor.webassembly.js - Remove BlazorWebAssemblyJSPath from package props (SWA pipeline handles it) - blazor.webassembly.js flows to Blazor Web Apps through client P2P reference chain Identity UI changes: - Replace custom GetIdentityUIAssets target with StaticWebAssetGroupDefinition for Bootstrap V4/V5 variants - Add StaticWebAssets.Groups.targets for consumer-side group selection - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Consumer migration: - Components/Directory.Build.props: ProjectReference with PrivateAssets=All + Groups.targets import (replaces workaround properties) - Components/Directory.Build.targets: Remove BlazorWebAssemblyJSPath override - Identity samples/testassets: Import Groups.targets, remove old targets
….WebAssembly Replace custom build/props/targets packaging with SDK StaticWebAssetGroupDefinition and StaticWebAssetGroup items (dotnet/sdk#53187). Assets.Internal changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata instead of custom _BlazorJSFile - Add StaticWebAssets.Groups.targets for consumer-side conditional inclusion - Remove blazor.webassembly.js (now provided by Components.WebAssembly) - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Components.WebAssembly changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata for blazor.webassembly.js - Add StaticWebAssetGroupDefinition + StaticWebAssets.Groups.targets (group keyed on UsingMicrosoftNETSdkBlazorWebAssembly) - Remove BlazorWebAssemblyJSPath from package props (SWA pipeline handles it) Identity UI changes: - Replace custom GetIdentityUIAssets target with StaticWebAssetGroupDefinition for Bootstrap V4/V5 variants - Add StaticWebAssets.Groups.targets for consumer-side group selection - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Consumer migration: - Components/Directory.Build.props: ProjectReference with PrivateAssets=All + Groups.targets imports (replaces workaround properties) - Components/Directory.Build.targets: Remove BlazorWebAssemblyJSPath override - Identity samples/testassets: Import Groups.targets, remove old targets
….WebAssembly Replace custom build/props/targets packaging with SDK StaticWebAssetGroupDefinition and StaticWebAssetGroup items (dotnet/sdk#53187). Assets.Internal changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata instead of custom _BlazorJSFile - Add StaticWebAssets.Groups.targets for consumer-side conditional inclusion - Remove blazor.webassembly.js (now provided by Components.WebAssembly) - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Components.WebAssembly changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata for blazor.webassembly.js - Add StaticWebAssetGroupDefinition + StaticWebAssets.Groups.targets (group keyed on UsingMicrosoftNETSdkBlazorWebAssembly) - Remove BlazorWebAssemblyJSPath from package props (SWA pipeline handles it) Identity UI changes: - Replace custom GetIdentityUIAssets target with StaticWebAssetGroupDefinition for Bootstrap V4/V5 variants - Add StaticWebAssets.Groups.targets for consumer-side group selection - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Consumer migration: - Components/Directory.Build.props: ProjectReference with PrivateAssets=All + Groups.targets imports (replaces workaround properties) - Components/Directory.Build.targets: Remove BlazorWebAssemblyJSPath override - Identity samples/testassets: Import Groups.targets, remove old targets
….WebAssembly Replace custom build/props/targets packaging with SDK StaticWebAssetGroupDefinition and StaticWebAssetGroup items (dotnet/sdk#53187). Assets.Internal changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata instead of custom _BlazorJSFile - Add StaticWebAssets.Groups.targets for consumer-side conditional inclusion - Remove blazor.webassembly.js (now provided by Components.WebAssembly) - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Components.WebAssembly changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata for blazor.webassembly.js - Add StaticWebAssetGroupDefinition + StaticWebAssets.Groups.targets (group keyed on UsingMicrosoftNETSdkBlazorWebAssembly) - Remove BlazorWebAssemblyJSPath from package props (SWA pipeline handles it) Identity UI changes: - Replace custom GetIdentityUIAssets target with StaticWebAssetGroupDefinition for Bootstrap V4/V5 variants - Add StaticWebAssets.Groups.targets for consumer-side group selection - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Consumer migration: - Components/Directory.Build.props: ProjectReference with PrivateAssets=All + Groups.targets imports (replaces workaround properties) - Components/Directory.Build.targets: Remove BlazorWebAssemblyJSPath override - Identity samples/testassets: Import Groups.targets, remove old targets
….WebAssembly Replace custom build/props/targets packaging with SDK StaticWebAssetGroupDefinition and StaticWebAssetGroup items (dotnet/sdk#53187). Assets.Internal changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata instead of custom _BlazorJSFile - Add StaticWebAssets.Groups.targets for consumer-side conditional inclusion - Remove blazor.webassembly.js (now provided by Components.WebAssembly) - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Components.WebAssembly changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata for blazor.webassembly.js - Add StaticWebAssetGroupDefinition + StaticWebAssets.Groups.targets (group keyed on UsingMicrosoftNETSdkBlazorWebAssembly) - Remove BlazorWebAssemblyJSPath from package props (SWA pipeline handles it) Identity UI changes: - Replace custom GetIdentityUIAssets target with StaticWebAssetGroupDefinition for Bootstrap V4/V5 variants - Add StaticWebAssets.Groups.targets for consumer-side group selection - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Consumer migration: - Components/Directory.Build.props: ProjectReference with PrivateAssets=All + Groups.targets imports (replaces workaround properties) - Components/Directory.Build.targets: Remove BlazorWebAssemblyJSPath override - Identity samples/testassets: Import Groups.targets, remove old targets
…6412) * Use SDK Asset Groups for Assets.Internal, Identity UI, and Components.WebAssembly Replace custom build/props/targets packaging with SDK StaticWebAssetGroupDefinition and StaticWebAssetGroup items (dotnet/sdk#53187). Assets.Internal changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata instead of custom _BlazorJSFile - Add StaticWebAssets.Groups.targets for consumer-side conditional inclusion - Remove blazor.webassembly.js (now provided by Components.WebAssembly) - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Components.WebAssembly changes: - Switch to Microsoft.NET.Sdk.Razor SDK - Add StaticWebAssetFrameworkPattern=**/*.js for framework asset marking - Use Content items with Link/ContentRoot metadata for blazor.webassembly.js - Add StaticWebAssetGroupDefinition + StaticWebAssets.Groups.targets (group keyed on UsingMicrosoftNETSdkBlazorWebAssembly) - Remove BlazorWebAssemblyJSPath from package props (SWA pipeline handles it) Identity UI changes: - Replace custom GetIdentityUIAssets target with StaticWebAssetGroupDefinition for Bootstrap V4/V5 variants - Add StaticWebAssets.Groups.targets for consumer-side group selection - Delete old build/, buildMultiTargeting/, buildTransitive/ directories Consumer migration: - Components/Directory.Build.props: ProjectReference with PrivateAssets=All + Groups.targets imports (replaces workaround properties) - Components/Directory.Build.targets: Remove BlazorWebAssemblyJSPath override - Identity samples/testassets: Import Groups.targets, remove old targets * Fix build: move Assets.Internal ref to targets, add RelativePathPrefix for Identity UI pack * Move Assets.Internal ProjectReference metadata to evaluation time The _ConfigureAssetsInternalReference target set ReferenceOutputAssembly=false during target execution (BeforeTargets=ResolveProjectReferences), but the E2E test pipeline invokes Components.TestServer and Wasm.Prerendered.Server with Targets='Build;Publish' and custom Properties, creating separate MSBuild project instances where the target-based approach didn't reliably prevent the compiler from trying to reference the non-existent Assets.Internal DLL. By setting the metadata via ProjectReference Update at evaluation time (outside any target), the metadata is always in effect regardless of which targets run or how many times the project is built with different global properties. * Add BlazorWebAssembly asset group workaround to test/sample server projects The BlazorWebAssembly group on blazor.webassembly.js should be cleared by the SDK when the asset crosses project boundaries as a framework asset. Until that SDK change is in place, declare the group in each hosting server project so the asset passes filtering. Simplify Assets.Internal groups to only declare BlazorFramework. Restore WebAssembly groups to final form (BlazorWebAssembly SDK only). * Fix BlazorFramework group condition to match original asset filtering The original _AddBlazorFrameworkStaticWebAssets target required both OutputType == Exe/WinExe AND UsingMicrosoftNETSdkWeb. The group condition must match: only executable web apps should opt into the BlazorFramework group for blazor.web.js and blazor.server.js. * Move _framework from BasePath to RelativePath for asset groups Change JS build output to include _framework/ in the directory structure so that it becomes part of RelativePath rather than BasePath. This ensures the _framework prefix survives framework asset materialization regardless of the consumer's BasePath. Changes: - Rollup config outputs to dist/Debug/_framework/blazor.*.js - Assets.Internal and WebAssembly csprojs: BasePath=/ with _framework in paths - Remove StaticWebAssetGroup workarounds from test/sample projects (no longer needed since the SDK properly filters by group before materialization) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Javier Calvarro Nelson <jacalvar@microsoft.com> * Preserve IdentityDefaultUIFramework property name for backward compat The StaticWebAssets.Groups.targets now falls back to the existing IdentityDefaultUIFramework property if IdentityUIFrameworkVersion is not explicitly set, so consuming projects don't need to rename their property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update SDK to preview.6, fix NU1510 pruning error, fix stale comment - Update global.json to 11.0.100-preview.6.26277.116 - Remove redundant Microsoft.Extensions.Options reference in Validation Localization tests (NU1510 warns-as-error with new SDK pruning) - Fix stale comment in Components Directory.Build.targets about which package provides blazor.webassembly.js Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
StaticWebAssetPathPattern.csline 135: ChangeIsPreferred = truetoIsPreferred = falsefor pack-only tokens and updateReplaceTokensskip logic (comment 2987371603)DefineStaticWebAssets.cs: Remove redundant null-check forincludeMatchersinceIncludePatternis already validated (comment 2947602098)ParseGroupDefinitions(comment 2947579304 - already addressed by existing validation)📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.