Skip to content

[browser] Don't copy framework assets to output during build#126407

Draft
maraf wants to merge 5 commits intomainfrom
maraf/WasmSdkCopyToBin
Draft

[browser] Don't copy framework assets to output during build#126407
maraf wants to merge 5 commits intomainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf
Copy link
Copy Markdown
Member

@maraf maraf commented Apr 1, 2026

Note

This PR was created with the assistance of GitHub Copilot.

Summary

During build, the WebAssembly SDK copies all .wasm and .js framework assets (~178 files) to bin/wwwroot/_framework/ via CopyToOutputDirectory=PreserveNewest. This is unnecessary because dotnet run uses the static web assets middleware, which serves files directly from their obj/ locations via the manifest (staticwebassets.runtime.json).

Changes

Set CopyToOutputDirectory=Never (was PreserveNewest) for three DefineStaticWebAssets / item-update calls in Microsoft.NET.Sdk.WebAssembly.Browser.targets:

  • Webcil-converted assets (Computed source type) — the .wasm files produced from .dll
  • Framework assets (Framework source type) — dotnet.js, dotnet.native.wasm, runtime JS, etc.
  • Materialized framework assets (post-UpdatePackageStaticWebAssets) — the per-project copies in obj/fx/

Validation

  • ✅ Build succeeds (dotnet build with TargetOS=browser)
  • dotnet run starts WasmAppHost dev server correctly
  • ✅ Playwright headless browser test confirms the WASM app loads and executes (outputs 42)
  • bin/wwwroot/_framework/ drops from ~178 files to 2 (only hot-reload module + dotnet.js from a separate code path)
  • CopyToPublishDirectory was already Never — publish is unaffected

Notes

  • The previous PreserveNewest was added for Blazor WASM hosted scenarios where a server project serves the client's framework files. This scenario needs separate validation.
  • Static web assets middleware resolves files from their source locations (obj/ dirs), so physical copies in bin/ are not needed for dotnet run.

During build, the WebAssembly SDK was copying all .wasm and .js framework
assets to bin/wwwroot/_framework/ via CopyToOutputDirectory=PreserveNewest.
This is unnecessary because dotnet run uses the static web assets middleware,
which serves files directly from their obj/ locations using the manifest.

Change CopyToOutputDirectory from PreserveNewest to Never for:
- Webcil-converted assets (Computed static web assets)
- Materialized framework assets (dotnet.js, dotnet.native.wasm, etc.)

This eliminates ~178 file copies during build while preserving correct
behavior for dotnet run (static web assets middleware) and publish
(CopyToPublishDirectory was already Never).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

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 changes the WebAssembly SDK build targets to stop copying runtime/framework static web assets into bin/wwwroot/_framework during build, relying instead on the static web assets manifest/middleware to serve them from their source locations (typically under obj/ or the runtime pack).

Changes:

  • Set CopyToOutputDirectory="Never" for build-time static web assets emitted from webcil conversion (SourceType="Computed").
  • Set CopyToOutputDirectory="Never" for build-time runtime pack assets registered as SourceType="Framework".
  • Set CopyToOutputDirectory="Never" for the post-UpdatePackageStaticWebAssets “materialized” framework assets (per-project obj/fx/{SourceId}/ copies).

Comment on lines 427 to 432
<!-- Materialized framework assets must be visible to referencing projects (e.g. Blazor WASM
hosted scenarios where the server project serves the client's framework files).
UpdatePackageStaticWebAssets defaults AssetMode to CurrentProject and CopyToOutputDirectory
to Never. Override both: AssetMode=All so assets flow through project references, and
CopyToOutputDirectory=PreserveNewest so they are copied from the intermediate materialized
path (obj/fx/{SourceId}/) to bin/wwwroot/_framework/ at build time. -->
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The comment above this ItemGroup still says CopyToOutputDirectory is overridden to PreserveNewest to copy materialized framework assets into bin/wwwroot/_framework at build time, but the code now sets CopyToOutputDirectory="Never". Please update the comment to reflect the new behavior/rationale (and avoid referencing build-time copying if that is no longer intended).

Copilot uses AI. Check for mistakes.
@maraf maraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build [browser] Don't copy framework assets to output during build Apr 1, 2026
@maraf maraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@maraf maraf added this to the 11.0.0 milestone Apr 1, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Member Author

maraf commented Apr 1, 2026

Note

This comment was generated with the assistance of GitHub Copilot.

Blazor WASM Hosted Scenario Analysis

Testing revealed that the existing MultiClientHostedBuild test in src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs explicitly asserts that framework files are physically present in bin/<config>/<tfm>/wwwroot/_framework/:

// Lines 169-179
string client1Framework = Path.Combine(client1Dir, "bin", config.ToString(),
    DefaultTargetFrameworkForBlazor, "wwwroot", "_framework");

Assert.True(Directory.Exists(client1Framework), ...);
Assert.Contains(client1Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));

This test creates two Blazor WASM client projects hosted by a single server and verifies that each client gets its own physical framework files — validating the Framework SourceType materialization path that gives each client unique per-project Identity for shared runtime pack files (dotnet.native.js, ICU data, etc.).

Impact Summary

Scenario Status
dotnet build (standalone WASM) ✅ Works — assets no longer copied to bin/wwwroot/_framework/
dotnet run (standalone WASM via WasmAppHost) ✅ Works — static web assets middleware serves from obj/ via manifest
dotnet run (Blazor WASM hosted) ✅ Should work — UseBlazorFrameworkFiles() uses static web assets middleware
MultiClientHostedBuild test Will fail — asserts physical files in bin/wwwroot/_framework/
Non-publish deployment (xcopy from bin/) ⚠️ Would break if anyone depends on files being in bin/ at build time

Next Steps

Options to consider:

  1. Update the test assertions to check obj/ locations (via manifest) instead of bin/wwwroot/_framework/
  2. Make the behavior opt-in via a property (e.g. <WasmCopyFrameworkFilesToOutput>false</WasmCopyFrameworkFilesToOutput>)
  3. Only suppress copy for non-hosted scenarios where no other project references the WASM client

Copy link
Copy Markdown
Member Author

maraf commented Apr 2, 2026

Note

This comment was generated by GitHub Copilot.

CI Failure Analysis

All 4 failing jobs are caused by this PR. None are infrastructure or known issues.

Summary

All 4 jobs are WasmBuildTests (Mono + CoreCLR × Linux + Windows). Every failure has the same root cause — tests expect framework files in bin/<config>/net11.0/wwwroot/_framework/ but they're no longer copied there due to the CopyToOutputDirectory=Never change.

Failing Tests (13 unique across 4 jobs)

All fail with: Could not find <file> in bundle directory: .../bin/.../wwwroot/_framework

Missing File Tests Affected
dotnet.js.map DefaultTemplate_WithoutWorkload (Debug, Release), WorkloadNotRequiredForInvariantGlobalization, LoadAppSettingsBasedOnApplicationEnvironment (3 cases), LoadFilesToVfs, NeverFetchMoreThanMaxAllowed (2 cases)
dotnet.diagnostics.js BuildWithDefaultLevel (Debug, Release), BuildWithExplicitValue

Root Cause in Test Code

The common assertion point is ProjectProviderBase.AssertDotNetFilesSet() which scans bin/.../wwwroot/_framework/ for expected framework files. With CopyToOutputDirectory=Never, these files remain in obj/ and are served via the static web assets manifest — but the test assertions still expect them physically in bin/.

Known Issues

Build Analysis flagged 3 known issues (#109653, #125244, #117164) but none match these failures. These are genuinely new failures introduced by the PR.

Next Steps

To land this change, the test assertions in ProjectProviderBase.cs (and possibly MiscTests.cs for MultiClientHostedBuild) need to be updated to reflect the new behavior where framework files are no longer copied to bin/wwwroot/_framework/ during build.

With CopyToOutputDirectory=Never, framework assets are no longer copied
to bin/wwwroot/_framework/ during build. They are served from obj/
locations via the static web assets middleware during dotnet run.

Only assert bundle file layout for publish, where files are still
physically copied to the output. Build-time tests that run the app
(via dotnet run or xharness) still validate the app works correctly -
they just don't check for files in bin/ that are intentionally no
longer there.

The Blazor-specific AssertBundle already had this guard
WasmTemplateTestsBase path needed updating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

🤖 Copilot Code Review — PR #126407

Note

This review was generated by Copilot and may contain inaccuracies. A human reviewer should validate these findings.

Holistic Assessment

Motivation: This PR optimizes WASM development builds by eliminating ~178 unnecessary file copies to bin/wwwroot/_framework/. During development, dotnet run uses the static web assets middleware to serve framework files directly from their obj/ intermediate locations via a manifest, so copying to bin/ is wasteful. The motivation is sound and well-justified.

Approach: Changing CopyToOutputDirectory from PreserveNewest to Never for all three categories of WASM build-time assets (webcil-converted, framework, and materialized framework) is the correct and minimal approach. Publish behavior is unaffected since CopyToPublishDirectory was already Never (publish uses its own pipeline). The test adjustment to skip bundle file assertions for build-only scenarios is the right companion change.

Summary: ⚠️ Needs Human Review. The core approach is correct and the code changes are clean. However, there is a stale comment that contradicts the new behavior and should be updated. Additionally, I've identified two existing tests that appear to directly inspect bin/_framework/ after build (not publish) — if satellite/resource assemblies go through the same DefineStaticWebAssets pipeline, those tests may break. The PR author is a codeowner of this area and may have already verified this, but a human reviewer should confirm.


Detailed Findings

⚠️ Stale Comment — XML comment contradicts code behavior

File: src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets, lines 427–432

The XML comment still describes the old behavior:

<!-- ...Override both: AssetMode=All so assets flow through project references, and
     CopyToOutputDirectory=PreserveNewest so they are copied from the intermediate materialized
     path (obj/fx/{SourceId}/) to bin/wwwroot/_framework/ at build time. -->

But the code now sets CopyToOutputDirectory="Never" (line 435). The comment should be updated to reflect that assets are no longer copied to bin/wwwroot/_framework/ during build, and are instead served from obj/ locations via the static web assets middleware.

This is misleading for future maintainers who will read the comment and assume files are being copied when they aren't.

⚠️ Potential Test Breakage — Existing tests check bin/_framework/ after build

Two existing tests (not modified by this PR) directly inspect bin/_framework/ contents after a build (not publish):

  1. SatelliteLoadingTests.SatelliteAssembliesFromPackageReference (SatelliteLoadingTests.cs:113):

    string binFrameworkDir = GetBinFrameworkDir(config, forPublish: false);
    // ... checks for satellite assembly directories in binFrameworkDir
  2. Blazor/BuildPublishTests.DefaultTemplate_WithResources_Publish (BuildPublishTests.cs:100):

    AssertResourcesDlls(GetBlazorBinFrameworkDir(config, forPublish: false));

If satellite assemblies and project resource DLLs go through ConvertDllsToWebcil_WebcilAssetsCandidatesDefineStaticWebAssets (which now sets CopyToOutputDirectory="Never"), these files would no longer be present in bin/_framework/ during build, and these tests would fail.

Low confidence flag: I'm not certain whether these specific asset types flow through the same pipeline modified by this PR. The PR author (@maraf) is a codeowner of this area and may have already verified this. A human reviewer should confirm whether these tests pass in CI.

✅ Correctness — Build/publish separation is correct

The IsPublish guard in WasmTemplateTestsBase.cs (line 262) correctly restricts bundle assertions to publish builds. BuildOptions.IsPublish defaults to false and PublishOptions.IsPublish defaults to true, so the assertion will fire exactly when expected. This aligns with the architecture: build uses the static web assets middleware (no files in bin/), publish physically copies files for deployment.

✅ Test Coverage — Build+run tests provide functional validation

Skipping AssertWasmSdkBundle for builds does lose file-layout assertions, but this is well-mitigated by the 25+ test files that build the app and then run it via RunForBuildWithDotnetRundotnet run --no-build. These tests validate that the static web assets middleware correctly serves framework files from obj/ locations. The runtime behavior is still thoroughly validated.

✅ Scope — Change is focused and consistent

All three CopyToOutputDirectory changes target the same logical pipeline (WASM build-time static web assets). CopyToPublishDirectory values are untouched. The commit is well-structured into two logical pieces: the targets change and the corresponding test adjustment.

💡 Comment Quality — Test comment could be more concise

The comment block in WasmTemplateTestsBase.cs (lines 258–261) is well-written and explains the "why" clearly. Minor suggestion: it could be shortened to 2 lines, but this is not blocking.

Generated by Code Review for issue #126407 ·

Copilot AI review requested due to automatic review settings April 10, 2026 08:09
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

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

Comment on lines +259 to +260
// no longer contains framework files in bin/_framework/. The static web
// assets middleware serves them from obj/ locations during dotnet run.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The new comment says build output no longer contains framework files in bin/_framework/, but the bundle path used by these tests/providers is bin/<config>/<tfm>/wwwroot/_framework (see WasmSdkBasedProjectProvider.GetBinFrameworkDir). Consider correcting the path in the comment to avoid confusion for future test failures/investigations.

Suggested change
// no longer contains framework files in bin/_framework/. The static web
// assets middleware serves them from obj/ locations during dotnet run.
// no longer contains framework files in
// bin/<configuration>/<tfm>/wwwroot/_framework/. The static web assets
// middleware serves them from obj/ locations during dotnet run.

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +265
if (buildOptions.AssertAppBundle)
{
_provider.AssertWasmSdkBundle(configuration, buildOptions, IsUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs, res.Output);
// With CopyToOutputDirectory=Never for framework assets, build output
// no longer contains framework files in bin/_framework/. The static web
// assets middleware serves them from obj/ locations during dotnet run.
// Only assert bundle contents for publish, where files are still copied.
if (buildOptions.IsPublish)
{
_provider.AssertWasmSdkBundle(configuration, buildOptions, IsUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs, res.Output);
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Gating AssertWasmSdkBundle(...) to publish-only removes bundle verification for all build-based tests that rely on AssertAppBundle=true (the default). Instead of skipping the assertion entirely for build, consider updating the assertion logic to validate framework assets at their new source locations (e.g., obj/fx/... / static web assets manifest) so build scenarios remain covered.

Copilot uses AI. Check for mistakes.
Comment on lines 427 to +435
<!-- Materialized framework assets must be visible to referencing projects (e.g. Blazor WASM
hosted scenarios where the server project serves the client's framework files).
UpdatePackageStaticWebAssets defaults AssetMode to CurrentProject and CopyToOutputDirectory
to Never. Override both: AssetMode=All so assets flow through project references, and
CopyToOutputDirectory=PreserveNewest so they are copied from the intermediate materialized
path (obj/fx/{SourceId}/) to bin/wwwroot/_framework/ at build time. -->
<ItemGroup>
<_WasmMaterializedFrameworkAssets Update="@(_WasmMaterializedFrameworkAssets)"
AssetMode="All" CopyToOutputDirectory="PreserveNewest" />
AssetMode="All" CopyToOutputDirectory="Never" />
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

The comment above this ItemGroup still states that CopyToOutputDirectory is overridden to PreserveNewest for hosted scenarios, but the code now sets it to Never. Please update/remove the outdated comment text, and consider dropping the explicit CopyToOutputDirectory="Never" metadata override here since UpdatePackageStaticWebAssets already defaults it to Never (per the comment at lines 429-430).

Copilot uses AI. Check for mistakes.
maraf and others added 2 commits April 10, 2026 08:50
With CopyToOutputDirectory=Never, framework files are no longer copied
to bin/_framework/ during build. Update MultiClientHostedBuildAndPublish
to assert framework files exist in obj/<config>/<tfm>/fx/<ProjectName>/
_framework/ for build, matching the materialization path used by
UpdatePackageStaticWebAssets. Publish assertions remain unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Revert the IsPublish guard that skipped AssertWasmSdkBundle for builds.
Instead, add AssertBuildBundle that validates framework files are in their
correct obj/ subdirectories with CopyToOutputDirectory=Never:

- dotnet.js (boot config) in obj/{config}/{tfm}/
- dotnet.runtime.js, maps, ICU in obj/{config}/{tfm}/fx/{name}/_framework/
- dotnet.native.* in fx/_framework/ (non-native) or wasm/for-build/ (native)
- webcil assemblies in obj/{config}/{tfm}/webcil/
- framework files NOT in bin/_framework/

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-Build-mono os-browser Browser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants