ai-chatbot/apps/editor/lib/bootstrap.ts
134 c3faeba1c8 Squashed 'services/pascal-editor/' content from commit 312064e6
git-subtree-dir: services/pascal-editor
git-subtree-split: 312064e6e8122f958d2961e28ea85ad8b9db40c5
2026-07-12 00:43:59 +08:00

89 lines
3.3 KiB
TypeScript

import {
type AnyNodeDefinition,
discoverPlugins,
loadPlugin,
nodeRegistry,
registerNode,
setPluginDiscovery,
} from '@pascal-app/core'
import { builtinPlugin } from '@pascal-app/nodes'
import { treesPlugin } from '@pascal-app/plugin-trees'
// Idempotency guards: HMR can reload this module, but `registerNode`
// throws on duplicate kinds. Flags live in the module closure so they
// reset on a hard reload but survive within a session.
let builtinsLoaded = false
let externalsKickedOff = false
function isDev(): boolean {
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
?.env
return env?.NODE_ENV !== 'production'
}
/**
* Synchronously register every built-in node kind. Runs as a side
* effect at module import time so the registry is populated *before*
* any downstream React tree renders — the previous async kick-off
* (`void loadBuiltinNodes()`) only registered in a microtask, letting
* the first SSR / hydration pass see an empty registry. The mismatch
* surfaced as a hydration error at the `<html>` element and every
* `NodeRenderer` resolving to `null` until later renders.
*
* `discoverPlugins()` (which may hit the network for external packs)
* stays async and runs separately via `loadExternalPlugins()`.
*/
function loadBuiltinsSync(): void {
if (builtinsLoaded) return
builtinsLoaded = true
for (const def of builtinPlugin.nodes ?? []) {
// Skip kinds the registry already has. The module-closure flag
// above resets on HMR, but the registry singleton (in @pascal-app/core)
// persists — without this guard we'd throw on the first duplicate.
if (nodeRegistry.has((def as AnyNodeDefinition).kind)) continue
registerNode(def as AnyNodeDefinition)
}
if (isDev()) {
const kinds = Array.from(nodeRegistry.entries(), ([k]) => k)
if (typeof console !== 'undefined') {
console.info(
`[pascal:registry] loaded ${builtinPlugin.id} v${builtinPlugin.apiVersion} (${kinds.length} kinds: ${kinds.join(', ') || '∅'})`,
)
}
// Expose the registry on globalThis for ad-hoc dev inspection. In
// prod the registry is reachable through @pascal-app/core's
// exports only.
if (typeof globalThis !== 'undefined') {
;(globalThis as { __pascalNodeRegistry?: typeof nodeRegistry }).__pascalNodeRegistry =
nodeRegistry
}
}
}
/**
* Phase 6 plugin discovery hook — runs once, asynchronously, after the
* synchronous builtins are already registered. Apps that ship external
* node packs override the discovery via `setPluginDiscovery(...)`
* before this module loads. See `wiki/architecture/plugin-authoring.md`.
*/
export async function loadExternalPlugins(): Promise<void> {
if (externalsKickedOff) return
externalsKickedOff = true
const externals = await discoverPlugins()
for (const plugin of externals) {
await loadPlugin(plugin)
}
if (isDev() && externals.length > 0 && typeof console !== 'undefined') {
console.info(`[pascal:registry] + ${externals.length} discovered plugin(s)`)
}
}
// Register the first-party example plugin (trees node + presets rail panel)
// through the same discovery hook a third-party pack would use. Must be set
// before `loadExternalPlugins()` reads it below.
setPluginDiscovery(async () => [treesPlugin])
loadBuiltinsSync()
void loadExternalPlugins()