From b278ae149d1d4658ee0283ee3054564fd522aa3f Mon Sep 17 00:00:00 2001 From: Wonkyung Lee Date: Sat, 16 May 2026 13:30:43 +0900 Subject: [PATCH] Add frontend extension example --- .gitignore | 3 +- custom_nodes/example_node.py.example | 5 +- custom_nodes/frontend_extension.js.example | 113 +++++++++++++++++++++ 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 custom_nodes/frontend_extension.js.example diff --git a/.gitignore b/.gitignore index fc426eda4..7f7d207b7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,9 @@ __pycache__/ !/input/example.png /models/ /temp/ -/custom_nodes/ +/custom_nodes/* !custom_nodes/example_node.py.example +!custom_nodes/frontend_extension.js.example extra_model_paths.yaml /.vs .vscode/ diff --git a/custom_nodes/example_node.py.example b/custom_nodes/example_node.py.example index 779c35787..0dd35c7cf 100644 --- a/custom_nodes/example_node.py.example +++ b/custom_nodes/example_node.py.example @@ -105,8 +105,9 @@ class Example(io.ComfyNode): #def fingerprint_inputs(s, image, string_field, int_field, float_field, print_to_screen): # return "" -# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension -# WEB_DIRECTORY = "./somejs" +# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension. +# See frontend_extension.js.example for a commented JavaScript example. +# WEB_DIRECTORY = "./js" # Add custom API routes, using router diff --git a/custom_nodes/frontend_extension.js.example b/custom_nodes/frontend_extension.js.example new file mode 100644 index 000000000..820682935 --- /dev/null +++ b/custom_nodes/frontend_extension.js.example @@ -0,0 +1,113 @@ +/* +Example frontend extension for a ComfyUI custom node. + +To try this with custom_nodes/example_node.py.example: + +1. Copy example_node.py.example to your custom node package as __init__.py. +2. Set WEB_DIRECTORY = "./js" in that Python file. +3. Create a js directory next to __init__.py. +4. Copy this file to js/frontend_extension.js. + +ComfyUI loads every .js file in WEB_DIRECTORY in the browser. This JavaScript +does not run your Python node. It customizes the frontend representation of +nodes after the backend has registered their schemas. +*/ + +import { app } from "../../scripts/app.js"; + +const EXTENSION_NAME = "comfy.example.frontend_extension"; +const EXAMPLE_NODE_CLASS = "Example"; + +app.registerExtension({ + name: EXTENSION_NAME, + + /* + setup() runs once after the frontend has initialized. Use it for extension + level initialization that is not tied to one specific node instance. + */ + async setup() { + console.log(`${EXTENSION_NAME} loaded`); + }, + + /* + beforeRegisterNodeDef() runs when the frontend is about to register a node + class. This is the safest place to wrap prototype methods, because every + node instance created later will inherit the wrapped behavior. + + nodeData.name is the class name sent by the Python node schema. For the + Python example node this is "Example". + */ + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== EXAMPLE_NODE_CLASS) { + return; + } + + const originalOnNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function () { + const result = originalOnNodeCreated?.apply(this, arguments); + + /* + This function runs for each frontend node instance. At this point + `this` is the LiteGraph node object shown on the canvas. + + Use properties for data that should be serialized with the workflow. + Plain fields on `this` are useful for transient UI state. + */ + this.properties = this.properties || {}; + this.properties.example_frontend_extension_enabled ??= true; + + /* + addWidget() adds a control to the node body. This button only logs + information, so it is safe as an example and does not change queueing + or execution behavior. + */ + this.addWidget("button", "Log node info", null, () => { + console.log("Node id:", this.id); + console.log("Node title:", this.title); + console.log("Node properties:", this.properties); + console.log("Output images:", this.imgs); + }); + + return result; + }; + + /* + Prototype methods can be hooked by saving the original method and + calling it from your replacement. Keep the original call unless you + intentionally want to replace ComfyUI's default behavior. + + onExecuted() is called after this node receives execution results from + the backend. If the node produced image previews, ComfyUI may expose + those images on node.imgs. The field can be undefined or empty before + the node has executed, or for nodes that do not display images. + */ + const originalOnExecuted = nodeType.prototype.onExecuted; + + nodeType.prototype.onExecuted = function (message) { + const result = originalOnExecuted?.apply(this, arguments); + + if (this.imgs?.length) { + console.log(`Node ${this.id} is displaying ${this.imgs.length} image(s).`); + } else { + console.log(`Node ${this.id} executed without frontend images.`); + } + + console.debug("Execution message:", message); + return result; + }; + }, + + /* + nodeCreated() is another lifecycle hook. It runs for each node instance + after creation. Use it for per-node setup that does not need to alter the + node class prototype. + */ + async nodeCreated(node) { + if (node.comfyClass !== EXAMPLE_NODE_CLASS) { + return; + } + + node.exampleFrontendExtensionLoaded = true; + }, +});