fix(renderer): guard the root mount path against fragment/multi-root bodies#151
Merged
Conversation
📦 Bundle size
✅ Within budget (≤5% growth allowed). Baseline: Swift 6.3, WASM SDK 6.3-RELEASE, measured 2026-06-18. |
…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>
7a53d8c to
d9966a9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-valuedMountNode.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 tomount/replaceMountin RELEASE (where the DEBUG diagnostic is compiled out) — a silently broken mount.Fix
MountNode.singleRootDOMHandle: descends through fragments (viacollectDOMRoots) and traps in all builds (precondition—-Osizerelease keeps it) with actionable guidance if the node resolves to anything but exactly one DOM root. Wired into the three root-path sites inRenderer— the first-mountmount, the identity-swapreplaceMount, andresyncFullRemount'sreplaceMount— replacing the baredomHandle.collectDOMRootsand never hit the root path.Test plan
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.divroot normally.🤖 Generated with Claude Code