Road to No Explicit Any Part 6: Composables and Extensions (#8083)

## Summary
- Type `onExecuted` callbacks with `NodeExecutionOutput` in saveMesh.ts
and uploadAudio.ts
- Type composable parameters and return values properly
(useLoad3dViewer, useImageMenuOptions, useJobMenu, useResultGallery,
useContextMenuTranslation)
- Type `taskRef` as `TaskItemImpl` with updated test mocks
- Fix error catch and index signature patterns without `any`
- Add `NodeOutputWith<T>` generic helper for typed access to passthrough
properties on `NodeExecutionOutput`

## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] Unit tests pass for affected files
- [x] Sourcegraph checks confirm no external usage of modified types

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8083-Road-to-No-Explicit-Any-Part-6-Composables-and-Extensions-2e96d73d3650810fb033d745bf88a22b)
by [Unito](https://www.unito.io)
This commit is contained in:
Johnpaul Chiwetelu
2026-01-16 00:27:28 +01:00
committed by GitHub
parent 0d5ca96a2b
commit c56e8425d4
16 changed files with 159 additions and 87 deletions

View File

@@ -1,8 +1,13 @@
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
import type { NodeOutputWith } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
type ImageCompareOutput = NodeOutputWith<{
a_images?: Record<string, string>[]
b_images?: Record<string, string>[]
}>
useExtensionService().registerExtension({
name: 'Comfy.ImageCompare',
@@ -14,15 +19,10 @@ useExtensionService().registerExtension({
const onExecuted = node.onExecuted
node.onExecuted = function (output: NodeExecutionOutput) {
node.onExecuted = function (output: ImageCompareOutput) {
onExecuted?.call(this, output)
const aImages = (output as Record<string, unknown>).a_images as
| Record<string, string>[]
| undefined
const bImages = (output as Record<string, unknown>).b_images as
| Record<string, string>[]
| undefined
const { a_images: aImages, b_images: bImages } = output
const rand = app.getRandParam()
const beforeUrl =

View File

@@ -15,7 +15,11 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
import type { NodeOutputWith } from '@/schemas/apiSchema'
type Load3dPreviewOutput = NodeOutputWith<{
result?: [string?, CameraState?, string?]
}>
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { api } from '@/scripts/api'
import { ComfyApp, app } from '@/scripts/app'
@@ -496,13 +500,11 @@ useExtensionService().registerExtension({
config.configure(settings)
}
node.onExecuted = function (output: NodeExecutionOutput) {
node.onExecuted = function (output: Load3dPreviewOutput) {
onExecuted?.call(this, output)
const result = (output as Record<string, unknown>).result as
| unknown[]
| undefined
const filePath = result?.[0] as string | undefined
const result = output.result
const filePath = result?.[0]
if (!filePath) {
const msg = t('toastMessages.unableToGetModelFilePath')
@@ -510,8 +512,8 @@ useExtensionService().registerExtension({
useToastStore().addAlert(msg)
}
const cameraState = result?.[1] as CameraState | undefined
const bgImagePath = result?.[2] as string | undefined
const cameraState = result?.[1]
const bgImagePath = result?.[2]
modelWidget.value = filePath?.replaceAll('\\', '/')

View File

@@ -6,7 +6,12 @@ import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { NodeOutputWith, ResultItem } from '@/schemas/apiSchema'
type SaveMeshOutput = NodeOutputWith<{
'3d'?: ResultItem[]
}>
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { useExtensionService } from '@/services/extensionService'
import { useLoad3dService } from '@/services/load3dService'
@@ -70,22 +75,26 @@ useExtensionService().registerExtension({
const onExecuted = node.onExecuted
node.onExecuted = function (message: any) {
onExecuted?.apply(this, arguments as any)
node.onExecuted = function (output: SaveMeshOutput) {
onExecuted?.call(this, output)
const fileInfo = message['3d'][0]
const fileInfo = output['3d']?.[0]
if (!fileInfo) return
useLoad3d(node).waitForLoad3d((load3d) => {
const modelWidget = node.widgets?.find((w) => w.name === 'image')
if (load3d && modelWidget) {
const filePath = fileInfo['subfolder'] + '/' + fileInfo['filename']
const filePath =
(fileInfo.subfolder ?? '') + '/' + (fileInfo.filename ?? '')
modelWidget.value = filePath
const config = new Load3DConfiguration(load3d, node.properties)
config.configureForSaveMesh(fileInfo['type'], filePath)
const loadFolder = fileInfo.type as 'input' | 'output'
config.configureForSaveMesh(loadFolder, filePath)
}
})
}

View File

@@ -15,6 +15,7 @@ import {
getResourceURL,
splitFilePath
} from '@/renderer/extensions/vueNodes/widgets/utils/audioUtils'
import type { NodeExecutionOutput } from '@/schemas/apiSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { DOMWidget } from '@/scripts/domWidget'
import { useAudioService } from '@/services/audioService'
@@ -112,14 +113,17 @@ app.registerExtension({
audioUIWidget.element.classList.add('empty-audio-widget')
// Populate the audio widget UI on node execution.
const onExecuted = node.onExecuted
node.onExecuted = function (message: any) {
// @ts-expect-error fixme ts strict error
onExecuted?.apply(this, arguments)
const audios = message.audio
if (!audios) return
node.onExecuted = function (output: NodeExecutionOutput) {
onExecuted?.call(this, output)
const audios = output.audio
if (!audios?.length) return
const audio = audios[0]
audioUIWidget.element.src = api.apiURL(
getResourceURL(audio.subfolder, audio.filename, audio.type)
getResourceURL(
audio.subfolder ?? '',
audio.filename ?? '',
audio.type
)
)
audioUIWidget.element.classList.remove('empty-audio-widget')
}
@@ -139,22 +143,23 @@ app.registerExtension({
}
}
},
onNodeOutputsUpdated(nodeOutputs: Record<NodeLocatorId, any>) {
onNodeOutputsUpdated(
nodeOutputs: Record<NodeLocatorId, NodeExecutionOutput>
) {
for (const [nodeLocatorId, output] of Object.entries(nodeOutputs)) {
if ('audio' in output) {
const node = getNodeByLocatorId(app.rootGraph, nodeLocatorId)
if (!node) continue
if (!output.audio?.length) continue
// @ts-expect-error fixme ts strict error
const audioUIWidget = node.widgets.find(
(w) => w.name === 'audioUI'
) as unknown as DOMWidget<HTMLAudioElement, string>
const audio = output.audio[0]
audioUIWidget.element.src = api.apiURL(
getResourceURL(audio.subfolder, audio.filename, audio.type)
)
audioUIWidget.element.classList.remove('empty-audio-widget')
}
const node = getNodeByLocatorId(app.rootGraph, nodeLocatorId)
if (!node) continue
const audioUIWidget = node.widgets?.find(
(w) => w.name === 'audioUI'
) as unknown as DOMWidget<HTMLAudioElement, string>
const audio = output.audio[0]
audioUIWidget.element.src = api.apiURL(
getResourceURL(audio.subfolder ?? '', audio.filename ?? '', audio.type)
)
audioUIWidget.element.classList.remove('empty-audio-widget')
}
}
})