From f2a0e5102eb822710bc8dd246f973a5356c5a716 Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Thu, 11 Dec 2025 22:37:34 -0800 Subject: [PATCH] Cleanup app.graph usage (#7399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to the release of subgraphs, there was a single graph accessed through `app.graph`. Now that there's multiple graphs, there's a lot of code that needs to be reviewed and potentially updated depending on if it cares about nearby nodes, all nodes, or something else requiring specific attention. This was done by simply changing the type of `app.graph` to unknown so the typechecker will complain about every place it's currently used. References were then updated to `app.rootGraph` if the previous usage was correct, or actually rewritten. By not getting rid of `app.graph`, this change already ensures that there's no loss of functionality for custom nodes, but the prior typing of `app.graph` can always be restored if future dissuasion of `app.graph` usage creates issues. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7399-Cleanup-app-graph-usage-2c76d73d365081178743dfdcf07f44d0) by [Unito](https://www.unito.io) --- .../ComfyRunButton/ComfyQueueButton.vue | 2 +- .../breadcrumb/SubgraphBreadcrumbItem.vue | 2 +- .../dialog/content/ErrorDialogContent.vue | 2 +- src/components/graph/GraphCanvas.vue | 31 +++--- src/components/graph/TitleEditor.vue | 2 +- .../toast/RerouteMigrationToast.vue | 2 +- .../maskeditor/useMaskEditorSaver.ts | 4 +- src/composables/node/useNodeBadge.ts | 2 +- src/composables/useCanvasDrop.ts | 2 +- src/composables/useCoreCommands.ts | 6 +- src/extensions/core/groupNode.ts | 50 +++++----- src/extensions/core/groupNodeManage.ts | 18 ++-- src/extensions/core/nodeTemplates.ts | 2 +- src/extensions/core/uploadAudio.ts | 2 +- src/extensions/core/webcamCapture.ts | 2 +- .../cloud/MixpanelTelemetryProvider.ts | 2 +- .../management/stores/workflowStore.ts | 9 +- .../composables/useWorkflowPersistence.ts | 2 +- .../vueNodes/components/LGraphNode.vue | 6 +- .../vueNodes/components/NodeHeader.vue | 2 +- .../widgets/components/WidgetAudioUI.vue | 4 +- .../widgets/components/WidgetRecordAudio.vue | 4 +- .../components/audio/AudioPreviewPlayer.vue | 4 +- src/scripts/app.ts | 94 +++++++++---------- src/scripts/changeTracker.ts | 6 +- src/scripts/utils.ts | 2 +- src/services/litegraphService.ts | 8 +- src/services/subgraphService.ts | 4 +- src/stores/executionStore.ts | 13 ++- src/stores/subgraphNavigationStore.ts | 2 +- src/types/index.ts | 3 +- src/views/LinearView.vue | 2 +- .../composables/nodePack/useMissingNodes.ts | 2 +- .../composables/nodePack/useWorkflowPacks.ts | 12 +-- .../tests/composables/useCoreCommands.test.ts | 8 +- .../tests/composables/useMissingNodes.test.ts | 39 +++----- .../components/NodeHeader.subgraph.test.ts | 23 +++-- tests-ui/tests/store/executionStore.test.ts | 19 ++-- tests-ui/tests/store/workflowStore.test.ts | 2 +- 39 files changed, 192 insertions(+), 209 deletions(-) diff --git a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue index c9e8495549..036d575bf5 100644 --- a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue +++ b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue @@ -58,7 +58,7 @@ const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore()) const nodeDefStore = useNodeDefStore() const hasMissingNodes = computed(() => - graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) + graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName) ) const { t } = useI18n() diff --git a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue index c743f0d988..8ffbd1e792 100644 --- a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue +++ b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue @@ -83,7 +83,7 @@ const props = withDefaults(defineProps(), { const nodeDefStore = useNodeDefStore() const hasMissingNodes = computed(() => - graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) + graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName) ) const { t } = useI18n() diff --git a/src/components/dialog/content/ErrorDialogContent.vue b/src/components/dialog/content/ErrorDialogContent.vue index d51f1bb94f..93de7cbf90 100644 --- a/src/components/dialog/content/ErrorDialogContent.vue +++ b/src/components/dialog/content/ErrorDialogContent.vue @@ -128,7 +128,7 @@ onMounted(async () => { reportContent.value = generateErrorReport({ systemStats: systemStatsStore.systemStats!, serverLogs: logs, - workflow: app.graph.serialize(), + workflow: app.rootGraph.serialize(), exceptionType: error.exceptionType, exceptionMessage: error.exceptionMessage, traceback: error.traceback, diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index e0b7d05a6b..67d3ed3919 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -159,6 +159,7 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore' import { useWorkspaceStore } from '@/stores/workspaceStore' import { isNativeWindow } from '@/utils/envUtil' +import { forEachNode } from '@/utils/graphTraversalUtil' import SelectionRectangle from './SelectionRectangle.vue' @@ -269,20 +270,18 @@ watch( () => { if (!canvasStore.canvas) return - for (const n of comfyApp.graph.nodes) { - if (!n.widgets) continue + forEachNode(comfyApp.rootGraph, (n) => { + if (!n.widgets) return for (const w of n.widgets) { - if (w[IS_CONTROL_WIDGET]) { - updateControlWidgetLabel(w) - if (w.linkedWidgets) { - for (const l of w.linkedWidgets) { - updateControlWidgetLabel(l) - } - } + if (!w[IS_CONTROL_WIDGET]) continue + updateControlWidgetLabel(w) + if (!w.linkedWidgets) continue + for (const l of w.linkedWidgets) { + updateControlWidgetLabel(l) } } - } - comfyApp.graph.setDirtyCanvas(true) + }) + canvasStore.canvas.setDirty(true) } ) @@ -332,7 +331,7 @@ watch( } // Force canvas redraw to ensure progress updates are visible - canvas.graph.setDirtyCanvas(true, false) + canvas.setDirty(true, false) }, { deep: true } ) @@ -344,7 +343,7 @@ watch( (lastNodeErrors) => { if (!comfyApp.graph) return - for (const node of comfyApp.graph.nodes) { + forEachNode(comfyApp.rootGraph, (node) => { // Clear existing errors for (const slot of node.inputs) { delete slot.hasErrors @@ -354,7 +353,7 @@ watch( } const nodeErrors = lastNodeErrors?.[node.id] - if (!nodeErrors) continue + if (!nodeErrors) return const validErrors = nodeErrors.errors.filter( (error) => error.extra_info?.input_name !== undefined @@ -367,9 +366,9 @@ watch( node.inputs[inputIndex].hasErrors = true } }) - } + }) - comfyApp.canvas.draw(true, true) + comfyApp.canvas.setDirty(true, true) } ) diff --git a/src/components/graph/TitleEditor.vue b/src/components/graph/TitleEditor.vue index cf95c0ab5b..bcfef87578 100644 --- a/src/components/graph/TitleEditor.vue +++ b/src/components/graph/TitleEditor.vue @@ -58,7 +58,7 @@ const onEdit = (newValue: string) => { target.subgraph.name = trimmedTitle } - app.graph.setDirtyCanvas(true, true) + app.canvas.setDirty(true, true) } showInput.value = false titleEditorStore.titleEditorTarget = null diff --git a/src/components/toast/RerouteMigrationToast.vue b/src/components/toast/RerouteMigrationToast.vue index c084926d15..8c6fd4edb0 100644 --- a/src/components/toast/RerouteMigrationToast.vue +++ b/src/components/toast/RerouteMigrationToast.vue @@ -33,7 +33,7 @@ const toast = useToast() const workflowStore = useWorkflowStore() const migrateToLitegraphReroute = async () => { - const workflowJSON = app.graph.serialize() as unknown as WorkflowJSON04 + const workflowJSON = app.rootGraph.serialize() as unknown as WorkflowJSON04 const migratedWorkflowJSON = migrateLegacyRerouteNodes(workflowJSON) await app.loadGraphData( migratedWorkflowJSON, diff --git a/src/composables/maskeditor/useMaskEditorSaver.ts b/src/composables/maskeditor/useMaskEditorSaver.ts index 4af771399e..8aa6bb0da8 100644 --- a/src/composables/maskeditor/useMaskEditorSaver.ts +++ b/src/composables/maskeditor/useMaskEditorSaver.ts @@ -51,7 +51,7 @@ export function useMaskEditorSaver() { updateNodeWithServerReferences(sourceNode, outputData) - app.graph.setDirtyCanvas(true) + app.canvas.setDirty(true) } catch (error) { console.error('[MaskEditorSaver] Save failed:', error) throw error @@ -308,7 +308,7 @@ export function useMaskEditorSaver() { const mainImg = await loadImageFromUrl(dataUrl) node.imgs = [mainImg] - app.graph.setDirtyCanvas(true) + app.canvas.setDirty(true) } function updateNodeWithServerReferences( diff --git a/src/composables/node/useNodeBadge.ts b/src/composables/node/useNodeBadge.ts index 30c4487a38..2f31ad0ac5 100644 --- a/src/composables/node/useNodeBadge.ts +++ b/src/composables/node/useNodeBadge.ts @@ -55,7 +55,7 @@ export const useNodeBadge = () => { showApiPricingBadge ], () => { - app.graph?.setDirtyCanvas(true, true) + app.canvas?.setDirty(true, true) } ) diff --git a/src/composables/useCanvasDrop.ts b/src/composables/useCanvasDrop.ts index d3870b04b7..51379e98b4 100644 --- a/src/composables/useCanvasDrop.ts +++ b/src/composables/useCanvasDrop.ts @@ -41,7 +41,7 @@ export const useCanvasDrop = (canvasRef: Ref) => { } else if (node.data instanceof ComfyModelDef) { const model = node.data const pos = basePos - const nodeAtPos = comfyApp.graph.getNodeOnPos(pos[0], pos[1]) + const nodeAtPos = comfyApp.canvas.graph?.getNodeOnPos(pos[0], pos[1]) let targetProvider: ModelNodeProvider | null = null let targetGraphNode: LGraphNode | null = null if (nodeAtPos) { diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index cbbb6f06cd..843e9faade 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -104,7 +104,7 @@ export function useCoreCommands(): ComfyCommand[] { menubarLabel: 'New', category: 'essentials' as const, function: async () => { - const previousWorkflowHadNodes = app.graph._nodes.length > 0 + const previousWorkflowHadNodes = app.rootGraph._nodes.length > 0 await workflowService.loadBlankWorkflow() telemetry?.trackWorkflowCreated({ workflow_type: 'blank', @@ -127,7 +127,7 @@ export function useCoreCommands(): ComfyCommand[] { icon: 'pi pi-code', label: 'Load Default Workflow', function: async () => { - const previousWorkflowHadNodes = app.graph._nodes.length > 0 + const previousWorkflowHadNodes = app.rootGraph._nodes.length > 0 await workflowService.loadDefaultWorkflow() telemetry?.trackWorkflowCreated({ workflow_type: 'default', @@ -705,7 +705,7 @@ export function useCoreCommands(): ComfyCommand[] { 'Comfy.GroupSelectedNodes.Padding' ) group.resizeTo(group.children, padding) - app.graph.change() + app.canvas.setDirty(false, true) } } } diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index 5b4146c2d8..cbc8d25654 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -47,8 +47,8 @@ const Workflow = { const id = `${PREFIX}${SEPARATOR}${name}` // Check if lready registered/in use in this workflow // @ts-expect-error fixme ts strict error - if (app.graph.extra?.groupNodes?.[name]) { - if (app.graph.nodes.find((n) => n.type === id)) { + if (app.rootGraph.extra?.groupNodes?.[name]) { + if (app.rootGraph.nodes.find((n) => n.type === id)) { return Workflow.InUse.InWorkflow } else { return Workflow.InUse.Registered @@ -57,8 +57,8 @@ const Workflow = { return Workflow.InUse.Free }, storeGroupNode(name: string, data: GroupNodeWorkflowData) { - let extra = app.graph.extra - if (!extra) app.graph.extra = extra = {} + let extra = app.rootGraph.extra + if (!extra) app.rootGraph.extra = extra = {} let groupNodes = extra.groupNodes if (!groupNodes) extra.groupNodes = groupNodes = {} // @ts-expect-error fixme ts strict error @@ -118,7 +118,7 @@ class GroupNodeBuilder { sortNodes() { // Gets the builders nodes in graph execution order - const nodesInOrder = app.graph.computeExecutionOrder(false) + const nodesInOrder = app.rootGraph.computeExecutionOrder(false) this.nodes = this.nodes .map((node) => ({ index: nodesInOrder.indexOf(node), node })) // @ts-expect-error id might be string @@ -131,7 +131,7 @@ class GroupNodeBuilder { const storeLinkTypes = (config) => { // Store link types for dynamically typed nodes e.g. reroutes for (const link of config.links) { - const origin = app.graph.getNodeById(link[4]) + const origin = app.rootGraph.getNodeById(link[4]) // @ts-expect-error fixme ts strict error const type = origin.outputs[link[1]].type link.push(type) @@ -151,7 +151,7 @@ class GroupNodeBuilder { let type = output.type if (!output.links?.length) continue for (const l of output.links) { - const link = app.graph.links[l] + const link = app.rootGraph.links[l] if (!link) continue if (type === '*') type = link.type @@ -853,7 +853,7 @@ export class GroupNodeHandler { // The inner node is connected via the group node inputs const linkId = this.node.inputs[externalSlot].link // @ts-expect-error fixme ts strict error - let link = app.graph.links[linkId] + let link = app.rootGraph.links[linkId] // Use the outer link, but update the target to the inner node link = { @@ -980,7 +980,7 @@ export class GroupNodeHandler { // @ts-expect-error fixme ts strict error groupNode[GROUP].populateWidgets() // @ts-expect-error fixme ts strict error - app.graph.add(groupNode) + app.rootGraph.add(groupNode) // @ts-expect-error fixme ts strict error groupNode.setSize([ // @ts-expect-error fixme ts strict error @@ -1032,7 +1032,7 @@ export class GroupNodeHandler { const newNodes = [] for (let i = 0; i < selectedIds.length; i++) { const id = selectedIds[i] - const newNode = app.graph.getNodeById(id) + const newNode = app.rootGraph.getNodeById(id) const innerNode = innerNodes[i] newNodes.push(newNode) @@ -1111,17 +1111,17 @@ export class GroupNodeHandler { const reconnectInputs = (selectedIds) => { for (const innerNodeIndex in this.groupData.oldToNewInputMap) { const id = selectedIds[innerNodeIndex] - const newNode = app.graph.getNodeById(id) + const newNode = app.rootGraph.getNodeById(id) const map = this.groupData.oldToNewInputMap[innerNodeIndex] for (const innerInputId in map) { const groupSlotId = map[innerInputId] if (groupSlotId == null) continue const slot = node.inputs[groupSlotId] if (slot.link == null) continue - const link = app.graph.links[slot.link] + const link = app.rootGraph.links[slot.link] if (!link) continue // connect this node output to the input of another node - const originNode = app.graph.getNodeById(link.origin_id) + const originNode = app.rootGraph.getNodeById(link.origin_id) // @ts-expect-error fixme ts strict error originNode.connect(link.origin_slot, newNode, +innerInputId) } @@ -1140,9 +1140,11 @@ export class GroupNodeHandler { const links = [...output.links] for (const l of links) { const slot = this.groupData.newToOldOutputMap[groupOutputId] - const link = app.graph.links[l] - const targetNode = app.graph.getNodeById(link.target_id) - const newNode = app.graph.getNodeById(selectedIds[slot.node.index]) + const link = app.rootGraph.links[l] + const targetNode = app.rootGraph.getNodeById(link.target_id) + const newNode = app.rootGraph.getNodeById( + selectedIds[slot.node.index] + ) // @ts-expect-error fixme ts strict error newNode.connect(slot.slot, targetNode, link.target_slot) } @@ -1155,7 +1157,7 @@ export class GroupNodeHandler { const { newNodes, selectedIds } = addInnerNodes() reconnectInputs(selectedIds) reconnectOutputs(selectedIds) - app.graph.remove(this.node) + app.rootGraph.remove(this.node) return newNodes } finally { @@ -1291,7 +1293,7 @@ export class GroupNodeHandler { const handler = ({ detail }) => { const id = getId(detail) if (!id) return - const node = app.graph.getNodeById(id) + const node = app.rootGraph.getNodeById(id) if (node) return // @ts-expect-error fixme ts strict error @@ -1546,7 +1548,7 @@ export class GroupNodeHandler { } this.linkOutputs(node, i) - app.graph.remove(node) + app.rootGraph.remove(node) // Set internal ID to what is expected after workflow is reloaded node.id = `${this.node.id}:${i}` @@ -1565,10 +1567,10 @@ export class GroupNodeHandler { // Clone the links as they'll be changed if we reconnect const links = [...output.links] for (const l of links) { - const link = app.graph.links[l] + const link = app.rootGraph.links[l] if (!link) continue - const targetNode = app.graph.getNodeById(link.target_id) + const targetNode = app.rootGraph.getNodeById(link.target_id) const newSlot = this.groupData.oldToNewOutputMap[nodeId]?.[link.origin_slot] if (newSlot != null) { @@ -1582,7 +1584,7 @@ export class GroupNodeHandler { linkInputs() { for (const link of this.groupData.nodeData.links ?? []) { const [, originSlot, targetId, targetSlot, actualOriginId] = link - const originNode = app.graph.getNodeById(actualOriginId) + const originNode = app.rootGraph.getNodeById(actualOriginId) if (!originNode) continue // this node is in the group originNode.connect( originSlot, @@ -1621,7 +1623,7 @@ export class GroupNodeHandler { // @ts-expect-error fixme ts strict error groupNode[GROUP].populateWidgets() // @ts-expect-error fixme ts strict error - app.graph.add(groupNode) + app.rootGraph.add(groupNode) // Remove all converted nodes and relink them // @ts-expect-error fixme ts strict error @@ -1802,7 +1804,7 @@ const ext: ComfyExtension = { // Re-register group nodes so new ones are created with the correct options // @ts-expect-error fixme ts strict error Object.assign(globalDefs, defs) - const nodes = app.graph.extra?.groupNodes + const nodes = app.rootGraph.extra?.groupNodes if (nodes) { await GroupNodeConfig.registerFromWorkflow(nodes, {}) } diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts index 8e52ffccc3..7ea480614b 100644 --- a/src/extensions/core/groupNodeManage.ts +++ b/src/extensions/core/groupNodeManage.ts @@ -270,7 +270,7 @@ export class ManageGroupDialog extends ComfyDialog { this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex] const items = Object.keys(widgets ?? {}) // @ts-expect-error fixme ts strict error - const type = app.graph.extra.groupNodes[this.selectedGroup] + const type = app.rootGraph.extra.groupNodes[this.selectedGroup] const config = type.config?.[this.selectedNodeInnerIndex]?.input this.widgetsPage.replaceChildren( ...items.map((oldName) => { @@ -290,7 +290,7 @@ export class ManageGroupDialog extends ComfyDialog { const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex] const items = Object.keys(inputs ?? {}) // @ts-expect-error fixme ts strict error - const type = app.graph.extra.groupNodes[this.selectedGroup] + 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 @@ -324,7 +324,7 @@ export class ManageGroupDialog extends ComfyDialog { this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex] // @ts-expect-error fixme ts strict error - const type = app.graph.extra.groupNodes[this.selectedGroup] + 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' @@ -355,7 +355,7 @@ export class ManageGroupDialog extends ComfyDialog { // @ts-expect-error fixme ts strict error show(type?) { - const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort( + const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort( (a, b) => a.localeCompare(b) ) @@ -425,7 +425,7 @@ export class ManageGroupDialog extends ComfyDialog { 'button.comfy-btn', { onclick: () => { - const node = app.graph.nodes.find( + const node = app.rootGraph.nodes.find( (n) => n.type === `${PREFIX}${SEPARATOR}` + this.selectedGroup ) if (node) { @@ -440,7 +440,7 @@ export class ManageGroupDialog extends ComfyDialog { ) ) { // @ts-expect-error fixme ts strict error - delete app.graph.extra.groupNodes[this.selectedGroup] + delete app.rootGraph.extra.groupNodes[this.selectedGroup] LiteGraph.unregisterNodeType( `${PREFIX}${SEPARATOR}` + this.selectedGroup ) @@ -459,7 +459,7 @@ export class ManageGroupDialog extends ComfyDialog { const types = {} for (const g in this.modifications) { // @ts-expect-error fixme ts strict error - const type = app.graph.extra.groupNodes[g] + const type = app.rootGraph.extra.groupNodes[g] let config = (type.config ??= {}) let nodeMods = this.modifications[g]?.nodes @@ -515,7 +515,7 @@ export class ManageGroupDialog extends ComfyDialog { types[g] = type if (!nodesByType) { - nodesByType = app.graph.nodes.reduce((p, n) => { + nodesByType = app.rootGraph.nodes.reduce((p, n) => { // @ts-expect-error fixme ts strict error p[n.type] ??= [] // @ts-expect-error fixme ts strict error @@ -536,7 +536,7 @@ export class ManageGroupDialog extends ComfyDialog { } this.modifications = {} - this.app.graph.setDirtyCanvas(true, true) + this.app.canvas.setDirty(true, true) this.changeGroup(this.selectedGroup, false) } }, diff --git a/src/extensions/core/nodeTemplates.ts b/src/extensions/core/nodeTemplates.ts index c16ebed72d..457f54c482 100644 --- a/src/extensions/core/nodeTemplates.ts +++ b/src/extensions/core/nodeTemplates.ts @@ -367,7 +367,7 @@ const ext: ComfyExtension = { data = JSON.parse(data || '{}') const nodeIds = Object.keys(app.canvas.selected_nodes) for (let i = 0; i < nodeIds.length; i++) { - const node = app.graph.getNodeById(nodeIds[i]) + const node = app.canvas.graph?.getNodeById(nodeIds[i]) const nodeData = node?.constructor.nodeData let groupData = GroupNodeHandler.getGroupData(node) diff --git a/src/extensions/core/uploadAudio.ts b/src/extensions/core/uploadAudio.ts index dfefc88a0f..71c2295d10 100644 --- a/src/extensions/core/uploadAudio.ts +++ b/src/extensions/core/uploadAudio.ts @@ -142,7 +142,7 @@ app.registerExtension({ onNodeOutputsUpdated(nodeOutputs: Record) { for (const [nodeLocatorId, output] of Object.entries(nodeOutputs)) { if ('audio' in output) { - const node = getNodeByLocatorId(app.graph, nodeLocatorId) + const node = getNodeByLocatorId(app.rootGraph, nodeLocatorId) if (!node) continue // @ts-expect-error fixme ts strict error diff --git a/src/extensions/core/webcamCapture.ts b/src/extensions/core/webcamCapture.ts index f429ddda49..6c02229c5f 100644 --- a/src/extensions/core/webcamCapture.ts +++ b/src/extensions/core/webcamCapture.ts @@ -97,7 +97,7 @@ app.registerExtension({ const img = new Image() img.onload = () => { node.imgs = [img] - app.graph.setDirtyCanvas(true) + app.canvas.setDirty(true) } img.src = data } diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index d683288c41..86b3a5553f 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -433,7 +433,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { } const nodeCounts = reduceAllNodes( - app.graph, + app.rootGraph, (metrics, node) => { const nodeDef = nodeDefStore.nodeDefsByName[node.type] const isCustomNode = diff --git a/src/platform/workflow/management/stores/workflowStore.ts b/src/platform/workflow/management/stores/workflowStore.ts index e821d9c75e..4b8b64f3db 100644 --- a/src/platform/workflow/management/stores/workflowStore.ts +++ b/src/platform/workflow/management/stores/workflowStore.ts @@ -633,6 +633,7 @@ export const useWorkflowStore = defineStore('workflow', () => { return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs) } + //FIXME: use existing util function const executionIdToCurrentId = (id: string) => { const subgraph = activeSubgraph.value @@ -647,7 +648,7 @@ export const useWorkflowStore = defineStore('workflow', () => { const subgraphNodeIds = id.split(':') // Start from the root graph - const { graph } = comfyApp + const graph = comfyApp.rootGraph // If the last subgraph is the active subgraph, return the node ID const subgraphs = getSubgraphsFromInstanceIds(graph, subgraphNodeIds) @@ -714,7 +715,7 @@ export const useWorkflowStore = defineStore('workflow', () => { try { const subgraphs = getSubgraphsFromInstanceIds( - comfyApp.graph, + comfyApp.rootGraph, subgraphNodeIds.map((id) => String(id)) ) const immediateSubgraph = subgraphs[subgraphs.length - 1] @@ -779,7 +780,7 @@ export const useWorkflowStore = defineStore('workflow', () => { return null } - const path = findSubgraphPath(comfyApp.graph, subgraphUuid) + const path = findSubgraphPath(comfyApp.rootGraph, subgraphUuid) if (!path) return null // If we have a target subgraph, check if the path goes through it @@ -787,7 +788,7 @@ export const useWorkflowStore = defineStore('workflow', () => { targetSubgraph && !path.some((_, idx) => { const subgraphs = getSubgraphsFromInstanceIds( - comfyApp.graph, + comfyApp.rootGraph, path.slice(0, idx + 1).map((id) => String(id)) ) return subgraphs[subgraphs.length - 1] === targetSubgraph diff --git a/src/platform/workflow/persistence/composables/useWorkflowPersistence.ts b/src/platform/workflow/persistence/composables/useWorkflowPersistence.ts index a297b2a79f..09ffd58950 100644 --- a/src/platform/workflow/persistence/composables/useWorkflowPersistence.ts +++ b/src/platform/workflow/persistence/composables/useWorkflowPersistence.ts @@ -44,7 +44,7 @@ export function useWorkflowPersistence() { const persistCurrentWorkflow = () => { if (!workflowPersistenceEnabled.value) return - const workflow = JSON.stringify(comfyApp.graph.serialize()) + const workflow = JSON.stringify(comfyApp.rootGraph.serialize()) try { localStorage.setItem('workflow', workflow) diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 3367753f90..aa5f41cf3c 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -436,7 +436,7 @@ const handleEnterSubgraph = () => { useTelemetry()?.trackUiButtonClicked({ button_id: 'graph_node_open_subgraph_clicked' }) - const graph = app.graph?.rootGraph || app.graph + const graph = app.rootGraph if (!graph) { console.warn('LGraphNode: No graph available for subgraph navigation') return @@ -468,9 +468,7 @@ const nodeOutputLocatorId = computed(() => const lgraphNode = computed(() => { const locatorId = getLocatorIdFromNodeData(nodeData) - const rootGraph = app.graph?.rootGraph || app.graph - if (!rootGraph) return null - return getNodeByLocatorId(rootGraph, locatorId) + return getNodeByLocatorId(app.rootGraph, locatorId) }) const nodeMedia = computed(() => { diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue index 0df7acfabc..a6b8cdd19a 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -243,7 +243,7 @@ const isSubgraphNode = computed(() => { if (!nodeData?.id) return false // Get the underlying LiteGraph node - const graph = app.graph?.rootGraph || app.graph + const graph = app.rootGraph if (!graph) return false const locatorId = getLocatorIdFromNodeData(nodeData) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetAudioUI.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetAudioUI.vue index 2c54ed68ab..7b46670d21 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetAudioUI.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetAudioUI.vue @@ -39,8 +39,8 @@ defineEmits<{ // Get litegraph node const litegraphNode = computed(() => { - if (!props.nodeId || !app.rootGraph) return null - return app.rootGraph.getNodeById(props.nodeId) as LGraphNode | null + if (!props.nodeId || !app.canvas.graph) return null + return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null }) // Check if this is an output node (PreviewAudio, SaveAudio, etc) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue index 73c37111ff..aa0fc4df32 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue @@ -155,8 +155,8 @@ const isWaveformActive = computed(() => isRecording.value || isPlaying.value) const modelValue = defineModel({ default: '' }) const litegraphNode = computed(() => { - if (!props.nodeId || !app.rootGraph) return null - return app.rootGraph.getNodeById(props.nodeId) as LGraphNode | null + if (!props.nodeId || !app.canvas.graph) return null + return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null }) async function handleRecordingComplete(blob: Blob) { diff --git a/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue b/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue index 9f5eb24598..bf69add86c 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue @@ -185,8 +185,8 @@ const showVolumeTwo = computed(() => !isMuted.value && volume.value > 0.5) const showVolumeOne = computed(() => isMuted.value && volume.value > 0) const litegraphNode = computed(() => { - if (!props.nodeId || !app.rootGraph) return null - return app.rootGraph.getNodeById(props.nodeId) as LGraphNode | null + if (!props.nodeId || !app.canvas.graph) return null + return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null }) const hidden = computed(() => { diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 01e87b5d50..7201fb68ca 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -64,7 +64,7 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy' import { type ExtensionManager } from '@/types/extensionTypes' import type { NodeExecutionId } from '@/types/nodeIdentification' import { graphToPrompt } from '@/utils/executionUtil' -import { forEachNode } from '@/utils/graphTraversalUtil' +import { collectAllNodes, forEachNode } from '@/utils/graphTraversalUtil' import { getNodeByExecutionId, triggerCallbackOnAllNodes @@ -157,15 +157,15 @@ export class ComfyApp { // TODO: Migrate internal usage to the /** @deprecated Use {@link rootGraph} instead */ - get graph() { + get graph(): unknown { return this.rootGraphInternal! } - get rootGraph(): LGraph | undefined { + get rootGraph(): LGraph { if (!this.rootGraphInternal) { console.error('ComfyApp graph accessed before initialization') } - return this.rootGraphInternal + return this.rootGraphInternal! } // @ts-expect-error fixme ts strict error @@ -512,7 +512,7 @@ export class ComfyApp { } } - app.graph.setDirtyCanvas(true) + app.canvas.setDirty(true) useNodeOutputStore().updateNodeImages(node) } @@ -553,7 +553,7 @@ export class ComfyApp { useEventListener(this.canvasElRef, 'dragleave', async () => { if (!this.dragOverNode) return this.dragOverNode = null - this.graph.setDirtyCanvas(false, true) + this.canvas.setDirty(false, true) }) // Add handler for dropping onto a specific node @@ -562,7 +562,10 @@ export class ComfyApp { 'dragover', (event: DragEvent) => { this.canvas.adjustMouseEvent(event) - const node = this.graph.getNodeOnPos(event.canvasX, event.canvasY) + const node = this.canvas.graph?.getNodeOnPos( + event.canvasX, + event.canvasY + ) if (!node?.onDragOver?.(event)) { this.dragOverNode = null @@ -573,7 +576,7 @@ export class ComfyApp { // dragover event is fired very frequently, run this on an animation frame requestAnimationFrame(() => { - this.graph.setDirtyCanvas(false, true) + this.canvas.setDirty(false, true) }) }, false @@ -638,11 +641,11 @@ export class ComfyApp { }) api.addEventListener('progress', () => { - this.graph.setDirtyCanvas(true, false) + this.canvas.setDirty(true, false) }) api.addEventListener('executing', () => { - this.graph.setDirtyCanvas(true, false) + this.canvas.setDirty(true, false) }) api.addEventListener('executed', ({ detail }) => { @@ -653,14 +656,14 @@ export class ComfyApp { merge: detail.merge }) - const node = getNodeByExecutionId(this.graph, executionId) + const node = getNodeByExecutionId(this.rootGraph, executionId) if (node && node.onExecuted) { node.onExecuted(detail.output) } }) api.addEventListener('execution_start', () => { - triggerCallbackOnAllNodes(this.graph, 'onExecutionStart') + triggerCallbackOnAllNodes(this.rootGraph, 'onExecutionStart') }) api.addEventListener('execution_error', ({ detail }) => { @@ -844,7 +847,7 @@ export class ComfyApp { registerProxyWidgets(this.canvas) - this.graph.start() + this.rootGraph.start() // Ensure the canvas fills the window useResizeObserver(this.canvasElRef, ([canvasEl]) => { @@ -1194,17 +1197,18 @@ export class ComfyApp { try { // @ts-expect-error Discrepancies between zod and litegraph - in progress - this.graph.configure(graphData) + this.rootGraph.configure(graphData) // Save original renderer version before scaling (it gets modified during scaling) - const originalMainGraphRenderer = this.graph.extra.workflowRendererVersion + const originalMainGraphRenderer = + this.rootGraph.extra.workflowRendererVersion // Scale main graph ensureCorrectLayoutScale(originalMainGraphRenderer) // Scale all subgraphs that were loaded with the workflow // Use original main graph renderer as fallback (not the modified one) - for (const subgraph of this.graph.subgraphs.values()) { + for (const subgraph of this.rootGraph.subgraphs.values()) { ensureCorrectLayoutScale( subgraph.extra.workflowRendererVersion || originalMainGraphRenderer, subgraph @@ -1235,7 +1239,7 @@ export class ComfyApp { console.error(error) return } - for (const node of this.graph.nodes) { + forEachNode(this.rootGraph, (node) => { const size = node.computeSize() size[0] = Math.max(node.size[0], size[0]) size[1] = Math.max(node.size[1], size[1]) @@ -1284,7 +1288,7 @@ export class ComfyApp { } useExtensionService().invokeExtensions('loadedGraphNode', node) - } + }) if (missingNodeTypes.length && showMissingNodesDialog) { this.showMissingNodesError(missingNodeTypes) @@ -1309,14 +1313,14 @@ export class ComfyApp { useTelemetry()?.trackWorkflowImported(telemetryPayload) await useWorkflowService().afterLoadNewGraph( workflow, - this.graph.serialize() as unknown as ComfyWorkflowJSON + this.rootGraph.serialize() as unknown as ComfyWorkflowJSON ) requestAnimationFrame(() => { - this.graph.setDirtyCanvas(true, true) + this.canvas.setDirty(true, true) }) } - async graphToPrompt(graph = this.graph) { + async graphToPrompt(graph = this.rootGraph) { return graphToPrompt(graph, { sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave') }) @@ -1351,12 +1355,12 @@ export class ComfyApp { for (let i = 0; i < batchCount; i++) { // Allow widgets to run callbacks before a prompt has been queued // e.g. random seed before every gen - executeWidgetsCallback(this.graph.nodes, 'beforeQueued') - for (const subgraph of this.graph.subgraphs.values()) { - executeWidgetsCallback(subgraph.nodes, 'beforeQueued') - } + forEachNode(this.rootGraph, (node) => { + for (const widget of node.widgets ?? []) widget.beforeQueued?.() + }) - const p = await this.graphToPrompt(this.graph) + const p = await this.graphToPrompt(this.rootGraph) + const queuedNodes = collectAllNodes(this.rootGraph) try { api.authToken = comfyOrgAuthToken api.apiKey = comfyOrgApiKey ?? undefined @@ -1397,16 +1401,7 @@ export class ComfyApp { // Allow widgets to run callbacks after a prompt has been queued // e.g. random seed after every gen - executeWidgetsCallback( - p.workflow.nodes - .map((n) => this.graph.getNodeById(n.id)) - .filter((n) => !!n), - 'afterQueued' - ) - for (const subgraph of this.graph.subgraphs.values()) { - executeWidgetsCallback(subgraph.nodes, 'afterQueued') - } - + executeWidgetsCallback(queuedNodes, 'afterQueued') this.canvas.draw(true, true) await this.ui.queue.update() } @@ -1481,7 +1476,7 @@ export class ComfyApp { importA1111(this.graph, parameters) useWorkflowService().afterLoadNewGraph( fileName, - this.graph.serialize() as unknown as ComfyWorkflowJSON + this.rootGraph.serialize() as unknown as ComfyWorkflowJSON ) return } @@ -1512,24 +1507,25 @@ export class ComfyApp { } const ids = Object.keys(apiData) - app.graph.clear() + app.rootGraph.clear() for (const id of ids) { const data = apiData[id] const node = LiteGraph.createNode(data.class_type) if (!node) continue node.id = isNaN(+id) ? id : +id node.title = data._meta?.title ?? node.title - app.graph.add(node) + app.rootGraph.add(node) } + //TODO: Investigate repeat of for loop. Can compress? for (const id of ids) { const data = apiData[id] - const node = app.graph.getNodeById(id) + const node = app.rootGraph.getNodeById(id) for (const input in data.inputs ?? {}) { const value = data.inputs[input] if (value instanceof Array) { const [fromId, fromSlot] = value - const fromNode = app.graph.getNodeById(fromId) + const fromNode = app.rootGraph.getNodeById(fromId) // @ts-expect-error fixme ts strict error let toSlot = node.inputs?.findIndex((inp) => inp.name === input) if (toSlot == null || toSlot === -1) { @@ -1558,16 +1554,16 @@ export class ComfyApp { } } } - app.graph.arrange() + app.rootGraph.arrange() for (const id of ids) { const data = apiData[id] - const node = app.graph.getNodeById(id) + const node = app.rootGraph.getNodeById(id) for (const input in data.inputs ?? {}) { const value = data.inputs[input] if (value instanceof Array) { const [fromId, fromSlot] = value - const fromNode = app.graph.getNodeById(fromId) + const fromNode = app.rootGraph.getNodeById(fromId) // @ts-expect-error fixme ts strict error let toSlot = node.inputs?.findIndex((inp) => inp.name === input) if (toSlot == null || toSlot === -1) { @@ -1597,11 +1593,11 @@ export class ComfyApp { } } - app.graph.arrange() + app.rootGraph.arrange() useWorkflowService().afterLoadNewGraph( fileName, - this.graph.serialize() as unknown as ComfyWorkflowJSON + this.rootGraph.serialize() as unknown as ComfyWorkflowJSON ) } @@ -1653,7 +1649,7 @@ export class ComfyApp { this.registerNodeDef(nodeId, defs[nodeId]) } // Refresh combo widgets in all nodes including those in subgraphs - forEachNode(this.graph, (node) => { + forEachNode(this.rootGraph, (node) => { const def = defs[node.type] // Allow primitive nodes to handle refresh node.refreshComboInNode?.(defs) @@ -1718,8 +1714,8 @@ export class ComfyApp { // Subgraph does not properly implement `clear` and the parent class's // (`LGraph`) `clear` breaks the subgraph structure. - if (this.graph && !this.canvas.subgraph) { - this.graph.clear() + if (this.rootGraph && !this.canvas.subgraph) { + this.rootGraph.clear() } } diff --git a/src/scripts/changeTracker.ts b/src/scripts/changeTracker.ts index f2b2afc30b..fa09ad49af 100644 --- a/src/scripts/changeTracker.ts +++ b/src/scripts/changeTracker.ts @@ -96,13 +96,13 @@ export class ChangeTracker { const activeId = navigation.at(-1) if (activeId) { // Navigate to the saved subgraph - const subgraph = app.graph.subgraphs.get(activeId) + const subgraph = app.rootGraph.subgraphs.get(activeId) if (subgraph) { app.canvas.setGraph(subgraph) } } else { // Empty navigation array means root level - app.canvas.setGraph(app.graph) + app.canvas.setGraph(app.rootGraph) } } } @@ -130,7 +130,7 @@ export class ChangeTracker { checkState() { if (!app.graph || this.changeCount) return - const currentState = clone(app.graph.serialize()) as ComfyWorkflowJSON + const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON if (!this.activeState) { this.activeState = currentState return diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index 26327f6cbb..24cd535568 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -22,7 +22,7 @@ export function clone(obj: T): T { * There are external callers to this function, so we need to keep it for now */ export function applyTextReplacements(app: ComfyApp, value: string): string { - return _applyTextReplacements(app.graph, value) + return _applyTextReplacements(app.rootGraph, value) } /** @knipIgnoreUnusedButUsedByCustomNodes */ diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 8ad39f84a5..a64a77addb 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -265,7 +265,7 @@ export const useLitegraphService = () => { _initialMinSize = { width: 1, height: 1 } constructor() { - super(app.graph, subgraph, instanceData) + super(app.rootGraph, subgraph, instanceData) // Set up event listener for promoted widget registration subgraph.events.addEventListener('widget-promoted', (event) => { @@ -863,7 +863,7 @@ export const useLitegraphService = () => { } function goToNode(nodeId: NodeId) { - const graphNode = app.graph.getNodeById(nodeId) + const graphNode = app.canvas.graph?.getNodeById(nodeId) if (!graphNode) return app.canvas.animateToBounds(graphNode.boundingRect) } @@ -884,7 +884,9 @@ export const useLitegraphService = () => { const canvas = canvasStore.canvas if (!canvas) return - const bounds = createBounds(app.graph.nodes) + const nodes = canvas.graph?.nodes + if (!nodes) return + const bounds = createBounds(nodes) if (!bounds) return canvas.ds.fitToBounds(bounds) diff --git a/src/services/subgraphService.ts b/src/services/subgraphService.ts index b16bf9ca96..3a9f1f7c70 100644 --- a/src/services/subgraphService.ts +++ b/src/services/subgraphService.ts @@ -67,8 +67,8 @@ export const useSubgraphService = () => { // Assertion: overriding Zod schema for (const subgraphData of subgraphs as ExportedSubgraph[]) { const subgraph = - comfyApp.graph.subgraphs.get(subgraphData.id) ?? - comfyApp.graph.createSubgraph(subgraphData) + comfyApp.rootGraph.subgraphs.get(subgraphData.id) ?? + comfyApp.rootGraph.createSubgraph(subgraphData) registerNewSubgraph(subgraph, subgraphData) } diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index eabce26132..b9f89c29a1 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -97,7 +97,7 @@ function executionIdToNodeLocatorId( // It's an execution node ID const parts = nodeIdStr.split(':') const localNodeId = parts[parts.length - 1] - const subgraphs = getSubgraphsFromInstanceIds(app.graph, parts) + const subgraphs = getSubgraphsFromInstanceIds(app.rootGraph, parts) if (!subgraphs) return undefined const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId) return nodeLocatorId @@ -578,10 +578,10 @@ export const useExecutionStore = defineStore('execution', () => { * Propagates errors up subgraph chains. */ watch(lastNodeErrors, () => { - if (!app.graph || !app.graph.nodes) return + if (!app.rootGraph) return // Clear all error flags - forEachNode(app.graph, (node) => { + forEachNode(app.rootGraph, (node) => { node.has_errors = false if (node.inputs) { for (const slot of node.inputs) { @@ -596,7 +596,7 @@ export const useExecutionStore = defineStore('execution', () => { for (const [executionId, nodeError] of Object.entries( lastNodeErrors.value )) { - const node = getNodeByExecutionId(app.graph, executionId) + const node = getNodeByExecutionId(app.rootGraph, executionId) if (!node) continue node.has_errors = true @@ -618,7 +618,10 @@ export const useExecutionStore = defineStore('execution', () => { const parts = executionId.split(':') for (let i = parts.length - 1; i > 0; i--) { const parentExecutionId = parts.slice(0, i).join(':') - const parentNode = getNodeByExecutionId(app.graph, parentExecutionId) + const parentNode = getNodeByExecutionId( + app.rootGraph, + parentExecutionId + ) if (parentNode) { parentNode.has_errors = true } diff --git a/src/stores/subgraphNavigationStore.ts b/src/stores/subgraphNavigationStore.ts index b986299517..8c29f2723f 100644 --- a/src/stores/subgraphNavigationStore.ts +++ b/src/stores/subgraphNavigationStore.ts @@ -49,7 +49,7 @@ export const useSubgraphNavigationStore = defineStore( */ const navigationStack = computed(() => idStack.value - .map((id) => app.graph.subgraphs.get(id)) + .map((id) => app.rootGraph.subgraphs.get(id)) .filter(isNonNullish) ) diff --git a/src/types/index.ts b/src/types/index.ts index 6947f433fc..25f8e20900 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,3 @@ -import type { LGraph } from '@/lib/litegraph/src/litegraph' import type { DeviceStats, EmbeddingsResponse, @@ -71,6 +70,6 @@ declare global { app?: ComfyApp /** For use by extensions and in the browser console. Where possible, import `app` and access via `app.graph` instead. */ - graph?: LGraph + graph?: unknown } } diff --git a/src/views/LinearView.vue b/src/views/LinearView.vue index 3c81f3b395..1a02d37dc1 100644 --- a/src/views/LinearView.vue +++ b/src/views/LinearView.vue @@ -55,7 +55,7 @@ const nodeDatas = computed(() => { widgets } } - return app.graph.nodes + return app.rootGraph.nodes .filter((node) => node.mode === 0 && node.widgets?.length) .map(nodeToNodeData) }) diff --git a/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts index 580a687fcd..96f1d4403d 100644 --- a/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts +++ b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts @@ -61,7 +61,7 @@ export const useMissingNodes = createSharedComposable(() => { } const missingCoreNodes = computed>(() => { - const missingNodes = collectAllNodes(app.graph, isMissingCoreNode) + const missingNodes = collectAllNodes(app.rootGraph, isMissingCoreNode) return groupBy(missingNodes, (node) => String(node.properties?.ver || '')) }) diff --git a/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts b/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts index b83508dddb..e2580ee8de 100644 --- a/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts +++ b/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts @@ -7,7 +7,7 @@ import { useComfyRegistryStore } from '@/stores/comfyRegistryStore' import { useNodeDefStore } from '@/stores/nodeDefStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' import type { components } from '@/types/comfyRegistryTypes' -import { collectAllNodes } from '@/utils/graphTraversalUtil' +import { mapAllNodes } from '@/utils/graphTraversalUtil' import { useNodePacks } from '@/workbench/extensions/manager/composables/nodePack/useNodePacks' import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes' @@ -112,13 +112,9 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => { * Get the node packs for all nodes in the workflow (including subgraphs). */ const getWorkflowPacks = async () => { - if (!app.graph) return [] - const allNodes = collectAllNodes(app.graph) - if (!allNodes.length) { - workflowPacks.value = [] - return [] - } - const packs = await Promise.all(allNodes.map(workflowNodeToPack)) + if (!app.rootGraph) return [] + const packPromises = mapAllNodes(app.rootGraph, workflowNodeToPack) + const packs = await Promise.all(packPromises) workflowPacks.value = packs.filter((pack) => pack !== undefined) } diff --git a/tests-ui/tests/composables/useCoreCommands.test.ts b/tests-ui/tests/composables/useCoreCommands.test.ts index 6ab5d58db7..3c86faa5a2 100644 --- a/tests-ui/tests/composables/useCoreCommands.test.ts +++ b/tests-ui/tests/composables/useCoreCommands.test.ts @@ -32,7 +32,7 @@ vi.mock('@/scripts/app', () => { } }), canvas: mockCanvas, - graph: { + rootGraph: { clear: mockGraphClear } } @@ -161,7 +161,7 @@ describe('useCoreCommands', () => { await clearCommand.function() expect(app.clean).toHaveBeenCalled() - expect(app.graph.clear).toHaveBeenCalled() + expect(app.rootGraph.clear).toHaveBeenCalled() expect(api.dispatchCustomEvent).toHaveBeenCalledWith('graphCleared') }) @@ -178,7 +178,7 @@ describe('useCoreCommands', () => { await clearCommand.function() expect(app.clean).toHaveBeenCalled() - expect(app.graph.clear).not.toHaveBeenCalled() + expect(app.rootGraph.clear).not.toHaveBeenCalled() // Should only remove user nodes, not input/output nodes expect(mockSubgraph.remove).toHaveBeenCalledTimes(2) @@ -212,7 +212,7 @@ describe('useCoreCommands', () => { // Should not clear anything when user cancels expect(app.clean).not.toHaveBeenCalled() - expect(app.graph.clear).not.toHaveBeenCalled() + expect(app.rootGraph.clear).not.toHaveBeenCalled() expect(api.dispatchCustomEvent).not.toHaveBeenCalled() }) }) diff --git a/tests-ui/tests/composables/useMissingNodes.test.ts b/tests-ui/tests/composables/useMissingNodes.test.ts index 41bd098bb0..04a4e5ffd0 100644 --- a/tests-ui/tests/composables/useMissingNodes.test.ts +++ b/tests-ui/tests/composables/useMissingNodes.test.ts @@ -1,8 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick, ref } from 'vue' -import type { LGraphNode } from '@/lib/litegraph/src/litegraph' -import { app } from '@/scripts/app' +import type { LGraphNode, LGraph } from '@/lib/litegraph/src/litegraph' import { useNodeDefStore } from '@/stores/nodeDefStore' import { collectAllNodes } from '@/utils/graphTraversalUtil' import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' @@ -40,12 +39,10 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({ })) })) +const mockApp: { rootGraph?: Partial } = vi.hoisted(() => ({})) + vi.mock('@/scripts/app', () => ({ - app: { - graph: { - nodes: [] - } - } + app: mockApp })) vi.mock('@/utils/graphTraversalUtil', () => ({ @@ -107,9 +104,8 @@ describe('useMissingNodes', () => { nodeDefsByName: {} }) - // Reset app.graph.nodes - // @ts-expect-error - app.graph.nodes is readonly, but we need to modify it for testing. - app.graph.nodes = [] + // Reset app.rootGraph.nodes + mockApp.rootGraph = { nodes: [] } // Default mock for collectAllNodes - returns empty array mockCollectAllNodes.mockReturnValue([]) @@ -306,13 +302,7 @@ describe('useMissingNodes', () => { }) describe('missing core nodes detection', () => { - const createMockNode = ( - type: string, - packId?: string, - version?: string - ): LGraphNode => - // @ts-expect-error - Creating a partial mock of LGraphNode for testing. - // We only need specific properties for our tests, not the full LGraphNode interface. + const createMockNode = (type: string, packId?: string, version?: string) => ({ type, properties: { cnr_id: packId, ver: version }, @@ -325,7 +315,7 @@ describe('useMissingNodes', () => { mode: 0, inputs: [], outputs: [] - }) + }) as unknown as LGraphNode it('identifies missing core nodes not in nodeDefStore', () => { const coreNode1 = createMockNode('CoreNode1', 'comfy-core', '1.2.0') @@ -467,8 +457,7 @@ describe('useMissingNodes', () => { it('calls collectAllNodes with the app graph and filter function', () => { const mockGraph = { nodes: [], subgraphs: new Map() } - // @ts-expect-error - Mocking app.graph for testing - app.graph = mockGraph + mockApp.rootGraph = mockGraph const { missingCoreNodes } = useMissingNodes() // Access the computed to trigger the function @@ -490,8 +479,7 @@ describe('useMissingNodes', () => { it('filter function correctly identifies missing core nodes', () => { const mockGraph = { nodes: [], subgraphs: new Map() } - // @ts-expect-error - Mocking app.graph for testing - app.graph = mockGraph + mockApp.rootGraph = mockGraph mockUseNodeDefStore.mockReturnValue({ nodeDefsByName: { @@ -579,14 +567,13 @@ describe('useMissingNodes', () => { subgraph: mockSubgraph, type: 'SubgraphContainer', properties: { cnr_id: 'custom-pack' } - } + } as unknown as LGraphNode const mockMainGraph = { nodes: [mainMissingNode, mockSubgraphNode] - } + } as Partial as LGraph - // @ts-expect-error - Mocking app.graph for testing - app.graph = mockMainGraph + mockApp.rootGraph = mockMainGraph mockUseNodeDefStore.mockReturnValue({ nodeDefsByName: { diff --git a/tests-ui/tests/renderer/extensions/vueNodes/components/NodeHeader.subgraph.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/components/NodeHeader.subgraph.test.ts index 740b9e7dbb..0f7ca7aefa 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/components/NodeHeader.subgraph.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/components/NodeHeader.subgraph.test.ts @@ -5,15 +5,19 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { + LGraph, + LGraphNode, + SubgraphNode +} from '@/lib/litegraph/src/litegraph' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue' import { getNodeByLocatorId } from '@/utils/graphTraversalUtil' +const mockApp: { rootGraph?: Partial } = vi.hoisted(() => ({})) // Mock dependencies vi.mock('@/scripts/app', () => ({ - app: { - graph: null as any - } + app: mockApp })) vi.mock('@/utils/graphTraversalUtil', () => ({ @@ -55,17 +59,12 @@ vi.mock('@/i18n', () => ({ describe('NodeHeader - Subgraph Functionality', () => { // Helper to setup common mocks const setupMocks = async (isSubgraph = true, hasGraph = true) => { - const { app } = await import('@/scripts/app') - - if (hasGraph) { - ;(app as any).graph = { rootGraph: {} } - } else { - ;(app as any).graph = null - } + if (hasGraph) mockApp.rootGraph = {} + else mockApp.rootGraph = undefined vi.mocked(getNodeByLocatorId).mockReturnValue({ - isSubgraphNode: () => isSubgraph - } as any) + isSubgraphNode: (): this is SubgraphNode => isSubgraph + } as LGraphNode) } beforeEach(() => { diff --git a/tests-ui/tests/store/executionStore.test.ts b/tests-ui/tests/store/executionStore.test.ts index 3c56059a71..7c893bd601 100644 --- a/tests-ui/tests/store/executionStore.test.ts +++ b/tests-ui/tests/store/executionStore.test.ts @@ -38,9 +38,9 @@ vi.mock('@/composables/node/useNodeProgressText', () => ({ // Mock the app import with proper implementation vi.mock('@/scripts/app', () => ({ app: { - graph: { + rootGraph: { getNodeById: vi.fn(), - _nodes: [] // Add _nodes array for workflowStore iteration + nodes: [] // Add nodes array for workflowStore iteration }, revokePreviews: vi.fn(), nodePreviewImages: {} @@ -66,7 +66,7 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { // Mock subgraph structure const mockSubgraph = { id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', - _nodes: [] + nodes: [] } const mockNode = { @@ -75,8 +75,8 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { subgraph: mockSubgraph } as any - // Mock app.graph.getNodeById to return the mock node - vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode) + // Mock app.rootGraph.getNodeById to return the mock node + vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode) const result = store.executionIdToNodeLocatorId('123:456') @@ -98,8 +98,8 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { }) it('should return undefined when conversion fails', () => { - // Mock app.graph.getNodeById to return null (node not found) - vi.mocked(app.graph.getNodeById).mockReturnValue(null) + // Mock app.rootGraph.getNodeById to return null (node not found) + vi.mocked(app.rootGraph.getNodeById).mockReturnValue(null) expect(store.executionIdToNodeLocatorId('999:456')).toBe(undefined) }) @@ -171,7 +171,8 @@ describe('useExecutionStore - Node Error Lookups', () => { const subgraphUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' const mockSubgraph = { id: subgraphUuid, - _nodes: [] + getNodeById: vi.fn(), + nodes: [] } const mockNode = { @@ -180,7 +181,7 @@ describe('useExecutionStore - Node Error Lookups', () => { subgraph: mockSubgraph } as any - vi.mocked(app.graph.getNodeById).mockReturnValue(mockNode) + vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode) store.lastNodeErrors = { '123:456': { diff --git a/tests-ui/tests/store/workflowStore.test.ts b/tests-ui/tests/store/workflowStore.test.ts index 6e82559feb..7dd549e087 100644 --- a/tests-ui/tests/store/workflowStore.test.ts +++ b/tests-ui/tests/store/workflowStore.test.ts @@ -622,7 +622,7 @@ describe('useWorkflowStore', () => { mockSubgraph.rootGraph = mockRootGraph as any - vi.mocked(comfyApp).graph = mockRootGraph as any + vi.mocked(comfyApp).rootGraph = mockRootGraph as any vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any store.activeSubgraph = mockSubgraph as any })