diff --git a/src/schemas/nodeDef/migration.test.ts b/src/schemas/nodeDef/migration.test.ts index 7250918f118..18a140825c1 100644 --- a/src/schemas/nodeDef/migration.test.ts +++ b/src/schemas/nodeDef/migration.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration' import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema' @@ -537,4 +537,151 @@ describe('ComfyNodeDefImpl', () => { expect(result.api_node).toBe(expected) } ) + + describe('defaultInput migration', () => { + it('should not set forceInput on optional input with defaultInput', () => { + const nodeDef = new ComfyNodeDefImpl({ + name: 'TestNode', + display_name: 'Test Node', + category: 'Test', + python_module: 'test_module', + description: 'A test node', + input: { + optional: { + seed_override: ['INT', { defaultInput: true, default: 0 }] + } + }, + output: [], + output_is_list: [], + output_name: [], + output_node: false + } as ComfyNodeDefV1) + + expect(nodeDef.inputs['seed_override']).toBeDefined() + expect(nodeDef.inputs['seed_override'].forceInput).toBeUndefined() + }) + + it('should preserve widget type on optional input with defaultInput', () => { + const nodeDef = new ComfyNodeDefImpl({ + name: 'TestNode', + display_name: 'Test Node', + category: 'Test', + python_module: 'test_module', + description: 'A test node', + input: { + optional: { + my_param: [ + 'FLOAT', + { defaultInput: true, default: 0.5, min: 0, max: 1 } + ] + } + }, + output: [], + output_is_list: [], + output_name: [], + output_node: false + } as ComfyNodeDefV1) + + const inputSpec = nodeDef.inputs['my_param'] + expect(inputSpec.type).toBe('FLOAT') + expect(inputSpec.isOptional).toBe(true) + expect(inputSpec.default).toBe(0.5) + expect(inputSpec.forceInput).toBeUndefined() + }) + + it('should not affect required inputs with defaultInput', () => { + const nodeDef = new ComfyNodeDefImpl({ + name: 'TestNode', + display_name: 'Test Node', + category: 'Test', + python_module: 'test_module', + description: 'A test node', + input: { + required: { + value: ['INT', { defaultInput: true, default: 42 }] + } + }, + output: [], + output_is_list: [], + output_name: [], + output_node: false + } as ComfyNodeDefV1) + + expect(nodeDef.inputs['value']).toBeDefined() + expect(nodeDef.inputs['value'].forceInput).toBeUndefined() + }) + + it('should preserve explicit forceInput when set alongside defaultInput', () => { + const nodeDef = new ComfyNodeDefImpl({ + name: 'TestNode', + display_name: 'Test Node', + category: 'Test', + python_module: 'test_module', + description: 'A test node', + input: { + optional: { + forced: ['INT', { forceInput: true, defaultInput: true }] + } + }, + output: [], + output_is_list: [], + output_name: [], + output_node: false + } as ComfyNodeDefV1) + + expect(nodeDef.inputs['forced'].forceInput).toBe(true) + }) + + it('should emit deprecation warning for optional input with defaultInput', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + new ComfyNodeDefImpl({ + name: 'TestNode', + display_name: 'Test Node', + category: 'Test', + python_module: 'test_module', + description: 'A test node', + input: { + optional: { + seed: ['INT', { defaultInput: true }] + } + }, + output: [], + output_is_list: [], + output_name: [], + output_node: false + } as ComfyNodeDefV1) + + expect(warnSpy).toHaveBeenCalledWith( + 'Use of defaultInput on optional input test_module:TestNode:seed is deprecated and ignored. Remove defaultInput. Use forceInput only if you intentionally want a socket-only input.' + ) + + warnSpy.mockRestore() + }) + + it('should not mutate the original node definition', () => { + const originalDef = { + name: 'TestNode', + display_name: 'Test Node', + category: 'Test', + python_module: 'test_module', + description: 'A test node', + input: { + optional: { + param: ['INT', { defaultInput: true }] + } + }, + output: [], + output_is_list: [], + output_name: [], + output_node: false + } as ComfyNodeDefV1 + + new ComfyNodeDefImpl(originalDef) + + expect( + originalDef.input!.optional!['param'][1]!.forceInput + ).toBeUndefined() + }) + }) }) diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index 2c0703948a2..2577c789249 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -103,7 +103,14 @@ export class ComfyNodeDefImpl /** * @internal - * Migrate default input options to forceInput. + * Migrate default input options. Since frontend version 1.16+, widget and + * input socket co-exist on every input, so `defaultInput` no longer needs + * special handling. We only emit deprecation warnings. + * + * Previously `defaultInput` on optional inputs was migrated to `forceInput`, + * which prevented the widget from being created at all and made it impossible + * for users to toggle back to widget mode after reload. + * See: https://github.com/Comfy-Org/ComfyUI_frontend/issues/1500 */ private static _migrateDefaultInput(nodeDef: ComfyNodeDefV1): ComfyNodeDefV1 { const def = _.cloneDeep(nodeDef) @@ -118,16 +125,16 @@ export class ComfyNodeDefImpl ) } } - // For optional inputs, defaultInput is used to distinguish the null state. - // We migrate it to forceInput. One example is the "seed_override" input usage. - // User can connect the socket to override the seed. + // For optional inputs, defaultInput previously converted the widget into + // a socket-only input. Since 1.16+ widgets and sockets co-exist, we no + // longer need to set forceInput. The widget will be created alongside the + // socket, and users can choose how to provide the value. for (const [name, spec] of Object.entries(def.input.optional ?? {})) { const inputOptions = spec[1] if (inputOptions && inputOptions.defaultInput) { console.warn( - `Use of defaultInput on optional input ${nodeDef.python_module}:${nodeDef.name}:${name} is deprecated. Please use forceInput instead.` + `Use of defaultInput on optional input ${nodeDef.python_module}:${nodeDef.name}:${name} is deprecated and ignored. Remove defaultInput. Use forceInput only if you intentionally want a socket-only input.` ) - inputOptions.forceInput = true } } return def