Skip to content

fix(compiler,ui): wrap multi-child component children in DocumentFragment [#2821]#2827

Merged
viniciusdacal merged 2 commits into
mainfrom
fix/multi-child-component-fragment
Apr 19, 2026
Merged

fix(compiler,ui): wrap multi-child component children in DocumentFragment [#2821]#2827
viniciusdacal merged 2 commits into
mainfrom
fix/multi-child-component-fragment

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

Closes #2821.

RouterContext.Provider (and any other component that treats children as a renderable slot — Suspense, ErrorBoundary, ThemeProvider, …) crashed at mount when given multiple JSX children:

<RouterContext.Provider value={router}>
  <aside>sidebar</aside>
  <main></main>
</RouterContext.Provider>
Uncaught TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.
    at __append (chunk-…:288:10)
    at App (src/app.tsx:…)

Root cause: the compiler emitted Component({ children: () => [a, b] }) for multi-child components. Consumers called children() expecting a single Node, got an array, and downstream appendChild rejected it. The dev-only guard in Context.Provider that was supposed to produce a friendly error was gated on process.env.NODE_ENV, which is not polyfilled in the browser — so users saw the generic DOM error instead.

Public API Changes

Fixed

  • Compiler — multi-child component children now compile to a DocumentFragment-returning thunk (same shape fragments use), so <Provider><a/><b/></Provider> behaves like <Provider><><a/><b/></></Provider>. No more "children must have a single root element" requirement.
  • Context.Provider — array results from hand-written callers are flattened via resolveChildren into a DocumentFragment, instead of throwing. Replaces the unreliable dev-only throw.

Breaking

None. The previous behavior was a crash; the new behavior is correct rendering.

Test plan

  • New compiler tests: component_children_multiple now asserts createDocumentFragment output; new component_member_expression_multi_child_uses_fragment reproduces the exact RouterContext.Provider case from router: RouterContext.Provider with multiple JSX children throws generic appendChild error #2821.
  • Provider unit tests: array-children → fragment; primitives coerce to text; null/undefined/boolean filtered; direct (non-thunk) arrays; nested arrays and thunks flatten.
  • cargo test --all — 1127 compiler tests + all sub-crates pass.
  • cargo clippy --all-targets -- -D warnings — clean.
  • cargo fmt --all -- --check — clean.
  • packages/ui/ typecheck clean.
  • context.test.ts — 40/40 tests pass.
  • Full GitHub CI (monitored after push).

Files

  • native/vertz-compiler-core/src/jsx_transformer.rs — build_children_thunk now emits a fragment for ≥2 children.
  • packages/ui/src/component/context.ts — Provider flattens array results via resolveChildren.
  • packages/ui/src/component/tests/context.test.ts — updated + new tests.
  • .changeset/fix-multi-child-component-fragment.md — patch changeset for @vertz/ui and @vertz/native-compiler.

🤖 Generated with Claude Code

viniciusdacal and others added 2 commits April 19, 2026 03:04
…agment [#2821]

Previously, a component with multiple JSX children compiled to
`Component({ children: () => [a, b] })`. Consumers such as
`Context.Provider`, `Suspense`, and `ErrorBoundary` call `children()`
and expect a single Node — they got an array, which downstream
`appendChild` calls rejected with

    TypeError: Failed to execute 'appendChild' on 'Node':
    parameter 1 is not of type 'Node'.

The compiler now emits a `DocumentFragment`-returning thunk for
multi-child components, mirroring how `<>...</>` fragments are
already handled. `Context.Provider` also flattens any hand-written
array result into a `DocumentFragment` (via `resolveChildren`) as a
defensive fallback, replacing the previous dev-only throw — that
check was unreliable in the browser because `process.env.NODE_ENV`
is not polyfilled there.

Closes #2821.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…2821]

Two CI follow-ups for the fragment-children change:

- The NAPI parity test in `native/vertz-compiler/__tests__/jsx-transform.test.ts`
  still asserted the old `children: () => [...]` array shape; update it to
  expect the `createDocumentFragment` thunk so it matches the Rust unit test.

- `packages/ui/src/component/context.ts` now references `document` (via
  `createDocumentFragment` / `createTextNode`) inside the multi-child
  fallback. Add it to the `audit-window-document-refs.sh` allowlist with
  the rest of the client-only @vertz/ui rendering files — the references
  only run when `Provider`'s children thunk produces an array, which is
  always behind the JSX runtime / SSR DOM shim.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@viniciusdacal viniciusdacal merged commit 7e80041 into main Apr 19, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

router: RouterContext.Provider with multiple JSX children throws generic appendChild error

1 participant