Road to No Explicit Any Part 11 (#8565)

## Summary

This PR removes `any` types from widgets, services, stores, and test
files, replacing them with proper TypeScript types.

### Key Changes

#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across widgets and services
- Added proper type imports (TgpuRoot, Point, StyleValue, etc.)
- Created typed interfaces (NumericWidgetOptions, TestWindow,
ImportFailureDetail, etc.)
- Fixed function return types to be non-nullable where appropriate
- Added type guards and null checks instead of non-null assertions
- Used `ComponentProps` from vue-component-type-helpers for component
testing

#### Widget System
- Added index signature to IWidgetOptions for Record compatibility
- Centralized disabled logic in WidgetInputNumberInput
- Moved template type assertions to computed properties
- Fixed ComboWidget getOptionLabel type assertions
- Improved remote widget type handling with runtime checks

#### Services & Stores
- Fixed getOrCreateViewer to return non-nullable values
- Updated addNodeOnGraph to use specific options type `{ pos?: Point }`
- Added proper type assertions for settings store retrieval
- Fixed executionIdToCurrentId return type (string | undefined)

#### Test Infrastructure
- Exported GraphOrSubgraph from litegraph barrel to avoid circular
dependencies
- Updated test fixtures with proper TypeScript types (TestInfo,
LGraphNode)
- Replaced loose Record types with ComponentProps in tests
- Added proper error handling in WebSocket fixture

#### Code Organization
- Created shared i18n-types module for locale data types
- Made ImportFailureDetail non-exported (internal use only)
- Added @public JSDoc tag to ElectronWindow type
- Fixed console.log usage in scripts to use allowed methods

### Files Changed

**Widgets & Components:**
-
src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
- src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.ts
- src/lib/litegraph/src/widgets/ComboWidget.ts
- src/lib/litegraph/src/types/widgets.ts
- src/components/common/LazyImage.vue
- src/components/load3d/Load3dViewerContent.vue

**Services & Stores:**
- src/services/litegraphService.ts
- src/services/load3dService.ts
- src/services/colorPaletteService.ts
- src/stores/maskEditorStore.ts
- src/stores/nodeDefStore.ts
- src/platform/settings/settingStore.ts
- src/platform/workflow/management/stores/workflowStore.ts

**Composables & Utils:**
- src/composables/node/useWatchWidget.ts
- src/composables/useCanvasDrop.ts
- src/utils/widgetPropFilter.ts
- src/utils/queueDisplay.ts
- src/utils/envUtil.ts

**Test Files:**
- browser_tests/fixtures/ComfyPage.ts
- browser_tests/fixtures/ws.ts
- browser_tests/tests/actionbar.spec.ts
-
src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts
- src/lib/litegraph/src/subgraph/subgraphUtils.test.ts
- src/components/rightSidePanel/shared.test.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
-
src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts

**Scripts & Types:**
- scripts/i18n-types.ts (new shared module)
- scripts/diff-i18n.ts
- scripts/check-unused-i18n-keys.ts
- src/workbench/extensions/manager/types/conflictDetectionTypes.ts
- src/types/algoliaTypes.ts
- src/types/simplifiedWidget.ts

**Infrastructure:**
- src/lib/litegraph/src/litegraph.ts (added GraphOrSubgraph export)
- src/lib/litegraph/src/infrastructure/CustomEventTarget.ts
- src/platform/assets/services/assetService.ts

**Stories:**
- apps/desktop-ui/src/views/InstallView.stories.ts
- src/components/queue/job/JobDetailsPopover.stories.ts

**Extension Manager:**
- src/workbench/extensions/manager/composables/useConflictDetection.ts
- src/workbench/extensions/manager/composables/useManagerQueue.ts
- src/workbench/extensions/manager/services/comfyManagerService.ts
- src/workbench/extensions/manager/utils/conflictMessageUtil.ts

### Testing

- [x] All TypeScript type checking passes (`pnpm typecheck`)
- [x] ESLint passes without errors (`pnpm lint`)
- [x] Format checks pass (`pnpm format:check`)
- [x] Knip (unused exports) passes (`pnpm knip`)
- [x] Pre-commit and pre-push hooks pass

Part of the "Road to No Explicit Any" initiative.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499

---------

Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Johnpaul Chiwetelu
2026-02-06 01:29:28 +01:00
committed by GitHub
parent 7f81e1afac
commit 90a701dd67
49 changed files with 240 additions and 153 deletions

View File

@@ -1,6 +1,8 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
import { nextTick, provide } from 'vue'
import type { ElectronWindow } from '@/utils/envUtil'
import { createMemoryHistory, createRouter } from 'vue-router'
import InstallView from './InstallView.vue'
@@ -42,16 +44,21 @@ const meta: Meta<typeof InstallView> = {
const router = createMockRouter()
// Mock electron API
;(window as any).electronAPI = {
;(window as ElectronWindow).electronAPI = {
getPlatform: () => 'darwin',
Config: {
getDetectedGpu: () => Promise.resolve('mps')
},
Events: {
trackEvent: (_eventName: string, _data?: any) => {}
trackEvent: (
_eventName: string,
_data?: Record<string, unknown>
) => {}
},
installComfyUI: (_options: any) => {},
changeTheme: (_theme: any) => {},
installComfyUI: (
_options: Parameters<ElectronAPI['installComfyUI']>[0]
) => {},
changeTheme: (_theme: Parameters<ElectronAPI['changeTheme']>[0]) => {},
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
@@ -240,8 +247,8 @@ export const DesktopSettings: Story = {
export const WindowsPlatform: Story = {
render: () => {
// Override the platform to Windows
;(window as any).electronAPI.getPlatform = () => 'win32'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
;(window as ElectronWindow).electronAPI.getPlatform = () => 'win32'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('nvidia')
return {
@@ -259,8 +266,8 @@ export const MacOSPlatform: Story = {
name: 'macOS Platform',
render: () => {
// Override the platform to macOS
;(window as any).electronAPI.getPlatform = () => 'darwin'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
;(window as ElectronWindow).electronAPI.getPlatform = () => 'darwin'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('mps')
return {
@@ -327,7 +334,7 @@ export const ManualInstall: Story = {
export const ErrorState: Story = {
render: () => {
// Override validation to return an error
;(window as any).electronAPI.validateInstallPath = () =>
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: false,
exists: false,
@@ -375,7 +382,7 @@ export const ErrorState: Story = {
export const WarningState: Story = {
render: () => {
// Override validation to return a warning about non-default drive
;(window as any).electronAPI.validateInstallPath = () =>
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: true,
exists: false,

View File

@@ -1,5 +1,9 @@
import { test as base } from '@playwright/test'
interface TestWindow extends Window {
__ws__?: Record<string, WebSocket>
}
export const webSocketFixture = base.extend<{
ws: { trigger(data: unknown, url?: string): Promise<void> }
}>({

View File

@@ -2,10 +2,7 @@
import { execSync } from 'child_process'
import * as fs from 'fs'
import { globSync } from 'glob'
interface LocaleData {
[key: string]: any
}
import type { LocaleData } from './i18n-types'
// Configuration
const SOURCE_PATTERNS = ['src/**/*.{js,ts,vue}', '!src/locales/**/*']
@@ -45,7 +42,7 @@ function getStagedLocaleFiles(): string[] {
}
// Extract all keys from a nested object
function extractKeys(obj: any, prefix = ''): string[] {
function extractKeys(obj: LocaleData, prefix = ''): string[] {
const keys: string[] = []
for (const [key, value] of Object.entries(obj)) {
@@ -166,17 +163,17 @@ async function checkNewUnusedKeys() {
// Report results
if (unusedNewKeys.length > 0) {
console.log('\n⚠ Warning: Found unused NEW i18n keys:\n')
console.warn('\n⚠ Warning: Found unused NEW i18n keys:\n')
for (const key of unusedNewKeys.sort()) {
console.log(` - ${key}`)
console.warn(` - ${key}`)
}
console.log(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
console.log(
console.warn(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
console.warn(
'\nThese keys were added but are not used anywhere in the codebase.'
)
console.log('Consider using them or removing them in a future update.')
console.warn('Consider using them or removing them in a future update.')
// Changed from process.exit(1) to process.exit(0) for warning only
process.exit(0)

View File

@@ -7,6 +7,7 @@ import {
writeFileSync
} from 'fs'
import { dirname, join } from 'path'
import type { LocaleData } from './i18n-types'
// Ensure directories exist
function ensureDir(dir: string) {
@@ -41,8 +42,8 @@ function getAllJsonFiles(dir: string): string[] {
}
// Find additions in new object compared to base
function findAdditions(base: any, updated: any): Record<string, any> {
const additions: Record<string, any> = {}
function findAdditions(base: LocaleData, updated: LocaleData): LocaleData {
const additions: LocaleData = {}
for (const key in updated) {
if (!(key in base)) {
@@ -74,7 +75,7 @@ function capture(srcLocaleDir: string, tempBaseDir: string) {
ensureDir(dirname(targetPath))
writeFileSync(targetPath, readFileSync(file, 'utf8'))
}
console.log('Captured current locale files to temp/base/')
console.warn('Captured current locale files to temp/base/')
}
// Diff command
@@ -94,7 +95,7 @@ function diff(srcLocaleDir: string, tempBaseDir: string, tempDiffDir: string) {
if (Object.keys(additions).length > 0) {
ensureDir(dirname(diffPath))
writeFileSync(diffPath, JSON.stringify(additions, null, 2))
console.log(`Wrote diff to ${diffPath}`)
console.warn(`Wrote diff to ${diffPath}`)
}
}
}
@@ -116,9 +117,9 @@ switch (command) {
// Remove temp directory recursively
if (existsSync('temp')) {
rmSync('temp', { recursive: true, force: true })
console.log('Removed temp directory')
console.warn('Removed temp directory')
}
break
default:
console.log('Please specify either "capture" or "diff" command')
console.error('Please specify either "capture" or "diff" command')
}

5
scripts/i18n-types.ts Normal file
View File

@@ -0,0 +1,5 @@
/**
* Shared types for i18n-related scripts
*/
export type LocaleData = { [key: string]: string | LocaleData }

View File

@@ -38,6 +38,7 @@
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
import { computed, onUnmounted, ref, watch } from 'vue'
import type { StyleValue } from 'vue'
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
import { useMediaCache } from '@/services/mediaCacheService'
@@ -55,7 +56,7 @@ const {
alt?: string
containerClass?: ClassValue
imageClass?: ClassValue
imageStyle?: Record<string, any>
imageStyle?: StyleValue
rootMargin?: string
}>()

View File

@@ -131,7 +131,7 @@ const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
onModelDrop: async (file) => {
await viewer.handleModelDrop(file)
},
disabled: viewer.isPreview.value || isStandaloneMode
disabled: viewer.isPreview.value || !!isStandaloneMode
})
onMounted(async () => {

View File

@@ -158,7 +158,7 @@ export const Queued: Story = {
prompt_id: 'p1'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -217,7 +217,7 @@ export const QueuedParallel: Story = {
prompt_id: 'p2'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -258,7 +258,7 @@ export const Running: Story = {
prompt_id: 'p1'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -303,7 +303,7 @@ export const QueuedZeroAheadSingleRunning: Story = {
prompt_id: 'p1'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -360,7 +360,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
prompt_id: 'p2'
}
}
} as any
}
return { args: { ...args, jobId } }
},

View File

@@ -138,7 +138,6 @@ describe('flatAndCategorizeSelectedItems', () => {
expect(result.nodes).toEqual([testNode1])
expect(result.groups).toEqual([testGroup1, testGroup2])
expect(result.nodeToParentGroup.get(testNode1)).toBe(testGroup2)
expect(result.nodeToParentGroup.has(testGroup2 as any)).toBe(false)
})
it('should handle mixed selection of nodes and groups', () => {

View File

@@ -52,7 +52,7 @@ export interface SafeWidgetData {
isDOMWidget?: boolean
label?: string
nodeType?: string
options?: IWidgetOptions<unknown>
options?: IWidgetOptions
spec?: InputSpec
slotMetadata?: WidgetSlotMetadata
}
@@ -145,7 +145,7 @@ interface SharedWidgetEnhancements {
/** Widget label */
label?: string
/** Widget options */
options?: Record<string, any>
options?: IWidgetOptions
}
/**
@@ -170,7 +170,7 @@ export function getSharedWidgetEnhancements(
? 'ring ring-component-node-widget-advanced'
: undefined,
label: widget.label,
options: widget.options
options: widget.options as IWidgetOptions
}
}

View File

@@ -47,7 +47,7 @@ export const useComputedWithWidgetWatch = (
const { widgetNames, triggerCanvasRedraw = false } = options
// Create a reactive trigger based on widget values
const widgetValues = ref<Record<string, any>>({})
const widgetValues = ref<Record<string, unknown>>({})
// Initialize widget observers
if (node.widgets) {
@@ -56,7 +56,7 @@ export const useComputedWithWidgetWatch = (
: node.widgets
// Initialize current values
const currentValues: Record<string, any> = {}
const currentValues: Record<string, unknown> = {}
widgetsToObserve.forEach((widget) => {
currentValues[widget.name] = widget.value
})

View File

@@ -2,7 +2,7 @@ import type { Ref } from 'vue'
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { usePragmaticDroppable } from '@/composables/usePragmaticDragAndDrop'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, Point } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
@@ -33,7 +33,7 @@ export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
if (node.data instanceof ComfyNodeDefImpl) {
const nodeDef = node.data
const pos = [...basePos]
const pos: Point = [...basePos]
// Add an offset on y to make sure after adding the node, the cursor
// is on the node (top left corner)
pos[1] += LiteGraph.NODE_TITLE_HEIGHT

View File

@@ -2,7 +2,7 @@ import type { NeverNever, PickNevers } from '@/lib/litegraph/src/types/utility'
type EventListeners<T> = {
readonly [K in keyof T]:
| ((this: EventTarget, ev: CustomEvent<T[K]>) => any)
| ((this: EventTarget, ev: CustomEvent<T[K]>) => unknown)
| EventListenerObject
| null
}

View File

@@ -68,7 +68,7 @@ describe.skip('subgraphUtils', () => {
describe.skip('findUsedSubgraphIds', () => {
it('should handle graph with no subgraphs', () => {
const graph = new LGraph()
const registry = new Map<UUID, any>()
const registry = new Map<UUID, LGraph>()
const result = findUsedSubgraphIds(graph, registry)
expect(result.size).toBe(0)
@@ -87,7 +87,7 @@ describe.skip('subgraphUtils', () => {
const node2 = createTestSubgraphNode(subgraph2)
subgraph1.add(node2)
const registry = new Map<UUID, any>([
const registry = new Map<UUID, LGraph>([
[subgraph1.id, subgraph1],
[subgraph2.id, subgraph2]
])
@@ -115,7 +115,7 @@ describe.skip('subgraphUtils', () => {
const node3 = createTestSubgraphNode(subgraph1, { id: 3 })
subgraph2.add(node3)
const registry = new Map<UUID, any>([
const registry = new Map<UUID, LGraph>([
[subgraph1.id, subgraph1],
[subgraph2.id, subgraph2]
])
@@ -139,7 +139,7 @@ describe.skip('subgraphUtils', () => {
rootGraph.add(node2)
// Only register subgraph1
const registry = new Map<UUID, any>([[subgraph1.id, subgraph1]])
const registry = new Map<UUID, LGraph>([[subgraph1.id, subgraph1]])
const result = findUsedSubgraphIds(rootGraph, registry)
expect(result.size).toBe(2)

View File

@@ -37,6 +37,14 @@ export interface IWidgetOptions<TValues = unknown[]> {
getOptionLabel?: (value?: string | null) => string
callback?: IWidget['callback']
iconClass?: string
// Vue widget options
disabled?: boolean
useGrouping?: boolean
placeholder?: string
showThumbnails?: boolean
showItemNavigators?: boolean
hidden?: boolean
}
interface IWidgetSliderOptions extends IWidgetOptions<number[]> {

View File

@@ -35,11 +35,10 @@ export class ComboWidget
override get _displayValue() {
if (this.computedDisabled) return ''
if (this.options.getOptionLabel) {
const getOptionLabel = this.options.getOptionLabel
if (getOptionLabel) {
try {
return this.options.getOptionLabel(
this.value ? String(this.value) : null
)
return getOptionLabel(this.value ? String(this.value) : null)
} catch (e) {
console.error('Failed to map value:', e)
return this.value ? String(this.value) : ''
@@ -155,9 +154,12 @@ export class ComboWidget
}
const menu = new LiteGraph.ContextMenu([], menuOptions)
const getOptionLabel = this.options.getOptionLabel
for (const value of values_list) {
try {
const label = this.options.getOptionLabel(String(value))
const label = getOptionLabel
? getOptionLabel(String(value))
: String(value)
menu.addItem(label, value, menuOptions)
} catch (err) {
console.error('Failed to map value:', err)

View File

@@ -6427,7 +6427,9 @@
"Load3D": {
"display_name": "بارگذاری ۳بعدی و انیمیشن",
"inputs": {
"clear": {},
"clear": {
"": "پاک‌سازی"
},
"height": {
"name": "ارتفاع"
},
@@ -6437,8 +6439,12 @@
"model_file": {
"name": "فایل مدل"
},
"upload 3d model": {},
"upload extra resources": {},
"upload 3d model": {
"": "بارگذاری مدل سه‌بعدی"
},
"upload extra resources": {
"": "بارگذاری منابع اضافی"
},
"width": {
"name": "عرض"
}

View File

@@ -6427,7 +6427,9 @@
"Load3D": {
"display_name": "Carregar 3D & Animação",
"inputs": {
"clear": {},
"clear": {
"": "limpar"
},
"height": {
"name": "altura"
},
@@ -6437,8 +6439,12 @@
"model_file": {
"name": "arquivo_do_modelo"
},
"upload 3d model": {},
"upload extra resources": {},
"upload 3d model": {
"": "enviar modelo 3D"
},
"upload extra resources": {
"": "enviar recursos extras"
},
"width": {
"name": "largura"
}

View File

@@ -487,7 +487,7 @@ function createAssetService() {
url: string
name: string
tags?: string[]
user_metadata?: Record<string, any>
user_metadata?: Record<string, unknown>
preview_id?: string
}): Promise<AssetItem & { created_new: boolean }> {
const res = await api.fetchApi(ASSETS_ENDPOINT, {
@@ -525,7 +525,7 @@ function createAssetService() {
data: string
name: string
tags?: string[]
user_metadata?: Record<string, any>
user_metadata?: Record<string, unknown>
}): Promise<AssetItem & { created_new: boolean }> {
// Validate that data is a data URL
if (!params.data || !params.data.startsWith('data:')) {

View File

@@ -38,7 +38,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
useErrorHandling: vi.fn(() => ({
wrapWithErrorHandlingAsync: vi.fn(
(fn, errorHandler) =>
async (...args: any[]) => {
async (...args: Parameters<typeof fn>) => {
try {
return await fn(...args)
} catch (error) {

View File

@@ -46,7 +46,7 @@ function onChange(
}
export const useSettingStore = defineStore('setting', () => {
const settingValues = ref<Record<string, any>>({})
const settingValues = ref<Partial<Settings>>({})
const settingsById = ref<Record<string, SettingParams>>({})
const {
@@ -87,7 +87,7 @@ export const useSettingStore = defineStore('setting', () => {
* @param key - The key of the setting to check.
* @returns Whether the setting exists.
*/
function exists(key: string) {
function exists<K extends keyof Settings>(key: K) {
return settingValues.value[key] !== undefined
}
@@ -118,7 +118,7 @@ export const useSettingStore = defineStore('setting', () => {
*/
function get<K extends keyof Settings>(key: K): Settings[K] {
// Clone the value when returning to prevent external mutations
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key))
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key)!)
}
/**

View File

@@ -222,7 +222,7 @@ interface WorkflowStore {
activeSubgraph: Subgraph | undefined
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
updateActiveGraph: () => void
executionIdToCurrentId: (id: string) => any
executionIdToCurrentId: (id: string) => string | undefined
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
nodeToNodeLocatorId: (node: LGraphNode) => NodeLocatorId
nodeExecutionIdToNodeLocatorId: (
@@ -718,7 +718,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
}
//FIXME: use existing util function
const executionIdToCurrentId = (id: string) => {
const executionIdToCurrentId = (id: string): string | undefined => {
const subgraph = activeSubgraph.value
// Short-circuit: ID belongs to the parent workflow / no active subgraph

View File

@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as I18n from 'vue-i18n'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { WorkflowDraftSnapshot } from '@/platform/workflow/persistence/base/draftCache'
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useWorkflowDraftStore } from '@/platform/workflow/persistence/stores/workflowDraftStore'
import { defaultGraphJSON } from '@/scripts/defaultGraph'
@@ -191,7 +192,7 @@ describe('useWorkflowPersistence', () => {
const drafts = JSON.parse(
localStorage.getItem('Comfy.Workflow.Drafts') ?? '{}'
) as Record<string, any>
) as Record<string, WorkflowDraftSnapshot>
expect(Object.keys(drafts).length).toBe(32)
expect(drafts['workflows/Draft0.json']).toBeUndefined()

View File

@@ -36,6 +36,7 @@ import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import { RenderShape } from '@/lib/litegraph/src/litegraph'
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
import NodeSlots from '@/renderer/extensions/vueNodes/components/NodeSlots.vue'
@@ -69,8 +70,11 @@ const nodeData = computed<VueNodeData>(() => {
options: {
hidden: input.hidden,
advanced: input.advanced,
values: input.type === 'COMBO' ? input.options : undefined // For combo widgets
}
values:
input.type === 'COMBO' && Array.isArray(input.options)
? input.options
: undefined
} satisfies IWidgetOptions
}))
const inputs: INodeInputSlot[] = Object.entries(nodeDef.inputs || {})

View File

@@ -16,10 +16,12 @@ import type { ChartData } from 'chart.js'
import Chart from 'primevue/chart'
import { computed } from 'vue'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']>
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']> &
IWidgetOptions
const value = defineModel<ChartData>({ required: true })

View File

@@ -38,10 +38,12 @@ import {
filterWidgetProps
} from '@/utils/widgetPropFilter'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
type WidgetOptions = { format?: ColorFormat } & Record<string, unknown>
type WidgetOptions = IWidgetOptions & { format?: ColorFormat }
const props = defineProps<{
widget: SimplifiedWidget<string, WidgetOptions>

View File

@@ -67,30 +67,40 @@ function updateValue(e: UIEvent) {
const { target } = e
if (!(target instanceof HTMLInputElement)) return
const parsed = evaluateInput(unformatValue(target.value))
if (parsed !== undefined)
modelValue.value = Math.min(
filteredProps.value.max,
Math.max(filteredProps.value.min, parsed)
)
else target.value = formattedValue.value
if (parsed !== undefined) {
const max = filteredProps.value.max ?? Number.MAX_VALUE
const min = filteredProps.value.min ?? -Number.MAX_VALUE
modelValue.value = Math.min(max, Math.max(min, parsed))
} else target.value = formattedValue.value
textEdit.value = false
}
const canDecrement = computed(
() =>
modelValue.value > filteredProps.value.min &&
!props.widget.options?.disabled
)
const canIncrement = computed(
() =>
modelValue.value < filteredProps.value.max &&
!props.widget.options?.disabled
)
interface NumericWidgetOptions {
min: number
max: number
step?: number
step2?: number
precision?: number
disabled?: boolean
useGrouping?: boolean
}
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
)
const filteredProps = computed(() => {
const filtered = filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
return filtered as Partial<NumericWidgetOptions>
})
const isDisabled = computed(() => props.widget.options?.disabled ?? false)
const canDecrement = computed(() => {
const min = filteredProps.value.min ?? -Number.MAX_VALUE
return modelValue.value > min && !isDisabled.value
})
const canIncrement = computed(() => {
const max = filteredProps.value.max ?? Number.MAX_VALUE
return modelValue.value < max && !isDisabled.value
})
// Get the precision value for proper number formatting
const precision = computed(() => {
@@ -108,7 +118,7 @@ const stepValue = computed(() => {
// Use step / 10 for custom large step values (> 10) to match litegraph behavior
// This is important for extensions like Impact Pack that use custom step values (e.g., 640)
// We skip default step values (1, 10) to avoid affecting normal widgets
const step = props.widget.options?.step
const step = props.widget.options?.step as number | undefined
if (step !== undefined && step > 10) {
return Number(step) / 10
}
@@ -140,17 +150,16 @@ const buttonsDisabled = computed(() => {
})
function updateValueBy(delta: number) {
modelValue.value = Math.min(
filteredProps.value.max,
Math.max(filteredProps.value.min, modelValue.value + delta)
)
const max = filteredProps.value.max ?? Number.MAX_VALUE
const min = filteredProps.value.min ?? -Number.MAX_VALUE
modelValue.value = Math.min(max, Math.max(min, modelValue.value + delta))
}
const dragValue = ref<number>()
const dragDelta = ref(0)
function handleMouseDown(e: PointerEvent) {
if (e.button > 0) return
if (props.widget.options?.disabled) return
if (isDisabled.value) return
const { target } = e
if (!(target instanceof HTMLElement)) return
target.setPointerCapture(e.pointerId)
@@ -163,10 +172,9 @@ function handleMouseMove(e: PointerEvent) {
const unclippedValue =
dragValue.value + ((dragDelta.value / 10) | 0) * stepValue.value
dragDelta.value %= 10
dragValue.value = Math.min(
filteredProps.value.max,
Math.max(filteredProps.value.min, unclippedValue)
)
const max = filteredProps.value.max ?? Number.MAX_VALUE
const min = filteredProps.value.min ?? -Number.MAX_VALUE
dragValue.value = Math.min(max, Math.max(min, unclippedValue))
}
function handleMouseUp() {
const newValue = dragValue.value
@@ -248,7 +256,7 @@ const sliderWidth = computed(() => {
:value="formattedValue"
role="spinbutton"
tabindex="0"
:disabled="widget.options?.disabled"
:disabled="isDisabled"
autocomplete="off"
autocorrect="off"
spellcheck="false"

View File

@@ -5,6 +5,7 @@ import type { InputTextProps } from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import { describe, expect, it } from 'vitest'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetInputText from './WidgetInputText.vue'
@@ -18,7 +19,7 @@ describe('WidgetInputText Value Binding', () => {
name: 'test_input',
type: 'string',
value,
options,
options: options as IWidgetOptions,
callback
})

View File

@@ -53,7 +53,7 @@ const props = defineProps<Props>()
const modelValue = defineModel<string | undefined>({
default(props: Props) {
return props.widget.options?.values?.[0] || ''
return props.widget.options?.values?.[0] ?? ''
}
})

View File

@@ -31,7 +31,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
value: string = 'img_001.png',
options: {
values?: string[]
getOptionLabel?: (value: string | null) => string
getOptionLabel?: (value?: string | null) => string
} = {},
spec?: ComboInputSpec
): SimplifiedWidget<string | undefined> => ({
@@ -82,7 +82,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
describe('when custom labels are provided via getOptionLabel', () => {
it('displays custom labels while preserving original values', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (!value) return 'No file'
const mapping: Record<string, string> = {
'img_001.png': 'Vacation Photo',
@@ -112,7 +112,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('emits original values when items with custom labels are selected', async () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (!value) return 'No file'
return `Custom: ${value}`
})
@@ -134,7 +134,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('falls back to original value when label mapping fails', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (value === 'photo_abc.jpg') {
throw new Error('Mapping failed')
}
@@ -163,7 +163,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('falls back to original value when label mapping returns empty string', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (value === 'photo_abc.jpg') {
return ''
}
@@ -185,7 +185,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('falls back to original value when label mapping returns undefined', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (value === 'hash789.png') {
return undefined as unknown as string
}
@@ -209,7 +209,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
describe('output items with custom label mapping', () => {
it('applies custom label mapping to output items from queue history', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (!value) return 'No file'
return `Output: ${value}`
})

View File

@@ -57,7 +57,7 @@ provide(
const modelValue = defineModel<string | undefined>({
default(props: Props) {
return props.widget.options?.values?.[0] || ''
return props.widget.options?.values?.[0] ?? ''
}
})
@@ -73,7 +73,8 @@ const combinedProps = computed(() => ({
}))
const getAssetData = () => {
const nodeType = props.widget.options?.nodeType ?? props.nodeType
const nodeType: string | undefined =
props.widget.options?.nodeType ?? props.nodeType
if (props.isAssetMode && nodeType) {
return useAssetWidgetData(toRef(nodeType))
}
@@ -134,11 +135,11 @@ const inputItems = computed<FormDropdownItem[]>(() => {
return []
}
return values.map((value: string, index: number) => ({
return values.map((value, index) => ({
id: `input-${index}`,
preview_url: getMediaUrl(value, 'input'),
name: value,
label: getDisplayLabel(value)
preview_url: getMediaUrl(String(value), 'input'),
name: String(value),
label: getDisplayLabel(String(value))
}))
})
const outputItems = computed<FormDropdownItem[]>(() => {

View File

@@ -15,8 +15,8 @@
v-model="modelValue"
:class="cn(WidgetInputBaseClass, 'size-full text-xs resize-none')"
:placeholder
:readonly="widget.options?.read_only"
:disabled="widget.options?.read_only"
:readonly="isReadOnly"
:disabled="isReadOnly"
fluid
data-capture-wheel="true"
@pointerdown.capture.stop
@@ -58,4 +58,6 @@ const filteredProps = computed(() =>
const displayName = computed(() => widget.label || widget.name)
const id = useId()
const isReadOnly = computed(() => widget.options?.read_only ?? false)
</script>

View File

@@ -32,7 +32,7 @@ async function getAuthHeaders() {
return {}
}
const dataCache = new Map<string, CacheEntry<any>>()
const dataCache = new Map<string, CacheEntry<unknown>>()
const createCacheKey = (config: RemoteWidgetConfig): string => {
const { route, query_params = {}, refresh = 0 } = config
@@ -49,7 +49,9 @@ const getBackoff = (retryCount: number) =>
Math.min(1000 * Math.pow(2, retryCount), 512)
const isInitialized = (entry: CacheEntry<unknown> | undefined) =>
entry?.data && entry?.timestamp && entry.timestamp > 0
entry?.data !== undefined &&
entry?.timestamp !== undefined &&
entry.timestamp > 0
const isStale = (entry: CacheEntry<unknown> | undefined, ttl: number) =>
entry?.timestamp && Date.now() - entry.timestamp >= ttl
@@ -128,9 +130,11 @@ export function useRemoteWidget<
return !isLoaded && isInitialized(dataCache.get(cacheKey))
}
const onFirstLoad = (data: T[]) => {
const onFirstLoad = (data: T | T[]) => {
isLoaded = true
widget.value = data[0]
const nextValue =
Array.isArray(data) && data.length > 0 ? data[0] : undefined
widget.value = nextValue ?? (Array.isArray(data) ? defaultValue : data)
widget.callback?.(widget.value)
node.graph?.setDirtyCanvas(true)
}
@@ -138,13 +142,16 @@ export function useRemoteWidget<
const fetchValue = async () => {
const entry = dataCache.get(cacheKey)
if (isFailed(entry)) return entry!.data
if (isFailed(entry)) return entry!.data as T
const isValid =
isInitialized(entry) && (isPermanent || !isStale(entry, refresh))
if (isValid || isBackingOff(entry) || isFetching(entry)) return entry!.data
if (isValid || isBackingOff(entry) || isFetching(entry))
return entry!.data as T
const currentEntry: CacheEntry<T> = entry || { data: defaultValue }
const currentEntry: CacheEntry<T> = (entry as
| CacheEntry<T>
| undefined) || { data: defaultValue }
dataCache.set(cacheKey, currentEntry)
try {

View File

@@ -188,7 +188,7 @@ export const useColorPaletteService = () => {
* @param schema - The Zod schema object to analyze.
* @returns Array of optional key names.
*/
const getOptionalKeys = (schema: z.ZodObject<any, any>) => {
const getOptionalKeys = (schema: z.ZodObject<z.ZodRawShape>) => {
const optionalKeys: string[] = []
const shape = schema.shape

View File

@@ -849,7 +849,7 @@ export const useLitegraphService = () => {
function addNodeOnGraph(
nodeDef: ComfyNodeDefV1 | ComfyNodeDefV2,
options: Record<string, any> = {}
options: Record<string, unknown> & { pos?: Point } = {}
): LGraphNode {
options.pos ??= getCanvasCenter()

View File

@@ -32,7 +32,6 @@ type UseLoad3dViewerFn = (node?: LGraphNode) => {
handleModelDrop: (file: File) => Promise<void>
handleSeek: (progress: number) => void
needApplyChanges: { value: boolean }
[key: string]: unknown
}
// Type for SkeletonUtils module
@@ -81,7 +80,7 @@ interface Load3DNode extends LGraphNode {
syncLoad3dConfig?: () => void
}
const viewerInstances = new Map<NodeId, any>()
const viewerInstances = new Map<NodeId, ReturnType<UseLoad3dViewerFn>>()
export class Load3dService {
private static instance: Load3dService
@@ -165,12 +164,15 @@ export class Load3dService {
* Only works after useLoad3dViewer has been loaded.
* Returns null if module not yet loaded - use async version instead.
*/
getOrCreateViewerSync(node: LGraphNode, useLoad3dViewer: UseLoad3dViewerFn) {
getOrCreateViewerSync<T extends UseLoad3dViewerFn>(
node: LGraphNode,
useLoad3dViewer: T
): ReturnType<T> {
if (!viewerInstances.has(node.id)) {
viewerInstances.set(node.id, useLoad3dViewer(node))
}
return viewerInstances.get(node.id)
return viewerInstances.get(node.id) as ReturnType<T>
}
removeViewer(node: LGraphNode) {
@@ -288,6 +290,7 @@ export class Load3dService {
async handleViewerClose(node: LGraphNode) {
const viewer = await useLoad3dService().getOrCreateViewer(node)
if (!viewer) return
if (viewer.needApplyChanges.value) {
await viewer.applyChanges()

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import _ from 'es-toolkit/compat'
import type { TgpuRoot } from 'typegpu'
import {
BrushShape,
@@ -71,7 +72,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
const canvasHistory = useCanvasHistory(20)
const tgpuRoot = ref<any>(null)
const tgpuRoot = ref<TgpuRoot | null>(null)
const colorInput = ref<HTMLInputElement | null>(null)

View File

@@ -86,7 +86,7 @@ export class ComfyNodeDefImpl
// V2 fields
readonly inputs: Record<string, InputSpecV2>
readonly outputs: OutputSpecV2[]
readonly hidden?: Record<string, any>
readonly hidden?: Record<string, boolean>
// ComfyNodeDefImpl fields
readonly nodeSource: NodeSource

View File

@@ -72,7 +72,7 @@ export interface NodesIndexSuggestion {
exact_nb_hits: number
facets: {
exact_matches: Record<string, number>
analytics: Record<string, any>
analytics: Record<string, unknown>
}
}
objectID: RegistryNodePack['id']

View File

@@ -3,6 +3,7 @@
* Removes all DOM manipulation and positioning concerns
*/
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
/** Valid types for widget values */
export type WidgetValue =
@@ -39,7 +40,7 @@ export type SafeControlWidget = {
export interface SimplifiedWidget<
T extends WidgetValue = WidgetValue,
O = Record<string, any>
O extends IWidgetOptions = IWidgetOptions
> {
/** Display name of the widget */
name: string
@@ -68,7 +69,7 @@ export interface SimplifiedWidget<
nodeType?: string
/** Optional serialization method for custom value handling */
serializeValue?: () => any
serializeValue?: () => unknown
/** Optional input specification backing this widget */
spec?: InputSpecV2
@@ -78,7 +79,7 @@ export interface SimplifiedWidget<
export interface SimplifiedControlWidget<
T extends WidgetValue = WidgetValue,
O = Record<string, any>
O extends IWidgetOptions = IWidgetOptions
> extends SimplifiedWidget<T, O> {
controlWidget: SafeControlWidget
}

View File

@@ -1,7 +1,11 @@
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
// Extend Window interface to include electronAPI
type ElectronWindow = typeof window & {
/**
* Extend Window interface to include electronAPI
* Used by desktop-ui app storybook stories
* @public
*/
export type ElectronWindow = typeof window & {
electronAPI?: ElectronAPI
}

View File

@@ -4,7 +4,7 @@ import { formatDuration } from '@/utils/formatUtil'
import { clampPercentInt, formatPercent0 } from '@/utils/numberUtil'
export type BuildJobDisplayCtx = {
t: (k: string, v?: Record<string, any>) => string
t: (k: string, v?: Record<string, unknown>) => string
locale: string
formatClockTimeFn: (ts: number, locale: string) => string
isActive: boolean

View File

@@ -55,13 +55,13 @@ export const BADGE_EXCLUDED_PROPS = [
* @param excludeList - List of property names to exclude
* @returns Filtered props object
*/
export function filterWidgetProps<T extends Record<string, any>>(
export function filterWidgetProps<T extends object>(
props: T | undefined,
excludeList: readonly string[]
): Partial<T> {
if (!props) return {}
const filtered: Record<string, any> = {}
const filtered: Record<string, unknown> = {}
for (const [key, value] of Object.entries(props)) {
if (!excludeList.includes(key)) {
filtered[key] = value

View File

@@ -5,6 +5,7 @@ import PrimeVue from 'primevue/config'
import { describe, expect, it } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import type { ComponentProps } from 'vue-component-type-helpers'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
@@ -12,9 +13,11 @@ import GridSkeleton from './GridSkeleton.vue'
import PackCardSkeleton from './PackCardSkeleton.vue'
describe('GridSkeleton', () => {
const mountComponent = ({
function mountComponent({
props = {}
}: Record<string, any> = {}): VueWrapper => {
}: {
props?: Partial<ComponentProps<typeof GridSkeleton>>
} = {}): VueWrapper {
const i18n = createI18n({
legacy: false,
locale: 'en',

View File

@@ -19,6 +19,7 @@ import type {
ConflictDetail,
ConflictDetectionResponse,
ConflictDetectionResult,
ImportFailureMap,
Node,
NodeRequirements,
SystemEnvironment
@@ -336,7 +337,7 @@ export function useConflictDetection() {
* Gets installed packages and checks each one for import failures using bulk API.
* @returns Promise that resolves to import failure data
*/
async function fetchImportFailInfo(): Promise<Record<string, any>> {
async function fetchImportFailInfo(): Promise<ImportFailureMap> {
try {
const comfyManagerService = useComfyManagerService()
@@ -362,7 +363,7 @@ export function useConflictDetection() {
if (bulkResult) {
// Filter out null values (packages without import failures)
const importFailures: Record<string, any> = {}
const importFailures: ImportFailureMap = {}
Object.entries(bulkResult).forEach(([packageId, failInfo]) => {
if (failInfo !== null) {
@@ -389,10 +390,7 @@ export function useConflictDetection() {
* @returns Array of conflict detection results for failed imports
*/
function detectImportFailConflicts(
importFailInfo: Record<
string,
{ error?: string; traceback?: string } | null
>
importFailInfo: ImportFailureMap
): ConflictDetectionResult[] {
const results: ConflictDetectionResult[] = []
if (!importFailInfo || typeof importFailInfo !== 'object') {

View File

@@ -24,7 +24,7 @@ const MANAGER_WS_TASK_STARTED_NAME = 'cm-task-started'
export const useManagerQueue = (
taskHistory: Ref<ManagerTaskHistory>,
taskQueue: Ref<ManagerTaskQueue>,
installedPacks: Ref<Record<string, any>>
installedPacks: Ref<Record<string, unknown>>
) => {
// Task queue state (read-only from server)
const maxHistoryItems = ref(64)

View File

@@ -156,7 +156,7 @@ export const useComfyManagerService = () => {
const getImportFailInfo = async (signal?: AbortSignal) => {
const errorContext = 'Fetching import failure information'
return executeRequest<any>(
return executeRequest<Record<string, unknown>>(
() => managerApiClient.get(ManagerRoute.IMPORT_FAIL_INFO, { signal }),
{ errorContext }
)

View File

@@ -78,3 +78,16 @@ export interface ConflictDetectionResponse {
results: ConflictDetectionResult[]
detected_system_environment?: Partial<SystemEnvironment>
}
/**
* Detailed information about a Python import failure
*/
interface ImportFailureDetail {
error?: string
traceback?: string
}
/**
* Map of package IDs to their import failure information
*/
export type ImportFailureMap = Record<string, ImportFailureDetail | null>

View File

@@ -10,7 +10,7 @@ import type { ConflictDetail } from '@/workbench/extensions/manager/types/confli
*/
export function getConflictMessage(
conflict: ConflictDetail,
t: (key: string, params?: Record<string, any>) => string
t: (key: string, params?: Record<string, unknown>) => string
): string {
const messageKey = `manager.conflicts.conflictMessages.${conflict.type}`
@@ -53,7 +53,7 @@ export function getConflictMessage(
*/
export function getJoinedConflictMessages(
conflicts: ConflictDetail[],
t: (key: string, params?: Record<string, any>) => string,
t: (key: string, params?: Record<string, unknown>) => string,
separator = '; '
): string {
return conflicts