refactor(subgraph): key promoted widget state by entityId, drop encoded host-state name

Replace the encoded `[name, sourceNodeId, sourceWidgetName].join(':')`
host-state-name with the canonical `WidgetEntityId` per ADR 0009. Producers
and consumers flip in the same diff:

- Delete `getPromotedWidgetHostStateName` (was internal disambiguator)
- `PromotedWidgetView` reads/writes via `widgetValueIO` keyed by `entityId`
- `SubgraphNode.serialize` reads via `readWidgetValue(widget.entityId)`
- `getExplicitHostWidgetValue` drops its now-unused `subgraphNode` param
- `safeWidgetMapper` no longer overrides `storeNodeId/storeName`; those
  fields are removed from `SafeWidgetData`
- `useProcessedWidgets` looks up state via `getWidgetState(entityId)`
- `getWidgetIdentity` collapses to `${entityId}:${type}` (type suffix
  preserves the PR #9896 invariant for node-def schema drift)

Net -66 LOC. No fallback / accept-either branches: entityId is the single
source of truth for widget identity across the renderer.

Amp-Thread-ID: https://ampcode.com/threads/T-019e2260-c2ba-70b4-9962-1638be4bf645
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
DrJKL
2026-05-13 11:57:29 -07:00
parent 9233025ab4
commit c1515374b8
9 changed files with 95 additions and 163 deletions

View File

@@ -7,6 +7,7 @@ import { computed, nextTick, watch } from 'vue'
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import { createPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetView'
import { BaseWidget, LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { widgetEntityId } from '@/world/entityIds'
import {
createTestSubgraph,
createTestSubgraphNode
@@ -440,8 +441,9 @@ describe('Nested promoted widget mapping', () => {
expect(mappedWidget?.type).toBe('combo')
// Promoted widgets read from a host-scoped store entry so per-host values
// stay independent across SubgraphNode instances sharing the same subgraph.
expect(mappedWidget?.storeNodeId).toBe(String(subgraphNodeB.id))
expect(mappedWidget?.storeName).toBe('b_input:11:a_input')
expect(mappedWidget?.entityId).toBe(
widgetEntityId(graph.id, subgraphNodeB.id, 'b_input')
)
})
it('preserves distinct store identity for duplicate-named promoted widgets', () => {
@@ -475,13 +477,15 @@ describe('Nested promoted widget mapping', () => {
const widgets = nodeData?.widgets
expect(widgets).toHaveLength(2)
// Both promoted widgets live on the same host node, so storeNodeId is
// shared; storeName encodes the source identity to keep them distinct.
expect(widgets?.[0]?.storeNodeId).toBe(String(subgraphNode.id))
expect(widgets?.[1]?.storeNodeId).toBe(String(subgraphNode.id))
expect(widgets?.[0]?.storeName).toBe(`first_seed:${firstNode.id}:seed`)
expect(widgets?.[1]?.storeName).toBe(`second_seed:${secondNode.id}:seed`)
expect(widgets?.[0]?.storeName).not.toBe(widgets?.[1]?.storeName)
// Both promoted widgets live on the same host node; their entityIds use
// the distinct subgraph input names to keep them independent.
expect(widgets?.[0]?.entityId).toBe(
widgetEntityId(graph.id, subgraphNode.id, 'first_seed')
)
expect(widgets?.[1]?.entityId).toBe(
widgetEntityId(graph.id, subgraphNode.id, 'second_seed')
)
expect(widgets?.[0]?.entityId).not.toBe(widgets?.[1]?.entityId)
})
})

View File

@@ -7,10 +7,7 @@ import { reactive, shallowReactive } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
import {
getPromotedWidgetHostStateName,
isPromotedWidgetView
} from '@/core/graph/subgraph/promotedWidgetTypes'
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
import { matchPromotedInput } from '@/core/graph/subgraph/matchPromotedInput'
import {
resolveConcretePromotedWidget,
@@ -69,9 +66,7 @@ export interface SafeWidgetData {
*/
entityId?: WidgetEntityId
nodeId?: NodeId
storeNodeId?: NodeId
name: string
storeName?: string
type: string
/** Callback to invoke when widget value changes (wraps LiteGraph callback + triggerDraw) */
callback?: ((value: unknown) => void) | undefined
@@ -339,23 +334,14 @@ function safeWidgetMapper(
: undefined
const name = sourceWidgetName ?? displayName
// Promoted widgets carry per-host values that the migration writes to a
// host-scoped store entry. Vue rendering must read from that same entry
// (not the shared interior key) for per-instance independence.
let storeNodeId: NodeId | undefined = nodeId
let storeName = sourceWidgetName
if (isPromotedWidgetView(widget)) {
widget.ensureHostWidgetState()
storeNodeId = String(node.id)
storeName = getPromotedWidgetHostStateName(widget)
}
// Promoted widgets carry per-host values keyed by `widget.entityId`.
// Seed the store entry so Vue rendering can read it on first render.
if (isPromotedWidgetView(widget)) widget.ensureHostWidgetState()
return {
entityId: widget.entityId,
nodeId,
storeNodeId,
name,
storeName,
type: effectiveWidget.type,
...sharedEnhancements,
callback,

View File

@@ -52,12 +52,6 @@ export interface PromotedWidgetView extends IBaseWidget {
ensureHostWidgetState(): void
}
export function getPromotedWidgetHostStateName(
widget: PromotedWidgetView
): string {
return [widget.name, widget.sourceNodeId, widget.sourceWidgetName].join(':')
}
export function isPromotedWidgetView(
widget: IBaseWidget
): widget is PromotedWidgetView {

View File

@@ -23,18 +23,13 @@ import { matchPromotedInput } from '@/core/graph/subgraph/matchPromotedInput'
import { hasWidgetNode } from '@/core/graph/subgraph/widgetNodeTypeGuard'
import type { WidgetEntityId } from '@/world/entityIds'
import { widgetEntityId } from '@/world/entityIds'
import { ensureWidgetState, getWidgetState } from '@/world/widgetValueIO'
import {
getPromotedWidgetHostStateName,
isPromotedWidgetView
} from './promotedWidgetTypes'
import { isPromotedWidgetView } from './promotedWidgetTypes'
import type { PromotedWidgetView as IPromotedWidgetView } from './promotedWidgetTypes'
export type { PromotedWidgetView } from './promotedWidgetTypes'
export {
getPromotedWidgetHostStateName,
isPromotedWidgetView
} from './promotedWidgetTypes'
export { isPromotedWidgetView } from './promotedWidgetTypes'
interface SubgraphSlotRef {
name: string
@@ -230,11 +225,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
}
private getHostWidgetState(): WidgetState | undefined {
return useWidgetValueStore().getWidget(
this.graphId,
this.subgraphNode.id,
this.hostWidgetStateName
)
return getWidgetState(this.entityId)
}
private setHostWidgetState(value: IBaseWidget['value']): void {
@@ -251,9 +242,9 @@ class PromotedWidgetView implements IPromotedWidgetView {
/**
* Idempotently register a host-scoped widget state seeded with the current
* effective value. Vue rendering reads from this entry (via `safeWidgetMapper`
* exposing the host-scoped `storeNodeId`/`storeName` for promoted widgets),
* so the entry must exist before first render even if migration has not run.
* effective value. Vue rendering reads from this entry keyed by
* {@link entityId}, so it must exist before first render even if migration
* has not run.
*/
ensureHostWidgetState(): void {
if (this.getHostWidgetState()) return
@@ -262,9 +253,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
private registerHostWidgetState(value: IBaseWidget['value']): void {
const resolved = this.resolveDeepest()
useWidgetValueStore().registerWidget(this.graphId, {
nodeId: this.subgraphNode.id,
name: this.hostWidgetStateName,
ensureWidgetState(this.entityId, {
type: resolved?.widget.type ?? 'button',
value,
// Clone — never share the interior widget's options reference, or
@@ -277,10 +266,6 @@ class PromotedWidgetView implements IPromotedWidgetView {
})
}
private get hostWidgetStateName(): string {
return getPromotedWidgetHostStateName(this)
}
get label(): string | undefined {
const slot = this.getBoundSubgraphSlot()
if (slot) return slot.label ?? slot.displayName ?? slot.name

View File

@@ -1,9 +1,6 @@
import * as Sentry from '@sentry/vue'
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
import {
getPromotedWidgetHostStateName,
isPromotedWidgetView
} from '@/core/graph/subgraph/promotedWidgetTypes'
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
import { t } from '@/i18n'
import type {
IContextMenuValue,
@@ -21,7 +18,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLitegraphService } from '@/services/litegraphService'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { readWidgetValue } from '@/world/widgetValueIO'
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
@@ -143,10 +140,7 @@ function applySubgraphInputOrder(
const rows = subgraphNode.subgraph.inputs.map((input, index) => ({
subgraphInput: input,
hostInput: subgraphNode.inputs[index],
value: getExplicitHostWidgetValue(
subgraphNode,
subgraphNode.inputs[index]?._widget
)
value: getExplicitHostWidgetValue(subgraphNode.inputs[index]?._widget)
}))
const orderedRows = orderedIndices.flatMap((index) => rows[index] ?? [])
@@ -177,18 +171,13 @@ function applySubgraphInputOrder(
}
function getExplicitHostWidgetValue(
subgraphNode: SubgraphNode,
widget: IBaseWidget | undefined
): IBaseWidget['value'] | undefined {
if (!widget) return undefined
if (!isPromotedWidgetView(widget)) return widget.value
const state = useWidgetValueStore().getWidget(
subgraphNode.rootGraph.id,
subgraphNode.id,
getPromotedWidgetHostStateName(widget)
)
return state && isWidgetValue(state.value) ? state.value : undefined
const value = readWidgetValue(widget.entityId)
return isWidgetValue(value) ? value : undefined
}
function isSamePromotedWidget(left: IBaseWidget, right: IBaseWidget): boolean {

View File

@@ -34,7 +34,6 @@ import type {
} from '@/lib/litegraph/src/types/widgets'
import {
createPromotedWidgetView,
getPromotedWidgetHostStateName,
isPromotedWidgetView
} from '@/core/graph/subgraph/promotedWidgetView'
import type { PromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetView'
@@ -48,7 +47,7 @@ import { parsePreviewExposures } from '@/core/schemas/previewExposureSchema'
import { parseProxyWidgetErrorQuarantine } from '@/core/schemas/proxyWidgetQuarantineSchema'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { readWidgetValue } from '@/world/widgetValueIO'
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
import type { ExecutableLGraphNode, ExecutionId } from './ExecutableNodeDTO'
@@ -1168,16 +1167,11 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
serialized.properties = serializedProperties
const widgetStore = useWidgetValueStore()
const widgetValues = this.inputs.flatMap((input) => {
const widget = input._widget
if (!widget || !isPromotedWidgetView(widget)) return []
const state = widgetStore.getWidget(
rootGraphId,
this.id,
getPromotedWidgetHostStateName(widget)
)
return [state && isWidgetValue(state.value) ? state.value : undefined]
const value = readWidgetValue(widget.entityId)
return [isWidgetValue(value) ? value : undefined]
})
if (widgetValues.some((value) => value !== undefined)) {

View File

@@ -12,6 +12,9 @@ import type {
} from '@/composables/graph/useGraphNodeManager'
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { widgetEntityId } from '@/world/entityIds'
const GRAPH_ID = 'graph-test'
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
@@ -126,28 +129,35 @@ describe('NodeWidgets', () => {
})
it('deduplicates widgets with identical render identity while keeping distinct promoted sources', () => {
const duplicateEntityId = widgetEntityId(
GRAPH_ID,
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
'string_a'
)
const distinctEntityId = widgetEntityId(
GRAPH_ID,
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:20',
'string_a'
)
const duplicateA = createMockWidget({
name: 'string_a',
type: 'text',
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeName: 'string_a',
entityId: duplicateEntityId,
slotName: 'string_a'
})
const duplicateB = createMockWidget({
name: 'string_a',
type: 'text',
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeName: 'string_a',
entityId: duplicateEntityId,
slotName: 'string_a'
})
const distinct = createMockWidget({
name: 'string_a',
type: 'text',
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:20',
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:20',
storeName: 'string_a',
entityId: distinctEntityId,
slotName: 'string_a'
})
const nodeData = createMockNodeData('SubgraphNode', [
@@ -162,12 +172,16 @@ describe('NodeWidgets', () => {
})
it('prefers a visible duplicate over a hidden duplicate when identities collide', () => {
const sharedEntityId = widgetEntityId(
GRAPH_ID,
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
'string_a'
)
const hiddenDuplicate = createMockWidget({
name: 'string_a',
type: 'text',
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeName: 'string_a',
entityId: sharedEntityId,
slotName: 'string_a',
options: { hidden: true }
})
@@ -175,8 +189,7 @@ describe('NodeWidgets', () => {
name: 'string_a',
type: 'text',
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeName: 'string_a',
entityId: sharedEntityId,
slotName: 'string_a',
options: { hidden: false }
})
@@ -191,20 +204,23 @@ describe('NodeWidgets', () => {
})
it('does not deduplicate entries that share names but have different widget types', () => {
const sharedEntityId = widgetEntityId(
GRAPH_ID,
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
'string_a'
)
const textWidget = createMockWidget({
name: 'string_a',
type: 'text',
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeName: 'string_a',
entityId: sharedEntityId,
slotName: 'string_a'
})
const comboWidget = createMockWidget({
name: 'string_a',
type: 'combo',
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
storeName: 'string_a',
entityId: sharedEntityId,
slotName: 'string_a'
})
const nodeData = createMockNodeData('SubgraphNode', [
@@ -220,18 +236,14 @@ describe('NodeWidgets', () => {
it('keeps unresolved same-name promoted entries distinct by source execution identity', () => {
const firstTransientEntry = createMockWidget({
nodeId: undefined,
storeNodeId: undefined,
name: 'string_a',
storeName: 'string_a',
slotName: 'string_a',
type: 'text',
sourceExecutionId: '65:18'
})
const secondTransientEntry = createMockWidget({
nodeId: undefined,
storeNodeId: undefined,
name: 'string_a',
storeName: 'string_a',
slotName: 'string_a',
type: 'text',
sourceExecutionId: '65:19'
@@ -251,16 +263,14 @@ describe('NodeWidgets', () => {
name: 'text',
type: 'text',
nodeId: 'outer-subgraph:1',
storeNodeId: 'outer-subgraph:1',
storeName: 'text',
entityId: widgetEntityId(GRAPH_ID, 'outer-subgraph:1', 'text'),
slotName: 'text'
})
const secondPromoted = createMockWidget({
name: 'text',
type: 'text',
nodeId: 'outer-subgraph:2',
storeNodeId: 'outer-subgraph:2',
storeName: 'text',
entityId: widgetEntityId(GRAPH_ID, 'outer-subgraph:2', 'text'),
slotName: 'text'
})

View File

@@ -13,6 +13,9 @@ import {
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { widgetEntityId } from '@/world/entityIds'
const GRAPH_ID = 'graph-test'
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
@@ -41,37 +44,19 @@ const createMockWidget = (
})
describe('getWidgetIdentity', () => {
it('returns stable dedupeIdentity for widgets with storeNodeId', () => {
const widget = createMockWidget({
storeNodeId: 'subgraph:19',
storeName: 'text',
slotName: 'text',
type: 'text'
})
it('keys dedupeIdentity by entityId and widget type', () => {
const entityId = widgetEntityId(GRAPH_ID, 'subgraph:19', 'text')
const widget = createMockWidget({ entityId, name: 'text', type: 'text' })
const { dedupeIdentity, renderKey } = getWidgetIdentity(widget, '1', 0)
expect(dedupeIdentity).toBe('node:19:text:text:text')
expect(dedupeIdentity).toBe(`${entityId}:text`)
expect(renderKey).toBe(dedupeIdentity)
})
it('returns transient renderKey for widgets without stable identity', () => {
const widget = createMockWidget({
nodeId: undefined,
storeNodeId: undefined,
sourceExecutionId: undefined
})
it('falls back to a transient renderKey when no entityId is set', () => {
const widget = createMockWidget({ entityId: undefined })
const { dedupeIdentity, renderKey } = getWidgetIdentity(widget, '5', 3)
expect(dedupeIdentity).toBeUndefined()
expect(renderKey).toBe('transient:5:test_widget:test_widget:combo:3')
})
it('uses sourceExecutionId for identity when no nodeId', () => {
const widget = createMockWidget({
nodeId: undefined,
storeNodeId: undefined,
sourceExecutionId: '65:18'
})
const { dedupeIdentity } = getWidgetIdentity(widget, '1', 0)
expect(dedupeIdentity).toBe('exec:65:18:test_widget:test_widget:combo')
expect(renderKey).toBe('transient:5:test_widget:combo:3')
})
})
@@ -211,8 +196,7 @@ describe('computeProcessedWidgets borderStyle', () => {
name: 'text',
type: 'combo',
nodeId: 'inner-subgraph:1',
storeNodeId: 'inner-subgraph:1',
storeName: 'text',
entityId: widgetEntityId(GRAPH_ID, 'inner-subgraph:1', 'text'),
slotName: 'text',
promotedLabel: 'Text'
})
@@ -245,8 +229,7 @@ describe('computeProcessedWidgets borderStyle', () => {
name: 'text',
type: 'combo',
nodeId: 'inner-subgraph:1',
storeNodeId: 'inner-subgraph:1',
storeName: 'text',
entityId: widgetEntityId(GRAPH_ID, 'inner-subgraph:1', 'text'),
slotName: 'text'
})
@@ -306,12 +289,12 @@ describe('computeProcessedWidgets borderStyle', () => {
})
it('deduplication keeps visible widget over hidden duplicate', () => {
const sharedEntityId = widgetEntityId(GRAPH_ID, '1', 'text')
const hiddenWidget = createMockWidget({
name: 'text',
type: 'combo',
nodeId: '1',
storeNodeId: '1',
storeName: 'text',
entityId: sharedEntityId,
slotName: 'text',
options: { hidden: true }
})
@@ -320,8 +303,7 @@ describe('computeProcessedWidgets borderStyle', () => {
name: 'text',
type: 'combo',
nodeId: '1',
storeNodeId: '1',
storeName: 'text',
entityId: sharedEntityId,
slotName: 'text'
})

View File

@@ -31,6 +31,7 @@ import {
} from '@/stores/widgetValueStore'
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { getWidgetState } from '@/world/widgetValueIO'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import type {
LinkedUpstreamInfo,
@@ -125,26 +126,13 @@ export function getWidgetIdentity(
dedupeIdentity?: string
renderKey: string
} {
const rawWidgetId = widget.storeNodeId ?? widget.nodeId
const storeWidgetName = widget.storeName ?? widget.name
const slotNameForIdentity = widget.slotName ?? widget.name
const stableIdentityRoot = rawWidgetId
? `node:${String(stripGraphPrefix(rawWidgetId))}`
: widget.sourceExecutionId
? `exec:${widget.sourceExecutionId}`
: undefined
const dedupeIdentity = stableIdentityRoot
? `${stableIdentityRoot}:${storeWidgetName}:${slotNameForIdentity}:${widget.type}`
const dedupeIdentity = widget.entityId
? `${widget.entityId}:${widget.type}`
: undefined
const renderKey =
dedupeIdentity ??
`transient:${String(nodeId ?? '')}:${storeWidgetName}:${slotNameForIdentity}:${widget.type}:${index}`
return {
dedupeIdentity,
renderKey
}
`transient:${String(nodeId ?? '')}:${widget.name}:${widget.type}:${index}`
return { dedupeIdentity, renderKey }
}
export function isWidgetVisible(
@@ -193,13 +181,15 @@ export function computeProcessedWidgets({
if (!shouldRenderAsVue(widget)) continue
const identity = getWidgetIdentity(widget, nodeId, index)
const storeWidgetName = widget.storeName ?? widget.name
const bareWidgetId = String(
stripGraphPrefix(widget.storeNodeId ?? widget.nodeId ?? nodeId ?? '')
)
const widgetState = graphId
? widgetValueStore.getWidget(graphId, bareWidgetId, storeWidgetName)
: undefined
const widgetState = widget.entityId
? getWidgetState(widget.entityId)
: graphId
? widgetValueStore.getWidget(
graphId,
String(stripGraphPrefix(widget.nodeId ?? nodeId ?? '')),
widget.name
)
: undefined
const mergedOptions: IWidgetOptions = {
...(widget.options ?? {}),
...(widgetState?.options ?? {})
@@ -247,9 +237,7 @@ export function computeProcessedWidgets({
widgetState,
identity: { renderKey }
} of uniqueWidgets) {
const bareWidgetId = String(
stripGraphPrefix(widget.storeNodeId ?? widget.nodeId ?? nodeId ?? '')
)
const bareWidgetId = String(stripGraphPrefix(widget.nodeId ?? nodeId ?? ''))
const vueComponent =
getComponent(widget.type) ||