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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin
Apr 21, 2026
Merged

[browser] Don't copy framework assets to output during build#126407
maraf merged 17 commits into
mainfrom
maraf/WasmSdkCopyToBin

Conversation

@maraf

@marafmaraf commented Apr 1, 2026

Copy link
Copy Markdown
Member

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.

Downstream impact: dotnet/sdk BlazorWasm test baselines

The Microsoft.NET.Sdk.BlazorWebAssembly.TestsStaticWebAssetsBaselines/*.Build.*.json files in dotnet/sdk currently assert that framework assets live under ${ProjectPath}\bin\Debug\${Tfm}\wwwroot\_framework\ after Build. Across the Build baselines there are ~1,940 bin\Debug references covering ~481 framework files:

  • dotnet.* runtime/loader JS + .wasm + .map (+ .gz) — 12 files
  • icudt_*.dat.gz — 3 files
  • blazor.webassembly.js (+ .gz) — 2 files
  • ~233 managed framework .wasm assemblies (+ .gz copies)
  • Localized .resources.wasm satellites

Once this change flows to dotnet/sdk, these baselines will need to be regenerated so framework assets point at their intermediate/RestorePath locations instead of bin\. App-project outputs (blazorwasm.wasm/.pdb) and project references (RazorClassLibrary.*) are unaffected and remain under bin\.

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>
CopilotAI review requested due to automatic review settings April 1, 2026 14:47
@github-actions

This comment has been minimized.

This comment was marked as resolved.

@marafmaraf changed the title Stop copying WASM framework assets to bin/wwwroot/_framework during build[browser] Don't copy framework assets to output during buildApr 1, 2026
@marafmaraf added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Apr 1, 2026
@marafmaraf 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.

@marafGitHub Codespaces

This comment was marked as resolved.

@marafGitHub Codespaces

This comment was marked as resolved.

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

This comment has been minimized.

CopilotAI review requested due to automatic review settings April 10, 2026 08:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs Outdated
marafand 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>
CopilotAI review requested due to automatic review settings April 14, 2026 09:44
With framework assets no longer copied to bin/_framework/ during build,
update the three remaining failing tests to look for files in the obj
subdirectories where they actually live:
- Blazor BuildPublishTests.DefaultTemplate_WithResources_Publish:
probe satellite assemblies under obj/{config}/{tfm}/webcil/{locale}/
(webcil) or obj/{config}/{tfm}/fx/{name}/_framework/{locale}/ (non-webcil)
for the build-time assertion.
- BuildPublishTests.BuildThenPublishWithAOT: stat framework files for the
first build from obj/{config}/{tfm}/fx/{name}/_framework/ (JS, source
maps) and obj/{config}/{tfm}/wasm/for-build/ (native outputs). Boot
config is read from obj/{config}/{tfm}/dotnet.js via a new optional
bootConfigDir parameter on GetFilesTable used by fingerprint resolution.
- ModuleConfigTests.SymbolMapFileEmitted(isPublish=false): search for
dotnet.native*.js.symbols in obj/wasm/for-build/ (native rebuild) and
in obj/fx/*/_framework/ as a fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 17, 2026 10:26

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment threadsrc/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs Outdated
Comment threadsrc/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126407

Note

This review was AI/Copilot-generated. Models used: Claude Opus 4.6 (primary), Claude Sonnet 4.5.

Holistic Assessment

Motivation: The PR addresses a genuine build performance issue — ~178 unnecessary file copies during build. The static web assets middleware already serves files from obj/ locations via the manifest, making the bin/wwwroot/_framework/ copies redundant during build. This is well-motivated.

Approach: The approach is clean and minimal — 3 attribute value changes in production code (CopyToOutputDirectory from PreserveNewest to Never), with the remaining 8 files being test adaptations. Publish behavior is unchanged. The approach is correct.

Summary: ⚠️Needs Human Review. The production change and test updates are sound, but there are maintainability concerns (code duplication) and a minor test coverage reduction worth human judgment. No blocking correctness issues found.


Detailed Findings

✅ Production Change — Correct and well-scoped

The three changes in Microsoft.NET.Sdk.WebAssembly.Browser.targets correctly switch CopyToOutputDirectory from PreserveNewest to Never for:

  • Webcil-converted assets (line 390, Computed static web assets)
  • Framework pass-through candidates (line 409, Framework static web assets)
  • Materialized framework assets (line 438, post-UpdatePackageStaticWebAssets)

The updated XML comments (lines 430–435) accurately describe the new behavior. CopyToPublishDirectory was already Never, and publish has its own copy mechanism, so publish is unaffected.

✅ New AssertBuildBundle method — Thorough validation

The new AssertBuildBundle method in WasmSdkBasedProjectProvider.cs (lines 205–309) is well-structured with clear sections for each asset category. It validates:

  • Files exist in the correct obj/ subdirectories
  • Files do NOT exist in wrong locations (obj root, bin/_framework/)
  • Boot config is well-formed
  • Native files match runtime pack when expected
  • Multi-threaded worker files (added in review feedback)

The nullable isNativeBuild parameter flows correctly through GetExpectedFileType which handles all three states (true, false, null) with explicit comparisons.

⚠️ Code Duplication — "Discover fx subdirectory" pattern repeated 5 times

The following pattern appears verbatim in 5 locations across test files:

stringfxBaseDir=Path.Combine(objDir,"fx");string[]fxSubDirs=Directory.GetDirectories(fxBaseDir);Assert.True(fxSubDirs.Length==1,$"Expected exactly one subdirectory under {fxBaseDir}, ...");stringfxFrameworkDir=Path.Combine(fxSubDirs[0],"_framework");

Locations: WasmSdkBasedProjectProvider.AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore.

If the obj/ layout convention ever changes, all 5 places need updating in lockstep. Consider extracting a shared helper like GetMaterializedFrameworkDir(string objDir) on the provider base class. (Flagged by both review models.)

⚠️NonDefaultFrameworkDir skips all build assertions — coverage gap

In AssertWasmSdkBundle (lines 187–192), when NonDefaultFrameworkDir is set and IsPublish is false, zero build bundle assertions run:

elseif(string.IsNullOrEmpty(buildOptions.NonDefaultFrameworkDir)){AssertBuildBundle(...);}// When NonDefaultFrameworkDir is set, skip build bundle assertions.

Before this PR, AssertBundle was called for all cases (including NonDefaultFrameworkDir). Tests like SimpleRunTests.BlazorBuildRunTest and WasmTemplateTests.BuildAndRunForDifferentOutputPaths pass NonDefaultFrameworkDir for non-artifacts builds too, so they now get no file-layout verification. The tests do verify the app runs via dotnet run, which is the primary concern, but the file-layout gap should be acknowledged. Would be worth at minimum logging a message when assertions are skipped to aid debugging.

💡 Minor: Duplicate Path.Combine in ModuleConfigTests.cs

In SymbolMapFileEmittedCore (lines 139–144), Path.Combine(objDir, "fx") is evaluated twice in the spread expression — once for Directory.Exists and once for Directory.GetDirectories. Extract to a local variable for clarity and consistency with the pattern used elsewhere.

💡 Minor: dotnet.native.worker.mjs not compared against runtime pack

In AssertBuildBundle, the multi-threaded worker file gets location assertions (exists in nativeDir, not in obj root) but is not compared against the runtime pack, unlike dotnet.native.js and dotnet.native.wasm. The publish-path AssertBundle has special-case handling that explicitly skips this comparison with an explanatory comment. Adding a similar comment in AssertBuildBundle would explain the intentional omission.

✅ Accidental symlink removal — Good cleanup

The deletion of src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/WasmAppHost (a symlink pointing to a local Codespace path /workspaces/runtime/artifacts/bin/WasmAppHost/wasm/Debug) is appropriate cleanup that was caught in review.

✅ No public API changes detected

No changes to ref/ assemblies or new public API surface. All code changes are in MSBuild targets (build infrastructure) and test files.

Generated by Code Review for issue #126407 ·

marafand others added 2 commits April 17, 2026 11:29
Addresses review feedback: centralizes the obj/.../fx/<source-id>/_framework/
discovery logic in WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir
and uses it from AssertBuildBundle, BuildPublishTests and Blazor/MiscTests
so the per-project fx subdir name is discovered instead of hardcoded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Apply GetMaterializedFrameworkDir helper to the remaining three
locations flagged by the review bot: Blazor/BuildPublishTests
GetBuildSatelliteBaseDir, SatelliteLoadingTests, and ModuleConfigTests
(the last still inlines the extra 'wasm/for-build' search path but
deduplicates the fx-base Path.Combine).
- Explain why dotnet.native.worker.mjs is not compared against the
runtime pack in AssertBuildBundle (parity with AssertBundle's publish
path comment).
- Log when AssertWasmSdkBundle skips AssertBuildBundle because
NonDefaultFrameworkDir is set, so the coverage gap is visible in
test output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

Comment authored with assistance from GitHub Copilot.

Thanks for the thorough pass. Addressing the findings in 00635ac + c6fe5e5:

✅ Code duplication (5× fx discovery) — extracted WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir) and applied it at all 5 sites (AssertBuildBundle, BuildPublishTests.BuildThenPublishWithAOT, Blazor/BuildPublishTests.GetBuildSatelliteBaseDir, SatelliteLoadingTests.SatelliteAssembliesFromPackageReference, ModuleConfigTests.SymbolMapFileEmittedCore). Blazor/MiscTests.MultiClientHostedBuildAndPublish also switched to the helper (was previously hardcoding Client1/Client2).

NonDefaultFrameworkDir skip — coverage gapAssertWasmSdkBundle now emits a _testOutput.WriteLine when it skips AssertBuildBundle for NonDefaultFrameworkDir builds, so the skipped file-layout verification is visible in test logs. Keeping the skip itself for now since the obj/ layout genuinely diverges for UseArtifactsOutput; a proper layout check for that shape can be a follow-up.

✅ Duplicate Path.Combine(objDir, "fx") in ModuleConfigTests — hoisted into a fxBaseDir local.

dotnet.native.worker.mjs runtime-pack comparison omission — added a comment in AssertBuildBundle explaining the intentional omission, mirroring the existing comment in the publish-path AssertBundle.

Left as-is:

  • Hosted dotnet build + dotnet run server test (raised on Microsoft.NET.Sdk.WebAssembly.Browser.targets): out of scope for this PR. Hosted publish is still exercised end-to-end by MultiClientHostedBuildAndPublish; an end-to-end build + serve test would require new Kestrel + HTTP infra in Wasm.Build.Tests. Will file a follow-up issue.

marafand others added 2 commits April 20, 2026 12:47
…n obj/fx
With CopyToOutputDirectory=Never, the materialized framework dir under
obj/{config}/{tfm}/fx/{source-id}/_framework/ contains dotnet.runtime.js
under its canonical (non-fingerprinted) name during build. Fingerprinting
is applied later when publishing to bin. GetFilesTable rewrites the entry
from the boot config (which already reflects the publish layout), yielding
a fingerprinted path that does not exist in obj/fx and causes CompareStat
to report the file as missing.
Override the dotnet.runtime.js entry to the unfingerprinted obj/fx path
for the build-phase stat, mirroring the existing native-file override.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings April 21, 2026 08:36
@maraf
maraf marked this pull request as ready for review April 21, 2026 08:36
@maraf
maraf requested a review from a teamApril 21, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 9 out of 9 changed files in this pull request and generated no new comments.

@maraf

Copy link
Copy Markdown
MemberAuthor

@javiercn Do we have scenario that would still rely on the actual files in the bin folder?

Run the server via 'dotnet run --no-build' and use Playwright's
APIRequestContext to verify that each referenced client's framework
files are served from the obj/ materialized directory at the
/<client>/_framework/* base path. This guards the hosted scenario
from PR #126407 review feedback (build-only, server statically serves
client assets).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@marafGitHub Codespaces

Copy link
Copy Markdown
MemberAuthor

Note

This comment was generated with the assistance of GitHub Copilot.

CI analysis for the latest push (build 1388930):

  • runtime: 12/13 green, 1 failed
  • runtime-dev-innerloop: ✅ green
  • dotnet-linker-tests: ✅ green

The single failure is browser-wasm linux Release CoreCLR_WasmBuildTests — Helix work item WBT-NoWorkload-CLR-ST-Wasm.Build.Tests.WasmRunOutOfAppBundleTests exited with code -4:

Unable to pull image mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-26.04-helix-webassembly-amd64

This is the well-known Helix Docker-pull infrastructure flake tracked in #117164 — tests never started; the failing test (WasmRunOutOfAppBundleTests) doesn't exercise any code path touched by this PR. Unrelated to the change. Will retry with /azp run runtime if a fully green run is required before merge.

@maraf
maraf merged commit 98bebeb into mainApr 21, 2026
37 of 39 checks passed
@maraf
maraf deleted the maraf/WasmSdkCopyToBin branch April 21, 2026 17:45
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 22, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

arch-wasmWebAssembly architecturearea-Build-monoos-browserBrowser variant of arch-wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@maraf@lewing