Skip to content

Fix WASM publish path for Framework SourceType materialization#126210

Closed
lewing wants to merge 11 commits intodotnet:mainfrom
lewing:fix-wasm-framework-sourcetype
Closed

Fix WASM publish path for Framework SourceType materialization#126210
lewing wants to merge 11 commits intodotnet:mainfrom
lewing:fix-wasm-framework-sourcetype

Conversation

@lewing
Copy link
Copy Markdown
Member

@lewing lewing commented Mar 27, 2026

Summary

PR #125329 added Framework SourceType handling in Browser.targets for the build path, but the publish path (ProcessPublishFilesForWasm) was not updated. When a server hosts multiple Blazor WASM clients and publishes, DiscoverPrecompressedAssets crashes with duplicate Identity keys for shared runtime pack files (dotnet.js.map, ICU data, etc.).

Root Cause

The publish-time ConvertDllsToWebcil call (line 790) did not capture PassThroughCandidates, so pass-through files weren't routed through DefineStaticWebAssets(SourceType="Framework") + UpdatePackageStaticWebAssets for per-project materialization.

Fix

Mirror the build-time Framework materialization in ProcessPublishFilesForWasm:

  • Capture PassThroughCandidates from publish-time ConvertDllsToWebcil
  • Remove pass-throughs from webcil candidates
  • Route through DefineStaticWebAssets(SourceType="Framework", AssetKind="Publish")
  • Route through UpdatePackageStaticWebAssets for materialization
  • Set AssetMode=All so materialized assets flow to the server

Testing

  • Added MultiClientHostedPublish test (complements existing MultiClientHostedBuild)
  • Fixes Publish_HostingMultipleBlazorWebApps_Works in sdk#53613

Dependencies

Requires SDK support for SourceType="Framework" from sdk#53135 (already merged).

lewing and others added 11 commits March 9, 2026 09:01
… 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 <223556219+Copilot@users.noreply.github.com>
- Add PassThroughCandidates output to ConvertDllsToWebcil task so the
  task itself classifies which files are pass-throughs (non-DLL files
  from shared locations like the NuGet cache) vs converted webcil files.
  This replaces the fragile MSBuild string-comparison split that checked
  StartsWith(_WasmBuildWebcilPath) and incorrectly classified native
  build outputs in obj/wasm/for-build/ as Framework candidates.

- Set AssetMode=All on materialized framework assets so they flow
  through project references to the host/server project in Blazor WASM
  hosted scenarios. UpdatePackageStaticWebAssets defaults to
  CurrentProject which prevents the server project from seeing client
  framework assets during publish.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarify that WasmNativeBuildOutput exclusion only applies when IsEnabled
is true. When IsEnabled is false, all candidates appear unfiltered.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ToOutputDirectory

Two fixes:

1. ConvertDllsToWebcil: When IsEnabled=false, apply the same filtering
   for PassThroughCandidates as the enabled path — only non-DLL items
   without WasmNativeBuildOutput metadata qualify as Framework
   pass-throughs. Previously all candidates (including DLLs) were
   marked as pass-throughs, causing them all to go through Framework
   materialization which triggered NETSDK1147 workload detection in
   NoWebcil/NoWorkload test configurations.

2. Browser.targets: Set CopyToOutputDirectory=PreserveNewest on
   materialized framework assets after UpdatePackageStaticWebAssets.
   The SDK task defaults materialized assets to CopyToOutputDirectory=
   Never, but WASM framework files (dotnet.native.js, ICU data, etc.)
   must be copied to bin/wwwroot/_framework for the app to work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Validates that two Blazor WASM client projects can be built by a single
server project without duplicate static web asset Identity collisions.
This is the scenario that the Framework SourceType + UpdatePackageStaticWebAssets
materialization path was designed to fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The build path correctly routes WASM pass-through files (JS, maps, ICU data)
through DefineStaticWebAssets(SourceType="Framework") and
UpdatePackageStaticWebAssets for per-project materialization. However, the
publish path (ProcessPublishFilesForWasm) was not updated, causing
DiscoverPrecompressedAssets to crash with duplicate Identity keys when a
server project hosts multiple Blazor WASM clients.

Mirror the build-time Framework materialization in the publish path:
- Capture PassThroughCandidates from publish-time ConvertDllsToWebcil
- Remove pass-throughs from webcil candidates
- Route through DefineStaticWebAssets(SourceType="Framework", AssetKind="Publish")
- Route through UpdatePackageStaticWebAssets for per-project materialization
- Set AssetMode=All so materialized assets flow to the server

Also add MultiClientHostedPublish test to validate the publish path alongside
the existing MultiClientHostedBuild test.

Fixes the Publish_HostingMultipleBlazorWebApps_Works failure in sdk#53613.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 27, 2026 17:17
@lewing
Copy link
Copy Markdown
Member Author

lewing commented Mar 27, 2026

Closing — branch had stale history from merged #125329. Reopening with clean branch.

@lewing lewing closed this Mar 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes multi-client Blazor WASM publish failures by extending the existing Framework SourceType materialization approach (previously applied to build) into the publish pipeline, preventing duplicate static web asset Identity collisions for shared runtime-pack files.

Changes:

  • Extend ConvertDllsToWebcil to also output PassThroughCandidates for Framework materialization.
  • Update Microsoft.NET.Sdk.WebAssembly.Browser.targets publish path (ProcessPublishFilesForWasm) to classify pass-through assets as Framework and materialize them per project via UpdatePackageStaticWebAssets.
  • Add new build and publish tests validating multi-client hosted scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs Adds PassThroughCandidates output to distinguish pass-through assets needing Framework materialization.
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets Mirrors build-time Framework materialization in publish pipeline; avoids duplicate Identity collisions during publish.
src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Adds MultiClientHostedBuild and MultiClientHostedPublish coverage for multi-client hosted build/publish.

Comment on lines +193 to +197
string rootDir = Path.GetDirectoryName(_projectDir)!;
string client1Dir = _projectDir;
string client2Dir = Path.Combine(rootDir, "App2");
string serverDir = Path.Combine(rootDir, "Server");

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same cleanup concern as in MultiClientHostedBuild: App2 and Server are created under rootDir (parent of _projectDir), so the standard per-test cleanup that deletes _projectDir won't remove them. Prefer nesting these under _projectDir or otherwise ensuring rootDir is cleaned up to avoid leaving large publish outputs behind.

Copilot uses AI. Check for mistakes.
Comment on lines 116 to 120
Directory.Delete(tmpDir, true);

WebcilCandidates = webcilCandidates.ToArray();
PassThroughCandidates = passThroughCandidates.ToArray();
return true;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directory.Delete(tmpDir, true) is outside the conversion loop's try/catch. If a conversion throws and the method returns early, tmpDir is left behind. Consider restructuring Execute to use a try/finally around the tmpDir lifetime (and delete tmpDir in the finally, ideally best-effort) so temp directories don't accumulate on failures.

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +117
// _projectDir is now .../App. Go up to the root and create a second client + server.
string rootDir = Path.GetDirectoryName(_projectDir)!;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests create App2 and Server as siblings of _projectDir (under rootDir). Per-test cleanup deletes _projectDir (the App directory) but will leave App2/Server behind, which can bloat temp storage across runs. Consider creating these directories under _projectDir so they get cleaned up automatically, or adjust cleanup to delete rootDir (e.g., via a try/finally that updates _projectDir to rootDir).

Suggested change
// _projectDir is now .../App. Go up to the root and create a second client + server.
string rootDir = Path.GetDirectoryName(_projectDir)!;
// _projectDir is now .../App. Create a second client + server under this directory so they get cleaned up.
string rootDir = _projectDir;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants