refactor nodes
This commit is contained in:
101
docs/NODE_TYPE_EXTENSIBILITY_PROPOSAL.md
Normal file
101
docs/NODE_TYPE_EXTENSIBILITY_PROPOSAL.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Node Type Extensibility — Proposal
|
||||
|
||||
## Goal
|
||||
|
||||
Allow developers to add new node types and their rendering/logic without editing core app code. Each new type should provide: its React component, default data/style, connection rules, help text, and context-menu entry.
|
||||
|
||||
## Current State
|
||||
|
||||
- **Registration:** In `App.tsx`, `nodeTypes` is a hardcoded map (config → ConfigNode, render → RenderingNode, etc.).
|
||||
- **Defaults & IDs:** `flowUtils.ts` has `PREFIX_BY_TYPE`, `DEFAULT_NODE_STYLE`, `DEFAULT_DATA`, `getDefaultDataForType`, `getResetDataForType` — all branch on type.
|
||||
- **Validation:** `isValidConnection` in `App.tsx` encodes rules (e.g. nothing → variable, only config → render) with explicit type checks.
|
||||
- **UI:** Context menu “Create node” items, paste allowlist (`VALID_NODE_TYPES`), node help (`nodeHelp.tsx`), footer indicators (`NODE_HAS_INPUT` / `NODE_HAS_OUTPUT`), and `NodeMenubar` / `AnimatedEdge` use type strings or fixed type unions.
|
||||
|
||||
Adding a fifth type today requires editing App, flowUtils, nodeHelp, NodeFooterEdgeIndicators, and possibly NodeMenubar/AnimatedEdge.
|
||||
|
||||
---
|
||||
|
||||
## Alternative 1: Central Node Type Registry (single module)
|
||||
|
||||
**Idea:** One module (e.g. `src/lib/nodeRegistry.ts` or `src/config/nodeTypes.ts`) holds a **single registry object** keyed by type id. Each entry describes the type:
|
||||
|
||||
- `component` — React component for the node
|
||||
- `defaultStyle`, `defaultData`, `idPrefix`
|
||||
- `hasInput` / `hasOutput` (for footer and validation)
|
||||
- `allowedSourceTypes` / `allowedTargetTypes` (for connection validation)
|
||||
- `help` (title + content for NodeHelpPopover)
|
||||
- `menuLabel`, `menuIcon` (for context menu “Create node”)
|
||||
|
||||
App and other consumers **import the registry** and iterate: build `nodeTypes` from registry, build context menu from registry, call `getDefaultDataForType(registry, type)`, and run connection validation using registry metadata.
|
||||
|
||||
**Pros:** Single source of truth; no new concepts (no plugins).
|
||||
**Cons:** Extensions still require editing that one file (or a dedicated “contrib” section inside it). Good for a small, curated set of types.
|
||||
|
||||
---
|
||||
|
||||
## Alternative 2: Plugin / Contribution API (mutable registry + `registerNodeType()`)
|
||||
|
||||
**Idea:** The registry is **mutable**. A bootstrap phase (e.g. in `main.tsx` or a dedicated `registerBuiltinNodes.ts`) calls `registerNodeType(descriptor)` for each built-in type. Third-party code (or feature modules) can call the same API to add types without touching the core registry module.
|
||||
|
||||
- Export `registerNodeType(descriptor: NodeTypeDescriptor)` and `getRegisteredNodeTypes()` (and optionally `getNodeType(id)`).
|
||||
- Descriptor shape: same as in Alternative 1 (component, defaults, connection rules, help, menu).
|
||||
- Built-in types are registered at app init; the registry is then read-only for the rest of the session (or remains open for dynamic plugins).
|
||||
|
||||
**Pros:** True extensibility: new types = call `registerNodeType` (e.g. from a separate package or an async-loaded chunk).
|
||||
**Cons:** Need a clear descriptor type and lifecycle (when registration runs, ordering). Slightly more indirection when reading “what types exist.”
|
||||
|
||||
---
|
||||
|
||||
## Alternative 3: Convention-based discovery (folder or config)
|
||||
|
||||
**Idea:** Node types are **discovered** from the filesystem or a config file.
|
||||
|
||||
- **Variant A:** Each type lives under a folder, e.g. `src/nodes/<typeId>/index.ts` exporting a descriptor. A loader (at build or runtime) imports all and registers them.
|
||||
- **Variant B:** A config file (e.g. `nodeTypes.config.ts`) lists type ids and module paths; the app dynamically imports and registers them.
|
||||
|
||||
**Pros:** “Add a folder” or “add a line” to add a type; no direct call to a central registry from feature code.
|
||||
**Cons:** Build/runtime setup (e.g. `import.meta.glob` or dynamic `import()`), ordering and error handling. May be overkill for an in-repo extension story.
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Use Alternative 2 (Plugin / Contribution API)** with a single registry module:
|
||||
|
||||
1. **Define** a `NodeTypeDescriptor` type and a small API: `registerNodeType(descriptor)`, `getRegisteredNodeTypes()`, `getNodeType(id)`.
|
||||
2. **Move** all type-specific data (component, defaultStyle, defaultData, idPrefix, hasInput, hasOutput, connection rules, help, menu label/icon) into the descriptor. Connection rules can be expressed as `allowedSourceTypes` / `allowedTargetTypes` so validation is data-driven.
|
||||
3. **Register** the four built-in types (config, render, variable, function) at startup from a single “builtin” registration file (or from within the registry module). No change to the public shape of existing components; they are just passed to `registerNodeType`.
|
||||
4. **Refactor** App, flowUtils, nodeHelp, NodeFooterEdgeIndicators, and related UI to use the registry: `nodeTypes` and context menu from `getRegisteredNodeTypes()`, defaults and validation from descriptor, help from descriptor (or still from a function that reads descriptor.help).
|
||||
5. **Result:** Adding a fifth type = implement a component + call `registerNodeType({ id: 'mytype', component: MyNode, ... })` (e.g. in a feature module or a separate entry that runs before the app). No edits to App’s branching logic or to flowUtils/nodeHelp’s type unions.
|
||||
|
||||
Optional follow-ups: move help content into the descriptor (so `nodeHelp.tsx` only re-exports from registry), and add a small “node type list” config so the context menu order is explicit.
|
||||
|
||||
---
|
||||
|
||||
## Implementation outline (Alternative 2)
|
||||
|
||||
- **New:** `src/lib/nodeRegistry.ts` — `NodeTypeDescriptor` type, `registerNodeType()`, `getRegisteredNodeTypes()`, `getNodeType(id)`. Help content can live in the descriptor or be merged from current `nodeHelp.tsx` for built-ins.
|
||||
- **New (optional):** `src/lib/registerBuiltinNodes.ts` — imports ConfigNode, RenderingNode, VariableNode, FunctionNode and their help/defaults, calls `registerNodeType` for each; invoked from `main.tsx` before React render.
|
||||
- **Refactor:** `flowUtils.ts` — `getDefaultDataForType`, `getResetDataForType`, `DEFAULT_NODE_STYLE`, `PREFIX_BY_TYPE` (and `getNextNodeId`) take type and optionally the registry, or read from registry when called (registry imported inside flowUtils). Prefer: registry holds defaults/prefix; flowUtils exports thin wrappers that use registry.
|
||||
- **Refactor:** `App.tsx` — `nodeTypes` from `getRegisteredNodeTypes().map(...)`, context menu from same, `createNode`/paste use registry for valid types and defaults, `isValidConnection` uses descriptor’s `allowedSourceTypes`/`allowedTargetTypes`.
|
||||
- **Refactor:** `nodeHelp.tsx` — `getNodeHelp(type)` returns descriptor.help from registry (built-ins register help in descriptor).
|
||||
- **Refactor:** `NodeFooterEdgeIndicators.tsx` — `hasInput`/`hasOutput` from descriptor.
|
||||
- **Refactor:** `NodeMenubar` / `NodeHelpPopover` — keep `nodeType` as string; descriptor is looked up by id where needed.
|
||||
- **Refactor:** `AnimatedEdge` — edge label by target type can stay as a small map or move to descriptor (e.g. `connectionLabel?: string` when this type is target).
|
||||
|
||||
After this, a new node type is added by: (1) implementing a React component, (2) calling `registerNodeType({ id: 'mytype', component: MyNode, ... })` at app init.
|
||||
|
||||
---
|
||||
|
||||
## Implemented (Alternative 2)
|
||||
|
||||
- **`src/lib/nodeRegistry.ts`** — `NodeTypeDescriptor`, `registerNodeType()`, `getRegisteredNodeTypes()`, `getNodeType()`, `getDefaultDataForType()`, `getResetDataForType()`, `getIdPrefix()`, `getDefaultStyle()`, `isConnectionAllowed()`, `getConnectionLabelForTarget()`, `getNodeHelp()`.
|
||||
- **`src/lib/registerBuiltinNodes.tsx`** — Registers config, render, variable, function; invoked from `main.tsx` before render.
|
||||
- **`src/lib/flowUtils.ts`** — `getNextNodeId()` uses registry; default/reset data delegate to registry.
|
||||
- **App, NodeHelpPopover, NodeFooterEdgeIndicators, NodeMenubar, AnimatedEdge** — Use registry for types, validation, help, and UI.
|
||||
|
||||
### Adding a new node type
|
||||
|
||||
1. Implement a React component that accepts React Flow node props.
|
||||
2. Call `registerNodeType({ id, component, defaultStyle, defaultData, idPrefix, hasInput, hasOutput, allowedSourceTypes?, allowedTargetTypes?, help, menuLabel, menuIcon, ... })` from a module that runs at startup (e.g. imported and called from `main.tsx`).
|
||||
3. No edits to `App.tsx` or core flow/utils are required.
|
||||
Reference in New Issue
Block a user