Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/extensions/core/previewAny.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 65 additions & 20 deletions src/lib/litegraph/src/LGraphNodeProperties.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { LGraphNode } from './LGraphNode'

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}

/**
* Default properties to track
*/
Expand All @@ -11,7 +15,6 @@ const DEFAULT_TRACKED_PROPERTIES: string[] = [
'color',
'bgcolor'
]

/**
* Manages node properties with optional change tracking and instrumentation.
*/
Expand All @@ -37,6 +40,34 @@ export class LGraphNodeProperties {
}
}

#resolveTargetObject(parts: string[]): {
targetObject: Record<string, unknown>
propertyName: string
} {
// LGraphNode supports dynamic property access at runtime
let targetObject: Record<string, unknown> = this.node as unknown as Record<
string,
unknown
>

if (parts.length === 1) {
return { targetObject, propertyName: parts[0] }
}

for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i]
const next = targetObject[key]
if (isRecord(next)) {
targetObject = next
}
}
Comment on lines +57 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent failure when intermediate path is not a record.

If an intermediate path segment is not a valid record (e.g., was set to null or a primitive externally), the loop silently continues without updating targetObject, returning the wrong parent object. While #ensureNestedPath is called first for nested paths, consider adding a defensive check or early return to prevent hard-to-debug issues if this invariant is violated.

Suggested defensive handling
 for (let i = 0; i < parts.length - 1; i++) {
   const key = parts[i]
   const next = targetObject[key]
   if (isRecord(next)) {
     targetObject = next
+  } else {
+    // Intermediate path is not traversable; return current state
+    break
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i]
const next = targetObject[key]
if (isRecord(next)) {
targetObject = next
}
}
for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i]
const next = targetObject[key]
if (isRecord(next)) {
targetObject = next
} else {
// Intermediate path is not traversable; return current state
break
}
}


return {
targetObject,
propertyName: parts[parts.length - 1]
}
}

/**
* Instruments a single property to track changes
*/
Expand All @@ -47,15 +78,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,
Expand All @@ -64,11 +87,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)
Expand Down Expand Up @@ -108,13 +131,20 @@ export class LGraphNodeProperties {
*/
#createInstrumentedDescriptor(
propertyPath: string,
initialValue: any
initialValue: unknown
): PropertyDescriptor {
let value = initialValue
return this.#createInstrumentedDescriptorTyped(propertyPath, initialValue)
}

#createInstrumentedDescriptorTyped<TValue>(
propertyPath: string,
initialValue: TValue
): PropertyDescriptor {
let value: TValue = initialValue

return {
get: () => value,
set: (newValue: any) => {
set: (newValue: TValue) => {
const oldValue = value
value = newValue
this.#emitPropertyChange(propertyPath, oldValue, newValue)
Expand All @@ -129,8 +159,16 @@ export class LGraphNodeProperties {
*/
#emitPropertyChange(
propertyPath: string,
oldValue: any,
newValue: any
oldValue: unknown,
newValue: unknown
): void {
this.#emitPropertyChangeTyped(propertyPath, oldValue, newValue)
}

#emitPropertyChangeTyped<TValue>(
propertyPath: string,
oldValue: TValue,
newValue: TValue
): void {
this.node.graph?.trigger('node:property:changed', {
nodeId: this.node.id,
Expand All @@ -145,15 +183,22 @@ export class LGraphNodeProperties {
*/
#ensureNestedPath(path: string): void {
const parts = path.split('.')
let current: any = this.node
// LGraphNode supports dynamic property access at runtime
let current: Record<string, unknown> = 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++) {
const part = parts[i]
if (!current[part]) {
current[part] = {}
}
current = current[part]
const next = current[part]
if (isRecord(next)) {
current = next
}
}
}

Expand All @@ -175,7 +220,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
}
}
40 changes: 17 additions & 23 deletions src/lib/litegraph/src/LiteGraphGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -971,41 +971,35 @@ export class LiteGraphGlobal {
}
}

extendClass(target: any, origin: any): void {
extendClass(
target: Record<string, unknown> & { prototype?: object },
origin: Record<string, unknown> & { prototype?: object }
): void {
for (const i in origin) {
// copy class properties
// eslint-disable-next-line no-prototype-builtins
if (target.hasOwnProperty(i)) continue
target[i] = origin[i]
}

if (origin.prototype) {
if (origin.prototype && target.prototype) {
const originProto = origin.prototype as Record<string, unknown>
const targetProto = target.prototype as Record<string, unknown>

// 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)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/lib/litegraph/src/MapProxyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class MapProxyHandler<V> implements ProxyHandler<
return [...target.keys()].map(String)
}

get(target: Map<number | string, V>, p: string | symbol): any {
get(target: Map<number | string, V>, 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
Expand All @@ -42,7 +42,7 @@ export class MapProxyHandler<V> implements ProxyHandler<
set(
target: Map<number | string, V>,
p: string | symbol,
newValue: any
newValue: V
): boolean {
if (typeof p === 'symbol') return false

Expand All @@ -55,7 +55,7 @@ export class MapProxyHandler<V> implements ProxyHandler<
return target.delete(p as number | string)
}

static bindAllMethods(map: Map<any, any>): void {
static bindAllMethods(map: Map<unknown, unknown>): void {
map.clear = map.clear.bind(map)
map.delete = map.delete.bind(map)
map.forEach = map.forEach.bind(map)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/litegraph/src/litegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
size?: Size
min_height?: number
slot_start_y?: number
widgets_info?: any
widgets_info?: Record<string, unknown>
collapsable?: boolean
color?: string
bgcolor?: string
Expand Down
2 changes: 1 addition & 1 deletion src/lib/litegraph/src/types/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export interface IBaseWidget<

// TODO: Confirm this format
callback?(
value: any,
value: unknown,
canvas?: LGraphCanvas,
node?: LGraphNode,
pos?: Point,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/litegraph/src/widgets/BaseWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export abstract class BaseWidget<
tooltip?: string
element?: HTMLElement
callback?(
value: any,
value: TWidget['value'],
canvas?: LGraphCanvas,
node?: LGraphNode,
pos?: Point,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/litegraph/src/widgets/ButtonWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}