From e479498c59de87ad494ec83599aa9e021dcc217c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=8B=E3=81=BF=E3=81=AA=E3=83=BC=E5=85=88=E7=94=9F?= Date: Thu, 12 Nov 2026 18:55:44 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E3=81=97=E3=81=84=E3=83=8E?= =?UTF-8?q?=E3=83=BC=E3=83=892.0=E4=BA=92=E6=8F=9B=E6=80=A7=E3=81=AB?= =?UTF-8?q?=E9=96=A2=E3=81=99=E3=82=8B=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .spec/nodes-2-compatibility.md | 77 +++++++++++++++++++++++++++++++ tests/runtime_layout.test.mjs | 84 +++++++++++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 .spec/nodes-2-compatibility.md diff --git a/.spec/nodes-2-compatibility.md b/.spec/nodes-2-compatibility.md new file mode 100644 index 0000000..07cba16 --- /dev/null +++ b/.spec/nodes-2-compatibility.md @@ -0,0 +1,77 @@ +# Nodes 2.0 compatibility + +## Scope + +This repository supports both the Legacy Nodes renderer and Nodes 2.0 without +migrating the Python nodes to the V3 API. + +The compatibility boundary is intentionally limited to the existing node +contracts and frontend extension behavior. Node identifiers, input and output +names, widget serialization, and Python execution behavior must remain stable. + +## Official implementation checked + +The current ComfyUI frontend main branch (package version 1.61.0) was checked +on 2026-11-12: + +- Nodes 2.0 uses a Vue-based renderer while the Legacy renderer remains + selectable. +- Execution results are resolved to the target LiteGraph node and delivered by + calling `node.onExecuted(output)`. +- Widgets created with `node.addDOMWidget(...)` are registered in the widget + store used by Nodes 2.0. +- `node.widgets` may be undefined, so reads must remain guarded. +- Legacy `hidden` and `type = "hidden"` widget visibility writes are bridged to + the current visibility state. + +References: + +- https://docs.comfy.org/interface/nodes-2 +- https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/scripts/app.ts +- https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/docs/extensions/widgets-migration.md +- https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/scripts/domWidget.ts +- https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/lib/litegraph/src/widgets/BaseWidget.ts + +## Compatibility decisions + +| Concern | Existing implementation | Decision | +| --- | --- | --- | +| Execution title | Wraps each node type's `onExecuted` and assigns `node.title` | Keep unchanged; this is the shared Legacy/Nodes 2.0 path | +| Runtime editor | Uses a uniquely named `addDOMWidget` with `getValue` and `setValue` | Keep unchanged; it participates in the Nodes 2.0 widget store | +| Saved Runtime inputs | Keeps `options_text` and `state_key` serialized while hiding their built-in widgets | Keep unchanged to preserve workflow data and backend inputs | +| Widget lookup | Uses `node.widgets?.find(...)` | Keep unchanged; the undefined case is guarded | +| Runtime background tint | Uses `onDrawBackground` | Retain as a Legacy-only cosmetic decoration; do not write Vue internals or persistent node colors | +| Python node API | Uses `NODE_CLASS_MAPPINGS` and the existing class API | Keep V1 API; no V3 migration | + +The Runtime editor, synchronization, selection behavior, inputs, and outputs do +not depend on the Legacy-only background tint. + +## Stable workflow contracts + +The following node identifiers remain unchanged: + +- `PromptRandomChoice` +- `PromptRandomChoiceEx` +- `RuntimePromptRandomChoice` +- `RuntimePromptRandomChoiceEx` +- `SafeRandomSeed` + +The existing Python regression tests lock the Runtime input names and all node +mapping identifiers. The frontend regression tests lock the shared +`onExecuted` title path and the supported DOM widget registration path. + +## Verification boundary + +Automated checks cover Python behavior, mapping and input contracts, JavaScript +syntax, Runtime DOM widget layout options, and the frontend compatibility paths +listed above. + +The following require manual verification in a real ComfyUI installation: + +- Existing workflow load and execution with Legacy Nodes +- Existing workflow load and execution with Nodes 2.0 +- Title updates after execution in both renderers +- Runtime editor display, editing, resize, save/reload, and server sync in both + renderers +- Switching the same workflow between Legacy Nodes and Nodes 2.0 +- Runtime state behavior during queued execution diff --git a/tests/runtime_layout.test.mjs b/tests/runtime_layout.test.mjs index 21cef7e..0b51ce2 100644 --- a/tests/runtime_layout.test.mjs +++ b/tests/runtime_layout.test.mjs @@ -2,15 +2,87 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; -const source = readFileSync( +const runtimeSource = readFileSync( new URL("../web/runtime_prompt_random_choice.js", import.meta.url), "utf8", ); +const titleSource = readFileSync( + new URL("../web/prompt_random_choice.js", import.meta.url), + "utf8", +); + +function loadTitleExtension() { + let extension; + const app = { + graph: { setDirtyCanvas() {} }, + registerExtension(value) { + extension = value; + }, + }; + const executableSource = titleSource.replace(/^import .*;\r?\n/m, ""); + Function("app", executableSource)(app); + return extension; +} test("Runtime editor uses resizable DOM widget layout options", () => { - assert.doesNotMatch(source, /domWidget\.computeSize\s*=/); - assert.match(source, /getMinHeight:\s*\(\)\s*=>\s*96/); - assert.match(source, /getHeight:\s*\(\)\s*=>\s*"100%"/); - assert.match(source, /afterResize:/); - assert.match(source, /flex:\s*"1 1 auto"/); + assert.doesNotMatch(runtimeSource, /domWidget\.computeSize\s*=/); + assert.match(runtimeSource, /getMinHeight:\s*\(\)\s*=>\s*96/); + assert.match(runtimeSource, /getHeight:\s*\(\)\s*=>\s*"100%"/); + assert.match(runtimeSource, /afterResize:/); + assert.match(runtimeSource, /flex:\s*"1 1 auto"/); +}); + +test("Runtime editor uses the supported DOM widget compatibility path", () => { + assert.match(runtimeSource, /node\.widgets\?\.find/); + assert.match(runtimeSource, /node\.addDOMWidget\(/); + assert.match(runtimeSource, /"runtime_candidate_editor"/); + assert.match(runtimeSource, /serialize:\s*false/); + assert.match(runtimeSource, /widget\.type\s*=\s*"hidden"/); + assert.match(runtimeSource, /widget\.hidden\s*=\s*true/); + assert.doesNotMatch(runtimeSource, /useWidgetValueStore/); +}); + +test("Execution titles are updated through the node onExecuted callback", () => { + assert.match( + titleSource, + /const originalOnExecuted = nodeType\.prototype\.onExecuted/, + ); + assert.match( + titleSource, + /nodeType\.prototype\.onExecuted = function \(message\)/, + ); + assert.match(titleSource, /originalOnExecuted\?\.apply\(this, arguments\)/); + assert.match(titleSource, /this\.title\s*=/); + assert.doesNotMatch(titleSource, /api\.addEventListener\(\s*["']executed/); + assert.doesNotMatch(titleSource, /Number\(\s*nodeId\s*\)/); +}); + +test("Execution title callback preserves existing handlers and node names", () => { + const cases = [ + ["PromptRandomChoice", "Ch: blonde hair (2/3)"], + ["PromptRandomChoiceEx", "Ex: blonde hair (2/3)"], + ["RuntimePromptRandomChoice", "Rt: blonde hair (2/3)"], + ["RuntimePromptRandomChoiceEx", "RtEx: blonde hair (2/3)"], + ["SafeRandomSeed", "Seed: 123"], + ]; + + for (const [nodeName, expectedTitle] of cases) { + const extension = loadTitleExtension(); + let originalCalls = 0; + class NodeType {} + NodeType.prototype.onExecuted = function () { + originalCalls += 1; + }; + extension.beforeRegisterNodeDef(NodeType, { name: nodeName }); + const node = new NodeType(); + node.onExecuted({ + selected_text_title: ["blonde hair"], + repeat_index: [2], + change_every: [3], + seed_title: ["Seed: 123"], + }); + + assert.equal(originalCalls, 1); + assert.equal(node.title, expectedTitle); + } });