mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-18 09:48:09 +00:00
## ELI-5 Some custom nodes have a `getCustomWidgets()` function that's *supposed* to hand us a list of widgets. A few of them hand us back nothing (null/undefined) instead. We were trying to read that "nothing" like a list, which crashes with *"Cannot convert undefined or null to object"* — and because it happens while the app is still starting up, it can break the whole page. This PR just says "if there's nothing to register, skip it." ## What `registerCustomWidgets` called `Object.entries(newWidgets)` directly. When an extension's `getCustomWidgets()` resolves to `null`/`undefined` (it's typed non-null, but extensions are untrusted and routinely violate the type), this throws `TypeError: Cannot convert undefined or null to object`. The call site in `extensionService.ts` runs this inside a bare async IIFE, *outside* the `wrapWithErrorHandling` wrappers used for keybindings/settings, so the throw is unhandled and surfaces during app initialization. ## Why it matters In production this is one of the highest-volume unhandled frontend errors — ~2.6k events across **~1,160 distinct sessions/day**, all funneling through this one `Object.entries` call. Guarding the choke point silences it for every caller. ## Fix - Keep `registerCustomWidgets` typed `Record<string, ComfyWidgetConstructor>` (the correct internal contract) and early-return on nullish input. The runtime guard defends against untrusted extensions that violate the type at the boundary, without weakening the signature for legitimate callers. - Add a regression test asserting `registerCustomWidgets(null!/undefined!)` does not throw (the `!` casts simulate the boundary violation). ## Test plan - [x] `npx vitest run src/stores/widgetStore.test.ts` — 8 passing, including the new null/undefined case. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
41 lines
1.5 KiB
TypeScript
41 lines
1.5 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { computed, ref } from 'vue'
|
|
|
|
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
|
import { getInputSpecType } from '@/schemas/nodeDefSchema'
|
|
import type { InputSpec as InputSpecV1 } from '@/schemas/nodeDefSchema'
|
|
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
|
import { ComfyWidgets } from '@/scripts/widgets'
|
|
|
|
export const useWidgetStore = defineStore('widget', () => {
|
|
const coreWidgets = ComfyWidgets
|
|
const customWidgets = ref<Map<string, ComfyWidgetConstructor>>(new Map())
|
|
const widgets = computed<Map<string, ComfyWidgetConstructor>>(
|
|
() => new Map([...customWidgets.value, ...Object.entries(coreWidgets)])
|
|
)
|
|
|
|
function inputIsWidget(spec: InputSpecV2 | InputSpecV1) {
|
|
const type = Array.isArray(spec) ? getInputSpecType(spec) : spec.type
|
|
return widgets.value.has(type)
|
|
}
|
|
|
|
function registerCustomWidgets(
|
|
newWidgets: Record<string, ComfyWidgetConstructor>
|
|
) {
|
|
// Extensions are untrusted code: `getCustomWidgets` is typed to return
|
|
// `Record<string, ...>`, but in practice an extension can resolve it to
|
|
// null/undefined. Guard here so a single misbehaving custom node can't
|
|
// throw "Cannot convert undefined or null to object" and break app init.
|
|
if (!newWidgets) return
|
|
for (const [type, widget] of Object.entries(newWidgets)) {
|
|
customWidgets.value.set(type, widget)
|
|
}
|
|
}
|
|
|
|
return {
|
|
widgets,
|
|
inputIsWidget,
|
|
registerCustomWidgets
|
|
}
|
|
})
|