Add Framework SourceType for static web assets#53135
Conversation
|
Thanks for your PR, @@javiercn. |
bf32e7b to
7b30101
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new Framework SourceType to the Static Web Assets (SWA) pipeline so selected package assets can be “adopted” (materialized into a project-local intermediate location and transformed to behave like local discovered assets), reducing duplicate-asset conflicts when shared framework files flow through multiple references.
Changes:
- Pack-time:
GenerateStaticWebAssetsPropsFileclassifies assets matching$(StaticWebAssetFrameworkPattern)asSourceType=Frameworkin generated props. - Consume-time:
UpdatePackageStaticWebAssetsmaterializesFrameworkassets intoobj/.../staticwebassets/fx/..., transforms metadata toDiscovered/CurrentProject, and remaps endpoints. - Pipeline support:
StaticWebAssetrecognizesFrameworkas a valid source type and updates target-path logic; targets/tests updated to exercise end-to-end behavior.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsLib/wwwroot/js/framework.js | Adds sample JS asset intended to be packed as Framework. |
| test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsLib/wwwroot/css/site.css | Adds sample CSS asset intended to remain Package. |
| test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsLib/FrameworkAssetsLib.csproj | New test library that sets StaticWebAssetFrameworkPattern. |
| test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsConsumer/Program.cs | Minimal consumer app entry point for integration scenario. |
| test/TestAssets/TestProjects/FrameworkAssetsSample/FrameworkAssetsConsumer/FrameworkAssetsConsumer.csproj | Consumer project that references packed library and validates isolation setup. |
| test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs | New unit tests for framework-asset materialization + endpoint remap behavior. |
| test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs | Adds tests for pack-time FrameworkPattern classification. |
| test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs | New integration tests covering pack + consume + incrementality. |
| src/StaticWebAssetsSdk/Tasks/UpdatePackageStaticWebAssets.cs | Implements framework-asset materialization, metadata transform, and endpoint remapping. |
| src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsPropsFile.cs | Adds FrameworkPattern matching to emit SourceType=Framework. |
| src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs | Registers Framework source type and updates validation/path computation. |
| src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets | Wires new task parameters + endpoint remove/add remapping into SWA targets. |
| src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Pack.targets | Passes $(StaticWebAssetFrameworkPattern) into pack props generation. |
src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
Show resolved
Hide resolved
99d80e9 to
e0624be
Compare
maraf
left a comment
There was a problem hiding this comment.
Looks good to me & my copilot 👍
Multiple StaticWebAssets MSBuild tasks crash with ArgumentException when candidate assets contain duplicate Identity keys. This happens when: - Hosted Blazor WASM projects reference multiple WASM client projects, causing dotnet.js.map from microsoft.netcore.app.runtime.mono.browser-wasm to appear multiple times via different dependency paths. - A Blazor Web App project references another Blazor Web App project, causing blazor.web.js from Microsoft.AspNetCore.App.Internal.Assets to appear twice. Fix all four code paths that build Identity-keyed dictionaries: - DiscoverPrecompressedAssets.Execute(): ToDictionary → ContainsKey loop - StaticWebAsset.ToAssetDictionary(): .Add → ContainsKey guard - GenerateStaticWebAssetsManifest: ToDictionary → ContainsKey loop - GenerateStaticWebAssetEndpointsManifest: ToDictionary → ContainsKey loop In all cases, the first occurrence is kept and subsequent duplicates are silently skipped. DiscoverPrecompressedAssets additionally logs skipped duplicates at Low message importance for diagnostics. Note: For the Blazor-references-Blazor scenario (dotnet#52089), this fix converts the unhandled ArgumentException crash into a proper diagnostic error (Conflicting assets with the same target path). The architectural fix for that scenario is tracked in dotnet#53135 (Framework SourceType), which prevents framework asset duplicates from being generated in the first place. Fixes dotnet#52089 Fixes dotnet#52647 Co-authored-by: Copilot <[email protected]>
… solutions
When multiple Blazor WebAssembly client projects reference the same runtime pack,
pass-through files (JS, maps, ICU data, native wasm) share a NuGet cache path.
This causes duplicate Identity keys in the static web assets pipeline, crashing
DiscoverPrecompressedAssets with an ArgumentException.
Instead of copying pass-throughs to the intermediate output path (which risks staleness
on incremental builds when the runtime pack changes), materialize them to a per-project
obj/fx/{PackageId}/ directory using the Framework SourceType convention from the SWA SDK.
This gives each project a unique Identity while properly modeling the relationship:
these are framework assets adopted by each consuming project.
- Pass-through files: copied to obj/fx/{PackageId}/, registered with SourceType=Framework
- WebCil-converted files: remain in obj/webcil/, registered with SourceType=Computed
- Satellite assemblies: placed in culture subdirectories in both cases
- Both groups get per-item ContentRoot for correct Identity resolution
Requires SDK support for SourceType=Framework (dotnet/sdk#53135).
Fixes duplicate-key crash introduced by dotnet#124125.
Co-authored-by: Copilot <[email protected]>
… solutions
When multiple Blazor WebAssembly client projects reference the same runtime pack,
pass-through files (JS, maps, ICU data, native wasm) share a NuGet cache path.
This causes duplicate Identity keys in the static web assets pipeline, crashing
DiscoverPrecompressedAssets with an ArgumentException.
Instead of copying pass-throughs to the intermediate output path (which risks staleness
on incremental builds when the runtime pack changes), materialize them to a per-project
obj/fx/{PackageId}/ directory using the Framework SourceType convention from the SWA SDK.
This gives each project a unique Identity while properly modeling the relationship:
these are framework assets adopted by each consuming project.
- Pass-through files: copied to obj/fx/{PackageId}/, registered with SourceType=Framework
- WebCil-converted files: remain in obj/webcil/, registered with SourceType=Computed
- Satellite assemblies: placed in culture subdirectories in both cases
- Both groups get per-item ContentRoot for correct Identity resolution
Requires SDK support for SourceType=Framework (dotnet/sdk#53135).
Fixes duplicate-key crash introduced by dotnet#124125.
Co-authored-by: Copilot <[email protected]>
|
I've created dotnet/runtime#125329 which depends on this PR. It uses the Framework SourceType convention to fix the duplicate Identity crash in multi-WASM-client solutions (caused by dotnet/runtime#124125). What the runtime PR does: In Browser.targets, pass-through files (JS, maps, ICU, native wasm) are materialized to Testing: Validated build + publish with multi-client projects, WebCil disabled, incremental rebuild, and aspnetcore Components.TestServer (3 WASM clients) — all passing. The runtime change is minimal (single file, ~50 lines added to Browser.targets) and only needs the |
… solutions
Multi-WASM-client solutions crash with duplicate Identity keys in
DiscoverPrecompressedAssets because pass-through files (JS, maps, ICU,
native wasm) share the same NuGet cache path across projects.
Instead of manually copying pass-throughs, use the SWA Framework
SourceType convention: DefineStaticWebAssets registers them as
SourceType="Framework", then UpdatePackageStaticWebAssets materializes
them to per-project obj/fx/{SourceId}/ directories, transforming
metadata (SourceType→Discovered, SourceId→PackageId,
AssetMode→CurrentProject) and giving each project a unique Identity.
This approach:
- Eliminates manual Copy tasks from Browser.targets
- Leverages the SDK's MaterializeFrameworkAsset for timestamp-based
incremental copies
- Properly models the relationship: framework assets adopted by each
consuming project
- Works with and without WebCil enabled
Depends on dotnet/sdk#53135 for Framework SourceType support in the SDK.
Co-authored-by: Copilot <[email protected]>
- Fix CreatePathString to include IsFramework() for consistent path computation - Remove MaterializedFrameworkAssets output (unused) - Change MaterializeFrameworkAsset return type to (StaticWebAsset, string) - Replace dead File.Copy throw with Log.LogError - Remove fingerprint/integrity/LastWriteTime recomputation (content identical) - Simplify ContentRoot to EnsureTrailingSlash(fxDir) - Fix EnsureTrailingSlash to handle both separator chars - Rework endpoint remapping to group by Identity (MSBuild Remove semantics) - Add Inputs/Outputs to UpdateExistingPackageStaticWebAssets target - Clean up restating comments in GenerateStaticWebAssetsPropsFile
The task already handles copy incrementality via timestamp comparison (File.GetLastWriteTimeUtc). Target-level Inputs/Outputs is problematic because empty Inputs (no Framework assets) causes MSBuild to skip the entire target, which also handles Package assets. This matches the pattern used by other tasks in the framework (e.g., GenerateStaticWebAssetsDevelopmentManifest, GenerateStaticWebAssetEndpointsManifest).
- Move Endpoints != null && assetMapping.Count > 0 guard to call site to avoid allocations when no framework assets exist - Remove TaskItem clone in remapping loop; mutate AssetFile in-place - Still two passes over each group (detect then update) since we need to know if the group needs remapping before modifying any items
Mutate AssetFile as we iterate, then AddRange the group to output lists only if any endpoint was remapped.
Test project pair for validating the Framework Assets feature: - FrameworkAssetsLib: RCL with StaticWebAssetFrameworkPattern=**/*.js - FrameworkAssetsConsumer: Web app consuming the lib as a PackageReference
- UpdatePackageStaticWebAssetsTest: 16 unit tests covering - Package asset pass-through - Framework asset materialization (file copy, metadata mutation) - SourceType changed to Discovered, SourceId/BasePath/AssetMode updated - ContentRoot pointing to fx/ directory - Error handling for missing source files - Mixed asset processing (Package + Framework) - Fingerprint/Integrity preservation from original file - Incremental copy (skip when up-to-date, overwrite when stale) - Endpoint remapping (single, multi-identity, non-matching, null) - Subdirectory preservation in materialized paths - GenerateStaticWebAssetsPropsFileTest: 4 new tests for FrameworkPattern - SourceType=Framework emitted when pattern matches - All Package when pattern is null - Multiple semicolon-separated patterns - All Package when pattern matches nothing - FrameworkAssetsIntegrationTest: 6 integration tests - Pack produces nupkg with Framework SourceType in .props - Pack includes expected static web assets - Consumer build materializes framework assets to fx/ directory - Materialized file exists on disk - Endpoints remapped to materialized paths - Incremental build skips re-copy - Updated test assets for test infrastructure compatibility (AspNetTestTfm, AspNetTestPackageSource, EnsurePackagesExist target)
…ram.cs - Fix consumer Program.cs to use traditional Main() instead of top-level statements that depend on implicit usings unavailable in test env - Fix integration test assertions to look under staticwebassets/fx/ instead of fx/ (IntermediateOutputPath includes staticwebassets subdir) - Fix endpoint assertion to handle compressed endpoint variants
…omparer, Ordinal for routes - Remove OriginalRemappedEndpoints output; single RemappedEndpoints with cloned TaskItems - Use StaticWebAsset.NormalizeContentRootPath + asset.Normalize() instead of custom EnsureTrailingSlash - Switch assetMapping dictionary to OSPath.PathComparer for path keys - Use StringComparer.Ordinal for endpoint route grouping (matches RouteAndAssetComparer)
e0624be to
f2a0c0b
Compare
… solutions (#125329) ## Summary When multiple Blazor WebAssembly client projects reference the same runtime pack, pass-through files (JS, maps, ICU data, native wasm) share a NuGet cache path. This causes duplicate Identity keys in the static web assets pipeline, crashing `DiscoverPrecompressedAssets` with an `ArgumentException` on `ToDictionary`. ## Root Cause PR #124125 correctly changed WASM `ContentRoot` from project-specific `OutputPath` copies to per-item `%(RootDir)%(Directory)` (pointing to the real file in the NuGet cache). This fixed staleness on incremental builds. However, when two WASM client projects reference the same runtime pack, shared files like `dotnet.js.map` now resolve to identical NuGet cache paths, producing duplicate Identity values. ## Fix Use the SWA **Framework SourceType** convention (dotnet/sdk#53135) to let the SDK handle materialization, rather than manual Copy tasks: 1. `DefineStaticWebAssets` registers pass-through files with `SourceType="Framework"` (using their NuGet cache paths) 2. `UpdatePackageStaticWebAssets` materializes them to per-project `obj/fx/{SourceId}/` directories, transforming metadata: - `SourceType` → `Discovered` - `SourceId` → project `PackageId` - `BasePath` → project `StaticWebAssetBasePath` - `AssetMode` → `CurrentProject` - `Identity` → materialized file path (unique per project) **Asset classification:** - **Pass-through files** (JS, maps, ICU, native wasm, DLLs when WebCil disabled): Framework → materialized by SDK task - **WebCil-converted files**: `SourceType="Computed"` (already per-project in `obj/webcil/`) This eliminates manual Copy/culture-handling logic from Browser.targets and properly models the relationship: framework assets adopted by each consuming project. ## Dependencies Requires SDK support for `SourceType="Framework"` from dotnet/sdk#53135. ## Testing Validated with patched SDK (Framework SourceType + MaterializeFrameworkAsset) + Browser.targets: - aspnetcore Components.TestServer (3 WASM clients): ✅ Build succeeded - WasmMinimal: 22 framework + 205 webcil files - WasmRemoteAuthentication: 20 framework + 204 webcil files - BasicTestApp: 29 framework + 218 webcil files - Multi-client build (2 WASM clients): ✅ Framework assets materialized per project - Multi-client publish: ✅ Both clients produce fingerprinted files - Incremental rebuild: ✅ Timestamp-based skip - WebCil disabled (`WasmEnableWebcil=false`): ✅ ## Related - Fixes crash introduced by #124125 - Depends on dotnet/sdk#53135 (Framework SourceType support) - Supersedes #125309 (copy-based approach) Co-authored-by: Copilot <[email protected]>
…add test (#126211) ## Summary Fixes the publish-time static web assets pipeline for multi-client hosted Blazor WASM scenarios and adds a test. ## Root Cause `ComputeWasmPublishAssets.GroupResolvedFilesToPublish` processes all `ResolvedFileToPublish` items — including those marked `CopyToPublishDirectory=Never` (like the HotReload dll from `_WasmImplicitlyReferenceHotReload`). When it finds a matching filename, it treats the `ResolvedFileToPublish` item as a "linked version" and replaces the build-time Framework-materialized asset (which has a unique per-project path from `UpdatePackageStaticWebAssets`) with the raw SDK path. In multi-client hosted scenarios, both Client1 and Client2 get their materialized copies replaced by the same raw SDK path → `ApplyCompressionNegotiation` crashes on a duplicate key in `ToAssetDictionary`. ## Fix Skip `CopyToPublishDirectory=Never` items in `GroupResolvedFilesToPublish`. These items were explicitly marked as not for publish — they should not be used to replace build-time assets. Build-time Framework-materialized assets (#125329) keep their unique per-project Identity through publish. ## Test Adds `MultiClientHostedBuildAndPublish` test with a dedicated `BlazorMultiClientHosted` test asset (Server + Client1 + Client2). Parametrized over Debug/Release × build/publish. Verifies: - Both clients' framework directories exist - Both contain `dotnet.*.js` and `dotnet.native*.wasm` files (publish only) - No duplicate Identity crash in `ApplyCompressionNegotiation` ## Related - Completes work from #125329 (build-time Framework materialization) - Depends on dotnet/sdk#53135 (Framework SourceType infrastructure) - Traced via binlog: `ComputeWasmPublishAssets` replaces materialized per-client asset with raw SDK-path `CopyToPublishDirectory=Never` item → duplicate key in server's `ApplyCompressionNegotiation` --------- Co-authored-by: Copilot <[email protected]>
Add Framework SourceType for Static Web Assets
Summary
This PR adds a new
Frameworksource type to the Static Web Assets (SWA) pipeline. Assets marked asFrameworkin a NuGet package are "adopted" by each consuming library: copied from the package cache to a project-local intermediate directory and transformed so the pipeline treats them as localDiscoveredassets owned by the consuming project.The feature has two halves:
GenerateStaticWebAssetsPropsFileevaluates a$(StaticWebAssetFrameworkPattern)glob against each asset'sRelativePathand emits matching assets withSourceType="Framework"instead of the default"Package".UpdatePackageStaticWebAssetsidentifies Framework assets, materializes them to a project-local path, transforms their metadata to match the consuming project, and remaps associated endpoints.Motivation
When multiple Root-mode libraries reference the same NuGet package containing static web assets, the consuming project receives duplicate asset definitions with different
SourceIdvalues — one per referencing library. The pipeline treats these as conflicting claims to the sameRelativePath, producing "Duplicate Asset" build errors even though they point to the same physical file.The primary scenario is Blazor's
blazor.web.js— multiple libraries depend on a common package (Assets.Internal) containing the same framework file. Without this feature, the consuming project fails with a conflict because it receives definitions for the same file via multiple library references with different SourceIds.Goals
SourceType→Discovered,SourceId→$(PackageId),BasePath→$(StaticWebAssetBasePath),AssetMode→CurrentProject).$(StaticWebAssetFrameworkPattern)glob at pack time.Design Overview
The design introduces a new
Frameworksource type that flows through two distinct pipeline stages — pack and consume — touching three existing primitives with no new tasks or targets required.Component Diagram
graph TD subgraph "Pack Time (Library)" A["ComputeReferenceStaticWebAssetItems"] --> B["GenerateStaticWebAssetsPropsFile"] FP["$(StaticWebAssetFrameworkPattern)"] -.-> B B --> |"SourceType=Framework for glob matches"| C[".nupkg props file"] end subgraph "Consume Time (Consumer Project)" D["NuGet restore imports .props"] --> E["StaticWebAsset items<br/>(SourceType=Framework)"] E --> F["UpdatePackageStaticWebAssets"] F --> |"Materialize + Transform"| G["Adopted assets<br/>(SourceType=Discovered,<br/>AssetMode=CurrentProject)"] F --> |"Remap AssetFile"| H["Updated StaticWebAssetEndpoints"] G --> I["Rest of SWA pipeline"] H --> I end subgraph "StaticWebAsset.cs" J["SourceTypes.Framework"] K["IsFramework()"] L["Validate() accepts Framework"] M["ComputeTargetPath suppresses BasePath"] end C -.-> D J -.-> F K -.-> FModified Primitives
StaticWebAsset (Tasks/Data/StaticWebAsset.cs)
Register
Frameworkas a valid source type. Add type-check helpers. Update path computation to suppressBasePathfor Framework assets (same behavior as Discovered/Computed). Update validation to accept Framework.public static class SourceTypes { public const string Discovered = nameof(Discovered); public const string Computed = nameof(Computed); public const string Project = nameof(Project); public const string Package = nameof(Package); + public const string Framework = nameof(Framework); public static bool IsPackage(string sourceType); + public static bool IsFramework(string sourceType); }Validate(): Addscase SourceTypes.Framework: break;arm.ComputeTargetPath()/CreatePathString(): Condition that suppressesBasePathchanges fromIsDiscovered() || IsComputed()toIsDiscovered() || IsComputed() || IsFramework().UpdatePackageStaticWebAssets (Tasks/UpdatePackageStaticWebAssets.cs)
Extend the existing task to identify, materialize, and transform Framework assets. Add endpoint remapping.
public class UpdatePackageStaticWebAssets : Task { [Required] public ITaskItem[] Assets { get; set; } + public string IntermediateOutputPath { get; set; } + public string ProjectPackageId { get; set; } + public string ProjectBasePath { get; set; } + public ITaskItem[] Endpoints { get; set; } [Output] public ITaskItem[] UpdatedAssets { get; set; } [Output] public ITaskItem[] OriginalAssets { get; set; } + [Output] + public ITaskItem[] RemappedEndpoints { get; set; } + [Output] + public ITaskItem[] OriginalRemappedEndpoints { get; set; } }For each Framework asset,
Execute():{IntermediateOutputPath}fx/{OriginalSourceId}/{RelativePath}with timestamp-based incremental copy.Identity→ materialized path,SourceType→Discovered,SourceId→$(PackageId),BasePath→$(StaticWebAssetBasePath),AssetMode→CurrentProject.AssetFilefor any endpoint referencing a materialized asset's old identity (group-based to handle MSBuild'sRemove-by-ItemSpec semantics).GenerateStaticWebAssetsPropsFile (Tasks/GenerateStaticWebAssetsPropsFile.cs)
Add
FrameworkPatterninput. Evaluate each asset'sRelativePathagainst the glob usingStaticWebAssetGlobMatcherand emit matching assets withSourceType="Framework"in the output XML.public class GenerateStaticWebAssetsPropsFile : Task { + public string FrameworkPattern { get; set; } }Target Changes
UpdateExistingPackageStaticWebAssets (Microsoft.NET.Sdk.StaticWebAssets.targets): Pass new parameters, capture endpoint remapping outputs, apply remove/add for remapped endpoints.
GenerateStaticWebAssetsPackFiles (Microsoft.NET.Sdk.StaticWebAssets.Pack.targets): Pass
$(StaticWebAssetFrameworkPattern)toGenerateStaticWebAssetsPropsFile.<GenerateStaticWebAssetsPropsFile StaticWebAssets="@(_PackStaticWebAssets)" - TargetPropsFilePath="$(_GeneratedStaticWebAssetsPropsFile)" /> + TargetPropsFilePath="$(_GeneratedStaticWebAssetsPropsFile)" + FrameworkPattern="$(StaticWebAssetFrameworkPattern)" />Scenarios
Scenario 1: Packaging Framework Assets
A library author sets
<StaticWebAssetFrameworkPattern>**/*.js</StaticWebAssetFrameworkPattern>in its project file. DuringGenerateStaticWebAssetsPackFiles,GenerateStaticWebAssetsPropsFilereceives the glob viaFrameworkPattern, builds aStaticWebAssetGlobMatcher, and evaluates each asset'sRelativePath. Matching.jsfiles are emitted withSourceType="Framework"in the generated props file; non-matching files retainSourceType="Package". The physical package layout is unchanged.Scenario 2: Consuming Framework Assets
After NuGet restore, the package's
.propschain definesStaticWebAssetitems withSourceType="Framework". WhenUpdateExistingPackageStaticWebAssetsruns (first inResolveCoreStaticWebAssets),UpdatePackageStaticWebAssetsmaterializes each Framework asset to$(_StaticWebAssetsIntermediateOutputPath)fx/{OriginalSourceId}/{RelativePath}, transforms metadata (SourceType→Discovered, SourceId→$(PackageId), BasePath→$(StaticWebAssetBasePath), AssetMode→CurrentProject), and remaps associated endpoints. Downstream targets see ordinary local Discovered assets.Scenario 3: Shared Framework Dependency
Two Root-mode libraries (
LibraryA,LibraryB) both reference the same package (Assets.Internal) containing Framework assets. Each library independently materializes the framework files and transforms them with their ownSourceIdandBasePath. Both libraries set distinctStaticWebAssetBasePathvalues, so the materialized assets resolve to different target paths (_content/LibraryA/js/framework.jsand_content/LibraryB/js/framework.js). When a consuming project receives these via project references, no duplicate asset error occurs because the paths do not collide.Key Design Decisions
UpdatePackageStaticWebAssetsDiscoveredCurrentProjectCurrentProjectassets flow through references. In Default mode, prevents leaking to references — each project materializes its own copy.GenerateStaticWebAssetsPropsFileDiscovered/Computed. Mirrors howPackageworks today.Removeoperates on ItemSpec (route). Grouping ensures all endpoints in a route group are processed together, preventing orphans.FromV1TaskItemError Handling
No custom error handling for framework asset resolution. Standard I/O exceptions propagate if source files don't exist. Two packages cannot conflict at the same materialization path because the original package ID is included (
fx/{SourceId}/{RelativePath}). Existing SWA duplicate asset detection handles same-target-path conflicts. Zero glob matches is a no-op.Testing
GenerateStaticWebAssetsPropsFileglob matching (Framework vs. Package classification)UpdatePackageStaticWebAssetsframework asset materialization and metadata transformation