comfyui-node-frontend について
comfyui-node-frontendは、JavaScriptでComfyUIのフロントエンドを拡張するカスタムノード開発者向けガイドです。ライフサイクルフック、ウィジェット、コマンド、設定、グラフ読み込み、サーバーとの通信を扱います。
主な機能
- 01拡張機能とライフサイクルフックの登録
- 02ウィジェットと画面タブの作成
- 03コマンド、キー割り当て、設定の追加
- 04実行イベントとグラフ読み込みの処理
- 05サーバーとフロントエンド間の通信
活用シーン
- 01カスタムノードへの操作コントロール追加
- 02ComfyUIへの拡張パネルやコマンドの追加
- 03ノード実行やグラフ読み込みへの対応
- 04Pythonサーバーのイベントをフロントエンドへ送信
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jtydhr88/ comfyui-custom-node-skills --skill comfyui-node-frontendインストール後、Claude Code で/comfyui-node-frontendを入力して読み込まれたか確認します。
同梱ファイルapi-reference.md
詳細
ComfyUI フロントエンド拡張機能
カスタムノードは、JavaScript を使って ComfyUI フロントエンドを拡張できます。拡張機能では、フック、ウィジェット、コマンド、設定、UI コンポーネントを登録できます。
クイックスタート
1. Python で WEB_DIRECTORY をエクスポートする
# __init__.py
WEB_DIRECTORY = "./js"
__all__ = ["WEB_DIRECTORY"]
2. JavaScript 拡張機能を作成する
// js/my_extension.js
import { app } from "../../scripts/app.js";
app.registerExtension({
name: "my_nodes.my_extension",
async setup() {
console.log("Extension loaded!");
},
});
ComfyUI の起動時に、WEB_DIRECTORY 内のすべての .js ファイルが自動的に読み込まれます。
拡張機能のフック(ライフサイクルの順序)
init — キャンバス作成後、ノードの読み込み前
app.registerExtension({
name: "my.ext",
async init(app) {
// Modify core behavior, add global listeners
},
});
addCustomNodeDefs — ノード定義を変更する
async addCustomNodeDefs(defs, app) {
// defs is a dict of all node definitions
// Can add or modify definitions before registration
defs["MyFrontendNode"] = {
input: { required: { text: ["STRING", {}] } },
output: ["STRING"],
output_name: ["text"],
name: "MyFrontendNode",
display_name: "My Frontend Node",
category: "custom",
};
},
getCustomWidgets — カスタムウィジェットの種類を登録する
getCustomWidgets(app) {
return {
MY_WIDGET(node, inputName, inputData, app) {
const widget = node.addWidget("text", inputName, "", () => {});
widget.serializeValue = () => widget.value;
return { widget }; // or { widget, minWidth, minHeight }; v1.54+ also accepts `return widget`
},
};
},
beforeRegisterNodeDef — ノードのプロトタイプを変更する
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MyNode") {
// Chain onto prototype methods
const origOnCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnCreated?.apply(this, arguments);
// Add custom widget, modify behavior, etc.
this.addWidget("button", "Run", null, () => {
console.log("Button clicked!");
});
};
}
},
nodeCreated — ノードインスタンスの作成後
nodeCreated(node, app) {
if (node.comfyClass === "MyNode") {
// Modify this specific node instance
node.color = "#335";
}
},
setup — アプリの読み込み完了後
async setup(app) {
// Add event listeners, register UI components
app.api.addEventListener("executed", (event) => {
console.log("Node executed:", event.detail);
});
},
loadedGraphNode — 保存済みのグラフを読み込むとき
loadedGraphNode(node, app) {
if (node.comfyClass === "MyNode") {
// Restore state from saved graph
}
},
registerCustomNodes — 追加のノードタイプを登録する
registerCustomNodes(app) {
// Register custom LiteGraph node types
},
beforeRegisterVueAppNodeDefs — Vue 登録前にノード定義を変更する
beforeRegisterVueAppNodeDefs(defs, app) {
// Modify definitions before they reach the Vue app
},
beforeConfigureGraph / afterConfigureGraph
async beforeConfigureGraph(graphData, missingNodeTypes, app) {
// Before graph data is applied
},
async afterConfigureGraph(missingNodeTypes, app) {
// After graph is fully configured
},
beforeLoadGraph / afterLoadGraph(v1.53.5+)/ onGraphLoadError(v1.54.2+)
ワークフロー全体の読み込みを囲みます(beforeConfigureGraph をスキップする API-JSON と A1111 のインポートを含みます)。順序:beforeLoadGraph → beforeConfigureGraph → afterConfigureGraph → afterLoadGraph。読み込みに失敗した場合は、after* フックの代わりに onGraphLoadError が実行されます。
beforeLoadGraph(app) { /* e.g. suspend your own listeners */ },
afterLoadGraph(app) { /* graph fully loaded */ },
onGraphLoadError(error, app) { /* undo whatever beforeLoadGraph started */ },
今後のリリース(未公開):グラフの読み込みはキューに入れられ、グラフ読み込みフック内から app.loadGraphData() / loadApiJson() / handleFile() を呼び出すと拒否されます(「cannot start a nested graph load」)。別のグラフを読み込む必要がある場合は、呼び出しを遅らせてください(setTimeout)。
getSelectionToolboxCommands — 選択ツールボックスにコマンドを追加する
getSelectionToolboxCommands(selectedItem) {
// Return array of command IDs to show when item is selected
return ["my.ext.doSomething"];
},
onNodeOutputsUpdated — 実行出力が変更されたとき
onNodeOutputsUpdated(nodeOutputs) {
// nodeOutputs: Record<NodeLocatorId, output data>, fired when node outputs update
},
認証フック
onAuthUserResolved(user, app) {
// Fires when user authentication resolves
},
onAuthTokenRefreshed() {
// Fires when auth token is refreshed
},
onAuthUserLogout() {
// Fires when user logs out
},
カスタムウィジェット
DOM ウィジェットの追加
beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "MyNode") {
const origOnCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
origOnCreated?.apply(this, arguments);
const container = document.createElement("div");
container.innerHTML = `<input type="color" value="#ff0000">`;
container.querySelector("input").addEventListener("change", (e) => {
this.widgets.find(w => w.name === "color").value = e.target.value;
});
this.addDOMWidget("colorPicker", "custom", container, {
serialize: true,
getValue() { return container.querySelector("input").value; },
setValue(v) { container.querySelector("input").value = v; },
});
};
}
},
ウィジェットフック
// Called before prompt is queued
// isPartialExecution is true when only selected nodes are queued
widget.beforeQueued = function ({ isPartialExecution } = {}) {
// Prepare widget value
};
// Called after prompt is queued
widget.afterQueued = function ({ isPartialExecution } = {}) {
// Reset or update widget. Core control_after_generate now also applies
// on partial runs (v1.53.5+); the flag is still passed for your own logic.
};
// Custom serialization (may be async; it is awaited)
widget.serializeValue = function (node, index) {
return JSON.stringify(this.value);
};
ウィジェットのルール(ストア連携ウィジェット、v1.53+)
ウィジェットの値は、graphId:nodeId:widgetName をキーとするストアに保存されます。
- ノードごとに名前を一意にする必要があります。 名前が重複すると
name#1、name#2… に変更されます。変更できない場合は拒否されます(v1.58+)。それ以前のバージョンでは、最初のウィジェットの値が警告なしに上書きされていました。 - 追加後にウィジェットの名前を変更しないでください。 保存された値は以前の名前のままです。ウィジェットを削除して、新しく追加してください。
node.widgetsの配列を直接編集するのではなく、node.removeWidget(widget)で削除してください。- 最初に割り当てるまでは、
node.widgetsがundefinedの場合があります。node.widgets ??= []、node.widgets?.length ?? 0を使用してください。 - ウィジェットは
addWidget/addCustomWidget/addDOMWidget(またはBaseWidgetのサブクラス)を使って作成してください。node.widgetsに追加した単純なオブジェクトは登録されません。
ウィジェットを非表示にする
widget.hidden = true; // hide everywhere at runtime (still serialized; works in Nodes 2.0)
widget.advanced = true; // show only when "show advanced" is on
// Per-surface policy at creation (v1.55.6+); all three keys are required
node.addWidget("text", "debug", "", null, {
surfaces: { canvas: "shown", vueNode: "never", panel: "never" }, // "shown" | "advanced" | "never"
});
options.canvasOnly(vueNode / panel: "never")は非推奨ですが、引き続き有効です。options.hideInPanel は右側のサイドパネルにのみ表示されないようにします。
宣言型拡張プロパティ
コマンド
app.registerExtension({
name: "my.ext",
commands: [
{
id: "my.ext.doSomething",
label: "Do Something",
icon: "pi pi-bolt",
function: () => { console.log("Executed!"); },
// v1.55.14+: refuse to run while the canvas is select-only
mutatesGraph: true, // or () => boolean
},
],
});
キーバインド
keybindings: [
{
commandId: "my.ext.doSomething",
combo: { key: "d", ctrl: true, shift: true },
},
],
設定
settings: [
{
id: "my.ext.mySetting",
name: "My Setting",
type: "boolean",
defaultValue: true,
onChange: (value) => { console.log("Setting changed:", value); },
},
{
id: "my.ext.mode",
name: "Processing Mode",
type: "combo",
options: ["fast", "quality", "balanced"], // or (currentValue) => [...] for dynamic options
defaultValue: "balanced",
},
],
onChange(newValue, oldValue) は非同期でもかまいません。
設定の種類:boolean、number、slider、knob、combo、radio、text、image、color、url、hidden、backgroundImage
サイドバータブ
async setup(app) {
app.extensionManager.registerSidebarTab({
id: "my-sidebar",
title: "My Panel",
icon: "pi pi-cog",
type: "custom",
render: (container) => {
container.innerHTML = "<h3>My Custom Panel</h3>";
},
destroy: () => {
// Cleanup
},
// v1.58+: return true to handle the click yourself (skips default open/close)
onToggle: async () => false,
});
},
type: "vue" のタブ(component: MyVueComponent)では、Vue UI ライブラリを独自にバンドルする必要があります。ホストから PrimeVue は提供されなくなります(近日公開。破壊的変更を参照)。
下部パネルのタブ
bottomPanelTabs: [
{
id: "my-panel",
title: "My Panel",
type: "custom",
render: (container) => {
container.innerHTML = "<div>Panel content</div>";
},
},
],
メニューコマンド
menuCommands: [
{
path: ["My Extension"],
commands: ["my.ext.doSomething"],
},
],
About ページのバッジ
aboutPageBadges: [
{ label: "v1.0.0", url: "https://github.com/...", icon: "pi pi-github", severity: "warn" },
// severity is optional: "danger" | "warn"
],
トップバーのバッジ
topbarBadges: [
{
text: "My Extension", // required
label: "BETA", // optional badge label
variant: "info", // "info" | "warning" | "error"
icon: "pi pi-star", // optional icon
tooltip: "Extension info", // optional tooltip
},
],
アクションバーのボタン
actionBarButtons: [
{
icon: "pi pi-bolt", // required
label: "My Action", // optional label
tooltip: "Run my action", // optional tooltip
onClick: () => { /* ... */ }, // required click handler
},
],
API イベント
実行イベントをリッスンします。
// Node execution completed
app.api.addEventListener("executed", ({ detail }) => {
const { node, output } = detail;
// output contains images, text, etc.
});
// Execution progress
app.api.addEventListener("progress", ({ detail }) => {
const { value, max, node } = detail;
});
// Execution started/completed
app.api.addEventListener("execution_start", ({ detail }) => {});
app.api.addEventListener("execution_success", ({ detail }) => {});
app.api.addEventListener("execution_error", ({ detail }) => {});
// Status updates
app.api.addEventListener("status", ({ detail }) => {
const { exec_info } = detail;
});
サーバーからクライアントへの通信
Python(サーバー側):
from server import PromptServer
PromptServer.instance.send_sync(
"my_extension.update",
{"status": "complete", "data": result}
)
JavaScript(クライアント側):
app.api.addEventListener("my_extension.update", ({ detail }) => {
console.log("Received:", detail);
});
独自のルートの呼び出し
// fetchApi prefixes /api and adds auth headers
const res = await app.api.fetchApi("/my_extension/process", {
method: "POST",
body: JSON.stringify(payload),
timeoutMs: null, // v1.55.2+: default aborts after 60 s without response headers (TimeoutError)
});
処理に時間のかかる同期ルートでは timeoutMs: null(またはより大きな値)を指定してください。独自の signal も引き続き有効です。
トースト通知
v1.58+ API — success / info / warning / error / loading(title, options?) は ToastId を返します:
const toast = app.extensionManager.toast;
const id = toast.loading("Processing…");
// Reusing `id` replaces that toast in place
toast.success("Done", {
id,
description: "3 images saved",
duration: 3000, // ms; default is no auto-dismiss
closable: true,
action: { label: "Open", onClick: () => { /* ... */ } }, // does not auto-dismiss
});
toast.dismiss(id); // returns true if a toast was removed
toast.dismissAll();
古いフロントエンドには add() しかありません。v1.58+ でも引き続き動作しますが、非推奨です。機能を検出してください:
if (toast.success) toast.success("Saved", { description: "Done", duration: 3000 });
else toast.add({ severity: "success", summary: "Saved", detail: "Done", life: 3000 });
// add() severities: "success" | "info" | "warn" | "error" ("secondary"/"contrast" → info).
// group/styleClass are ignored and remove()/removeAll() are gone on v1.58+.
ダイアログ
// Confirmation dialog — resolves true / false, or null if dismissed
const result = await app.extensionManager.dialog.confirm({
title: "Confirm Action",
message: "Are you sure?",
type: "delete", // optional: "default" | "overwrite" | "delete" | "info" | ...
itemList: ["a.png"], // optional bullet list under the message
hint: "Cannot be undone", // optional
key: "my-ext-confirm", // v1.53+: own dialog key so an already-open shared prompt can't swallow it
});
// Prompt dialog
const value = await app.extensionManager.dialog.prompt({
title: "Enter Value",
message: "Provide a name:",
defaultValue: "default",
});
ExtensionManager ユーティリティ
設定へのアクセス
// Read a setting value
const val = app.extensionManager.setting.get("my.ext.mySetting");
// Write a setting value
app.extensionManager.setting.set("my.ext.mySetting", newValue);
実行エラー(読み取り専用)
// Last node-level errors (keyed by node ID)
const nodeErrors = app.extensionManager.lastNodeErrors;
// Last execution-level error
const execError = app.extensionManager.lastExecutionError;
Markdown のレンダリング
// Render markdown to sanitized HTML (marked + DOMPurify, safe for innerHTML)
const html = app.extensionManager.renderMarkdownToHtml(markdownStr, baseUrl);
コンテキストメニュー項目
app.registerExtension({
name: "my.ext",
// Canvas right-click menu
getCanvasMenuItems(canvas) {
return [{
content: "My Action",
callback: () => { console.log("Canvas menu clicked"); },
}];
},
// Node right-click menu
getNodeMenuItems(node) {
if (node.comfyClass === "MyNode") {
return [{
content: "Custom Action",
callback: () => { console.log("Node:", node.id); },
}];
}
return [];
},
});
ノードインスタンスのプロパティ(LGraphNode 拡張)
// Available on node instances:
node.comfyClass // ComfyUI node type name
node.isVirtualNode // true for frontend-only nodes
node.imgs // preview images array
node.imageIndex // current preview image index
// Callbacks:
node.onExecuted = function(output) { /* execution result */ };
node.onExecutionStart = function() { /* about to execute */ };
node.onDragOver = function(event) { /* file drag over */ };
node.onDragDrop = function(event) { /* file dropped */ };
最近の破壊的変更(フロントエンド 1.4x~1.58、および今後の変更)
拡張機能の作成者が把握しておくべき変更です。詳細については、上流の移行ノートをご覧ください:https://github.com/Comfy-Org/ComfyUI_frontend/tree/main/docs/extensions(`widgets-migration.md`、`connection-callbacks-migration.md`、`serialization-callbacks-migration.md`、`node-geometry-migration.md`、`link-registration-migration.md`、`primevue-removal-migration.md`、`tooltip-migration.md`)。
- PrimeVue の削除(今後、v1.58 以降) —
$primevue、PrimeVue のテーマ、グローバルな--p-*CSS 変数はなくなります。代わりにデザイントークンを使用してください:--primary-background、--base-foreground、--muted-foreground、--base-background、--secondary-background、--border-default。PrimeVue コンポーネントを引き続き使用するには、PrimeVue をインストールした独自の Vue アプリをマウントしてください。pi pi-*アイコンクラスは引き続き使用できます。 - PrimeVue のツールチップを削除(v1.58 以降) —
v-tooltip/.p-tooltipはなくなります。表示中のツールチップは、document.body上の[data-slot="tooltip-content"]です。 - Toast API を置き換え(v1.58 以降) — Toast Notifications をご覧ください。
add()/addAlert()は非推奨です。remove()/removeAll()は削除されました。 - ウィジェットの値をストアで管理(v1.53 以降) — Widget Rules(名前の一意性、
removeWidget、node.widgets ??= [])をご覧ください。 input.link/output.linksは非推奨(v1.53 以降) — 読み取りは引き続き可能ですが、警告が表示され、グラフの最新状態が反映されない場合があります。書き込みではリンクは変更されませんが、input.link = nullによる切断は可能です。node.isInputConnected(i)、node.getInputLink(i)、node.isOutputConnected(i)、node.getOutputNodes(i)、node.connect()、node.disconnectInput(i)を使用してください。グラフイベントのnode:slot-links:changedとnode:slot-errors:changedは削除されました。複数のサブスクライバーに追加/削除を通知するには、graph.events.addEventListener("node:added" | "node:before-removed" | "node:removed", ...)を使用してください。onConfigure(data)には浅いコピーが渡されます(v1.55.2 以降) —dataのトップレベルキーを変更しても効果はありません。カスタム状態はnode.properties/graph.extraに保存してください。- コアのノードフィールドはアクセサーです(v1.53 以降) —
id、type、title、mode、flags、color、bgcolor、shapeは、Object.keys(node)/{...node}に表示されなくなりました。node.serialize()を使用してください。構築後にnode.typeを変更すると警告が表示されます。 node.sizeは要求されたサイズです —getBounding()/boundingRectは描画された領域を返します(より大きくなる場合があります)。canvas.repositionNodesVueMode()はcanvas.applyNodePositions()に置き換えられました(旧名も非推奨のエイリアスとして残されています)。canvas.selected_nodesは非推奨(今後) — 警告が表示され、スナップショットが返されます。canvas.selectedItems(スナップショットの Set。add/deleteの後に再取得してください)を読み取り、canvas.selectItems(items, addToCurrent?)で選択状態を変更してください。- 軽微な非推奨項目 —
node.badgePosition(バッジは常に右上に描画され、代入は無視されます)、node.widgets_up(widgets_start_yを使用してください)、options.canvasOnly(options.surfacesを使用してください)。 - グループノードを削除 — グループノードを新規作成できなくなりました。グループノードを含むワークフローは、読み込み時に自動的にサブグラフへ変換されます。
GroupNodeHandler/GroupNodeConfigは後方互換性のためにのみ再エクスポートされています。新しい開発ではサブグラフを使用してください。 LiteGraph.use_uuidsを削除 — ノード ID は常に数値です。このフラグの読み取りや設定は行わないでください(UUID は引き続きサブグラフ/スロット ID の内部で使用されます)。LGraph.onGetNodeMenuOptionsを削除、LGraph.onBeforeChangeは非推奨 — 代入は引き続き可能ですが、警告が表示されます。代わりにLGraphCanvas.onBeforeChangeを使用してください。IBaseWidget.entityIdを削除 — ウィジェットの識別には、ブランド付きキーwidgetId(graphId:nodeId:name)を使用します。昇格されたサブグラフウィジェットはストアで管理されます。node.sizeは Proxy で管理 —node.size[1] = hとnode.size = [w, h]のどちらも、Vue で描画されたノードのレイアウトを正しく再計算するようになりました。ポーリングによる回避策は不要です。
フロントエンドスクリプト API
カスタムノードの JavaScript から、フロントエンドの src/scripts/ モジュールをインポートできます。インポートには Vite のシムパターンを使用します。
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
シンボルには window.comfyAPI.<module>.<export> からもアクセスできます。直接宣言されたエクスポート(export const|let|var|function|class)のみがシム化されます。export { x } from "..." のような再エクスポートには、この方法ではアクセスできません。
安定性レベル
| レベル | モジュール | 備考 |
|---|---|---|
| 安定 | scripts/app, scripts/api | 公開 API として保証されています |
| 内部(コンソール警告) | scripts/widgets, scripts/domWidget, scripts/utils, scripts/pnginfo, scripts/changeTracker, scripts/defaultGraph, scripts/metadata/* | 使用できますが、変更される可能性があります |
| 非推奨 | scripts/ui | 削除予定です。Vue の代替機能を使用してください |
主なモジュール
scripts/api—ComfyApiクラス:fetchApi()、queuePrompt()、getNodeDefs()、WebSocket イベント、設定、ユーザーデータ、システム統計scripts/app—ComfyAppシングルトン(app):グラフ操作、registerExtension()、extensionManager、クリップボード、座標変換scripts/widgets—ComfyWidgetsレジストリ(INT、FLOAT、STRING、BOOLEAN、COMBO、IMAGEUPLOAD など)、addValueControlWidget(s)()、updateControlWidgetLabel()(v1.58 以降、IS_CONTROL_WIDGETはエクスポートされません)scripts/domWidget—addDOMWidget()、DOMWidgetImpl、ComponentWidgetImpl(Vue コンポーネントのラッパー)scripts/utils—clone()、addStylesheet()、uploadFile()、ストレージヘルパーscripts/pnginfo—getPngMetadata()、getWebpMetadata()、importA1111()、形式別の抽出関数
API の詳細については、API リファレンスを参照してください。
関連項目
comfyui-node-basics- バックエンドノードの構造comfyui-node-packaging- JS 拡張機能を含むプロジェクト構造comfyui-node-inputs- バックエンドの入力タイプ