Repository navigation
Nodekit Pack Pattern
@nynxz/zenkit-nodekit is how a ComfyUI node pack puts Vue components inside node bodies. A
widget is one .vue file; its filename decides the io type the Python schema refers to. The pack
works on plain ComfyUI and picks up extras (popout panels, slot links, the shared viewer) when
ZenKit is installed.
This page follows what ComfyUI-NynxzNodes and ComfyUI-ZenCut actually do. See also Nodekit media and Nodekit panels and canvas.
Five frontend files, two Python files and one command give you one node with a Vue body.
// pack.json
{ "namespace": "zencut", "displayName": "ZenCut", "category": "ZenCut" }// frontend/framework.ts — the pack's one import of nodekit
import { createNodekit } from '@nynxz/zenkit-nodekit'
import manifest from '../pack.json'
// Everything identity-free (defineNode, readMediaDrop, openZenPanel, the types, …)…
export * from '@nynxz/zenkit-nodekit'
// …and what createNodekit binds to this pack.
export const {
NAMESPACE,
DISPLAY_NAME,
nodeId,
typeId,
route,
routeUrl,
settingId,
registerNodes,
discoverNodes,
discoverWidgets,
widgetTypes,
mountWidget,
addNodeHeaderButton,
mediaLibrary,
thumbUrl,
uploadOrReuse,
mediaInfo,
} = createNodekit(manifest)// frontend/main.ts
import '@nynxz/zenkit-ui/style.css'
import '@nynxz/zenkit-ui/comfy-bridge.css'
import { DISPLAY_NAME, discoverWidgets, registerNodes, widgetTypes } from '@/framework'
const widgets = import.meta.glob('./widgets/*.vue', { eager: true })
registerNodes(discoverWidgets(widgets))
console.log(`[${DISPLAY_NAME}] widgets: ${widgetTypes(widgets).join(', ')}`)<!-- frontend/widgets/ZenCut.vue -> io type ZENCUT_ZEN_CUT -->
<script lang="ts">
import type { NodeDef, WidgetOptions } from '@/framework'
export const widgetOptions: WidgetOptions = {
name: 'edit',
fill: true,
minHeight: 560,
default: {},
}
export const nodeDef: Omit<NodeDef, 'widgets'> = { is: 'zencut.Edit', minSize: [980, 680] }
</script>
<script setup lang="ts">
const props = defineProps<{
widget?: { value: unknown; callback?: (v: unknown) => void }
node?: unknown
}>()
</script>
<template><div>…</div></template># nodes/cut/node.py
from comfy_api.latest import io
from .._zenkit import CATEGORY, node_id
from .._zenkit.io_types import widget_type
EditType = widget_type("ZenCut", dict) # -> ZENCUT_ZEN_CUT
class ZenCutEdit(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(node_id=node_id("Edit"), display_name="ZenCut", category=CATEGORY,
inputs=[EditType.Input("edit")],
outputs=[io.Video.Output(display_name="video")])# nodes/__init__.py
from ._zenkit import comfy_entrypoint # noqa: F401# __init__.py
from .nodes import comfy_entrypoint # noqa: F401
WEB_DIRECTORY = "./web"pnpm exec zenkit-nodekit sync # writes nodes/_zenkit/ from pack.json; commit itPlus vite.config.mts, tsconfig.json and package.json, covered under Build setup.
Both real packs use the same shape:
pack.json identity: namespace, displayName, category
package.json nodekit (dev) + zenkit-ui (dep)
.pnpmfile.cjs optional: link a local ZenKit checkout (ZENKIT_DEV)
vite.config.mts zenPluginConfig({ srcDir: './frontend', outDir: 'web' })
tsconfig.json
__init__.py V3: re-exports comfy_entrypoint; WEB_DIRECTORY = "./web"
nodes/__init__.py from ._zenkit import comfy_entrypoint
nodes/_zenkit/ generated by `zenkit-nodekit sync` (identity, io types, discovery,
media routes, jobs); committed, never edited
frontend/framework.ts createNodekit(manifest), re-exports
frontend/main.ts glob widgets -> registerNodes
frontend/widgets/*.vue one file = one widget io type
frontend/nodes/*.ts optional: explicit defineNode() modules
web/ build output (gitignored, built in CI)
Note:
zenPluginConfigdefaults tosrc/andjs/. Both real packs usefrontend/andweb/by passingsrcDirandoutDir.
pack.json holds the pack's identity. createNodekit(manifest) derives every id from it, so node
ids, io types, routes and setting keys agree.
| Field | Used for |
|---|---|
namespace |
Node-id prefix, io-type prefix, route prefix, extension name. Dots nest (acme.labs). |
displayName |
Console messages |
category |
Root menu category, exposed as CATEGORY
|
python |
Optional settings for zenkit-nodekit sync (below) |
| Identity member |
namespace: "nynxz" gives |
|---|---|
NAMESPACE, DISPLAY_NAME, CATEGORY
|
the manifest values |
ROUTE_PREFIX |
/nynxz (acme.labs gives /acme/labs) |
nodeId('Image.Compare') |
nynxz.Image.Compare |
typeId('LoraStack'), typeId('LORA_STACK')
|
NYNXZ_LORA_STACK |
route('media') |
/nynxz/media (no api.apiURL prefix) |
settingId('SaveImage.BaseDirectory') |
nynxz.SaveImage.BaseDirectory |
createNodekit is a factory rather than a configure() call because widget modules call
nodeId() while they are being evaluated, before main.ts runs. Keep it in one
framework.ts and import from @/framework everywhere else.
The Python half reads the same file through zenkit-nodekit sync, which writes it into
nodes/_zenkit/identity.py as NAMESPACE, CATEGORY, WIDGET_IO_PREFIX, node_id(), route(),
type_id() and setting_id(). Python never spells the namespace out; after editing pack.json, run
sync again (and sync --check in CI catches forgetting to).
createNodekit(pack: PackManifest): NodekitNodekit is the Identity above plus these bound helpers:
| Member | Signature | Purpose |
|---|---|---|
registerNodes |
(defs: NodeDef[], extensionName?: string) => void |
Register every def as one ComfyUI extension (name defaults to NAMESPACE) |
discoverWidgets |
(modules: Record<string, unknown>) => NodeDef[] |
Turn a widgets/*.vue glob into defs by filename |
discoverNodes |
(modules: Record<string, unknown>) => NodeDef[] |
Flatten a glob of modules that default-export a NodeDef or NodeDef[]
|
widgetTypes |
(modules) => string[] |
The io types a widget glob registers, sorted |
mountWidget |
(node, opts: MountOptions) => { widget: DOMWidget } |
Mount a component in a node body yourself (details) |
addNodeHeaderButton |
(node, widgetEl, opts) => NodeHeaderButtonHandle |
A button in the node title bar (details) |
routeUrl |
(path: string) => string |
The full URL of a pack route (route(path) through ComfyUI's api.apiURL, so base-path safe): what to fetch
|
mediaLibrary |
MediaLibrary |
Lists ComfyUI's media folders through the pack's /<ns>/media route (details). |
Everything else (defineNode, useDragSurface, viewUrl, openZenPanel, hasZenPanels,
openViewer, registerSlotLink, the media-ref helpers, the types) needs no identity, and
framework.ts re-exports it with export *. The identity-bound helpers exist only on the
createNodekit result, so the two never collide.
discoverWidgets turns each file in the glob into one NodeDef:
- The file name becomes the component name:
widgets/LoraStack.vuegivesLoraStack. -
typeId(name)gives the io type:NYNXZ_LORA_STACK. - The def is
{ ...nodeDef, widgets: [{ type, component: default, ...widgetOptions }] }.
Files are processed in path order. A file without a default export, or a second file mapping to the same io type, is skipped with a console warning.
One file gives one node def. When a second node reuses the same widget type, declare it with
defineNode in a separate glob (NynxzNodes uses frontend/nodes/*.ts):
// frontend/nodes/previewImage.ts
import { defineNode, typeId } from '@/framework'
import SavePreview, { widgetOptions } from '@/widgets/SavePreview.vue'
export default defineNode({
is: 'nynxz.Preview.Image',
minSize: [300, 320],
hideOutputImages: true,
widgets: [{ ...widgetOptions, type: typeId('SavePreview'), component: SavePreview }],
})// frontend/main.ts
const widgets = import.meta.glob('./widgets/*.vue', { eager: true })
const extraNodes = import.meta.glob('./nodes/*.ts', { eager: true })
registerNodes([...discoverWidgets(widgets), ...discoverNodes(extraNodes)])The convention is frontend/nodes/*.ts, but any glob of modules that default-export
defineNode(...) (or an array of them) works.
The io type is a plain string shared by both halves. TypeScript derives it from the filename; Python derives it from the same component name.
| Component name | io type (namespace nynxz) |
|---|---|
LoraStack |
NYNXZ_LORA_STACK |
H3Studio |
NYNXZ_H3_STUDIO |
SavePreview |
NYNXZ_SAVE_PREVIEW |
Rule: NAMESPACE.toUpperCase() with . and - replaced by _, then _, then the name in
SCREAMING_SNAKE_CASE (([a-z0-9])([A-Z]) and ([A-Z]+)([A-Z][a-z]) get an underscore).
nodes/_zenkit/io_types.py (vendored by zenkit-nodekit sync) is the Python half. Its prefix comes
from pack.json, and type_id() applies the same rule as typeId(), dots and dashes included.
from .._zenkit.io_types import widget_type
LoraStackType = widget_type("LoraStack", list) # -> NYNXZ_LORA_STACK
inputs = [LoraStackType.Input("stack", default=[])]| Helper | Use |
|---|---|
widget_type(name, value_type, doc=None) |
Declare a socketless widget input bound to widgets/<name>.vue. Use .Input(id, default=…) in the schema. |
inline_widget(inp, name) |
Keep a normal input's socket type but draw it with widgets/<name>.vue (sets extra_dict["widgetType"]). Do not use io.WidgetInput(widget_type=…): that replaces the socket type too. |
advanced(inp) |
Mark an input advanced via extra_dict (concrete Input classes don't forward extra_dict as a kwarg). |
widget_io_type(name) |
The raw string |
The socketless input drops tooltip, because ComfyUI would show it over the whole node body.
Pack-specific helpers build on these. NynxzNodes keeps its color_input() (a STRING drawn by
widgets/ColorField.vue) in its own nodes/_widgets.py, as inline_widget(io.String.Input(...), "ColorField").
Note: the io type is a bare string contract. If the two sides disagree, no widget renders and the prompt fails with "Required input is missing".
zenkit-nodekit check-widgetsdiffs everywidget_type("X"/inline_widget(…, "X")call in the node package againstfrontend/widgets/*.vue, both ways; put it inpnpm check.
registerNodes mounts each widget with mountWidget, which creates a Vue app with two props:
| Prop | Type | Notes |
|---|---|---|
widget |
DOMWidget |
widget.value is what serialises into the workflow and the prompt |
node |
the LiteGraph node | For node.id, node.graph, node.properties, … |
widget.value is not reactive. Keep state in Vue reactivity and write it through, calling
widget.callback so ComfyUI notices. ZenCut's useEdit:
export function useEdit(widget: DOMWidget | undefined) {
const edit = reactive(toEdit(widget?.value))
if (widget)
effectScope(true).run(() =>
watch(
edit,
() => {
const value = JSON.parse(JSON.stringify(edit))
widget.value = value
try {
widget.callback?.(value)
} catch {
/* no callback */
}
},
{ deep: true },
),
)
return edit
}Note: the real packs pass
props.widgetinto a composable (which also avoidsvue/no-mutating-props), and they declare a localinterface DOMWidget { value; callback? }withwidgetoptional rather than importing nodekit'sDOMWidget. Either works at runtime.
The widget's input name and default come from the Python schema unless widgetOptions overrides
them.
A widget file can export two extra things from a plain <script lang="ts"> block (module scope,
beside <script setup>):
// NynxzNodes frontend/widgets/ImageCompare.vue
export const widgetOptions: WidgetOptions = {
name: 'compare_view',
minHeight: 200,
fill: true,
serialize: false, // preview URLs go stale on restart
default: { beforeImages: [], afterImages: [] },
}
export const nodeDef: Omit<NodeDef, 'widgets'> = {
is: 'nynxz.Image.Compare',
minSize: [440, 360],
output: {
widget: 'compare_view',
from: (o) => ({
beforeImages: (o.a_images ?? []).map(viewUrl),
afterImages: (o.b_images ?? []).map(viewUrl),
}),
},
}WidgetOptions is NodeWidgetDef without type and component.
| Option | Type | Default | Description |
|---|---|---|---|
name |
string |
the Python input id | Widget name. output.widget must match it. |
minHeight |
number |
— | Minimum body height in px |
minWidth |
number |
— | Narrowest the node may be resized to |
fill |
boolean |
false |
Stretch to fill the node body (stays user-resizable) instead of growing to fit content |
dragThrough |
boolean |
false |
Visual-only body: presses fall through to drag the node. Interactive children need pointer-events: auto. |
serialize |
boolean |
true |
false for transient values such as run results |
default |
unknown |
the Python schema default
|
Initial value |
| Field | Type | Description |
|---|---|---|
is |
string | string[] |
comfyClass / V3 node_id this applies to. An array shares one def across node classes. Optional: widgets register globally by io type. |
minSize |
[number, number] |
Floor applied on create: setSize(max(current, min))
|
hideOutputImages |
boolean |
Hide ComfyUI's default image strip |
widgets |
NodeWidgetDef[] |
Widgets to mount. Each io type registers once (first declaration wins). |
output |
{ widget: string; from: (output) => unknown } |
On execute, map the node's ui output into the widget via its callback. Python side: io.NodeOutput(..., ui=...). |
slotLinks |
SlotLinkDef[] |
{ input?, output?, spawn }: middle-click that slot to spawn and wire spawn. ZenKit only. |
settings |
Record<string, unknown>[] |
ComfyUI settings merged into the extension. Build ids with settingId(). |
defineNode(def) returns def unchanged; it exists for typing.
Note:
outputnever assignswidget.value. DOM widgets ignoreserialize: falsein ComfyUI, so writing the value would persist run results into the workflow. nodekit callswidget.callback(value)instead; your component handles it.
The vendored package provides the entry point; the node package re-exports it:
# nodes/__init__.py
from ._zenkit import comfy_entrypoint # noqa: F401comfy_entrypoint calls setup() (the media routes, then the pack's own route modules) and returns
an extension whose get_node_list() is load_nodes(). A pack that needs its own ComfyExtension
calls setup() and load_nodes() from it instead.
load_nodes imports every module under nodes/ whose name does not start with _, collects the
io.ComfyNode subclasses defined there, skips abstract bases, logs (does not raise) import errors,
and de-duplicates by node_id.
V3 gotcha: expose comfy_entrypoint and never also define NODE_CLASS_MAPPINGS. ComfyUI
checks for NODE_CLASS_MAPPINGS first and returns early, so comfy_entrypoint is never called and
nothing registers.
nodekit ships the Python half too, as plain modules that the zenkit-nodekit bin vendors into the
pack. Vendoring rather than a pip dependency keeps a pack installable from the ComfyUI registry
with nothing extra; the generated files are committed like any other source.
zenkit-nodekit sync # write nodes/_zenkit/ from pack.json; only changed files are touched
zenkit-nodekit sync --check # exit 1 when a file is stale, missing or no longer generated
zenkit-nodekit check-widgets # exit 1 when a widget type has no .vue, or a .vue no widget typeRun from the pack root, or pass --root <dir>. Every generated file starts with a header naming
nodekit's version; editing one is pointless, since the next sync overwrites it and --check
fails until then. Upgrading nodekit makes --check fail until you sync, which is the point: both
halves move together.
| Module | Public API |
|---|---|
__init__ |
comfy_entrypoint(), setup(), load_nodes(), NODE_PACKAGE, and everything from identity
|
identity |
NAMESPACE, DISPLAY_NAME, CATEGORY, ROUTE_PREFIX, WIDGET_IO_PREFIX, LOG_PREFIX, node_id(), type_id(), route(), setting_id()
|
io_types |
widget_type(), inline_widget(), advanced(), widget_io_type(), widget_input()
|
autodiscover |
load_nodes(package, paths) |
media_api |
register(routes), path_of(ref), resolve(ref), media_info(path), list_media(), find_input(), media_kind()
|
jobs |
Job(name, total=0, …) with update() / done() / fail() (a context manager), emit()
|
server |
routes (ComfyUI's route table, None outside ComfyUI), route(), send(event, payload, client_id=None), request_allowed(request)
|
Importing a module never registers anything; setup() does, once.
All optional:
{
"namespace": "nynxz",
"displayName": "Nynxz's Custom Nodes",
"category": "Nynxz",
"python": {
"package": "nodes",
"vendor": "_zenkit",
"routes": ["server.api"],
"logPrefix": "[NynxzNodes]",
"widgets": "frontend/widgets"
}
}| Field | Default | Meaning |
|---|---|---|
package |
"nodes" |
The node package (it must have an __init__.py) |
vendor |
"_zenkit" |
The generated package's name inside it. Must start with _, so autodiscovery skips it. |
routes |
[] |
The pack's own route modules, relative to package. setup() imports each; a failure is logged, never fatal. |
logPrefix |
"[<displayName>]" |
Prefix of the generated code's log lines |
widgets |
"frontend/widgets" |
Where check-widgets looks for components |
List the module in python.routes and write it against _zenkit.server. Nothing in _zenkit needs
editing:
# nodes/_routes.py (pack.json: "python": { "routes": ["_routes"] })
from aiohttp import web
from ._zenkit.server import route, routes
if routes is not None: # None outside ComfyUI
@routes.get(route("studio/state")) # -> /<ns>/studio/state
async def studio_state(request):
return web.json_response({...})An underscore name keeps autodiscovery from also importing it as a node module. Modules that
autodiscovery imports anyway (NynxzNodes' */api.py) keep working as they are.
ComfyUI's origin middleware blocks cross-site pages, but not DNS rebinding, and it never checks
the content type. Every route that changes state (POST, PUT, PATCH, DELETE, or a GET with side
effects beyond a cache) starts with request_allowed, which returns None or the 403 to send:
from ._zenkit.server import request_allowed, route, routes
if routes is not None:
@routes.post(route("studio/state"))
async def studio_save(request):
if (denied := request_allowed(request)) is not None:
return denied
body = await request.json()
...| Check | Rule |
|---|---|
| Host | While ComfyUI listens only on loopback (the default), the Host must be localhost, 127.0.0.1 or [::1]. --listen <addr> also allows that address and this machine's own name; a wildcard --listen (0.0.0.0, ::) skips it. Behind a reverse proxy, list the names it serves ComfyUI under in Settings ▸ ZenKit ▸ Security ▸ Allowed host names (comma-separated); the proxy must pass the Host through. |
| Origin | When present, its host and port must equal the Host header's. |
| Content type | A non-GET request with a body must send application/json (a cross-site form can't without a CORS preflight). Send headers: { 'Content-Type': 'application/json' } from the frontend. |
A refused request gets 403 {"error": "forbidden: …"} and one warning in the log per path. The
media routes run the same check, so a rebound page can't list or probe ComfyUI's folders either.
media_api.path_of(ref) is the containment check for loading a ref a widget stored: it accepts only
input/…, output/… and temp/…, refuses anything that resolves outside that folder, and raises
ValueError with a message fit for the user when the file is missing. resolve(ref) is the same
but returns None.
from .._zenkit.jobs import Job
with Job("Export batch", total=len(files)) as job: # "start"
for i, f in enumerate(files, 1):
export(f)
job.update(current=i, message=f.name) # "progress"
# "done" on a clean exit; "error" with the exception's text if the block raisedEach event is ZenKit's zenkit.job with the job's full state (see Jobs): ids are
<namespace>:<time>:<n>, source is DISPLAY_NAME. Without ZenKit, or outside ComfyUI, the
events go nowhere. Run the work in a task or thread so the route that starts it returns.
import { zenPluginConfig } from '@nynxz/zenkit-nodekit/vite'
export default zenPluginConfig({
configUrl: import.meta.url,
srcDir: './frontend',
outDir: 'web', // matches WEB_DIRECTORY in __init__.py
})| Option | Default | Description |
|---|---|---|
name |
package.json name
|
Pack name; also the data-extension attribute on the injected <style>
|
configUrl |
required | Always import.meta.url
|
srcDir |
'./src' |
Frontend source; @ aliases to it |
entry |
${srcDir}/main.ts |
Entry file |
outDir |
'js' |
Must match WEB_DIRECTORY
|
alias |
{} |
Extra aliases, merged after @
|
sharedRuntime |
false |
Import vue, @nynxz/zenkit-ui, @nynxz/zenkit-client from /zenkit/runtime/ instead of bundling. Makes the pack require ComfyUI-ZenKit. Node packs leave it off. |
What the preset does:
- Externalises
@comfy/appand@comfy/apiand rewrites them to ComfyUI's/scripts/app.jsand/scripts/api.js. - Folds all CSS into
main.jsas an injected<style>(ComfyUI only loads.jsfromWEB_DIRECTORY). - Sets
resolve.dedupe: ['vue']. Two Vue copies silently break reactivity; this happens as soon as a@nynxz/zenkit-*package is consumed through a link. - Defines
process.env.NODE_ENV = "production", outputs one unminifiedmain.js. - Names code split off by a dynamic
import()chunks/*.mjs: ComfyUI imports every.jsunderWEB_DIRECTORYat startup, which would load it all up front.
What works in both packs:
Add frontend/vite-env.d.ts with /// <reference types="vite/client" /> for import.meta.glob.
Note: listing
comfy.d.tsunderincludewhile excludingnode_modulesdrops it silently. Usefiles. Extending@nynxz/zenkit-nodekit/tsconfig.pack.jsonis optional; neither real pack does.
{
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite build --watch",
"typecheck": "vue-tsc --noEmit",
"sync": "zenkit-nodekit sync",
"check": "zenkit-nodekit sync --check && zenkit-nodekit check-widgets && vue-tsc --noEmit && vite build"
},
"dependencies": { "@nynxz/zenkit-ui": "^0.3.0" },
"devDependencies": {
"@nynxz/zenkit-nodekit": "^0.3.0",
"vite": "^6.3.5",
"@vitejs/plugin-vue": "^5.2.3",
"vue-tsc": "^3.3.11"
},
"peerDependencies": { "vue": "3.5.42" }
}Install both packages from npm:
pnpm add @nynxz/zenkit-ui@^0.3.0
pnpm add -D @nynxz/zenkit-nodekit@^0.3.0To work on ZenKit and a pack at once, add this .pnpmfile.cjs to the pack. When ZENKIT_DEV
points at a ZenKit checkout it rewrites the two dependencies to link: and skips the lockfile;
without it, installs come from npm as usual.
const dir = process.env.ZENKIT_DEV;
const local = dir && {
"@nynxz/zenkit-ui": `link:${dir}/packages/ui`,
"@nynxz/zenkit-nodekit": `link:${dir}/packages/nodekit`,
};
function readPackage(pkg) {
if (!local) return pkg;
for (const field of ["dependencies", "devDependencies"])
for (const name of Object.keys(pkg[field] ?? {})) if (local[name]) pkg[field][name] = local[name];
return pkg;
}
module.exports = {
hooks: {
readPackage,
updateConfig: (config) => (local ? { ...config, useLockfile: false, optimisticRepeatInstall: false } : config),
},
};ZENKIT_DEV=../ZenKit pnpm install # link the checkout
pnpm install --frozen-lockfile --config.optimistic-repeat-install=false # back to npm- A link resolves through the package's
exports, which point atdist/. Runpnpm buildin ZenKit first, and again after changing it, or the pack sees stale code. - The lockfile is not written while linked, so it keeps describing the npm versions.
web/ is gitignored, and pyproject.toml lists it under [tool.comfy] includes = ["web"] so
the registry package still contains it. NynxzNodes' publish workflow builds web/ and force-adds
it before comfy node publish (comfy-cli only archives tracked files). Use node_modules (no trailing slash) in
.gitignore, because under a pnpm workspace it can be a symlink.
A node with no Python class (NynxzNodes' Group Switcher) registers its own LiteGraph type and calls
mountWidget itself. See Nodekit panels and canvas.
| Gotcha | Fix |
|---|---|
NODE_CLASS_MAPPINGS defined alongside comfy_entrypoint
|
Remove it; nothing registers otherwise |
widget.value changed but nothing updates |
It isn't reactive. Hold state in ref/reactive, write through, call widget.callback
|
| Run results persisted into the workflow | Use nodeDef.output + serialize: false, never assign widget.value
|
| No widget, "Required input is missing" | TS filename and Python widget_type name disagree; zenkit-nodekit check-widgets
|
| Python routes or io types use an old namespace | pack.json changed without zenkit-nodekit sync; sync --check in CI |
| Second node needs the same widget |
defineNode in another glob; one def per widget file |
| Two Vue copies | Keep dedupe: ['vue'] (the preset does this) |
comfy.d.ts types missing |
Put it in files, not include
|
| Linked ZenKit changes not picked up |
pnpm build in ZenKit |
Getting started
Guides
- Panels and workspaces
- Apps and routing
- Themes
- Backgrounds
- Channels and media
- Jobs
- Capabilities
- Storage and bus
Node packs
UI components
Reference
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "resolveJsonModule": true, "isolatedModules": true, "skipLibCheck": true, "noEmit": true, "paths": { "@/*": ["./frontend/*"] }, "typeRoots": ["node_modules/@types"], }, // `files`, not `include`: `include` entries are filtered by `exclude`, so node_modules would drop it. "files": ["./node_modules/@nynxz/zenkit-nodekit/comfy.d.ts"], "include": ["frontend/**/*.ts", "frontend/**/*.vue"], "exclude": ["node_modules", "web"], }