Skip to content

fix(renderer): guard the root mount path against fragment/multi-root bodies#151

Merged
zzal merged 1 commit into
mainfrom
feat/domhandle-fragment-guard
Jul 6, 2026
Merged

fix(renderer): guard the root mount path against fragment/multi-root bodies#151
zzal merged 1 commit into
mainfrom
feat/domhandle-fragment-guard

Conversation

@zzal

@zzal zzal commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Closes Part I Wave 3 #5 of the arch+DX audit. Fragment-rooted component bodies are only DEBUG-discouraged (the bare-fragment diagnostic in Diff.swift), but the root mount path used single-valued MountNode.domHandle — whose own doc warns it does not descend through fragments and is "only valid for single-rooted nodes." For a fragment root it returned the structural handle "the DOM never sees," feeding a bogus handle to mount/replaceMount in RELEASE (where the DEBUG diagnostic is compiled out) — a silently broken mount.

Fix

MountNode.singleRootDOMHandle: descends through fragments (via collectDOMRoots) and traps in all builds (precondition-Osize release keeps it) with actionable guidance if the node resolves to anything but exactly one DOM root. Wired into the three root-path sites in Renderer — the first-mount mount, the identity-swap replaceMount, and resyncFullRemount's replaceMount — replacing the bare domHandle.

  • The DEBUG bare-fragment diagnostic stays — it's the earlier, more precise dev-time warning and covers nested fragments too; this guard is the release safety net it was missing.
  • Nested fragment bodies are unaffected — they attach all their DOM roots to a parent via collectDOMRoots and never hit the root path.
  • Chose enforce single-root over make the root path multi-root-aware: the latter needs driver-level "attach a list" changes and reverses the framework's current "wrap in a single element" stance.

Test plan

  • 3 new host tests: the single-element root resolves correctly (agrees with domHandle), plus two exit-tests (#expect(processExitsWith: .failure)) proving a multi-root fragment and an empty root each trap — un-.disabled (unlike the DEBUG-only diagnostic tests) because the guard fires in every build.
  • Full Swift monorepo suite 1370/275 (+3).
  • HelloWorld wasm build + link clean.
  • Counter Playwright suite 11/11 locally — the guarded first-mount + resync still mount HelloWorld's single-div root normally.

🤖 Generated with Claude Code

@zzal zzal added the run-e2e Run the (expensive) Playwright e2e job on this PR label Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

📦 Bundle size

Artifact Baseline This PR Δ
App.wasm 5.06 MB 5.15 MB +1.91%
App.wasm (gzip) 1.80 MB 1.84 MB +2.20%
JS runtime 54.5 KB 54.5 KB +0.00%
JS runtime (gzip) 11.3 KB 11.3 KB +0.00%
Total (gzip) 1.81 MB 1.85 MB +2.19%

✅ Within budget (≤5% growth allowed).

Baseline: Swift 6.3, WASM SDK 6.3-RELEASE, measured 2026-06-18.

@zzal zzal added run-e2e Run the (expensive) Playwright e2e job on this PR and removed run-e2e Run the (expensive) Playwright e2e job on this PR labels Jul 6, 2026
…bodies

Closes Part I Wave 3 #5 of the arch+DX audit. Fragment-rooted component bodies
are only DEBUG-discouraged (Diff.swift bare-fragment diagnostic), but the root
mount path used single-valued `MountNode.domHandle`, whose own doc warns it does
NOT descend through fragments. For a fragment root it returned the structural
handle "the DOM never sees," feeding a bogus handle to mount/replaceMount in
RELEASE (where the DEBUG diagnostic is compiled out) — a silently broken mount.

Adds `MountNode.singleRootDOMHandle`: descends through fragments via
collectDOMRoots and traps in ALL builds (precondition; -Osize release keeps it)
with actionable guidance if the node resolves to != 1 DOM root. Wired into the
three root-path sites in Renderer (first-mount mount, identity-swap replaceMount,
and resyncFullRemount's replaceMount), replacing the bare domHandle. The DEBUG
bare-fragment diagnostic stays — it's the earlier, more precise dev-time warning
and covers nested fragments (which ARE handled, via collectDOMRoots on append);
this guard is the release safety net it was missing.

Nested fragment bodies are unaffected (they attach all their roots to a parent).
Chosen enforce-single-root over making the root path multi-root-aware — the
latter needs driver 'attach a list' changes and reverses the framework's current
'wrap in a single element' stance.

3 host tests: the single-element root resolves correctly (agrees with domHandle),
plus two exit-tests proving a multi-root fragment and an empty root each trap
(un-disabled, since the guard fires in all builds). Verified: host 1370/275,
HelloWorld wasm clean, counter e2e 11/11 (guarded root mount unaffected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zzal zzal force-pushed the feat/domhandle-fragment-guard branch from 7a53d8c to d9966a9 Compare July 6, 2026 18:45
@zzal zzal merged commit 8136396 into main Jul 6, 2026
5 of 6 checks passed
@zzal zzal deleted the feat/domhandle-fragment-guard branch July 6, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-e2e Run the (expensive) Playwright e2e job on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant