UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

235 lines (197 loc) • 11.7 kB
# Migrating v1 → v2 frontend extensions Maps legacy patterns (`app.registerExtension()`, `nodeType.prototype` patching, `api.addEventListener`, `widget.callback`) to their `@comfyorg/extension-api` v2 equivalents. See [`../SKILL.md`](../SKILL.md) for full authoring patterns. ## Contents - [The big picture](#the-big-picture): what v2 split apart, and why - [Top-level registration](#top-level-registration): `registerExtension` → `defineExtension` - [Lifecycle hooks](#lifecycle-hooks): `setup`/`init` → `onLoad`/`onUnload` - [Node prototype patching → `NodeHandle` events](#node-prototype-patching--nodehandle-events) - [Widgets](#widgets): `addWidget`/`widget.callback` → typed widget handles - [Events: `api.addEventListener` → typed namespaces](#events-apiaddeventlistener--typed-namespaces) - [Shell UI](#shell-ui): sidebar tabs, commands, keybindings, settings, toasts - [Cleanup / teardown](#cleanup--teardown): disposers replace manual unpatching - [Migration checklist](#migration-checklist): the ordered pass to run over an extension ## The big picture v1 packed everything into a single `app.registerExtension({...})` mega-call with mixed concerns (node patching, commands, keybindings, settings, UI). v2 splits each concern into its own `defineX`, each independently importable, testable, and disposable. ```ts // ── v1 ────────────────────────────────────────────────────────────────── import { app } from '../../scripts/app.js' app.registerExtension({ name: 'MyExt', async init() { /* early */ }, async setup() { /* late, app ready */ }, async beforeRegisterNodeDef(nodeType, nodeData, app) { /* filter + patch */ }, nodeCreated(node) { /* per-instance */ }, loadedGraphNode(node) { /* per-instance from saved workflow */ }, commands: [/* ... */], keybindings: [/* ... */], settings: [/* ... */], aboutPageBadges: [/* ... */] }) ``` ```ts // ── v2 ────────────────────────────────────────────────────────────────── import { defineNode, defineExtension, defineCommand, defineHotkey, defineSetting, defineAboutBadge } from '@comfyorg/extension-api' // One default export per file is conventional; or call defineX at module scope. export default defineNode({ name: 'my-org.my-ext', nodeTypes: ['MyNode'], // replaces beforeRegisterNodeDef filtering nodeCreated(node) { /* ... */ }, loadedGraphNode(node) { /* ... */ } }) // Each shell-UI concern is its own call: defineCommand({ id: 'my-org.cmd', function: () => {} }) defineHotkey({ keys: 'mod+k', commandId: 'my-org.cmd' }) defineSetting({ id: 'my-org.opt' as never, name: 'My Option', type: 'boolean', defaultValue: false }) defineAboutBadge({ label: 'Docs', url: 'https://…', icon: 'pi-book' }) ``` ## Top-level registration | v1 | v2 | |----|----| | `app.registerExtension({ name, ... })` | Split into `defineNode` / `defineExtension` + per-surface `defineX` | | `import { app } from '../../scripts/app.js'` | `import { defineExtension, ... } from '@comfyorg/extension-api'` | | Single object, mixed concerns | One `defineX` per concern, each returns a `DisposableHandle` | ## Lifecycle hooks | v1 | v2 | |----|----| | `init()` | `defineExtension({ setup() {...} })` — the `setup()` body runs at the early `init` point | | `setup()` (fires after core extensions ready) | `setup() { onMounted(() => {...}) }` — late work goes in the imported `onMounted` hook | | `nodeCreated(node)` | `defineNode({ nodeCreated(node) {...} })` | | `loadedGraphNode(node)` | `defineNode({ loadedGraphNode(node) {...} })` | | `beforeRegisterNodeDef(nodeType, nodeData)` filtering | `defineNode({ nodeTypes: ['Foo', 'Bar'] })` | > Both `init` and `setup` survive as `ExtensionOptions` fields for back-compat but are > deprecated. Move `init` bodies into `setup()`; move late `setup` work into > `onMounted(() => …)` inside `setup()`. A codemod ships in the package. ```ts // v1 app.registerExtension({ name: 'X', setup() { api.addEventListener('execution_start', onStart) } }) // v2 defineExtension({ name: 'X', setup() { onMounted(() => execution.on('start', onStart)) } }) ``` ## Node prototype patching → `NodeHandle` events The most common v1 anti-pattern was patching `nodeType.prototype.*`. v2 replaces all of it with typed `node.on(...)` subscriptions inside `defineNode`. | v1 prototype patch | v2 | |--------------------|----| | `nodeType.prototype.onExecuted = function(msg) {...}` | `node.on('executed', (e) => {... e.output ...})` | | `nodeType.prototype.onRemoved = function() {...}` | `node.on('removed', fn)` or `onNodeRemoved(fn)` | | `nodeType.prototype.onConfigure = function() {...}` | `node.on('configured', fn)` or `defineNode({ loadedGraphNode })` | | `nodeType.prototype.onNodeCreated = function() {...}` | `defineNode({ nodeCreated(node) {...} })` | | `nodeType.prototype.onDrawForeground = ...` | Deferred in Phase A — not yet on the v2 surface | | `nodeType.prototype.serialize = ...` | Widget-level `widget.on('beforeSerialize', fn)` | | `const orig = proto.onX; proto.onX = function(){ orig?.apply(this, arguments); ... }` | Just call `node.on('x', fn)` — multiple extensions compose without chaining originals | ```ts // v1 const onExecuted = nodeType.prototype.onExecuted nodeType.prototype.onExecuted = function (message) { onExecuted?.apply(this, arguments) this.widgets.find(w => w.name === 'preview').value = message.text.join('\n') } // v2 — but note: nodes can't enumerate widgets, so own the preview widget via // defineWidget and react there, or use the widget's own valueChange/mount. defineNode({ name: 'my-org.preview', nodeTypes: ['MyTextNode'], nodeCreated(node) { node.on('executed', (e) => { // do something with e.output['text'] }) } }) ``` ## Widgets | v1 | v2 | |----|----| | `widget.value = x` | `widget.setValue(x)` (dispatches undo-able command) | | `widget.callback = (v) => {...}` | `widget.on('valueChange', (e) => {... e.newValue ...})` | | `widget.options.min = 0` | `widget.setOption('min', 0)` | | `node.addWidget(type, name, default, opts)` | Declare in Python `INPUT_TYPES`; not creatable at runtime | | `node.addDOMWidget(name, type, el, opts)` | `defineWidget({ type, mount(host, ctx) {...} })` | | `widget.serializeValue = () => v` | `widget.on('beforeSerialize', (e) => e.setSerializedValue(v))` | | `widget.options.serialize = false` | No equivalent — don't make it a widget (no serialize-disable in v2) | | Read `widget.inputEl` / `widget.element` | Capture the host via closure inside `mount` — no element accessor | | `node.computeSize` re-assignment for DOM resize | `widget.setHeight(px)` | | `app.queuePrompt` monkey-patch for validation | `widget.on('beforeQueue', (e) => e.reject(msg))` | ```ts // v1 DOM widget const widget = node.addDOMWidget('myinput', 'custom', el, { ... }) // v2 — register the type, render in mount, capture el via closure defineWidget({ name: 'my-org.myinput', type: 'MY_CUSTOM', // referenced from Python INPUT_TYPES mount(host, ctx) { const el = document.createElement('div') el.textContent = String(ctx.widget.getValue() ?? '') host.appendChild(el) ctx.widget.on('valueChange', (e) => { el.textContent = String(e.newValue ?? '') }) return () => el.remove() } }) ``` ## Events: `api.addEventListener` → typed namespaces | v1 | v2 | |----|----| | `api.addEventListener('execution_start', fn)` | `execution.on('start', fn)` | | `api.addEventListener('progress', fn)` | `execution.on('progress', fn)` | | `api.addEventListener('executed', fn)` | per-node `node.on('executed', fn)` — the `execution` namespace carries run-level events (`execution_start`/`execution_success`/`progress`), **not** a per-node `executed` | | `api.addEventListener('status', fn)` | `server.on('status', fn)` | | `api.addEventListener('reconnected', fn)` | `server.on('reconnected', fn)` | | Custom node event: `api.addEventListener('my.event', fn)` | `server.on('my.event', fn)` | | `api.removeEventListener('x', fn)` | Call the `Unsubscribe` returned by `on(...)` | Narrow payloads (which default to `unknown`) via module augmentation of `ExecutionEventPayloads` / `GraphEventPayloads` / `ServerEventPayloads` / `WorkbenchEventPayloads`. See the SKILL for the augmentation snippet. ## Shell UI | v1 (array on the extension object) | v2 (`defineX`) | |------------------------------------|----------------| | `commands: [{ id, label, function }]` | `defineCommand({ id, label, function })` | | `keybindings: [{ combo, commandId }]` | `defineHotkey({ keys: 'mod+k', commandId })` | | `settings: [{ id, name, type, defaultValue }]` | `defineSetting({ id, name, type, defaultValue })` | | `aboutPageBadges: [{ label, url, icon }]` | `defineAboutBadge({ label, url, icon })` | | Action-bar buttons (undocumented) | `defineToolbarButton({ id, icon, onClick })` (net-new) | | Sidebar tabs via `app.extensionManager.registerSidebarTab(...)` | `defineSidebarTab({ id, title, type, component })` | | Bottom-panel tabs via the extension manager | `defineBottomPanelTab({ id, title, type, component })` | | `app.extensionManager.toast.add(...)` | `toast.show(...)` (imperative; no handle) | ```ts // v1 app.registerExtension({ name: 'X', commands: [{ id: 'x.run', label: 'Run', function: () => doRun() }], keybindings: [{ combo: { key: 'k', ctrl: true }, commandId: 'x.run' }], aboutPageBadges: [{ label: 'GitHub', url: 'https://…', icon: 'pi-github' }] }) // v2 defineCommand({ id: 'x.run', label: 'Run', function: () => doRun() }) defineHotkey({ keys: 'mod+k', commandId: 'x.run' }) defineAboutBadge({ label: 'GitHub', url: 'https://…', icon: 'pi-github' }) ``` ## Cleanup / teardown | v1 | v2 | |----|----| | Manual `removeEventListener`, restore patched prototypes | `Unsubscribe` from every `on(...)`; `dispose()` from every `defineX` | | No standard unregister for commands/tabs/settings | `handle.dispose()` (idempotent, synchronous) | | Cleanup on node removal: patch `onRemoved` | `onNodeRemoved(fn)` inside `nodeCreated` (auto-scoped) | ## Migration checklist 1. Replace `import { app } from '../../scripts/app.js'` with named imports from `@comfyorg/extension-api`. 2. Split the single `registerExtension` object: node concerns → `defineNode`; app lifecycle → `defineExtension`; each UI concern → its own `defineX`. 3. Convert `beforeRegisterNodeDef` filtering to `defineNode({ nodeTypes: [...] })`. 4. Replace every `nodeType.prototype.onX = ...` with `node.on('x', fn)` inside `nodeCreated` / `loadedGraphNode`. 5. Replace `widget.value =` with `setValue`, `widget.callback =` with `on('valueChange')`, `widget.options.k =` with `setOption`. 6. Move DOM widgets from `addDOMWidget` to `defineWidget({ mount })`; declare the widget in Python `INPUT_TYPES`. 7. Move all serialization logic to widget-level `on('beforeSerialize')`; drop `serialize: false` usages. 8. Replace `api.addEventListener('...')` with the matching `execution` / `graph` / `server` / `workbench` namespace; augment payload interfaces for types. 9. Move validation from `app.queuePrompt` patches to `widget.on('beforeQueue', e => e.reject(...))`. 10. Ensure lifecycle hooks (`onMounted`, `onNodeMounted`, `onNodeRemoved`) are called synchronously, never after an `await`. 11. Track every `Unsubscribe` / `DisposableHandle` you create outside a `setup()` context and dispose them on teardown.