feat: support dev-only nodes (#8359)

## Summary

Support `dev_only` property to node definitions that hides nodes from
search and menus unless dev mode is enabled. Dev-only nodes display a
"DEV" badge when visible.

This functionality is primarily intended to support unit-testing nodes
on Comfy Cloud, but also has other uses.

## Changes

- **What**: Nodes flagged as dev_only in the node schema will only
appear in search and menus if Dev Mode is on.

## Screenshots (if applicable)

With Dev Mode off:
<img width="2189" height="1003" alt="image"
src="https://github.com/user-attachments/assets/a08e1fd7-dca9-4ce1-9964-5f4f3b7b95ac"
/>

With Dev Mode on:
<img width="2201" height="1066" alt="image"
src="https://github.com/user-attachments/assets/7fe6cd1f-f774-4f48-b604-a528e286b584"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8359-feat-support-dev-only-nodes-2f66d73d36508102839ee7cd66a26129)
by [Unito](https://www.unito.io)
This commit is contained in:
guill
2026-01-28 19:41:45 -08:00
committed by GitHub
parent 2103dcc788
commit 9be853f6b5
8 changed files with 60 additions and 12 deletions

View File

@@ -21,16 +21,17 @@
</div>
</div>
<div class="option-badges">
<Tag
v-if="nodeDef.experimental"
:value="$t('g.experimental')"
severity="primary"
/>
<Tag
v-if="nodeDef.deprecated"
:value="$t('g.deprecated')"
severity="danger"
/>
<Tag
v-if="nodeDef.experimental"
:value="$t('g.experimental')"
severity="primary"
/>
<Tag v-if="nodeDef.dev_only" :value="$t('g.devOnly')" severity="info" />
<Tag
v-if="showNodeFrequency && nodeFrequency > 0"
:value="formatNumberWithSuffix(nodeFrequency, { roundToInt: true })"

View File

@@ -233,6 +233,14 @@ export class LGraphNode
static description?: string
static filter?: string
static skip_list?: boolean
static nodeData?: {
dev_only?: boolean
deprecated?: boolean
experimental?: boolean
output_node?: boolean
api_node?: boolean
name?: string
}
static resizeHandleSize = 15
static resizeEdgeSize = 5

View File

@@ -121,6 +121,7 @@
"customize": "Customize",
"experimental": "BETA",
"deprecated": "DEPR",
"devOnly": "DEV",
"loadWorkflow": "Load Workflow",
"goToNode": "Go to Node",
"setAsBackground": "Set as Background",

View File

@@ -174,6 +174,7 @@ export const zComfyNodeDef = z.object({
python_module: z.string(),
deprecated: z.boolean().optional(),
experimental: z.boolean().optional(),
dev_only: z.boolean().optional(),
api_node: z.boolean().optional()
})

View File

@@ -257,6 +257,7 @@ export const zComfyNodeDef = z.object({
python_module: z.string(),
deprecated: z.boolean().optional(),
experimental: z.boolean().optional(),
dev_only: z.boolean().optional(),
/**
* Whether the node is an API node. Running API nodes requires login to
* Comfy Org account.

View File

@@ -261,7 +261,7 @@ export const useLitegraphService = () => {
static comfyClass: string
static override title: string
static override category: string
static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
static override nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
_initialMinSize = { width: 1, height: 1 }
@@ -394,7 +394,7 @@ export const useLitegraphService = () => {
static comfyClass: string
static override title: string
static override category: string
static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
static override nodeData: ComfyNodeDefV1 & ComfyNodeDefV2
_initialMinSize = { width: 1, height: 1 }
@@ -496,6 +496,13 @@ export const useLitegraphService = () => {
// because `registerNodeType` will overwrite the assignments.
node.category = nodeDef.category
node.title = nodeDef.display_name || nodeDef.name
// Set skip_list for dev-only nodes based on current DevMode setting
// This ensures nodes registered after initial load respect the current setting
if (nodeDef.dev_only) {
const settingStore = useSettingStore()
node.skip_list = !settingStore.get('Comfy.DevMode')
}
}
/**

View File

@@ -1,9 +1,10 @@
import axios from 'axios'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { computed, ref, watchEffect } from 'vue'
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration'
import type {
@@ -17,6 +18,7 @@ import type {
ComfyOutputTypesSpec as ComfyOutputSpecV1,
PriceBadge
} from '@/schemas/nodeDefSchema'
import { useSettingStore } from '@/platform/settings/settingStore'
import { NodeSearchService } from '@/services/nodeSearchService'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
@@ -41,6 +43,7 @@ export class ComfyNodeDefImpl
readonly help: string
readonly deprecated: boolean
readonly experimental: boolean
readonly dev_only: boolean
readonly output_node: boolean
readonly api_node: boolean
/**
@@ -133,6 +136,7 @@ export class ComfyNodeDefImpl
this.deprecated = obj.deprecated ?? obj.category === ''
this.experimental =
obj.experimental ?? obj.category.startsWith('_for_testing')
this.dev_only = obj.dev_only ?? false
this.output_node = obj.output_node
this.api_node = !!obj.api_node
this.input = obj.input ?? {}
@@ -174,6 +178,7 @@ export class ComfyNodeDefImpl
get nodeLifeCycleBadgeText(): string {
if (this.deprecated) return '[DEPR]'
if (this.experimental) return '[BETA]'
if (this.dev_only) return '[DEV]'
return ''
}
}
@@ -299,12 +304,27 @@ export interface NodeDefFilter {
}
export const useNodeDefStore = defineStore('nodeDef', () => {
const settingStore = useSettingStore()
const nodeDefsByName = ref<Record<string, ComfyNodeDefImpl>>({})
const nodeDefsByDisplayName = ref<Record<string, ComfyNodeDefImpl>>({})
const showDeprecated = ref(false)
const showExperimental = ref(false)
const showDevOnly = computed(() => settingStore.get('Comfy.DevMode'))
const nodeDefFilters = ref<NodeDefFilter[]>([])
// Update skip_list on all registered node types when dev mode changes
// This ensures LiteGraph's getNodeTypesCategories/getNodeTypesInCategory
// correctly filter dev-only nodes from the right-click context menu
watchEffect(() => {
const devModeEnabled = showDevOnly.value
for (const nodeType of Object.values(LiteGraph.registered_node_types)) {
if (nodeType.nodeData?.dev_only) {
nodeType.skip_list = !devModeEnabled
}
}
})
const nodeDefs = computed(() => {
const subgraphStore = useSubgraphStore()
// Blueprints first for discoverability in the node library sidebar
@@ -422,6 +442,14 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
predicate: (nodeDef) => showExperimental.value || !nodeDef.experimental
})
// Dev-only nodes filter
registerNodeDefFilter({
id: 'core.dev_only',
name: 'Hide Dev-Only Nodes',
description: 'Hides nodes marked as dev-only unless dev mode is enabled',
predicate: (nodeDef) => showDevOnly.value || !nodeDef.dev_only
})
// Subgraph nodes filter
// Filter out litegraph typed subgraphs, saved blueprints are added in separately
registerNodeDefFilter({
@@ -446,6 +474,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
nodeDefsByDisplayName,
showDeprecated,
showExperimental,
showDevOnly,
nodeDefFilters,
nodeDefs,

View File

@@ -11,7 +11,7 @@ describe('nodeFilterUtil', () => {
): LGraphNode => {
// Create a custom class with the nodeData static property
class MockNode extends LGraphNode {
static nodeData = isOutputNode ? { output_node: true } : {}
static override nodeData = isOutputNode ? { output_node: true } : {}
}
const node = new MockNode('')
@@ -71,11 +71,11 @@ describe('nodeFilterUtil', () => {
})
it('should handle nodes with undefined output_node', () => {
class MockNodeWithOtherData extends LGraphNode {
static nodeData = { someOtherProperty: true }
class MockNodeWithEmptyData extends LGraphNode {
static override nodeData = {}
}
const node = new MockNodeWithOtherData('')
const node = new MockNodeWithEmptyData('')
node.id = 1
const result = filterOutputNodes([node])