diff --git a/src/stores/subgraphStore.test.ts b/src/stores/subgraphStore.test.ts index db322a8b0a4..a25ac9bc720 100644 --- a/src/stores/subgraphStore.test.ts +++ b/src/stores/subgraphStore.test.ts @@ -285,6 +285,26 @@ describe('useSubgraphStore', () => { consoleSpy.mockRestore() }) + it('should reject blueprints without a root node', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + await mockFetch({ + 'empty-node.json': { + ...mockGraph, + nodes: [] + } + }) + + const error = consoleSpy.mock.calls.find( + ([message]) => message === 'Failed to load subgraph blueprint' + )?.[1] + expect(error).toBeInstanceOf(TypeError) + expect((error as Error).message).toBe( + "Subgraph blueprint 'empty-node' must contain a root node" + ) + expect(store.subgraphBlueprints).toHaveLength(0) + consoleSpy.mockRestore() + }) + it('should handle global blueprint with rejected data promise gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) await mockFetch( diff --git a/src/stores/subgraphStore.ts b/src/stores/subgraphStore.ts index 99fe96ef63b..b394b8cfacb 100644 --- a/src/stores/subgraphStore.ts +++ b/src/stores/subgraphStore.ts @@ -39,6 +39,10 @@ async function confirmOverwrite(name: string): Promise { }) } +type ValidSubgraphWorkflowJSON = ComfyWorkflowJSON & { + definitions: NonNullable +} + export const useSubgraphStore = defineStore('subgraph', () => { class SubgraphBlueprint extends ComfyWorkflow { static override readonly basePath = 'subgraphs/' @@ -54,18 +58,20 @@ export const useSubgraphStore = defineStore('subgraph', () => { this.hasPromptedSave = !confirmFirstSave } - validateSubgraph() { - if (!this.activeState?.definitions) + validateSubgraph(): ValidSubgraphWorkflowJSON { + const activeState = this.activeState + if (!activeState?.definitions) throw new Error( 'The root graph of a subgraph blueprint must consist of only a single subgraph node' ) - const { subgraphs } = this.activeState.definitions - const { nodes } = this.activeState + const validState = activeState as ValidSubgraphWorkflowJSON + const { subgraphs } = validState.definitions + const { nodes } = validState //Instanceof doesn't function as nodes are serialized function isSubgraphNode(node: ComfyNode) { return node && subgraphs.some((s) => s.id === node.type) } - if (nodes.length == 1 && isSubgraphNode(nodes[0])) return + if (nodes.length == 1 && isSubgraphNode(nodes[0])) return validState const errors: Record = {} //mark errors for all but first subgraph node let firstSubgraphFound = false @@ -88,7 +94,7 @@ export const useSubgraphStore = defineStore('subgraph', () => { } override async save(): Promise { - this.validateSubgraph() + const activeState = this.validateSubgraph() if ( !this.hasPromptedSave && useSettingStore().get('Comfy.Workflow.WarnBlueprintOverwrite') @@ -97,7 +103,7 @@ export const useSubgraphStore = defineStore('subgraph', () => { this.hasPromptedSave = true } // Extract metadata from subgraph.extra to workflow.extra before saving - this.extractMetadataToWorkflowExtra() + this.extractMetadataToWorkflowExtra(activeState) const ret = await super.save() // Force reload to update initialState with saved metadata registerNodeDef(await this.load({ force: true }), { @@ -110,13 +116,14 @@ export const useSubgraphStore = defineStore('subgraph', () => { * Moves all properties (except workflowRendererVersion) from subgraph.extra * to workflow.extra, then removes from subgraph.extra to avoid duplication. */ - private extractMetadataToWorkflowExtra(): void { - if (!this.activeState) return - const subgraph = this.activeState.definitions?.subgraphs?.[0] + private extractMetadataToWorkflowExtra( + activeState: ValidSubgraphWorkflowJSON + ): void { + const subgraph = activeState.definitions.subgraphs?.[0] if (!subgraph?.extra) return const sgExtra = subgraph.extra as Record - const workflowExtra = (this.activeState.extra ??= {}) as Record< + const workflowExtra = (activeState.extra ??= {}) as Record< string, unknown > @@ -129,10 +136,10 @@ export const useSubgraphStore = defineStore('subgraph', () => { } override async saveAs(path: string) { - this.validateSubgraph() + const activeState = this.validateSubgraph() this.hasPromptedSave = true // Extract metadata from subgraph.extra to workflow.extra before saving - this.extractMetadataToWorkflowExtra() + this.extractMetadataToWorkflowExtra(activeState) const ret = await super.saveAs(path) // Force reload to update initialState with saved metadata registerNodeDef(await this.load({ force: true }), { @@ -146,14 +153,20 @@ export const useSubgraphStore = defineStore('subgraph', () => { if (!force && this.isLoaded) return await super.load({ force }) const loaded = await super.load({ force }) const st = loaded.activeState + const rootNode = st.nodes[0] + if (!rootNode) { + throw new TypeError( + `Subgraph blueprint '${this.filename}' must contain a root node` + ) + } const sg = (st.definitions?.subgraphs ?? []).find( - (sg) => sg.id == st.nodes[0].type + (sg) => sg.id == rootNode.type ) if (!sg) throw new Error( 'Loaded subgraph blueprint does not contain valid subgraph' ) - sg.name = st.nodes[0].title = this.filename + sg.name = rootNode.title = this.filename // Copy blueprint metadata from workflow extra to subgraph extra // so it's available when editing via canvas.subgraph.extra @@ -277,7 +290,11 @@ export const useSubgraphStore = defineStore('subgraph', () => { name: string = workflow.filename ) { const subgraphNode = workflow.changeTracker.initialState.nodes[0] - if (!subgraphNode) throw new Error('Invalid Subgraph Blueprint') + if (!subgraphNode) { + throw new TypeError( + `Subgraph blueprint '${name}' must contain a root node` + ) + } subgraphNode.inputs ??= [] subgraphNode.outputs ??= [] //NOTE: Types are cast to string. This is only used for input coloring on previews