From d23736b31014b9eb187e4a4db181b929e10cb7b5 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Sat, 10 Jan 2026 14:51:12 -0800 Subject: [PATCH] refactor(groupNode): remove all @ts-expect-error suppressions Amp-Thread-ID: https://ampcode.com/threads/T-019ba9df-6ee8-7541-9105-c657cd9ec692 Co-authored-by: Amp --- src/extensions/core/groupNode.ts | 144 ++++---- src/extensions/core/groupNodeManage.ts | 462 +++++++++++++------------ src/extensions/core/groupNodeTypes.ts | 47 +++ src/lib/litegraph/src/LGraph.ts | 17 +- src/lib/litegraph/src/litegraph.ts | 2 + src/types/litegraph-augmentation.d.ts | 4 +- 6 files changed, 376 insertions(+), 300 deletions(-) create mode 100644 src/extensions/core/groupNodeTypes.ts diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index a7af7361a..04924a0cd 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -1,12 +1,19 @@ import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants' import { t } from '@/i18n' import type { GroupNodeWorkflowData } from '@/lib/litegraph/src/LGraph' -import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink' +import type { + GroupNodeInputConfig, + GroupNodeInputsSpec, + GroupNodeOutputType, + PartialLinkInfo +} from './groupNodeTypes' +import { LLink, type SerialisedLLinkArray } from '@/lib/litegraph/src/LLink' import type { NodeId } from '@/lib/litegraph/src/LGraphNode' import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' import { type ExecutableLGraphNode, type ExecutionId, + type ISerialisedNode, LGraphNode, type LGraphNodeConstructor, LiteGraph, @@ -54,9 +61,8 @@ interface GroupNodeOutput { interface GroupNodeData extends Omit< GroupNodeWorkflowData['nodes'][number], - 'inputs' | 'outputs' + 'inputs' | 'outputs' | 'widgets_values' > { - title?: string widgets_values?: unknown[] inputs?: GroupNodeInput[] outputs?: GroupNodeOutput[] @@ -241,7 +247,13 @@ export class GroupNodeConfig { > nodeInputs: Record> outputVisibility: boolean[] - nodeDef: (ComfyNodeDef & { [GROUP]: GroupNodeConfig }) | undefined + nodeDef: + | (Omit & { + input: GroupNodeInputsSpec + output: GroupNodeOutputType[] + [GROUP]: GroupNodeConfig + }) + | undefined inputs!: unknown[] linksFrom!: LinksFromMap linksTo!: LinksToMap @@ -297,8 +309,11 @@ export class GroupNodeConfig { } this.#convertedToProcess = [] if (!this.nodeDef) return - await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef) - useNodeDefStore().addNodeDef(this.nodeDef) + const finalizedDef = this.nodeDef as ComfyNodeDef & { + [GROUP]: GroupNodeConfig + } + await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, finalizedDef) + useNodeDefStore().addNodeDef(finalizedDef) } getLinks() { @@ -520,9 +535,13 @@ export class GroupNodeConfig { node: GroupNodeData, inputName: string, seenInputs: Record, - config: unknown[], + inputConfig: unknown[], extra?: Record - ) { + ): { + name: string + config: GroupNodeInputConfig + customConfig: { name?: string; visible?: boolean } | undefined + } { const nodeConfig = this.nodeData.config?.[node.index ?? -1] as | NodeConfigEntry | undefined @@ -543,28 +562,34 @@ export class GroupNodeConfig { } seenInputs[key] = (seenInputs[key] ?? 1) + 1 + const typeName = String(inputConfig[0]) + let options = + typeof inputConfig[1] === 'object' && inputConfig[1] !== null + ? (inputConfig[1] as Record) + : undefined + if (inputName === 'seed' || inputName === 'noise_seed') { if (!extra) extra = {} extra.control_after_generate = `${prefix}control_after_generate` } - if (config[0] === 'IMAGEUPLOAD') { + if (typeName === 'IMAGEUPLOAD') { if (!extra) extra = {} const nodeIndex = node.index ?? -1 - const configOptions = - typeof config[1] === 'object' && config[1] !== null ? config[1] : {} const widgetKey = - 'widget' in configOptions && typeof configOptions.widget === 'string' - ? configOptions.widget + options && 'widget' in options && typeof options.widget === 'string' + ? options.widget : 'image' extra.widget = this.oldToNewWidgetMap[nodeIndex]?.[widgetKey] ?? 'image' } if (extra) { - const configObj = - typeof config[1] === 'object' && config[1] ? config[1] : {} - config = [config[0], { ...configObj, ...extra }] + options = { ...(options ?? {}), ...extra } } + const config: GroupNodeInputConfig = options + ? [typeName, options] + : [typeName] + return { name, config, customConfig } } @@ -608,7 +633,6 @@ export class GroupNodeConfig { inputs[inputName] as unknown[] ) if (this.nodeDef?.input?.required) { - // @ts-expect-error legacy dynamic input assignment this.nodeDef.input.required[name] = config } widgetMap[inputName] = name @@ -641,14 +665,15 @@ export class GroupNodeConfig { unknown, Record ] - const output = { widget: primitiveConfig } + const output = { widget: primitiveConfig } as unknown as Parameters< + typeof mergeIfValid + >[0] const config = mergeIfValid( - // @ts-expect-error slot type mismatch - legacy API output, - targetWidget, + targetWidget as Parameters[1], false, undefined, - primitiveConfig + primitiveConfig as Parameters[4] ) const inputConfig = inputs[inputName]?.[1] primitiveConfig[1] = @@ -713,7 +738,6 @@ export class GroupNodeConfig { if (customConfig?.visible === false) continue if (this.nodeDef?.input?.required) { - // @ts-expect-error legacy dynamic input assignment this.nodeDef.input.required[name] = config } inputMap[i] = this.inputCount++ @@ -757,7 +781,6 @@ export class GroupNodeConfig { ) if (this.nodeDef?.input?.required) { - // @ts-expect-error legacy dynamic input assignment this.nodeDef.input.required[name] = config } this.newToOldWidgetMap[name] = { node, inputName } @@ -851,8 +874,7 @@ export class GroupNodeConfig { node, slot: outputId } - // @ts-expect-error legacy dynamic output type assignment - this.nodeDef.output.push(defOutput[outputId]) + this.nodeDef.output.push(defOutput[outputId] as GroupNodeOutputType) this.nodeDef.output_is_list?.push( def.output_is_list?.[outputId] ?? false ) @@ -951,8 +973,13 @@ export class GroupNodeHandler { for (const w of innerNode.widgets ?? []) { if (w.type === 'converted-widget') { - // @ts-expect-error legacy widget property for converted widgets - w.serializeValue = w.origSerializeValue + type SerializeValueFn = (node: LGraphNode, index: number) => unknown + const convertedWidget = w as typeof w & { + origSerializeValue?: SerializeValueFn + } + if (convertedWidget.origSerializeValue) { + w.serializeValue = convertedWidget.origSerializeValue + } } } @@ -978,20 +1005,18 @@ export class GroupNodeHandler { return inputNode } - // @ts-expect-error returns partial link object, not full LLink - innerNode.getInputLink = (slot: number) => { + innerNode.getInputLink = ((slot: number): PartialLinkInfo | null => { const nodeIdx = innerNode.index ?? 0 const externalSlot = this.groupData.oldToNewInputMap[nodeIdx]?.[slot] if (externalSlot != null) { - // The inner node is connected via the group node inputs const linkId = this.node.inputs[externalSlot].link if (linkId == null) return null const existingLink = app.rootGraph.links[linkId] if (!existingLink) return null - // Use the outer link, but update the target to the inner node return { - ...existingLink, + origin_id: existingLink.origin_id, + origin_slot: existingLink.origin_slot, target_id: innerNode.id, target_slot: +slot } @@ -1001,21 +1026,18 @@ export class GroupNodeHandler { if (!innerLink) return null const linkSrcIdx = innerLink[0] if (linkSrcIdx == null) return null - // Use the inner link, but update the origin node to be inner node id return { origin_id: innerNodes[Number(linkSrcIdx)].id, origin_slot: innerLink[1], target_id: innerNode.id, target_slot: +slot } - } + }) as typeof innerNode.getInputLink } } - this.node.updateLink = (link) => { - // Replace the group node reference with the internal node - // @ts-expect-error Can this be removed? Or replaced with: LLink.create(link.asSerialisable()) - link = { ...link } + this.node.updateLink = (inputLink) => { + const link = LLink.create(inputLink.asSerialisable()) const output = this.groupData.newToOldOutputMap[link.origin_slot] if (!output || !this.innerNodes) return null const nodeIdx = output.node.index ?? 0 @@ -1063,8 +1085,7 @@ export class GroupNodeHandler { if (!n.type) return null const innerNode = LiteGraph.createNode(n.type) if (!innerNode) return null - // @ts-expect-error legacy node data format used for configure - innerNode.configure(n) + innerNode.configure(n as ISerialisedNode) innerNode.id = `${this.node.id}:${i}` innerNode.graph = this.node.graph return innerNode @@ -1085,10 +1106,9 @@ export class GroupNodeHandler { for (const node of this.innerNodes ?? []) { node.graph ??= this.node.graph - // Create minimal DTOs rather than cloning the node const currentId = String(node.id) - // @ts-expect-error temporary id reassignment for DTO creation - node.id = currentId.split(':').at(-1) + const shortId = currentId.split(':').at(-1) ?? currentId + node.id = shortId const aVeryRealNode = new ExecutableGroupNodeChildDTO( node, subgraphInstanceIdPath, @@ -1103,7 +1123,6 @@ export class GroupNodeHandler { return nodes } - // @ts-expect-error recreate returns null if creation fails this.node.recreate = async () => { const id = this.node.id const sz = this.node.size @@ -1139,11 +1158,9 @@ export class GroupNodeHandler { this.node as LGraphNode & { convertToNodes: () => LGraphNode[] } ).convertToNodes = () => { const addInnerNodes = () => { - // Clone the node data so we dont mutate it for other nodes const c = { ...this.groupData.nodeData } c.nodes = [...c.nodes] - // @ts-expect-error getInnerNodes called without args in legacy conversion code - const innerNodes = this.node.getInnerNodes?.() + const innerNodes = this.innerNodes const ids: (string | number)[] = [] for (let i = 0; i < c.nodes.length; i++) { let id: string | number | undefined = innerNodes?.[i]?.id @@ -1153,7 +1170,6 @@ export class GroupNodeHandler { } else { ids.push(id) } - // @ts-expect-error adding id to node copy for serialization c.nodes[i] = { ...c.nodes[i], id } } deserialiseAndCreate(JSON.stringify(c), app.canvas) @@ -1182,7 +1198,6 @@ export class GroupNodeHandler { if (!newNode.widgets || !innerNode) continue - // @ts-expect-error index property access on ExecutableLGraphNode const map = this.groupData.oldToNewWidgetMap[innerNode.index ?? 0] if (map) { const widgets = Object.keys(map) @@ -1305,14 +1320,13 @@ export class GroupNodeHandler { null, { content: 'Convert to nodes', - // @ts-expect-error async callback not expected by legacy menu API callback: async () => { const convertFn = ( handlerNode as LGraphNode & { convertToNodes?: () => LGraphNode[] } ).convertToNodes - return convertFn?.() + convertFn?.() } }, { @@ -1519,7 +1533,6 @@ export class GroupNodeHandler { if (!innerNode) continue if (innerNode.type === 'PrimitiveNode') { - // @ts-expect-error primitiveValue is a custom property on PrimitiveNode innerNode.primitiveValue = newValue const primitiveLinked = this.groupData.primitiveToWidget[nodeIdx] for (const linked of primitiveLinked ?? []) { @@ -1748,12 +1761,7 @@ export class GroupNodeHandler { this.groupData.oldToNewInputMap[Number(targetId)]?.[Number(targetSlot)] if (mappedSlot == null) continue if (typeof originSlot === 'number' || typeof originSlot === 'string') { - originNode.connect( - originSlot, - // @ts-expect-error Valid - uses deprecated interface (node ID instead of node reference) - this.node.id, - mappedSlot - ) + originNode.connect(originSlot, this.node, mappedSlot) } } } @@ -1783,13 +1791,13 @@ export class GroupNodeHandler { } static getHandler(node: LGraphNode): GroupNodeHandler | undefined { - // @ts-expect-error GROUP symbol indexing on LGraphNode - let handler = node[GROUP] as GroupNodeHandler | undefined - // Handler may not be set yet if nodeCreated async hook hasn't run - // Create it synchronously if needed + type GroupNodeWithHandler = LGraphNode & { + [GROUP]?: GroupNodeHandler + } + let handler = (node as GroupNodeWithHandler)[GROUP] if (!handler && GroupNodeHandler.isGroupNode(node)) { handler = new GroupNodeHandler(node) - ;(node as LGraphNode & { [GROUP]: GroupNodeHandler })[GROUP] = handler + ;(node as GroupNodeWithHandler)[GROUP] = handler } return handler } @@ -1948,8 +1956,9 @@ const ext: ComfyExtension = { items.push({ content: `Convert to Group Node (Deprecated)`, disabled: !convertEnabled, - // @ts-expect-error async callback - legacy menu API doesn't expect Promise - callback: async () => convertSelectedNodesToGroupNode() + callback: async () => { + await convertSelectedNodesToGroupNode() + } }) const groups = canvas.graph?.extra?.groupNodes @@ -1975,8 +1984,9 @@ const ext: ComfyExtension = { { content: `Convert to Group Node (Deprecated)`, disabled: !convertEnabled, - // @ts-expect-error async callback - legacy menu API doesn't expect Promise - callback: async () => convertSelectedNodesToGroupNode() + callback: async () => { + await convertSelectedNodesToGroupNode() + } } ] }, diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 0e91af317..8a0f77f86 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -1,5 +1,8 @@ +import { merge } from 'es-toolkit' + import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants' import { + type GroupNodeWorkflowData, type LGraphNode, type LGraphNodeConstructor, LiteGraph @@ -13,70 +16,56 @@ import { DraggableList } from '../../scripts/ui/draggableList' import { GroupNodeConfig, GroupNodeHandler } from './groupNode' import './groupNodeManage.css' -const ORDER: symbol = Symbol() +const ORDER: unique symbol = Symbol('ORDER') -// @ts-expect-error fixme ts strict error -function merge(target, source) { - if (typeof target === 'object' && typeof source === 'object') { - for (const key in source) { - const sv = source[key] - if (typeof sv === 'object') { - let tv = target[key] - if (!tv) tv = target[key] = {} - merge(tv, source[key]) - } else { - target[key] = sv - } - } - } - - return target +interface NodeModification { + name?: string + visible?: boolean } +interface OrderModification { + order: number +} + +type NodeModifications = Record & { + [ORDER]?: OrderModification +} + +type DragEndEvent = CustomEvent<{ + element: Element + oldPosition: number + newPosition: number +}> + export class ManageGroupDialog extends ComfyDialog { - // @ts-expect-error fixme ts strict error - tabs: Record< + tabs!: Record< 'Inputs' | 'Outputs' | 'Widgets', { tab: HTMLAnchorElement; page: HTMLElement } > selectedNodeIndex: number | null | undefined selectedTab: keyof ManageGroupDialog['tabs'] = 'Inputs' selectedGroup: string | undefined - modifications: Record< - string, - Record< - string, - Record< - string, - { name?: string | undefined; visible?: boolean | undefined } - > - > - > = {} - // @ts-expect-error fixme ts strict error - nodeItems: any[] + modifications: Record }> = + {} + nodeItems: HTMLLIElement[] = [] app: ComfyApp - // @ts-expect-error fixme ts strict error - groupNodeType: LGraphNodeConstructor - groupNodeDef: any - groupData: any + groupNodeType!: LGraphNodeConstructor + groupNodeDef: unknown + groupData: ReturnType | null = null - // @ts-expect-error fixme ts strict error - innerNodesList: HTMLUListElement - // @ts-expect-error fixme ts strict error - widgetsPage: HTMLElement - // @ts-expect-error fixme ts strict error - inputsPage: HTMLElement - // @ts-expect-error fixme ts strict error - outputsPage: HTMLElement - draggable: any + innerNodesList!: HTMLUListElement + widgetsPage!: HTMLElement + inputsPage!: HTMLElement + outputsPage!: HTMLElement + draggable: DraggableList | null = null - get selectedNodeInnerIndex() { - // @ts-expect-error fixme ts strict error - return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex + get selectedNodeInnerIndex(): number { + if (this.selectedNodeIndex == null) return 0 + const item = this.nodeItems[this.selectedNodeIndex] + return +(item?.dataset?.nodeindex ?? 0) } - // @ts-expect-error fixme ts strict error - constructor(app) { + constructor(app: ComfyApp) { super() this.app = app this.element = $el('dialog.comfy-group-manage', { @@ -84,19 +73,15 @@ export class ManageGroupDialog extends ComfyDialog { }) as HTMLDialogElement } - // @ts-expect-error fixme ts strict error - changeTab(tab) { + changeTab(tab: keyof ManageGroupDialog['tabs']) { this.tabs[this.selectedTab].tab.classList.remove('active') this.tabs[this.selectedTab].page.classList.remove('active') - // @ts-expect-error fixme ts strict error this.tabs[tab].tab.classList.add('active') - // @ts-expect-error fixme ts strict error this.tabs[tab].page.classList.add('active') this.selectedTab = tab } - // @ts-expect-error fixme ts strict error - changeNode(index, force?) { + changeNode(index: number, force?: boolean) { if (!force && this.selectedNodeIndex === index) return if (this.selectedNodeIndex != null) { @@ -126,19 +111,17 @@ export class ManageGroupDialog extends ComfyDialog { this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType) } - // @ts-expect-error fixme ts strict error - changeGroup(group, reset = true) { + changeGroup(group: string, reset = true) { this.selectedGroup = group this.getGroupData() - const nodes = this.groupData.nodeData.nodes - // @ts-expect-error fixme ts strict error + const nodes = this.groupData?.nodeData.nodes ?? [] this.nodeItems = nodes.map((n, i) => $el( 'li.draggable-item', { dataset: { - nodeindex: n.index + '' + nodeindex: String(n.index ?? i) }, onclick: () => { this.changeNode(i) @@ -159,7 +142,7 @@ export class ManageGroupDialog extends ComfyDialog { ) ] ) - ) + ) as HTMLLIElement[] this.innerNodesList.replaceChildren(...this.nodeItems) @@ -167,63 +150,76 @@ export class ManageGroupDialog extends ComfyDialog { this.selectedNodeIndex = null this.changeNode(0) } else { - const items = this.draggable.getAllItems() - // @ts-expect-error fixme ts strict error - let index = items.findIndex((item) => item.classList.contains('selected')) - if (index === -1) index = this.selectedNodeIndex + const items = this.draggable?.getAllItems() ?? [] + let index = items.findIndex((item: Element) => + item.classList.contains('selected') + ) + if (index === -1) index = this.selectedNodeIndex ?? 0 this.changeNode(index, true) } const ordered = [...nodes] this.draggable?.dispose() this.draggable = new DraggableList(this.innerNodesList, 'li') - this.draggable.addEventListener( - 'dragend', - // @ts-expect-error fixme ts strict error - ({ detail: { oldPosition, newPosition } }) => { - if (oldPosition === newPosition) return - ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]) - for (let i = 0; i < ordered.length; i++) { - this.storeModification({ - nodeIndex: ordered[i].index, - section: ORDER, - prop: 'order', - value: i - }) - } + this.draggable.addEventListener('dragend', (e: Event) => { + const detail = (e as DragEndEvent).detail + const { oldPosition, newPosition } = detail + if (oldPosition === newPosition) return + ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]) + for (let i = 0; i < ordered.length; i++) { + const nodeIndex = ordered[i].index + if (nodeIndex == null) continue + this.storeModification({ + nodeIndex, + section: ORDER, + prop: 'order', + value: i + }) } - ) + }) } storeModification(props: { nodeIndex?: number - section: symbol + section: string | typeof ORDER prop: string - value: any + value: unknown }) { const { nodeIndex, section, prop, value } = props - // @ts-expect-error fixme ts strict error + if (!this.selectedGroup) return + const groupMod = (this.modifications[this.selectedGroup] ??= {}) const nodesMod = (groupMod.nodes ??= {}) - const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {}) - const typeMod = (nodeMod[section] ??= {}) - if (typeof value === 'object') { - const objMod = (typeMod[prop] ??= {}) - Object.assign(objMod, value) + const nodeKey = String(nodeIndex ?? this.selectedNodeInnerIndex) + const nodeMod = (nodesMod[nodeKey] ??= {} as NodeModifications) + + if (section === ORDER) { + nodeMod[ORDER] = { order: value as number } } else { - typeMod[prop] = value + const sectionMod = (nodeMod[section] ??= {}) + if (typeof value === 'object' && value !== null) { + Object.assign(sectionMod, value) + } else { + Object.assign(sectionMod, { [prop]: value }) + } } } - // @ts-expect-error fixme ts strict error - getEditElement(section, prop, value, placeholder, checked, checkable = true) { + getEditElement( + section: string, + prop: string | number, + value: string, + placeholder: string, + checked: boolean, + checkable = true + ) { if (value === placeholder) value = '' - const mods = - // @ts-expect-error fixme ts strict error - this.modifications[this.selectedGroup]?.nodes?.[ - this.selectedNodeInnerIndex - ]?.[section]?.[prop] + const mods = this.selectedGroup + ? this.modifications[this.selectedGroup]?.nodes?.[ + this.selectedNodeInnerIndex + ]?.[section] + : undefined if (mods) { if (mods.name != null) { value = mods.name @@ -238,12 +234,11 @@ export class ManageGroupDialog extends ComfyDialog { value, placeholder, type: 'text', - // @ts-expect-error fixme ts strict error - onchange: (e) => { + onchange: (e: Event) => { this.storeModification({ section, - prop, - value: { name: e.target.value } + prop: String(prop), + value: { name: (e.target as HTMLInputElement).value } }) } }), @@ -252,12 +247,11 @@ export class ManageGroupDialog extends ComfyDialog { type: 'checkbox', checked, disabled: !checkable, - // @ts-expect-error fixme ts strict error - onchange: (e) => { + onchange: (e: Event) => { this.storeModification({ section, - prop, - value: { visible: !!e.target.checked } + prop: String(prop), + value: { visible: !!(e.target as HTMLInputElement).checked } }) } }) @@ -267,17 +261,22 @@ export class ManageGroupDialog extends ComfyDialog { buildWidgetsPage() { const widgets = - this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex] + this.groupData?.oldToNewWidgetMap[this.selectedNodeInnerIndex] const items = Object.keys(widgets ?? {}) - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[this.selectedGroup] - const config = type.config?.[this.selectedNodeInnerIndex]?.input + const type = this.selectedGroup + ? app.rootGraph.extra?.groupNodes?.[this.selectedGroup] + : undefined + const config = ( + type?.config as + | Record }> + | undefined + )?.[this.selectedNodeInnerIndex]?.input this.widgetsPage.replaceChildren( ...items.map((oldName) => { return this.getEditElement( 'input', oldName, - widgets[oldName], + widgets?.[oldName] ?? '', oldName, config?.[oldName]?.visible !== false ) @@ -287,54 +286,68 @@ export class ManageGroupDialog extends ComfyDialog { } buildInputsPage() { - const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex] - const items = Object.keys(inputs ?? {}) - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[this.selectedGroup] - const config = type.config?.[this.selectedNodeInnerIndex]?.input - this.inputsPage.replaceChildren( - // @ts-expect-error fixme ts strict error - ...items - .map((oldName) => { - let value = inputs[oldName] - if (!value) { - return - } + const inputs = this.groupData?.nodeInputs[this.selectedNodeInnerIndex] ?? {} + const items = Object.keys(inputs) + const type = this.selectedGroup + ? app.rootGraph.extra?.groupNodes?.[this.selectedGroup] + : undefined + const config = ( + type?.config as + | Record }> + | undefined + )?.[this.selectedNodeInnerIndex]?.input + const filteredElements = items + .map((oldName) => { + const value = inputs[oldName] + if (!value) { + return null + } - return this.getEditElement( - 'input', - oldName, - value, - oldName, - config?.[oldName]?.visible !== false - ) - }) - .filter(Boolean) - ) + return this.getEditElement( + 'input', + oldName, + value as string, + oldName, + config?.[oldName]?.visible !== false + ) + }) + .filter((el): el is HTMLDivElement => el !== null) + this.inputsPage.replaceChildren(...filteredElements) return !!items.length } buildOutputsPage() { - const nodes = this.groupData.nodeData.nodes - const innerNodeDef = this.groupData.getNodeDef( - nodes[this.selectedNodeInnerIndex] - ) - const outputs = innerNodeDef?.output ?? [] + const nodes = this.groupData?.nodeData.nodes ?? [] + const nodeData = nodes[this.selectedNodeInnerIndex] + const innerNodeDef = nodeData + ? this.groupData?.getNodeDef( + nodeData as Parameters[0] + ) + : undefined + const outputs = (innerNodeDef?.output ?? []) as string[] const groupOutputs = - this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex] + this.groupData?.oldToNewOutputMap[this.selectedNodeInnerIndex] - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[this.selectedGroup] - const config = type.config?.[this.selectedNodeInnerIndex]?.output - const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex] - const checkable = node.type !== 'PrimitiveNode' + const workflowType = this.selectedGroup + ? app.rootGraph.extra?.groupNodes?.[this.selectedGroup] + : undefined + const config = ( + workflowType?.config as + | Record< + number, + { output?: Record } + > + | undefined + )?.[this.selectedNodeInnerIndex]?.output + const node = nodes[this.selectedNodeInnerIndex] + const checkable = node?.type !== 'PrimitiveNode' this.outputsPage.replaceChildren( ...outputs - // @ts-expect-error fixme ts strict error - .map((type, slot) => { + .map((outputType: string, slot: number) => { const groupOutputIndex = groupOutputs?.[slot] - const oldName = innerNodeDef.output_name?.[slot] ?? type - let value = config?.[slot]?.name + const oldName = (innerNodeDef?.output_name?.[slot] ?? + outputType) as string + let value = config?.[slot]?.name ?? '' const visible = config?.[slot]?.visible || groupOutputIndex != null if (!value || value === oldName) { value = '' @@ -353,8 +366,7 @@ export class ManageGroupDialog extends ComfyDialog { return !!outputs.length } - // @ts-expect-error fixme ts strict error - show(type?) { + override show(type?: string) { const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort( (a, b) => a.localeCompare(b) ) @@ -371,24 +383,28 @@ export class ManageGroupDialog extends ComfyDialog { this.outputsPage ]) - this.tabs = [ - ['Inputs', this.inputsPage], - ['Widgets', this.widgetsPage], - ['Outputs', this.outputsPage] - // @ts-expect-error fixme ts strict error - ].reduce((p, [name, page]: [string, HTMLElement]) => { - // @ts-expect-error fixme ts strict error - p[name] = { - tab: $el('a', { - onclick: () => { - this.changeTab(name) - }, - textContent: name - }), - page - } - return p - }, {}) as any + type TabName = keyof ManageGroupDialog['tabs'] + this.tabs = ( + [ + ['Inputs', this.inputsPage], + ['Widgets', this.widgetsPage], + ['Outputs', this.outputsPage] + ] as [TabName, HTMLElement][] + ).reduce( + (p, [name, page]) => { + p[name] = { + tab: $el('a', { + onclick: () => { + this.changeTab(name) + }, + textContent: name + }) as HTMLAnchorElement, + page + } + return p + }, + {} as ManageGroupDialog['tabs'] + ) const outer = $el('div.comfy-group-manage-outer', [ $el('header', [ @@ -396,9 +412,8 @@ export class ManageGroupDialog extends ComfyDialog { $el( 'select', { - // @ts-expect-error fixme ts strict error - onchange: (e) => { - this.changeGroup(e.target.value) + onchange: (e: Event) => { + this.changeGroup((e.target as HTMLSelectElement).value) } }, groupNodes.map((g) => @@ -439,8 +454,9 @@ export class ManageGroupDialog extends ComfyDialog { `Are you sure you want to remove the node: "${this.selectedGroup}"` ) ) { - // @ts-expect-error fixme ts strict error - delete app.rootGraph.extra.groupNodes[this.selectedGroup] + if (this.selectedGroup && app.rootGraph.extra?.groupNodes) { + delete app.rootGraph.extra.groupNodes[this.selectedGroup] + } LiteGraph.unregisterNodeType( `${PREFIX}${SEPARATOR}` + this.selectedGroup ) @@ -454,97 +470,105 @@ export class ManageGroupDialog extends ComfyDialog { 'button.comfy-btn', { onclick: async () => { - let nodesByType - let recreateNodes = [] - const types = {} + let nodesByType: Record | null = null + const recreateNodes: LGraphNode[] = [] + const types: Record = {} for (const g in this.modifications) { - // @ts-expect-error fixme ts strict error - const type = app.rootGraph.extra.groupNodes[g] - let config = (type.config ??= {}) + const groupNodeData = app.rootGraph.extra?.groupNodes?.[g] + if (!groupNodeData) continue + + let config = (groupNodeData.config ??= {}) as Record< + number, + unknown + > let nodeMods = this.modifications[g]?.nodes if (nodeMods) { const keys = Object.keys(nodeMods) - // @ts-expect-error fixme ts strict error - if (nodeMods[keys[0]][ORDER]) { + const firstMod = nodeMods[keys[0]] + if (firstMod?.[ORDER]) { // If any node is reordered, they will all need sequencing - const orderedNodes = [] - const orderedMods = {} - const orderedConfig = {} + const orderedNodes: typeof groupNodeData.nodes = [] + const orderedMods: Record = {} + const orderedConfig: Record = {} for (const n of keys) { - // @ts-expect-error fixme ts strict error - const order = nodeMods[n][ORDER].order - orderedNodes[order] = type.nodes[+n] - // @ts-expect-error fixme ts strict error + const order = nodeMods[n]?.[ORDER]?.order ?? 0 + orderedNodes[order] = groupNodeData.nodes[+n] orderedMods[order] = nodeMods[n] orderedNodes[order].index = order } // Rewrite links - for (const l of type.links) { - // @ts-expect-error l[0]/l[2] used as node index - if (l[0] != null) l[0] = type.nodes[l[0]].index - // @ts-expect-error l[0]/l[2] used as node index - if (l[2] != null) l[2] = type.nodes[l[2]].index + for (const l of groupNodeData.links) { + const srcIdx = l[1] + const tgtIdx = l[3] + if (srcIdx != null) + l[1] = + groupNodeData.nodes[srcIdx as number]?.index ?? srcIdx + if (tgtIdx != null) + l[3] = + groupNodeData.nodes[tgtIdx as number]?.index ?? tgtIdx } // Rewrite externals - if (type.external) { - for (const ext of type.external) { + if (groupNodeData.external) { + for (const ext of groupNodeData.external) { if (ext[0] != null) { - // @ts-expect-error ext[0] used as node index - ext[0] = type.nodes[ext[0]].index + ext[0] = + groupNodeData.nodes[ext[0] as number]?.index ?? + ext[0] } } } // Rewrite modifications for (const id of keys) { - // @ts-expect-error id used as node index - if (config[id]) { - // @ts-expect-error fixme ts strict error - orderedConfig[type.nodes[id].index] = config[id] + const nodeIdx = +id + if (config[nodeIdx]) { + const newIdx = + groupNodeData.nodes[nodeIdx]?.index ?? nodeIdx + orderedConfig[newIdx] = config[nodeIdx] } - // @ts-expect-error id used as config key - delete config[id] + delete config[nodeIdx] } - type.nodes = orderedNodes + groupNodeData.nodes = orderedNodes nodeMods = orderedMods - type.config = config = orderedConfig + groupNodeData.config = config = orderedConfig } - merge(config, nodeMods) + merge(config, nodeMods as Record) } - // @ts-expect-error fixme ts strict error - types[g] = type + types[g] = groupNodeData if (!nodesByType) { - nodesByType = app.rootGraph.nodes.reduce((p, n) => { - // @ts-expect-error fixme ts strict error - p[n.type] ??= [] - // @ts-expect-error fixme ts strict error - p[n.type].push(n) - return p - }, {}) + nodesByType = app.rootGraph.nodes.reduce( + (p, n) => { + const nodeType = n.type ?? '' + ;(p[nodeType] ??= []).push(n) + return p + }, + {} as Record + ) } - // @ts-expect-error fixme ts strict error - const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g] - if (nodes) recreateNodes.push(...nodes) + const groupTypeNodes = nodesByType[`${PREFIX}${SEPARATOR}` + g] + if (groupTypeNodes) recreateNodes.push(...groupTypeNodes) } await GroupNodeConfig.registerFromWorkflow(types, []) for (const node of recreateNodes) { - node.recreate() + ;(node as LGraphNode & { recreate?: () => void }).recreate?.() } this.modifications = {} this.app.canvas.setDirty(true, true) - this.changeGroup(this.selectedGroup, false) + if (this.selectedGroup) { + this.changeGroup(this.selectedGroup, false) + } } }, 'Save' diff --git a/src/extensions/core/groupNodeTypes.ts b/src/extensions/core/groupNodeTypes.ts new file mode 100644 index 000000000..d24b83c25 --- /dev/null +++ b/src/extensions/core/groupNodeTypes.ts @@ -0,0 +1,47 @@ +import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink' +import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation' + +/** Serialized node data within a group node workflow, with group-specific index */ +export interface GroupNodeSerializedNode extends Partial { + /** Position of this node within the group */ + index?: number +} + +export interface GroupNodeWorkflowData { + external: (number | string)[][] + links: SerialisedLLinkArray[] + nodes: GroupNodeSerializedNode[] + config?: Record +} + +/** + * Input config tuple type for group nodes. + * First element is the input type name (e.g. 'INT', 'FLOAT', 'MODEL', etc.) + * Second element (optional) is the input options object. + */ +export type GroupNodeInputConfig = [string, Record?] + +/** + * Mutable inputs specification for group nodes that are built dynamically. + * Uses a more permissive type than ComfyInputsSpec to allow dynamic assignment. + */ +export interface GroupNodeInputsSpec { + required: Record + optional?: Record +} + +/** + * Output type for group nodes - can be a type string or an array of combo options. + */ +export type GroupNodeOutputType = string | (string | number)[] + +/** + * Partial link info used internally by group node getInputLink override. + * Contains only the properties needed for group node execution context. + */ +export interface PartialLinkInfo { + origin_id: string | number + origin_slot: number | string + target_id: string | number + target_slot: number +} diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index 32494c615..71dfef258 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -15,7 +15,7 @@ import { LGraphGroup } from './LGraphGroup' import { LGraphNode } from './LGraphNode' import type { NodeId } from './LGraphNode' import { LLink } from './LLink' -import type { LinkId, SerialisedLLinkArray } from './LLink' +import type { LinkId } from './LLink' import { MapProxyHandler } from './MapProxyHandler' import { Reroute } from './Reroute' import type { RerouteId } from './Reroute' @@ -63,6 +63,7 @@ import type { LGraphTriggerHandler, LGraphTriggerParam } from './types/graphTriggers' +import type { GroupNodeWorkflowData } from '@/extensions/core/groupNodeTypes' import type { ExportedSubgraph, ExposedWidget, @@ -74,6 +75,8 @@ import type { } from './types/serialisation' import { getAllNestedItems } from './utils/collections' +export type { GroupNodeWorkflowData } from '@/extensions/core/groupNodeTypes' + export type { LGraphTriggerAction, LGraphTriggerParam @@ -102,18 +105,6 @@ export interface LGraphConfig { links_ontop?: any } -export interface GroupNodeWorkflowData { - external: (number | string)[][] - links: SerialisedLLinkArray[] - nodes: { - index?: number - type?: string - inputs?: unknown[] - outputs?: unknown[] - }[] - config?: Record -} - export interface LGraphExtra extends Dictionary { reroutes?: SerialisableReroute[] linkExtensions?: { id: number; parentId: number | undefined }[] diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index 0b24eb47b..2e7baca1a 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -103,10 +103,12 @@ export type { Size } from './interfaces' export { + type GroupNodeWorkflowData, LGraph, type LGraphTriggerAction, type LGraphTriggerParam } from './LGraph' + export type { LGraphTriggerEvent } from './types/graphTriggers' export { BadgePosition, LGraphBadge } from './LGraphBadge' export { LGraphCanvas } from './LGraphCanvas' diff --git a/src/types/litegraph-augmentation.d.ts b/src/types/litegraph-augmentation.d.ts index 0b283b158..35ee4158b 100644 --- a/src/types/litegraph-augmentation.d.ts +++ b/src/types/litegraph-augmentation.d.ts @@ -113,7 +113,7 @@ declare module '@/lib/litegraph/src/litegraph' { ): ExecutableLGraphNode[] /** @deprecated groupNode */ convertToNodes?(): LGraphNode[] - recreate?(): Promise + recreate?(): Promise refreshComboInNode?(defs: Record) /** @deprecated groupNode */ updateLink?(link: LLink): LLink | null @@ -143,6 +143,8 @@ declare module '@/lib/litegraph/src/litegraph' { index?: number runningInternalNodeId?: NodeId + /** @deprecated Used by PrimitiveNode for group node value propagation */ + primitiveValue?: unknown comfyClass?: string