apps/editor: mount node-registry bootstrap at the root layout

The standalone editor app loaded `lib/bootstrap.ts` as a side-effect
import only from `components/scene-loader.tsx`. That worked for the
`/edit/[sceneId]` route but left every other page (homepage, settings,
viewer-only routes) hitting `<Viewer>` with an empty client-side
registry — node materials resolved to `null` and React surfaced a
`<html>`-level hydration mismatch on first paint.

Fix mirrors the community-app side that landed in pascalorg/private-
editor#27:

 - New `app/client-bootstrap.tsx` — thin client wrapper that imports
   `../lib/bootstrap` and renders children.
 - `app/layout.tsx` mounts `<ClientBootstrap>` around `{children}` so
   every page in the standalone editor gets the registry populated
   before its first `<Viewer>` / `<Editor>` mounts.
 - `lib/bootstrap.ts` switched to **synchronous** built-in registration
   via `registerNode(def)` per kind instead of `await loadPlugin(...)`.
   The previous async kick-off only resolved in a microtask, letting
   the first SSR / hydration pass see an empty registry. External
   plugin discovery (`discoverPlugins()`) stays async and runs via its
   own `loadExternalPlugins()` path, gated by `externalsKickedOff` so
   HMR doesn't re-fetch.
 - `components/scene-loader.tsx` drops the per-page side-effect import
   — the root provider handles it now.

`bun.lock` syncs `@pascal-app/editor` into `@pascal-app/nodes`'s
peerDependencies + devDependencies (already declared in
`packages/nodes/package.json`; only the lockfile lagged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-20 07:17:30 -04:00
co-authored by Claude Opus 4.7
parent 0bcec8e6ba
commit c4001a656f
5 changed files with 80 additions and 28 deletions
+16
View File
@@ -0,0 +1,16 @@
'use client'
// Loads `@pascal-app/nodes`' built-in plugin into the node registry on the
// client. Mounted from `layout.tsx` so every page in the standalone
// editor gets the registry populated before its first `<Viewer>` /
// `<Editor>` mounts — without this the registry is empty on the client
// (the server registers in its own module instance, which is unreachable
// from hydrated pages) and every `NodeRenderer` resolves to `null`. The
// `loaded` guard inside `../lib/bootstrap` keeps the side effect
// idempotent under HMR.
import '../lib/bootstrap'
import type { ReactNode } from 'react'
export function ClientBootstrap({ children }: { children: ReactNode }) {
return children
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { GeistPixelSquare } from 'geist/font/pixel'
import { Barlow } from 'next/font/google'
import localFont from 'next/font/local'
import Script from 'next/script'
import { ClientBootstrap } from './client-bootstrap'
import './globals.css'
const geistSans = localFont({
@@ -41,7 +42,7 @@ export default function RootLayout({
)}
</head>
<body className="font-sans">
{children}
<ClientBootstrap>{children}</ClientBootstrap>
{process.env.NODE_ENV === 'development' && <Agentation />}
</body>
</html>