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

@@ -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 {