git-subtree-dir: services/pascal-editor git-subtree-split: 312064e6e8122f958d2961e28ea85ad8b9db40c5
89 lines
3.3 KiB
TypeScript
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()
|