[refactor] Improve type safety across Vue node widget system

- Create WidgetValue union type for all valid widget values
- Replace 'any' types with proper generic constraints in SimplifiedWidget
- Add runtime validation for widget values in useGraphNodeManager
- Update WidgetSelect to use constrained generics instead of any
- Fix type assertions with proper validation functions
- Ensure SafeWidgetData uses WidgetValue type consistently

This eliminates most 'any' usage while maintaining runtime safety through validation.
This commit is contained in:
bymyself
2025-07-05 02:31:42 -07:00
parent 71c3c727cf
commit 290906e7cc
5 changed files with 82 additions and 17 deletions

View File

@@ -31,7 +31,7 @@ import type {
import { LODLevel } from '@/composables/graph/useLOD'
import { useWidgetRenderer } from '@/composables/graph/useWidgetRenderer'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
interface NodeWidgetsProps {
node?: LGraphNode
@@ -106,7 +106,7 @@ const getVueComponent = (widget: SafeWidgetData) => {
return component || WidgetInputText // Fallback to text input
}
const getWidgetValue = (widget: SafeWidgetData): unknown => {
const getWidgetValue = (widget: SafeWidgetData): WidgetValue => {
return widget.value
}

View File

@@ -25,13 +25,13 @@ import {
} from '@/utils/widgetPropFilter'
const props = defineProps<{
widget: SimplifiedWidget<any>
modelValue: any
widget: SimplifiedWidget<string | number | undefined>
modelValue: string | number | undefined
readonly?: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: any]
'update:modelValue': [value: string | number | undefined]
}>()
// Use the composable for consistent widget value handling

View File

@@ -5,6 +5,9 @@
import type { LGraph, LGraphNode } from '@comfyorg/litegraph'
import { nextTick, reactive, readonly } from 'vue'
import type { WidgetValue } from '@/types/simplifiedWidget'
import type { SpatialIndexDebugInfo } from '@/types/spatialIndex'
import { type Bounds, QuadTree } from '../../utils/spatial/QuadTree'
export interface NodeState {
@@ -18,7 +21,7 @@ export interface NodeMetadata {
lastRenderTime: number
cachedBounds: DOMRect | null
lodLevel: 'high' | 'medium' | 'low'
spatialIndex?: any
spatialIndex?: QuadTree<string>
}
export interface PerformanceMetrics {
@@ -35,7 +38,7 @@ export interface PerformanceMetrics {
export interface SafeWidgetData {
name: string
type: string
value: unknown
value: WidgetValue
options?: Record<string, unknown>
callback?: ((value: unknown) => void) | undefined
}
@@ -87,7 +90,7 @@ export interface GraphNodeManager {
spatialMetrics: SpatialMetrics
// Debug
getSpatialIndexDebugInfo(): any | null
getSpatialIndexDebugInfo(): SpatialIndexDebugInfo | null
}
export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
@@ -182,7 +185,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
return {
name: widget.name || 'unknown',
type: widget.type || 'text',
value: undefined,
value: undefined, // Already a valid WidgetValue
options: undefined,
callback: undefined
}
@@ -207,6 +210,33 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
return nodeRefs.get(id)
}
/**
* Validates that a value is a valid WidgetValue type
*/
const validateWidgetValue = (value: unknown): WidgetValue => {
if (value === null || value === undefined || value === void 0) {
return undefined
}
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value
}
if (typeof value === 'object') {
// Check if it's a File array
if (Array.isArray(value) && value.every((item) => item instanceof File)) {
return value as File[]
}
// Otherwise it's a generic object
return value as object
}
// If none of the above, return undefined
console.warn(`Invalid widget value type: ${typeof value}`, value)
return undefined
}
/**
* Updates Vue state when widget values change
*/
@@ -220,7 +250,7 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
if (!currentData?.widgets) return
const updatedWidgets = currentData.widgets.map((w) =>
w.name === widgetName ? { ...w, value: value } : w
w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w
)
vueNodeData.set(nodeId, {
...currentData,
@@ -236,13 +266,25 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
* Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync
*/
const createWrappedWidgetCallback = (
widget: any,
widget: { value?: unknown; name: string }, // LiteGraph widget with minimal typing
originalCallback: ((value: unknown) => void) | undefined,
nodeId: string
) => {
return (value: unknown) => {
// 1. Update the widget value in LiteGraph (critical for LiteGraph state)
widget.value = value as string | number | boolean | object | undefined
// Validate that the value is of an acceptable type
if (
value !== null &&
value !== undefined &&
typeof value !== 'string' &&
typeof value !== 'number' &&
typeof value !== 'boolean' &&
typeof value !== 'object'
) {
console.warn(`Invalid widget value type: ${typeof value}`)
return
}
widget.value = value
// 2. Call the original callback if it exists
if (originalCallback) {
@@ -362,6 +404,9 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
// Store non-reactive reference
nodeRefs.set(id, node)
// Set up widget callbacks BEFORE extracting data (critical order)
setupNodeWidgetCallbacks(node)
// Extract and store safe data for Vue
vueNodeData.set(id, extractVueNodeData(node))

View File

@@ -4,9 +4,12 @@
*/
import { type Ref, ref, watch } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
export interface UseWidgetValueOptions<T, U = T> {
export interface UseWidgetValueOptions<
T extends WidgetValue = WidgetValue,
U = T
> {
/** The widget configuration from LiteGraph */
widget: SimplifiedWidget<T>
/** The current value from parent component */
@@ -19,7 +22,10 @@ export interface UseWidgetValueOptions<T, U = T> {
transform?: (value: U) => T
}
export interface UseWidgetValueReturn<T, U = T> {
export interface UseWidgetValueReturn<
T extends WidgetValue = WidgetValue,
U = T
> {
/** Local value for immediate UI updates */
localValue: Ref<T>
/** Handler for user interactions */
@@ -38,7 +44,7 @@ export interface UseWidgetValueReturn<T, U = T> {
* })
* ```
*/
export function useWidgetValue<T, U = T>({
export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
widget,
modelValue,
defaultValue,

View File

@@ -3,7 +3,21 @@
* Removes all DOM manipulation and positioning concerns
*/
export interface SimplifiedWidget<T = any, O = Record<string, any>> {
/** Valid types for widget values */
export type WidgetValue =
| string
| number
| boolean
| object
| undefined
| null
| void
| File[]
export interface SimplifiedWidget<
T extends WidgetValue = WidgetValue,
O = Record<string, any>
> {
/** Display name of the widget */
name: string