mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 06:35:10 +00:00
Documents the decision to use pure function registration (defineNode, defineWidget) with a centralized loader (startExtensionSystem) rather than side-effect registration at import time. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
139 lines
4.8 KiB
Markdown
139 lines
4.8 KiB
Markdown
# 12. Pure Function Loader Pattern for Extension Registration
|
|
|
|
Date: 2026-05-12
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
The v2 extension API needs a mechanism for extensions to register themselves with the runtime. Two broad approaches exist:
|
|
|
|
### Side-Effect Registration (Vue 2 Plugin Pattern)
|
|
|
|
```ts
|
|
// Extension self-registers at import time
|
|
import { app } from '@comfyorg/core'
|
|
|
|
app.use({
|
|
install(app) {
|
|
app.component('MyWidget', MyWidget)
|
|
app.directive('my-directive', myDirective)
|
|
}
|
|
})
|
|
```
|
|
|
|
Problems:
|
|
|
|
- **Import order matters**: If extension A depends on extension B being registered first, import order must be carefully managed
|
|
- **Hard to test**: Side effects at import time make mocking difficult; tests must manipulate module cache
|
|
- **Hard to tree-shake**: Bundlers can't eliminate unused extensions — the import executes
|
|
- **Timing coupling**: Registration and activation are conflated; can't collect extensions first, then activate later
|
|
|
|
### Pure Function + Loader Pattern
|
|
|
|
```ts
|
|
// Extension declares intent — no side effects
|
|
export default defineNode({
|
|
name: 'my-extension',
|
|
nodeTypes: ['MyNode'],
|
|
nodeCreated(handle) {
|
|
// ...
|
|
}
|
|
})
|
|
|
|
// App bootstrap activates all registered extensions
|
|
startExtensionSystem()
|
|
```
|
|
|
|
## Decision
|
|
|
|
**Adopt the pure function + loader pattern** for v2 extension registration.
|
|
|
|
### Implementation
|
|
|
|
```ts
|
|
// Extension Registry (data collection only)
|
|
const nodeExtensions: NodeExtensionOptions[] = []
|
|
|
|
export function defineNode(options: NodeExtensionOptions): void {
|
|
nodeExtensions.push(options)
|
|
}
|
|
|
|
// Loader (activation)
|
|
export function startExtensionSystem(): void {
|
|
const world = getWorld()
|
|
watch(
|
|
() => world.entitiesWith(NodeTypeKey),
|
|
(nodeEntityIds) => {
|
|
for (const id of nodeEntityIds) {
|
|
mountExtensionsForNode(id)
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
}
|
|
```
|
|
|
|
### Key Properties
|
|
|
|
1. **Pure registration**: `defineNode()` has no side effects beyond pushing to an array. It doesn't touch the World, DOM, or any reactive state.
|
|
|
|
2. **Centralized activation**: `startExtensionSystem()` is called exactly once during app bootstrap. This single entry point controls when the extension system "goes live".
|
|
|
|
3. **Reactive mounting**: The loader watches the World for entity changes. Extensions are mounted/unmounted in response to ECS state, not imperative calls.
|
|
|
|
4. **Order independence**: Extensions can be defined in any order. The loader sorts by name (lexicographic, see D10b) for deterministic execution.
|
|
|
|
### Registration Flow
|
|
|
|
```
|
|
Extension files App bootstrap World
|
|
| | |
|
|
| defineNode({...}) | |
|
|
|--------------------->| |
|
|
| (push to array) | |
|
|
| | |
|
|
| | startExtensionSystem()
|
|
| |------------------>|
|
|
| | (watch for NodeType entities)
|
|
| | |
|
|
| | NodeType added |
|
|
| |<------------------|
|
|
| | |
|
|
| | mountExtensionsForNode(id)
|
|
| | (runs setup) |
|
|
```
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
- **Testability**: Extensions are plain objects; tests can construct them without side effects. `_clearExtensionsForTesting()` resets state between tests.
|
|
- **Tree-shakeable**: Bundlers can eliminate unused extension files if their exports are never referenced.
|
|
- **Order independent**: No import order bugs — the loader handles activation order.
|
|
- **Lazy activation**: Registration is instant; activation only happens when `startExtensionSystem()` is called.
|
|
- **SSR friendly**: Pure functions don't execute browser-only code at import time.
|
|
|
|
### Negative
|
|
|
|
- **Manual bootstrap**: App must call `startExtensionSystem()` — forgetting it silently disables extensions.
|
|
- **Two-step mental model**: Developers must understand "register" vs "activate" phases.
|
|
|
|
### Mitigations
|
|
|
|
- App bootstrap is a well-defined location; the call is hard to miss.
|
|
- Clear documentation and starter templates include the bootstrap call.
|
|
- Dev-mode warnings if extensions are defined but the system never starts.
|
|
|
|
## Notes
|
|
|
|
This pattern aligns with modern framework conventions:
|
|
|
|
- **Vite plugins**: `vite.config.ts` collects plugins as an array; Vite activates them at build time.
|
|
- **Vue 3 Composition API**: `setup()` returns reactive state; the framework activates it.
|
|
- **React hooks**: Pure functions declare effects; React schedules them.
|
|
|
|
The key insight is separating **declaration** (what do I want?) from **execution** (make it happen). This separation enables testing, lazy loading, and predictable behavior.
|