Comfy-Org / Comfy-Org/ComfyUI_frontend
ECS branch: Nodes 2.0 dispatches on the stored widget type, so runtime widget.type swaps stop working
- Dominant language
- TypeScript
- Stars
- 2k
- Forks
- 704
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 512
Description
Nodes 2.0 decides how to render a widget from `widgetValueStore`, not from the live widget, but `BaseWidget.type` is never written back to the store. An extension that swaps `widget.type` after the node is in the graph gets no effect in Nodes 2.0. On `main` it worked.
## Where
On `feature/ecs-migration` (PR #14246), `useProcessedWidgets.ts`:
```ts
if (!shouldRenderAsVue({ type: widgetState.type, options })) return null
...
vueComponent: getComponent(widgetState.type) || (renderState?.isDOMWidget ? WidgetDOM : WidgetLegacy)
```
Both decisions read `widgetState.type` from the store.
On `main`, `safeWidgetMapper` in `useGraphNodeManager.ts` rebuilt `SafeWidgetData` on every recompute with `type: widget.type`, so `shouldRenderAsVue(widget)` and `getComponent(widget.type)` both saw the live string.
## Why it does not update
`BaseWidget.type` is a plain instance field, assigned once in the constructor. `_state.type` is written only in the constructor and by `setNodeId` -> `useWidgetValueStore().registerWidget()`. There is no `type` setter, no proxy, and no writeback. `registerWidget` also early-returns an existing state when the type matches, so it is not a refresh path either.
The pre-registration case is handled and tested: `setNodeId` passes `type: this.type` rather than spreading the constructor `_state`, covered by `BaseWidget.test.ts` "registers the live widget type". The post-registration case is not.
## Why it matters now
The ECS Compatibility Pre-Release Report (2026-08-21) clears `comfyui-easy-use` (198,516 installers, top-5 by install weight) by code review only, on the reasoning that "neither renderer dispatches on the type string". That is not true of `main`'s Vue renderer, which dispatches on the live string in two places. The branch makes it true, so the clearance is reasoning about the post-change state and concluding the pack is safe from a property the change itself introduces.
Type repurposing at runtime is a common hide-a-widget idiom in the pack ecosystem (`widget.type = 'converted-widget'` / `'hidden'`, with the original stashed on `origType`).
## Fix options
1. Mirror `type` into the store on assignment: turn `BaseWidget.type` into an accessor over `_state.type`, matching how `label` and `disabled` already work.
2. Keep the field but fall back to the live widget in the renderer: `liveWidgets.get(id)?.type ?? widgetState.type`. `liveWidgets` is already built in `computeProcessedWidgets`.
Option 1 is the one consistent with the store being the source of truth. It also fixes `SimplifiedWidget.type` and `renderKey`, which have the same staleness.
## Verification this needs
A seeded control, not a code read: install a pack that swaps `widget.type` at runtime, or fake one, and confirm the widget hides in Nodes 2.0 on `main` and does not on the branch. This is the same class of gap as the compat battery having no seeded-defect control.
Found while reviewing PR #14246. Posted as https://github.com/Comfy-Org/ComfyUI_frontend/pull/14246#discussion_r3833412271
Contributor guide
Assessment
This issue has not been assessed yet.