From edc436c928139eda62531cff69dba2e81362bf8b Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 12 Dec 2025 03:35:44 +0100 Subject: [PATCH 1/5] fix: improve type safety in litegraph core files - DEFAULT_GROUP_FONT_SIZE: assign default value 24 (was `any` with no value; the internal fallback was already 24, making the constant ineffective) - getParameterNames: replace `any` with `unknown` in function signature - extendClass: replace deprecated __lookupGetter__/__defineGetter__ with Object.getOwnPropertyDescriptor/defineProperty and add proper types - LGraphNodeProperties: replace `any` with `unknown` and Record types for dynamic property access - IBaseWidget.callback: change value type from `any` to `unknown` - BaseWidget.callback: change value type to match generic TWidget['value'] --- src/lib/litegraph/src/LGraphNodeProperties.ts | 72 +++++++++++++------ src/lib/litegraph/src/LiteGraphGlobal.ts | 40 +++++------ src/lib/litegraph/src/types/widgets.ts | 2 +- src/lib/litegraph/src/utils/type.ts | 11 +++ src/lib/litegraph/src/widgets/BaseWidget.ts | 2 +- .../core/__snapshots__/litegraph.test.ts.snap | 2 +- 6 files changed, 83 insertions(+), 46 deletions(-) diff --git a/src/lib/litegraph/src/LGraphNodeProperties.ts b/src/lib/litegraph/src/LGraphNodeProperties.ts index 6e635e22819..04c3ac4429b 100644 --- a/src/lib/litegraph/src/LGraphNodeProperties.ts +++ b/src/lib/litegraph/src/LGraphNodeProperties.ts @@ -1,5 +1,7 @@ import type { LGraphNode } from './LGraphNode' +type LGraphNodeWithDynamicProps = LGraphNode & Record + /** * Default properties to track */ @@ -11,7 +13,6 @@ const DEFAULT_TRACKED_PROPERTIES: string[] = [ 'color', 'bgcolor' ] - /** * Manages node properties with optional change tracking and instrumentation. */ @@ -37,6 +38,28 @@ export class LGraphNodeProperties { } } + #resolveTargetObject(parts: string[]): { + targetObject: Record + propertyName: string + } { + const node: LGraphNodeWithDynamicProps = this + .node as LGraphNodeWithDynamicProps + + if (parts.length === 1) { + return { targetObject: node, propertyName: parts[0] } + } + + let targetObject: Record = node + for (let i = 0; i < parts.length - 1; i++) { + targetObject = targetObject[parts[i]] as Record + } + + return { + targetObject, + propertyName: parts[parts.length - 1] + } + } + /** * Instruments a single property to track changes */ @@ -47,15 +70,7 @@ export class LGraphNodeProperties { this.#ensureNestedPath(path) } - let targetObject: any = this.node - let propertyName = parts[0] - - if (parts.length > 1) { - for (let i = 0; i < parts.length - 1; i++) { - targetObject = targetObject[parts[i]] - } - propertyName = parts.at(-1)! - } + const { targetObject, propertyName } = this.#resolveTargetObject(parts) const hasProperty = Object.prototype.hasOwnProperty.call( targetObject, @@ -64,11 +79,11 @@ export class LGraphNodeProperties { const currentValue = targetObject[propertyName] if (!hasProperty) { - let value: any = undefined + let value: unknown = undefined Object.defineProperty(targetObject, propertyName, { get: () => value, - set: (newValue: any) => { + set: (newValue: unknown) => { const oldValue = value value = newValue this.#emitPropertyChange(path, oldValue, newValue) @@ -108,13 +123,20 @@ export class LGraphNodeProperties { */ #createInstrumentedDescriptor( propertyPath: string, - initialValue: any + initialValue: unknown + ): PropertyDescriptor { + return this.#createInstrumentedDescriptorTyped(propertyPath, initialValue) + } + + #createInstrumentedDescriptorTyped( + propertyPath: string, + initialValue: TValue ): PropertyDescriptor { - let value = initialValue + let value: TValue = initialValue return { get: () => value, - set: (newValue: any) => { + set: (newValue: TValue) => { const oldValue = value value = newValue this.#emitPropertyChange(propertyPath, oldValue, newValue) @@ -129,8 +151,16 @@ export class LGraphNodeProperties { */ #emitPropertyChange( propertyPath: string, - oldValue: any, - newValue: any + oldValue: unknown, + newValue: unknown + ): void { + this.#emitPropertyChangeTyped(propertyPath, oldValue, newValue) + } + + #emitPropertyChangeTyped( + propertyPath: string, + oldValue: TValue, + newValue: TValue ): void { this.node.graph?.trigger('node:property:changed', { nodeId: this.node.id, @@ -145,7 +175,9 @@ export class LGraphNodeProperties { */ #ensureNestedPath(path: string): void { const parts = path.split('.') - let current: any = this.node + const node: LGraphNodeWithDynamicProps = this + .node as LGraphNodeWithDynamicProps + let current: Record = node // Create all parent objects except the last property for (let i = 0; i < parts.length - 1; i++) { @@ -153,7 +185,7 @@ export class LGraphNodeProperties { if (!current[part]) { current[part] = {} } - current = current[part] + current = current[part] as Record } } @@ -175,7 +207,7 @@ export class LGraphNodeProperties { * Custom toJSON method for JSON.stringify * Returns undefined to exclude from serialization since we only use defaults */ - toJSON(): any { + toJSON(): undefined { return undefined } } diff --git a/src/lib/litegraph/src/LiteGraphGlobal.ts b/src/lib/litegraph/src/LiteGraphGlobal.ts index c48814235e0..6a9cb5db666 100644 --- a/src/lib/litegraph/src/LiteGraphGlobal.ts +++ b/src/lib/litegraph/src/LiteGraphGlobal.ts @@ -67,7 +67,7 @@ export class LiteGraphGlobal { DEFAULT_SHADOW_COLOR = 'rgba(0,0,0,0.5)' DEFAULT_GROUP_FONT = 24 - DEFAULT_GROUP_FONT_SIZE?: any + DEFAULT_GROUP_FONT_SIZE = 24 GROUP_FONT = 'Inter' WIDGET_BGCOLOR = '#222' @@ -716,7 +716,7 @@ export class LiteGraphGlobal { } // used to create nodes from wrapping functions - getParameterNames(func: (...args: any) => any): string[] { + getParameterNames(func: (...args: unknown[]) => unknown): string[] { return String(func) .replaceAll(/\/\/.*$/gm, '') // strip single-line comments .replaceAll(/\s+/g, '') // strip white space @@ -971,7 +971,10 @@ export class LiteGraphGlobal { } } - extendClass(target: any, origin: any): void { + extendClass( + target: Record & { prototype?: object }, + origin: Record & { prototype?: object } + ): void { for (const i in origin) { // copy class properties // eslint-disable-next-line no-prototype-builtins @@ -979,33 +982,24 @@ export class LiteGraphGlobal { target[i] = origin[i] } - if (origin.prototype) { + if (origin.prototype && target.prototype) { + const originProto = origin.prototype as Record + const targetProto = target.prototype as Record + // copy prototype properties - for (const i in origin.prototype) { + for (const i in originProto) { // only enumerable // eslint-disable-next-line no-prototype-builtins - if (!origin.prototype.hasOwnProperty(i)) continue + if (!originProto.hasOwnProperty(i)) continue // avoid overwriting existing ones // eslint-disable-next-line no-prototype-builtins - if (target.prototype.hasOwnProperty(i)) continue - - // copy getters - if (origin.prototype.__lookupGetter__(i)) { - target.prototype.__defineGetter__( - i, - origin.prototype.__lookupGetter__(i) - ) - } else { - target.prototype[i] = origin.prototype[i] - } + if (targetProto.hasOwnProperty(i)) continue - // and setters - if (origin.prototype.__lookupSetter__(i)) { - target.prototype.__defineSetter__( - i, - origin.prototype.__lookupSetter__(i) - ) + // Use Object.getOwnPropertyDescriptor to copy getters/setters properly + const descriptor = Object.getOwnPropertyDescriptor(originProto, i) + if (descriptor) { + Object.defineProperty(targetProto, i, descriptor) } } } diff --git a/src/lib/litegraph/src/types/widgets.ts b/src/lib/litegraph/src/types/widgets.ts index f383f940405..5ff6e20f2ae 100644 --- a/src/lib/litegraph/src/types/widgets.ts +++ b/src/lib/litegraph/src/types/widgets.ts @@ -340,7 +340,7 @@ export interface IBaseWidget< // TODO: Confirm this format callback?( - value: any, + value: unknown, canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, diff --git a/src/lib/litegraph/src/utils/type.ts b/src/lib/litegraph/src/utils/type.ts index 84891fb7f55..5da118d93ec 100644 --- a/src/lib/litegraph/src/utils/type.ts +++ b/src/lib/litegraph/src/utils/type.ts @@ -56,3 +56,14 @@ function intersection(...sets: string[][]): string[] { function isStrings(types: unknown[]): types is string[] { return types.every((t) => typeof t === 'string') } + +export type IfEquals = + (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B + +export type WritableKeys = { + [P in keyof T]-?: IfEquals< + { [Q in P]: T[P] }, + { -readonly [Q in P]: T[P] }, + P + > +}[keyof T] diff --git a/src/lib/litegraph/src/widgets/BaseWidget.ts b/src/lib/litegraph/src/widgets/BaseWidget.ts index 3ed2ffb12eb..a580fc69a61 100644 --- a/src/lib/litegraph/src/widgets/BaseWidget.ts +++ b/src/lib/litegraph/src/widgets/BaseWidget.ts @@ -78,7 +78,7 @@ export abstract class BaseWidget< tooltip?: string element?: HTMLElement callback?( - value: any, + value: TWidget['value'], canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, diff --git a/tests-ui/tests/litegraph/core/__snapshots__/litegraph.test.ts.snap b/tests-ui/tests/litegraph/core/__snapshots__/litegraph.test.ts.snap index 1cd27ced117..6bdd6b6788c 100644 --- a/tests-ui/tests/litegraph/core/__snapshots__/litegraph.test.ts.snap +++ b/tests-ui/tests/litegraph/core/__snapshots__/litegraph.test.ts.snap @@ -22,7 +22,7 @@ LiteGraphGlobal { "CurveEditor": [Function], "DEFAULT_FONT": "Inter", "DEFAULT_GROUP_FONT": 24, - "DEFAULT_GROUP_FONT_SIZE": undefined, + "DEFAULT_GROUP_FONT_SIZE": 24, "DEFAULT_POSITION": [ 100, 100, From d849e282b5fb968970cc1d5bb2858ae2d444027a Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 12 Dec 2025 03:36:55 +0100 Subject: [PATCH 2/5] fix: update callback usage for stricter widget typing - previewAny.ts: add explicit boolean type annotation for callback value - ButtonWidget.ts: pass widget value instead of widget instance to callback to match the IBaseWidget.callback signature --- src/extensions/core/previewAny.ts | 2 +- src/lib/litegraph/src/utils/type.ts | 11 ----------- src/lib/litegraph/src/widgets/ButtonWidget.ts | 4 ++-- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/extensions/core/previewAny.ts b/src/extensions/core/previewAny.ts index 89fcf3cd8aa..75e65d4fdb2 100644 --- a/src/extensions/core/previewAny.ts +++ b/src/extensions/core/previewAny.ts @@ -41,7 +41,7 @@ useExtensionService().registerExtension({ app ) - showAsPlaintextWidget.widget.callback = (value) => { + showAsPlaintextWidget.widget.callback = (value:boolean) => { showValueWidget.hidden = !value showValueWidget.options.hidden = !value showValueWidgetPlain.hidden = value diff --git a/src/lib/litegraph/src/utils/type.ts b/src/lib/litegraph/src/utils/type.ts index 5da118d93ec..84891fb7f55 100644 --- a/src/lib/litegraph/src/utils/type.ts +++ b/src/lib/litegraph/src/utils/type.ts @@ -56,14 +56,3 @@ function intersection(...sets: string[][]): string[] { function isStrings(types: unknown[]): types is string[] { return types.every((t) => typeof t === 'string') } - -export type IfEquals = - (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B - -export type WritableKeys = { - [P in keyof T]-?: IfEquals< - { [Q in P]: T[P] }, - { -readonly [Q in P]: T[P] }, - P - > -}[keyof T] diff --git a/src/lib/litegraph/src/widgets/ButtonWidget.ts b/src/lib/litegraph/src/widgets/ButtonWidget.ts index 38bc74bc74d..28d5952c675 100644 --- a/src/lib/litegraph/src/widgets/ButtonWidget.ts +++ b/src/lib/litegraph/src/widgets/ButtonWidget.ts @@ -65,7 +65,7 @@ export class ButtonWidget this.clicked = true canvas.setDirty(true) - // Call the callback with widget instance and other context - this.callback?.(this, canvas, node, pos, e) + // Call the callback with widget value + this.callback?.(this.value, canvas, node, pos, e) } } From bb3d2c180a7eb05e4698972a8082344f2fb1e58b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 12 Dec 2025 02:43:21 +0000 Subject: [PATCH 3/5] [automated] Apply ESLint and Prettier fixes --- src/extensions/core/previewAny.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/core/previewAny.ts b/src/extensions/core/previewAny.ts index 75e65d4fdb2..d9d093702ff 100644 --- a/src/extensions/core/previewAny.ts +++ b/src/extensions/core/previewAny.ts @@ -41,7 +41,7 @@ useExtensionService().registerExtension({ app ) - showAsPlaintextWidget.widget.callback = (value:boolean) => { + showAsPlaintextWidget.widget.callback = (value: boolean) => { showValueWidget.hidden = !value showValueWidget.options.hidden = !value showValueWidgetPlain.hidden = value From 906c95d59f86ca6e5199826c343c4a4086bc309b Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 12 Dec 2025 20:43:47 +0100 Subject: [PATCH 4/5] fix: improve type safety in MapProxyHandler and litegraph exports - MapProxyHandler: replace `any` with generic type V for get/set methods and `unknown` for bindAllMethods - litegraph.ts: replace `any` with `Record` for widgets_info --- src/lib/litegraph/src/MapProxyHandler.ts | 6 +++--- src/lib/litegraph/src/litegraph.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/litegraph/src/MapProxyHandler.ts b/src/lib/litegraph/src/MapProxyHandler.ts index ee27da8deeb..3b8aa5cfc14 100644 --- a/src/lib/litegraph/src/MapProxyHandler.ts +++ b/src/lib/litegraph/src/MapProxyHandler.ts @@ -30,7 +30,7 @@ export class MapProxyHandler implements ProxyHandler< return [...target.keys()].map(String) } - get(target: Map, p: string | symbol): any { + get(target: Map, p: string | symbol): V | undefined { // Workaround does not support link IDs of "values", "entries", "constructor", etc. if (p in target) return Reflect.get(target, p, target) if (typeof p === 'symbol') return @@ -42,7 +42,7 @@ export class MapProxyHandler implements ProxyHandler< set( target: Map, p: string | symbol, - newValue: any + newValue: V ): boolean { if (typeof p === 'symbol') return false @@ -55,7 +55,7 @@ export class MapProxyHandler implements ProxyHandler< return target.delete(p as number | string) } - static bindAllMethods(map: Map): void { + static bindAllMethods(map: Map): void { map.clear = map.clear.bind(map) map.delete = map.delete.bind(map) map.forEach = map.forEach.bind(map) diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index b15109d5cf2..0b24eb47b80 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -62,7 +62,7 @@ export interface LGraphNodeConstructor { size?: Size min_height?: number slot_start_y?: number - widgets_info?: any + widgets_info?: Record collapsable?: boolean color?: string bgcolor?: string From 80c3228e60efaef2119b3e5d66346a81b706c056 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 9 Jan 2026 21:10:41 +0100 Subject: [PATCH 5/5] refactor: use type guard instead of type casts in LGraphNodeProperties --- src/lib/litegraph/src/LGraphNodeProperties.ts | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/lib/litegraph/src/LGraphNodeProperties.ts b/src/lib/litegraph/src/LGraphNodeProperties.ts index 04c3ac4429b..ae51d373973 100644 --- a/src/lib/litegraph/src/LGraphNodeProperties.ts +++ b/src/lib/litegraph/src/LGraphNodeProperties.ts @@ -1,6 +1,8 @@ import type { LGraphNode } from './LGraphNode' -type LGraphNodeWithDynamicProps = LGraphNode & Record +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} /** * Default properties to track @@ -42,16 +44,22 @@ export class LGraphNodeProperties { targetObject: Record propertyName: string } { - const node: LGraphNodeWithDynamicProps = this - .node as LGraphNodeWithDynamicProps + // LGraphNode supports dynamic property access at runtime + let targetObject: Record = this.node as unknown as Record< + string, + unknown + > if (parts.length === 1) { - return { targetObject: node, propertyName: parts[0] } + return { targetObject, propertyName: parts[0] } } - let targetObject: Record = node for (let i = 0; i < parts.length - 1; i++) { - targetObject = targetObject[parts[i]] as Record + const key = parts[i] + const next = targetObject[key] + if (isRecord(next)) { + targetObject = next + } } return { @@ -175,9 +183,11 @@ export class LGraphNodeProperties { */ #ensureNestedPath(path: string): void { const parts = path.split('.') - const node: LGraphNodeWithDynamicProps = this - .node as LGraphNodeWithDynamicProps - let current: Record = node + // LGraphNode supports dynamic property access at runtime + let current: Record = this.node as unknown as Record< + string, + unknown + > // Create all parent objects except the last property for (let i = 0; i < parts.length - 1; i++) { @@ -185,7 +195,10 @@ export class LGraphNodeProperties { if (!current[part]) { current[part] = {} } - current = current[part] as Record + const next = current[part] + if (isRecord(next)) { + current = next + } } }