mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 13:32:11 +00:00
Compare commits
74 Commits
glary/resi
...
ext-api/i-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cc1457596 | ||
|
|
616a30ddb3 | ||
|
|
f182d1ff96 | ||
|
|
524830023d | ||
|
|
d70ead814d | ||
|
|
542256eeec | ||
|
|
5df5ee1d0b | ||
|
|
2f76a931a8 | ||
|
|
dcdc9e7bfa | ||
|
|
3f639da07d | ||
|
|
d66f989a96 | ||
|
|
de7730b67b | ||
|
|
528d014e15 | ||
|
|
b4fe848527 | ||
|
|
e4f2feb6f8 | ||
|
|
e9d51335c2 | ||
|
|
06fb27d233 | ||
|
|
c2ab350cb7 | ||
|
|
4eaf898cc2 | ||
|
|
83132ab5a1 | ||
|
|
9ab998003c | ||
|
|
42a1ba05f4 | ||
|
|
a059009dce | ||
|
|
0ffb475d74 | ||
|
|
14349fe23f | ||
|
|
73ba37a78e | ||
|
|
ca909b2832 | ||
|
|
5bf589f8d1 | ||
|
|
6b6d4773d9 | ||
|
|
a86dc1cc2b | ||
|
|
24d893d401 | ||
|
|
3d09f89251 | ||
|
|
dd5335df7c | ||
|
|
ee0537fdb5 | ||
|
|
d5d5692928 | ||
|
|
e56187adf3 | ||
|
|
446d0a216e | ||
|
|
bd4d195230 | ||
|
|
ff314491da | ||
|
|
b3b3b10fea | ||
|
|
fa3229b402 | ||
|
|
2f102353fa | ||
|
|
df921f3512 | ||
|
|
a058a410ac | ||
|
|
300be13a4c | ||
|
|
f069f540ce | ||
|
|
d4323d7ab1 | ||
|
|
c5d7fb113f | ||
|
|
6345359ca8 | ||
|
|
b2e9c8f749 | ||
|
|
20daf22a68 | ||
|
|
f83510a223 | ||
|
|
ba636765a7 | ||
|
|
d0614e595f | ||
|
|
8e71fd0436 | ||
|
|
8bc2ff0800 | ||
|
|
ceec47df88 | ||
|
|
aa0b00953b | ||
|
|
7b9ea4a01f | ||
|
|
e292976f8d | ||
|
|
a0478f66ea | ||
|
|
52146d918f | ||
|
|
fa0079dfb5 | ||
|
|
f10990df3a | ||
|
|
ccfd53bdf5 | ||
|
|
8da221b5db | ||
|
|
e74250fd8a | ||
|
|
bf272a784d | ||
|
|
e25e210933 | ||
|
|
5b05f2b793 | ||
|
|
3476d06fc9 | ||
|
|
c1748c6fe3 | ||
|
|
9a6fff645d | ||
|
|
2de2e07b36 |
88
.github/workflows/ci-tests-extension-api.yaml
vendored
Normal file
88
.github/workflows/ci-tests-extension-api.yaml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
# Description: Extension API test suite (I-TF) + compat-floor gate (I-TF.7)
|
||||
#
|
||||
# Runs on any PR touching extension-api declaration files, extension-api-v2
|
||||
# implementation/tests, or the touch-point DB/rollup (blast-radius changes).
|
||||
#
|
||||
# Two jobs:
|
||||
# test — vitest run against src/extension-api-v2/__tests__/
|
||||
# compat-floor — python scripts/check-compat-floor.py (exits 1 if any
|
||||
# blast_radius ≥ 2.0 category is missing a stub triple)
|
||||
#
|
||||
# The compat-floor job is the CI enforcement of PLAN.md §Compat-floor:
|
||||
# "Every blast_radius ≥ 2.0 pattern MUST pass v1 + v2 + migration before v2 ships."
|
||||
name: 'CI: Tests Extension API'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, dev*, core/*, extension-v2*]
|
||||
paths:
|
||||
- 'src/extension-api/**'
|
||||
- 'src/extension-api-v2/**'
|
||||
- 'packages/extension-api/**'
|
||||
- 'vitest.extension-api.config.mts'
|
||||
- 'research/touch-points/rollup.yaml'
|
||||
- 'research/touch-points/behavior-categories.yaml'
|
||||
- 'scripts/check-compat-floor.py'
|
||||
- 'pnpm-lock.yaml'
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
paths:
|
||||
- 'src/extension-api/**'
|
||||
- 'src/extension-api-v2/**'
|
||||
- 'packages/extension-api/**'
|
||||
- 'vitest.extension-api.config.mts'
|
||||
- 'research/touch-points/rollup.yaml'
|
||||
- 'research/touch-points/behavior-categories.yaml'
|
||||
- 'scripts/check-compat-floor.py'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Extension API tests (vitest)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Run extension-api test suite
|
||||
run: pnpm test:extension-api
|
||||
|
||||
- name: Run with coverage (push only)
|
||||
if: github.event_name == 'push'
|
||||
run: pnpm test:extension-api:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: github.event_name == 'push'
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
with:
|
||||
files: coverage/lcov.info
|
||||
flags: extension-api
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
compat-floor:
|
||||
name: Compat-floor gate (blast_radius ≥ 2.0)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PyYAML
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Check compat floor
|
||||
run: python3 scripts/check-compat-floor.py
|
||||
# Exits 1 if any blast_radius ≥ 2.0 behavior category is missing
|
||||
# any of its three stub files (v1/v2/migration). Enforces PLAN.md §Compat-floor.
|
||||
97
.github/workflows/extension-api-publish.yml
vendored
Normal file
97
.github/workflows/extension-api-publish.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
# Description: Publish @comfyorg/extension-api to npm with provenance attestation.
|
||||
#
|
||||
# Triggered by a tag push matching 'extension-api-v*' (e.g. extension-api-v0.1.0).
|
||||
# Also supports workflow_dispatch for a manual dry-run (set dry_run: true).
|
||||
#
|
||||
# Prerequisites (one-time human setup):
|
||||
# - NPM_TOKEN secret must be set in the repo/org settings with publish
|
||||
# access to the @comfyorg scope on npmjs.com.
|
||||
# - The @comfyorg npm scope already exists (used by @comfyorg/comfyui-frontend).
|
||||
#
|
||||
# PKG4.D4 (MIG1 / Phase A — surface-only shim)
|
||||
name: 'Extension API: Publish'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'extension-api-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run — build and verify without publishing'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write # needed to create GitHub Release
|
||||
id-token: write # needed for npm provenance via OIDC
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish @comfyorg/extension-api
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # full history for release notes
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Setup npm registry
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Build package
|
||||
run: pnpm --filter @comfyorg/extension-api build
|
||||
|
||||
- name: Typecheck package
|
||||
run: pnpm --filter @comfyorg/extension-api typecheck
|
||||
|
||||
- name: Verify package version matches tag
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}" # e.g. extension-api-v0.1.0
|
||||
PKG_VERSION=$(node -p "require('./packages/extension-api/package.json').version")
|
||||
TAG_VERSION="${TAG#extension-api-v}" # strip prefix → 0.1.0
|
||||
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "::error::Tag '$TAG' implies version '$TAG_VERSION' but packages/extension-api/package.json has '$PKG_VERSION'. Update the package.json before tagging."
|
||||
exit 1
|
||||
fi
|
||||
echo "Version check passed: $PKG_VERSION"
|
||||
|
||||
- name: Publish to npm (with provenance)
|
||||
if: github.event_name == 'push' || !inputs.dry_run
|
||||
run: |
|
||||
cd packages/extension-api
|
||||
npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Dry-run report
|
||||
if: inputs.dry_run
|
||||
run: |
|
||||
echo "=== DRY RUN — would publish ==="
|
||||
cd packages/extension-api
|
||||
npm pack --dry-run
|
||||
echo "=== End dry run ==="
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.event_name == 'push'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const tag = context.ref.replace('refs/tags/', '')
|
||||
const { data: release } = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: tag,
|
||||
name: tag,
|
||||
generate_release_notes: true,
|
||||
draft: false,
|
||||
prerelease: context.ref.includes('-alpha') || context.ref.includes('-beta') || context.ref.includes('-rc')
|
||||
})
|
||||
console.log(`Release created: ${release.html_url}`)
|
||||
65
.github/workflows/extension-api-typecheck.yml
vendored
Normal file
65
.github/workflows/extension-api-typecheck.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# Description: Typecheck and build the @comfyorg/extension-api package.
|
||||
# Runs on PRs and pushes touching the public type surface, the core .v2.ts
|
||||
# implementations, or the package scaffold — so regressions in the published
|
||||
# contract are caught before merge.
|
||||
#
|
||||
# PKG4.D3 (MIG1 / Phase A — surface-only shim)
|
||||
name: 'Extension API: Typecheck'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, dev*, core/*, extension-v2*]
|
||||
paths:
|
||||
- 'src/extension-api/**'
|
||||
- 'src/extensions/core/*.v2.ts'
|
||||
- 'src/services/extension-api-service.ts'
|
||||
- 'packages/extension-api/**'
|
||||
- '.github/workflows/extension-api-*.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
paths:
|
||||
- 'src/extension-api/**'
|
||||
- 'src/extensions/core/*.v2.ts'
|
||||
- 'src/services/extension-api-service.ts'
|
||||
- 'packages/extension-api/**'
|
||||
- '.github/workflows/extension-api-*.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Build + typecheck @comfyorg/extension-api
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Build package (emit declarations)
|
||||
run: pnpm --filter @comfyorg/extension-api build
|
||||
|
||||
- name: Typecheck package
|
||||
run: pnpm --filter @comfyorg/extension-api typecheck
|
||||
|
||||
- name: Smoke-test consumer (tsc --noEmit on minimal extension)
|
||||
# Verifies the published types are consumable from an external module
|
||||
# that imports from '@comfyorg/extension-api'. Uses a minimal fixture
|
||||
# checked in to packages/extension-api/test/smoke/.
|
||||
run: |
|
||||
cd packages/extension-api
|
||||
if [ -d test/smoke ]; then
|
||||
pnpm exec tsc --noEmit --project test/smoke/tsconfig.json
|
||||
else
|
||||
echo "No smoke test found — skipping (add packages/extension-api/test/smoke/ to enable)"
|
||||
fi
|
||||
@@ -6,6 +6,7 @@
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"ignorePatterns": [
|
||||
"packages/extension-api/build/**",
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"public/materialdesignicons.min.css",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
|
||||
108
docs/adr/0010-deprecate-node-level-serialization-control.md
Normal file
108
docs/adr/0010-deprecate-node-level-serialization-control.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 10. Deprecate Node-Level Serialization Control
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The v2 extension API initially included `node.on('beforeSerialize', handler)` as a migration path from v1 patterns like `node.onSerialize` and `nodeType.prototype.serialize` patching. This allowed extensions to:
|
||||
|
||||
1. **Append extra fields** to the serialized node object
|
||||
2. **Transform the entire serialized object** via a replace function
|
||||
|
||||
However, during design review (PR #12142), we questioned whether node-level serialization control is the right abstraction:
|
||||
|
||||
### The Problem
|
||||
|
||||
Node-level serialization control is fundamentally **wrong-layered**:
|
||||
|
||||
- **Extension state should live in widgets**, not as arbitrary fields on the node
|
||||
- Widget-level `beforeSerialize` already handles all legitimate use cases
|
||||
- Node-level hooks encourage storing extension state in ad-hoc `node.properties` or custom fields, which:
|
||||
- Breaks the clean separation between framework concerns and extension concerns
|
||||
- Creates hidden dependencies between serialization format and extension behavior
|
||||
- Makes migration and format evolution harder
|
||||
|
||||
### v1 Usage Analysis
|
||||
|
||||
Touch-point audit of `nodeType.prototype.serialize` and `node.onSerialize` patterns in the wild:
|
||||
|
||||
| Use Case | Proper v2 Alternative |
|
||||
| --------------------------- | --------------------------------------------------- |
|
||||
| Store extension state | Use widget values with `beforeSerialize` |
|
||||
| Persist per-instance config | Use `widget.setOption()` → `widget_options` sidecar |
|
||||
| Add metadata for export | Use a dedicated extension state widget |
|
||||
| Transform output format | Framework concern, not extension concern |
|
||||
|
||||
No use case requires node-level control that can't be better served by widget-level APIs.
|
||||
|
||||
## Decision
|
||||
|
||||
**Deprecate `node.on('beforeSerialize')`** — mark as `@deprecated` with clear guidance pointing to widget-level alternatives. Remove in v1.0.
|
||||
|
||||
Widget-level serialization control (`widget.on('beforeSerialize')`) remains fully supported as the correct abstraction.
|
||||
|
||||
### Migration Path
|
||||
|
||||
Extensions currently using `node.on('beforeSerialize')` should:
|
||||
|
||||
1. **Store state in widgets** instead of arbitrary node fields
|
||||
2. **Use `widget.on('beforeSerialize')`** to control serialization per-widget
|
||||
3. **Use `widget.setOption()`** for per-instance configuration
|
||||
|
||||
Example migration:
|
||||
|
||||
```ts
|
||||
// BEFORE (v1 / deprecated v2)
|
||||
node.on('beforeSerialize', (e) => {
|
||||
e.data['my_extension_state'] = computeState()
|
||||
})
|
||||
|
||||
// AFTER (recommended v2)
|
||||
const stateWidget = node.addWidget('STRING', '_my_state', '', {
|
||||
hidden: true,
|
||||
serialize: true
|
||||
})
|
||||
stateWidget.on('beforeSerialize', (e) => {
|
||||
e.setSerializedValue(JSON.stringify(computeState()))
|
||||
})
|
||||
```
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
1. Add `@deprecated` tag to `node.on('beforeSerialize')` with migration guidance
|
||||
2. Add console.warn when the deprecated event is used (dev mode only)
|
||||
3. Update documentation to recommend widget-level patterns
|
||||
4. Remove `NodeBeforeSerializeEvent` type and handler in v1.0
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Cleaner architecture**: Extension state flows through widgets, the designed data channel
|
||||
- **Better debuggability**: Widget values are visible in workflow JSON at predictable locations
|
||||
- **Easier migration**: Future format changes only need to consider widget serialization
|
||||
- **Reduced API surface**: One less event type to maintain and document
|
||||
|
||||
### Negative
|
||||
|
||||
- **Migration burden**: Extensions using node-level serialization must refactor
|
||||
- **Potential edge cases**: Some exotic use cases may require workarounds
|
||||
|
||||
### Risk Mitigation
|
||||
|
||||
- Deprecation warning gives extension authors runway to migrate
|
||||
- Widget-level APIs are already more capable than node-level alternatives
|
||||
- The `@deprecated` tag and docs provide clear migration path
|
||||
|
||||
## Notes
|
||||
|
||||
This decision was made during design review of PR #12142 (ext-api foundation). See `design-review-12142.md` Topic 11 for the full discussion thread.
|
||||
|
||||
Related decisions:
|
||||
|
||||
- Widget-level `beforeSerialize` remains the primary extension serialization hook
|
||||
- `setSerializeEnabled()` remains for simple static opt-out cases
|
||||
111
docs/adr/0011-immutability-via-fresh-copies.md
Normal file
111
docs/adr/0011-immutability-via-fresh-copies.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 11. Immutability Enforcement via Fresh Copies
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The extension API exposes collection-returning methods like `widgets()`, `inputs()`, `outputs()`, and object-returning methods like `getProperties()`. These methods need immutability guarantees to prevent extensions from accidentally or intentionally mutating internal state.
|
||||
|
||||
### The Problem
|
||||
|
||||
Without runtime immutability enforcement:
|
||||
|
||||
- Extensions could push items into `widgets()` array, corrupting internal state
|
||||
- Mutations to returned objects would silently affect internal data
|
||||
- Debugging would be difficult — state corruption could surface far from the mutation site
|
||||
- Internal framework code might inadvertently rely on returned arrays being stable
|
||||
|
||||
TypeScript's `readonly` modifier and JSDoc annotations provide compile-time protection, but:
|
||||
|
||||
- JavaScript consumers have no protection
|
||||
- Type assertions can bypass readonly
|
||||
- Agent-generated code may not respect type hints
|
||||
|
||||
### Options Considered
|
||||
|
||||
| Option | Pros | Cons |
|
||||
| ------------------------ | --------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| **1. `Object.freeze()`** | Runtime immutability, throws on mutation | Performance overhead, nested objects need deep freeze |
|
||||
| **2. Return fresh copy** | Simple, functional style, no mutation affects source | Slight memory overhead, multiple calls = multiple arrays |
|
||||
| **3. Proxy wrapper** | Helpful error messages, can intercept specific operations | Complexity, performance overhead, harder to debug |
|
||||
| **4. TypeScript only** | Zero runtime cost | No protection for JS consumers, can be bypassed |
|
||||
| **5. Private fields** | True encapsulation | Blocks read access too, not suitable for APIs |
|
||||
|
||||
## Decision
|
||||
|
||||
**Return fresh copies** (Option 2) for all collection-returning and object-returning methods in the extension API.
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```ts
|
||||
// CORRECT: Return fresh copy
|
||||
widgets(): readonly WidgetHandle[] {
|
||||
const container = world.getComponent(nodeId, WidgetComponentContainer)
|
||||
return (container?.widgetIds ?? []).map(createWidgetHandle)
|
||||
// Each call creates new array — mutations don't affect internal state
|
||||
}
|
||||
|
||||
getProperties(): Record<string, unknown> {
|
||||
return { ...world.getComponent(nodeId, NodeTypeKey)?.properties }
|
||||
// Shallow copy — mutations don't affect source
|
||||
}
|
||||
```
|
||||
|
||||
### Scope
|
||||
|
||||
Apply this pattern to:
|
||||
|
||||
- `NodeHandle.widgets()` — returns fresh `WidgetHandle[]`
|
||||
- `NodeHandle.inputs()` — returns fresh `SlotInfo[]`
|
||||
- `NodeHandle.outputs()` — returns fresh `SlotInfo[]`
|
||||
- `NodeHandle.getProperties()` — returns fresh `Record<string, unknown>`
|
||||
- `WidgetHandle` methods that return objects (if any)
|
||||
- Any future collection/object-returning methods
|
||||
|
||||
### Internal Callers
|
||||
|
||||
Framework-internal code must also use mutation APIs rather than mutating returned collections:
|
||||
|
||||
```ts
|
||||
// WRONG: Mutating returned array
|
||||
const widgets = node.widgets()
|
||||
widgets.push(newWidget) // No effect on node!
|
||||
|
||||
// CORRECT: Use mutation API
|
||||
node.addWidget(type, name, value, options)
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **True immutability**: Mutations to returned data never affect internal state
|
||||
- **Predictable behavior**: Each call returns fresh data reflecting current state
|
||||
- **Simple mental model**: "This is your copy, do what you want with it"
|
||||
- **JavaScript-safe**: Works regardless of TypeScript types
|
||||
|
||||
### Negative
|
||||
|
||||
- **Memory overhead**: Multiple calls create multiple arrays (usually negligible)
|
||||
- **No mutation detection**: Extensions silently get isolated copies, won't know their mutations are ignored
|
||||
- **Fresh reference each call**: Cannot use `===` to detect changes (use deep comparison or events)
|
||||
|
||||
### Mitigations
|
||||
|
||||
- Document that returned collections are snapshots
|
||||
- Use events (`valueChange`, `propertyChange`) to observe changes
|
||||
- The memory overhead is negligible for typical widget/slot counts
|
||||
|
||||
## Notes
|
||||
|
||||
This decision was made during design review of PR #12142 (ext-api foundation). See `design-review-12142.md` Topic 14 for the full discussion thread.
|
||||
|
||||
The alternative of `Object.freeze()` was rejected because:
|
||||
|
||||
- It requires deep freezing for nested objects
|
||||
- Performance overhead for each call
|
||||
- Fresh copies achieve the same goal more simply
|
||||
138
docs/adr/0012-pure-function-loader-pattern.md
Normal file
138
docs/adr/0012-pure-function-loader-pattern.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 12. Pure Function Loader Pattern for Extension Registration
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The v2 extension API needs a mechanism for extensions to register themselves with the runtime. Two broad approaches exist:
|
||||
|
||||
### Side-Effect Registration (Vue 2 Plugin Pattern)
|
||||
|
||||
```ts
|
||||
// Extension self-registers at import time
|
||||
import { app } from '@comfyorg/core'
|
||||
|
||||
app.use({
|
||||
install(app) {
|
||||
app.component('MyWidget', MyWidget)
|
||||
app.directive('my-directive', myDirective)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Problems:
|
||||
|
||||
- **Import order matters**: If extension A depends on extension B being registered first, import order must be carefully managed
|
||||
- **Hard to test**: Side effects at import time make mocking difficult; tests must manipulate module cache
|
||||
- **Hard to tree-shake**: Bundlers can't eliminate unused extensions — the import executes
|
||||
- **Timing coupling**: Registration and activation are conflated; can't collect extensions first, then activate later
|
||||
|
||||
### Pure Function + Loader Pattern
|
||||
|
||||
```ts
|
||||
// Extension declares intent — no side effects
|
||||
export default defineNode({
|
||||
name: 'my-extension',
|
||||
nodeTypes: ['MyNode'],
|
||||
nodeCreated(handle) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
|
||||
// App bootstrap activates all registered extensions
|
||||
startExtensionSystem()
|
||||
```
|
||||
|
||||
## Decision
|
||||
|
||||
**Adopt the pure function + loader pattern** for v2 extension registration.
|
||||
|
||||
### Implementation
|
||||
|
||||
```ts
|
||||
// Extension Registry (data collection only)
|
||||
const nodeExtensions: NodeExtensionOptions[] = []
|
||||
|
||||
export function defineNode(options: NodeExtensionOptions): void {
|
||||
nodeExtensions.push(options)
|
||||
}
|
||||
|
||||
// Loader (activation)
|
||||
export function startExtensionSystem(): void {
|
||||
const world = getWorld()
|
||||
watch(
|
||||
() => world.entitiesWith(NodeTypeKey),
|
||||
(nodeEntityIds) => {
|
||||
for (const id of nodeEntityIds) {
|
||||
mountExtensionsForNode(id)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
1. **Pure registration**: `defineNode()` has no side effects beyond pushing to an array. It doesn't touch the World, DOM, or any reactive state.
|
||||
|
||||
2. **Centralized activation**: `startExtensionSystem()` is called exactly once during app bootstrap. This single entry point controls when the extension system "goes live".
|
||||
|
||||
3. **Reactive mounting**: The loader watches the World for entity changes. Extensions are mounted/unmounted in response to ECS state, not imperative calls.
|
||||
|
||||
4. **Order independence**: Extensions can be defined in any order. The loader sorts by name (lexicographic, see D10b) for deterministic execution.
|
||||
|
||||
### Registration Flow
|
||||
|
||||
```
|
||||
Extension files App bootstrap World
|
||||
| | |
|
||||
| defineNode({...}) | |
|
||||
|--------------------->| |
|
||||
| (push to array) | |
|
||||
| | |
|
||||
| | startExtensionSystem()
|
||||
| |------------------>|
|
||||
| | (watch for NodeType entities)
|
||||
| | |
|
||||
| | NodeType added |
|
||||
| |<------------------|
|
||||
| | |
|
||||
| | mountExtensionsForNode(id)
|
||||
| | (runs setup) |
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Testability**: Extensions are plain objects; tests can construct them without side effects. `_clearExtensionsForTesting()` resets state between tests.
|
||||
- **Tree-shakeable**: Bundlers can eliminate unused extension files if their exports are never referenced.
|
||||
- **Order independent**: No import order bugs — the loader handles activation order.
|
||||
- **Lazy activation**: Registration is instant; activation only happens when `startExtensionSystem()` is called.
|
||||
- **SSR friendly**: Pure functions don't execute browser-only code at import time.
|
||||
|
||||
### Negative
|
||||
|
||||
- **Manual bootstrap**: App must call `startExtensionSystem()` — forgetting it silently disables extensions.
|
||||
- **Two-step mental model**: Developers must understand "register" vs "activate" phases.
|
||||
|
||||
### Mitigations
|
||||
|
||||
- App bootstrap is a well-defined location; the call is hard to miss.
|
||||
- Clear documentation and starter templates include the bootstrap call.
|
||||
- Dev-mode warnings if extensions are defined but the system never starts.
|
||||
|
||||
## Notes
|
||||
|
||||
This pattern aligns with modern framework conventions:
|
||||
|
||||
- **Vite plugins**: `vite.config.ts` collects plugins as an array; Vite activates them at build time.
|
||||
- **Vue 3 Composition API**: `setup()` returns reactive state; the framework activates it.
|
||||
- **React hooks**: Pure functions declare effects; React schedules them.
|
||||
|
||||
The key insight is separating **declaration** (what do I want?) from **execution** (make it happen). This separation enables testing, lazy loading, and predictable behavior.
|
||||
@@ -8,16 +8,19 @@ An Architecture Decision Record captures an important architectural decision mad
|
||||
|
||||
## ADR Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
| -------------------------------------------------------- | ---------------------------------------- | -------- | ---------- |
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
|
||||
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
|
||||
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
|
||||
| ADR | Title | Status | Date |
|
||||
| ---------------------------------------------------------- | ------------------------------------------ | -------- | ---------- |
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
|
||||
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
|
||||
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
|
||||
| [0010](0010-deprecate-node-level-serialization-control.md) | Deprecate Node-Level Serialization Control | Accepted | 2026-05-12 |
|
||||
| [0011](0011-immutability-via-fresh-copies.md) | Immutability Enforcement via Fresh Copies | Accepted | 2026-05-12 |
|
||||
| [0012](0012-pure-function-loader-pattern.md) | Pure Function Loader Pattern | Accepted | 2026-05-12 |
|
||||
|
||||
## Creating a New ADR
|
||||
|
||||
|
||||
93
docs/research/coordinate-systems.md
Normal file
93
docs/research/coordinate-systems.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Research: Canvas vs Client/Pixel Coordinate Usage
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
How should the extension API handle coordinate systems? Should it expose canvas coordinates, screen/client coordinates, or both?
|
||||
|
||||
## Coordinate Systems in ComfyUI
|
||||
|
||||
### 1. Canvas Space (Logical Units)
|
||||
|
||||
Node positions and sizes are in canvas logical units:
|
||||
|
||||
- Independent of zoom/pan
|
||||
- `[0, 0]` is the canvas origin
|
||||
- Moving a node to `[100, 200]` places it at canvas position (100, 200) regardless of viewport state
|
||||
|
||||
### 2. Screen/Client Space (Pixels)
|
||||
|
||||
DOM elements use pixel coordinates relative to the viewport:
|
||||
|
||||
- Affected by zoom/pan/scroll
|
||||
- `clientX`/`clientY` from mouse events
|
||||
- `getBoundingClientRect()` returns pixel values
|
||||
|
||||
### 3. Widget Height (Pixels)
|
||||
|
||||
DOM widgets reserve height in pixels:
|
||||
|
||||
```ts
|
||||
addDOMWidget({ name: 'preview', element: img, height: 200 }) // 200px
|
||||
```
|
||||
|
||||
## Current Extension API
|
||||
|
||||
| Method | Coordinate System | Notes |
|
||||
| -------------------------- | ----------------- | ----------------------------------------- |
|
||||
| `getPosition()` | Canvas | Returns `[x, y]` in canvas units |
|
||||
| `setPosition()` | Canvas | Accepts `[x, y]` in canvas units |
|
||||
| `getSize()` | Canvas | Returns `[width, height]` in canvas units |
|
||||
| `setSize()` | Canvas | Accepts `[width, height]` in canvas units |
|
||||
| `addDOMWidget({ height })` | Pixels | Reserved height in pixels |
|
||||
| `widget.setHeight(px)` | Pixels | Widget height in pixels |
|
||||
|
||||
## Analysis
|
||||
|
||||
### When Extensions Need Canvas Coordinates
|
||||
|
||||
1. **Node positioning**: Placing nodes relative to each other
|
||||
2. **Layout algorithms**: Auto-arranging nodes in a pattern
|
||||
3. **Collision detection**: Checking if nodes overlap
|
||||
|
||||
### When Extensions Need Screen Coordinates
|
||||
|
||||
1. **Custom overlays**: Drawing UI at a specific screen location
|
||||
2. **Drag-and-drop from external sources**: Converting mouse position to canvas position
|
||||
3. **Context menus**: Positioning menus near the cursor
|
||||
|
||||
### Current State
|
||||
|
||||
The extension API currently exposes:
|
||||
|
||||
- **Canvas coordinates** for node position/size — appropriate, as these are logical values
|
||||
- **Pixel values** for DOM widget height — appropriate, as these are DOM measurements
|
||||
|
||||
**Missing**: No conversion helpers between canvas and screen coordinates.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**The current approach is appropriate.** Extensions that manipulate node positions should work in canvas space. This is the natural abstraction — extensions shouldn't need to account for zoom/pan when laying out nodes.
|
||||
|
||||
### For Advanced Cases
|
||||
|
||||
Extensions needing coordinate conversion (e.g., custom overlays) should either:
|
||||
|
||||
1. **Use LiteGraph's existing transform utilities** (available on `app.canvas`)
|
||||
2. **Access the transform state** via a future canvas API (not part of node/widget handles)
|
||||
|
||||
### Why Not Expose Conversion Helpers on NodeHandle?
|
||||
|
||||
- **Wrong abstraction level**: Coordinate conversion is a canvas concern, not a node concern
|
||||
- **State dependency**: Conversion requires current zoom/pan state, which changes frequently
|
||||
- **Rare use case**: Most extensions work entirely in canvas space
|
||||
|
||||
## Future Considerations
|
||||
|
||||
If multiple extensions need coordinate conversion, consider:
|
||||
|
||||
1. **Canvas API**: `canvas.screenToCanvas(point)` / `canvas.canvasToScreen(point)`
|
||||
2. **Events with both coordinates**: `positionChanged` could include both canvas and screen positions
|
||||
|
||||
For now, no changes are needed — the current API serves the common cases well.
|
||||
93
docs/research/dom-widget-convergence.md
Normal file
93
docs/research/dom-widget-convergence.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Research: DOM Widget Convergence with Base Widget
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
Should DOM widgets be unified with base widgets, or kept as a separate concept?
|
||||
|
||||
## Current State
|
||||
|
||||
### Creation APIs
|
||||
|
||||
- `node.addWidget(type, name, value, options)` — creates a standard widget
|
||||
- `node.addDOMWidget({ name, element, height })` — creates a DOM-backed widget
|
||||
|
||||
### Internal Implementation
|
||||
|
||||
Both use the same underlying `CreateWidget` command:
|
||||
|
||||
```ts
|
||||
addWidget(type, name, defaultValue, options) {
|
||||
return dispatch({ type: 'CreateWidget', widgetType: type, ... })
|
||||
}
|
||||
|
||||
addDOMWidget(opts) {
|
||||
return dispatch({ type: 'CreateWidget', widgetType: 'DOM', ... })
|
||||
}
|
||||
```
|
||||
|
||||
DOM widgets are just widgets with `widgetType: 'DOM'` and an element reference.
|
||||
|
||||
### Shared WidgetHandle Interface
|
||||
|
||||
Both widget types share the same `WidgetHandle` interface:
|
||||
|
||||
| Method | Standard Widget | DOM Widget |
|
||||
| -------------------------------- | --------------- | ----------------------- |
|
||||
| `entityId`, `name`, `widgetType` | ✓ | ✓ |
|
||||
| `getValue()` / `setValue()` | ✓ (scalar) | ✓ (often unused) |
|
||||
| `isHidden()` / `setHidden()` | ✓ | ✓ |
|
||||
| `isDisabled()` / `setDisabled()` | ✓ | ✓ |
|
||||
| `setHeight(px)` | no-op | ✓ (updates reservation) |
|
||||
| `on('valueChange')` | ✓ | ✓ |
|
||||
| `getOption()` / `setOption()` | ✓ | ✓ |
|
||||
|
||||
## Analysis
|
||||
|
||||
### Arguments FOR Full Convergence
|
||||
|
||||
1. **Single mental model**: Extensions learn one widget concept, not two.
|
||||
2. **Consistent behavior**: All widgets appear in `node.widgets()`, serialize the same way.
|
||||
3. **Simpler API surface**: Fewer methods to document and maintain.
|
||||
|
||||
### Arguments FOR Keeping Separate APIs
|
||||
|
||||
1. **Different ergonomics**: Standard widgets are data-driven (name, value, options); DOM widgets are element-driven (pass an HTMLElement).
|
||||
2. **Type safety**: `addDOMWidget` can require `element: HTMLElement` at compile time; merging would make it optional with runtime checks.
|
||||
3. **Clear intent**: Separate APIs signal different use cases.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Keep the current partial convergence.** The implementation is unified (`CreateWidget` command), but the creation APIs remain separate for ergonomic reasons.
|
||||
|
||||
### Rationale
|
||||
|
||||
1. **Creation differs, usage is unified.** Extensions create DOM widgets differently (need an element), but interact with them the same way (via `WidgetHandle`).
|
||||
|
||||
2. **Type safety is valuable.** `addDOMWidget({ element })` is clearer than `addWidget('DOM', name, null, { element })`.
|
||||
|
||||
3. **Already well-integrated.** DOM widgets appear in `node.widgets()`, get the same events, and use the same serialization infrastructure.
|
||||
|
||||
### What "Convergence" Means Here
|
||||
|
||||
The widgets are already converged at:
|
||||
|
||||
- **Entity level**: Same `WidgetEntityId` brand
|
||||
- **Interface level**: Same `WidgetHandle` type
|
||||
- **Command level**: Same `CreateWidget` command internally
|
||||
|
||||
The APIs are intentionally separate at:
|
||||
|
||||
- **Creation level**: `addWidget` vs `addDOMWidget`
|
||||
|
||||
This is the right split — unified where it matters (runtime behavior), separate where it improves DX (creation ergonomics).
|
||||
|
||||
## Future Considerations
|
||||
|
||||
If we add more widget creation patterns (e.g., `addCanvasWidget`, `addThreeJSWidget`), we might consider:
|
||||
|
||||
1. **Factory pattern**: `node.widgets.create('DOM', { element })` / `node.widgets.create('INT', { min, max })`
|
||||
2. **Builder pattern**: `node.addWidget('DOM').withElement(el).withHeight(200).build()`
|
||||
|
||||
For now, two explicit methods (`addWidget`, `addDOMWidget`) serve the common cases well.
|
||||
112
docs/research/identity-encapsulation.md
Normal file
112
docs/research/identity-encapsulation.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Research: Identity Encapsulation in the Extension API
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
When do extensions need access to raw entity IDs (`NodeEntityId`, `WidgetEntityId`, `SlotEntityId`)? Should these be exposed or hidden?
|
||||
|
||||
## Current State
|
||||
|
||||
The v2 extension API exposes entity IDs as read-only properties:
|
||||
|
||||
```ts
|
||||
interface NodeHandle {
|
||||
readonly entityId: NodeEntityId
|
||||
// ...
|
||||
}
|
||||
|
||||
interface WidgetHandle {
|
||||
readonly entityId: WidgetEntityId
|
||||
// ...
|
||||
}
|
||||
|
||||
interface SlotInfo {
|
||||
readonly entityId: SlotEntityId
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
All IDs are **branded types** to prevent accidental mixing at compile time.
|
||||
|
||||
## Use Cases for Raw Entity IDs
|
||||
|
||||
### 1. Per-Instance State Mapping
|
||||
|
||||
Extensions maintaining external state per node:
|
||||
|
||||
```ts
|
||||
const nodeCache = new Map<NodeEntityId, CachedData>()
|
||||
|
||||
defineNode({
|
||||
name: 'my-cache-extension',
|
||||
nodeCreated(handle) {
|
||||
nodeCache.set(handle.entityId, computeExpensiveData())
|
||||
onNodeRemoved(() => nodeCache.delete(handle.entityId))
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Logging and Debugging
|
||||
|
||||
```ts
|
||||
node.on('executed', (e) => {
|
||||
console.log(`[${node.entityId}] Output:`, e.output)
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Inter-Extension Communication
|
||||
|
||||
Extensions that need to coordinate across multiple nodes:
|
||||
|
||||
```ts
|
||||
// Extension A stores data
|
||||
globalState.set(nodeA.entityId, data)
|
||||
|
||||
// Extension B retrieves it
|
||||
const data = globalState.get(nodeB.entityId)
|
||||
```
|
||||
|
||||
### 4. External System Interop
|
||||
|
||||
Extensions integrating with analytics, debugging tools, or external services that need stable node identifiers.
|
||||
|
||||
## Analysis
|
||||
|
||||
### Arguments FOR Exposing Entity IDs
|
||||
|
||||
1. **Legitimate need exists** — The use cases above are real and common.
|
||||
2. **Branded types prevent misuse** — Can't accidentally use `NodeEntityId` where `WidgetEntityId` is expected.
|
||||
3. **Read-only access** — Extensions can't mutate the ID or corrupt internal state.
|
||||
4. **Opaque value** — The format (`node:<graphUuid>:<localId>`) is an implementation detail; extensions should treat it as an opaque string.
|
||||
|
||||
### Arguments AGAINST Exposing Entity IDs
|
||||
|
||||
1. **Format coupling** — Extensions might parse the ID string and break if format changes.
|
||||
2. **Internal detail leakage** — Knowing the ID scheme reveals ECS architecture.
|
||||
3. **Future migration friction** — Changing ID representation requires careful deprecation.
|
||||
|
||||
### Mitigations
|
||||
|
||||
- **Document as opaque**: JSDoc clearly states IDs are opaque, not to be parsed.
|
||||
- **Branded types**: TypeScript prevents misuse across entity categories.
|
||||
- **Phase A format**: Current format includes graph UUID + local ID; this can evolve via semver.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Keep exposing entity IDs.** The use cases are legitimate, the branded types provide safety, and the read-only nature limits risk. Document that IDs are opaque strings — extensions should never parse or construct them.
|
||||
|
||||
### Guidelines for Extension Authors
|
||||
|
||||
1. **Use IDs only for keying** — Maps, Sets, logging, external system references.
|
||||
2. **Never parse IDs** — The format is an implementation detail subject to change.
|
||||
3. **Prefer handles over IDs** — When passing references between functions, use the handle object, not the raw ID.
|
||||
4. **Clean up on removal** — Always use `onNodeRemoved()` to clean up Maps keyed by entityId.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
If the ID format needs to change significantly, the branded types allow us to:
|
||||
|
||||
1. Introduce a new branded type (e.g., `NodeEntityIdV2`)
|
||||
2. Deprecate the old ID with migration guidance
|
||||
3. Keep both supported during a transition period
|
||||
121
docs/research/serialization-context.md
Normal file
121
docs/research/serialization-context.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Research: Serialization Context Simplification
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
Can the serialization context be simplified from 4 values to fewer?
|
||||
|
||||
Current contexts:
|
||||
|
||||
- `'workflow'` — saving workflow to disk
|
||||
- `'prompt'` — queueing a run (API call)
|
||||
- `'clone'` — copy/paste operation
|
||||
- `'subgraph-promote'` — widget becoming subgraph IO
|
||||
|
||||
## Use Case Analysis
|
||||
|
||||
### Context: 'workflow'
|
||||
|
||||
**Purpose**: Full persistence of user's work.
|
||||
|
||||
**What extensions need**: Serialize everything the user configured.
|
||||
|
||||
**Example**: A widget storing user preferences needs to include all settings.
|
||||
|
||||
### Context: 'prompt'
|
||||
|
||||
**Purpose**: Sending data to the backend for execution.
|
||||
|
||||
**What extensions need**:
|
||||
|
||||
- Transform values (dynamic prompts → resolved text)
|
||||
- Skip preview-only widgets
|
||||
- Materialize async sources (webcam → frame data)
|
||||
|
||||
**Example**:
|
||||
|
||||
```ts
|
||||
widget.on('beforeSerialize', async (e) => {
|
||||
if (e.context === 'prompt') {
|
||||
e.setSerializedValue(await captureFrame())
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Context: 'clone'
|
||||
|
||||
**Purpose**: Copy/paste should yield independent copy.
|
||||
|
||||
**What extensions need**: Reset instance-specific state while keeping user settings.
|
||||
|
||||
**Example**: A random seed widget might want a new seed on paste.
|
||||
|
||||
### Context: 'subgraph-promote'
|
||||
|
||||
**Purpose**: Widget becomes an input/output on a subgraph.
|
||||
|
||||
**What extensions need**: Convert internal representation to subgraph IO format.
|
||||
|
||||
**Example**: Internal state becomes an exposed parameter.
|
||||
|
||||
## Simplification Options
|
||||
|
||||
### Option A: Keep All 4 (Current State)
|
||||
|
||||
| Pro | Con |
|
||||
| ---------------------------------------- | ----------------- |
|
||||
| Each context has distinct semantics | 4 cases to handle |
|
||||
| Type system enforces valid values | More complex API |
|
||||
| Clear intent for each serialization path | |
|
||||
|
||||
### Option B: Collapse to 2 ('persist' | 'execute')
|
||||
|
||||
```ts
|
||||
context: 'persist' | 'execute'
|
||||
// 'persist' = workflow, clone, subgraph-promote
|
||||
// 'execute' = prompt
|
||||
```
|
||||
|
||||
| Pro | Con |
|
||||
| ------------------------------------------ | ------------------------------- |
|
||||
| Simpler mental model | Loses clone/promote distinction |
|
||||
| Most extensions only care about this split | Can't reset seed on clone |
|
||||
|
||||
### Option C: Remove Context Entirely
|
||||
|
||||
Extensions always transform regardless of context. The framework handles differences.
|
||||
|
||||
| Pro | Con |
|
||||
| ---------------------------- | ---------------------------------------------- |
|
||||
| Simplest API | Loses control for edge cases |
|
||||
| Framework handles all nuance | Some extensions need context-specific behavior |
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Keep all 4 contexts.** The use cases are genuinely different:
|
||||
|
||||
1. **workflow vs prompt**: Very common distinction. Dynamic prompts only process on prompt; preview widgets skip prompt. This is the most important split.
|
||||
|
||||
2. **clone**: Less common, but needed for stateful widgets (random seeds, generated IDs, captured frames).
|
||||
|
||||
3. **subgraph-promote**: Specialized, but necessary for the subgraph feature to work correctly.
|
||||
|
||||
### Rationale
|
||||
|
||||
- Extensions that don't care can ignore the context.
|
||||
- Extensions that do care have the information they need.
|
||||
- The 4 values map to 4 distinct operations in the framework.
|
||||
- Collapsing contexts would remove functionality with no real simplification gain.
|
||||
|
||||
### Mitigation for Complexity
|
||||
|
||||
- Document common patterns clearly
|
||||
- Most extensions only need: `if (context === 'prompt')`
|
||||
- Provide examples in JSDoc
|
||||
|
||||
## Note on Deprecation
|
||||
|
||||
The `NodeBeforeSerializeEvent` is deprecated (ADR-0010). The `WidgetBeforeSerializeEvent` remains supported and uses the same 4 contexts.
|
||||
|
||||
Since node-level serialization is being removed, this research applies to widget-level serialization only.
|
||||
148
docs/research/widget-state-categories.md
Normal file
148
docs/research/widget-state-categories.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Widget State Categories
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Overview
|
||||
|
||||
Widget state in the v2 extension API is organized into distinct categories, each with different characteristics for mutability, persistence, and event handling.
|
||||
|
||||
## Categories
|
||||
|
||||
### 1. Identity (Read-Only Invariants)
|
||||
|
||||
Set at construction, never change.
|
||||
|
||||
| Property | Type | Notes |
|
||||
| ------------ | ---------------- | ------------------------------------ |
|
||||
| `entityId` | `WidgetEntityId` | Branded, stable for widget lifetime |
|
||||
| `name` | `string` | Widget name as registered |
|
||||
| `widgetType` | `string` | e.g., `'INT'`, `'STRING'`, `'COMBO'` |
|
||||
| `label` | `string` | Display label, defaults to `name` |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- No setters exist for these properties
|
||||
- Extensions cannot modify identity after creation
|
||||
- Attempting to change identity is a design error
|
||||
|
||||
### 2. Value (First-Class, Every Widget)
|
||||
|
||||
The primary user-edited data.
|
||||
|
||||
| Method | Notes |
|
||||
| ------------------- | ----------------------------------- |
|
||||
| `getValue()` | Returns current value |
|
||||
| `setValue(v)` | Dispatches `SetWidgetValue` command |
|
||||
| `on('valueChange')` | Fires on value mutation |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Type varies by widget type (`number` for INT, `string` for STRING, etc.)
|
||||
- Persisted to `widgets_values` in workflow JSON
|
||||
- Included in API prompt by default (unless `setSerializeEnabled(false)`)
|
||||
- Changes are undo-able via command dispatch
|
||||
|
||||
### 3. Properties (First-Class, Every Widget)
|
||||
|
||||
Common properties all widgets share.
|
||||
|
||||
| Property | Getter | Setter | Event |
|
||||
| ----------- | ---------------------- | ------------------------ | ---------------- |
|
||||
| `hidden` | `isHidden()` | `setHidden(b)` | `propertyChange` |
|
||||
| `disabled` | `isDisabled()` | `setDisabled(b)` | `propertyChange` |
|
||||
| `serialize` | `isSerializeEnabled()` | `setSerializeEnabled(b)` | `propertyChange` |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Boolean values only
|
||||
- `hidden` affects UI visibility, not serialization
|
||||
- `disabled` makes widget read-only in UI
|
||||
- `serialize` controls inclusion in workflow/prompt output
|
||||
- Changes fire `propertyChange`, not `valueChange`
|
||||
|
||||
### 4. Options Bag (Type-Specific)
|
||||
|
||||
Per-instance overrides for type-specific configuration.
|
||||
|
||||
| Method | Notes |
|
||||
| ----------------------- | ---------------------------------------------- |
|
||||
| `getOption(key)` | Returns per-instance override or class default |
|
||||
| `setOption(key, value)` | Persists to `widget_options` sidecar |
|
||||
| `on('optionChange')` | Fires on option mutation |
|
||||
|
||||
**Common options by widget type:**
|
||||
|
||||
| Widget Type | Options |
|
||||
| ----------- | ---------------------------------- |
|
||||
| INT, FLOAT | `min`, `max`, `step`, `precision` |
|
||||
| STRING | `multiline`, `placeholder`, `rows` |
|
||||
| COMBO | `values` |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Options are JSON-serializable values
|
||||
- Persisted separately from `widgets_values` (additive, backward-compatible)
|
||||
- Extensions can add custom options
|
||||
- Option keys should be documented per widget type
|
||||
|
||||
### 5. DOM-Specific
|
||||
|
||||
Properties unique to DOM widgets.
|
||||
|
||||
| Method | Notes |
|
||||
| --------------- | ------------------------------------------ |
|
||||
| `setHeight(px)` | Updates reserved height, triggers relayout |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Only meaningful for `addDOMWidget()` widgets
|
||||
- No-op for non-DOM widgets
|
||||
- Measured in pixels (screen space)
|
||||
- No event fired; relayout is automatic
|
||||
|
||||
## Category Interaction Rules
|
||||
|
||||
### Event Separation
|
||||
|
||||
Each category has its own event:
|
||||
|
||||
| Category | Event |
|
||||
| ---------- | ---------------- |
|
||||
| Value | `valueChange` |
|
||||
| Properties | `propertyChange` |
|
||||
| Options | `optionChange` |
|
||||
|
||||
**Rule**: Events do not cross categories. Changing `hidden` does not fire `valueChange`.
|
||||
|
||||
### Serialization Behavior
|
||||
|
||||
| Category | Serialization |
|
||||
| ---------- | ---------------------------------------------------------------- |
|
||||
| Identity | Not serialized (derived from node type) |
|
||||
| Value | `widgets_values` array |
|
||||
| Properties | `hidden`/`disabled` not persisted; `serialize` affects inclusion |
|
||||
| Options | `widget_options` sidecar object |
|
||||
|
||||
### Mutability Summary
|
||||
|
||||
| Category | Mutable | Undo-able | Fires Event |
|
||||
| ---------- | ------- | --------- | ---------------- |
|
||||
| Identity | ✗ | — | — |
|
||||
| Value | ✓ | ✓ | `valueChange` |
|
||||
| Properties | ✓ | ✓ | `propertyChange` |
|
||||
| Options | ✓ | ✓ | `optionChange` |
|
||||
| DOM Height | ✓ | ✗ | — |
|
||||
|
||||
## Agent Implementation Notes
|
||||
|
||||
Agents working with widget state should:
|
||||
|
||||
1. **Respect category boundaries**: Don't try to `setValue()` to change visibility; use `setHidden()`.
|
||||
|
||||
2. **Use appropriate events**: Listen to `propertyChange` for UI state, `valueChange` for data.
|
||||
|
||||
3. **Handle type-specific options carefully**: Check widget type before accessing type-specific options.
|
||||
|
||||
4. **Preserve identity invariants**: Never try to change `entityId`, `name`, `widgetType`, or `label`.
|
||||
|
||||
5. **Consider serialization context**: Options persist to a sidecar; values persist to the main array.
|
||||
@@ -103,7 +103,9 @@ export default defineConfig([
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts'
|
||||
'vite.types.config.mts',
|
||||
'packages/extension-api/scripts/build-docs.ts',
|
||||
'packages/extension-api/vite.config.mts'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ const config: KnipConfig = {
|
||||
'src/assets/css/style.css',
|
||||
'src/scripts/ui/menu/index.ts',
|
||||
'src/types/index.ts',
|
||||
// Public extension API surface — published package entry point.
|
||||
// Per AGENTS.md, this barrel is the explicit exception to the
|
||||
// no-barrel-files-in-src rule because it IS the package entry.
|
||||
'src/extension-api/index.ts',
|
||||
'src/storybook/mocks/**/*.ts'
|
||||
],
|
||||
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}', '!.claude/**']
|
||||
@@ -32,6 +36,12 @@ const config: KnipConfig = {
|
||||
'packages/ingest-types': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
},
|
||||
'packages/extension-api': {
|
||||
// Build output is committed for npm package visibility
|
||||
ignore: ['build/**'],
|
||||
// typedoc is invoked via execSync in scripts/build-docs.ts
|
||||
ignoreDependencies: ['typedoc']
|
||||
},
|
||||
'apps/website': {
|
||||
entry: ['src/scripts/**/*.ts']
|
||||
}
|
||||
@@ -60,7 +70,30 @@ const config: KnipConfig = {
|
||||
// Agent review check config, not part of the build
|
||||
'.agents/checks/eslint.strict.config.js',
|
||||
// Devtools extensions, included dynamically
|
||||
'tools/devtools/web/**'
|
||||
'tools/devtools/web/**',
|
||||
// Deprecated stub re-exporting from `@/extension-api`. Will be removed
|
||||
// once PKG2 (`@comfyorg/extension-api`) ships and downstream imports
|
||||
// migrate to the package path.
|
||||
'src/types/extensionV2.ts',
|
||||
// D18 Phase 1 scaffolding — empty registries the loader will populate
|
||||
// in Phase 2 once side-effect registration moves out of
|
||||
// extension-api-service. See decisions/D18-pure-functions-loader-registration.md.
|
||||
'src/services/registries/**',
|
||||
// D18 Phase 1 — brand symbol + isBrandedExtension guard. Currently
|
||||
// consumed only by the define* call sites inside extension-api-service;
|
||||
// the type-guard and getBrandKind are exported for the Phase 2 loader.
|
||||
'src/extension-api/brand.ts',
|
||||
// Strangler-pattern v2 conversions of core extensions. Not yet wired
|
||||
// into the bootstrap (registration lands in a follow-up PR alongside
|
||||
// the v1→v2 cut-over). Tracked by I-EXT (#12144).
|
||||
'src/extensions/core/noteNode.v2.ts',
|
||||
'src/extensions/core/rerouteNode.v2.ts',
|
||||
'src/extensions/core/slotDefaults.v2.ts',
|
||||
// W6.P3.D — defineWidget+mount showcase port (D-widget-converge / A12).
|
||||
'src/extensions/core/webcamCapture.v2.ts',
|
||||
// W6.P4.D — canvas-units canary + escape-hatch annotation example
|
||||
// (D-coord-space / A13).
|
||||
'src/extensions/core/coordSpaceDemo.v2.ts'
|
||||
],
|
||||
vite: {
|
||||
config: ['vite?(.*).config.mts']
|
||||
@@ -79,7 +112,15 @@ const config: KnipConfig = {
|
||||
tags: [
|
||||
'-knipIgnoreUnusedButUsedByCustomNodes',
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch',
|
||||
'-knipIgnoreUsedByStackedPR'
|
||||
'-knipIgnoreUsedByStackedPR',
|
||||
// Public API surface consumed externally by extension authors and the
|
||||
// TypeDoc docgen pipeline (PKG2). Mark exports with @publicAPI when they
|
||||
// are part of `@comfyorg/extension-api` but not internally referenced.
|
||||
'-publicAPI',
|
||||
// Per D20, the three *EntityId brand re-exports in src/extension-api/{node,widget}.ts
|
||||
// are demoted to @internal — they stay available for internal package modules
|
||||
// but are removed from the public barrel and from TypeDoc output.
|
||||
'-internal'
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,12 @@ export default {
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames: string[]) {
|
||||
const joinedPaths = toJoinedRelativePaths(fileNames)
|
||||
// Exclude package build directories from linting
|
||||
const filtered = fileNames.filter(
|
||||
(f) => !f.includes('/packages/') || !f.includes('/build/')
|
||||
)
|
||||
if (filtered.length === 0) return []
|
||||
const joinedPaths = toJoinedRelativePaths(filtered)
|
||||
return [
|
||||
`pnpm exec oxfmt --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:unit": "nx run test",
|
||||
"test:extension-api": "[ -f vitest.extension-api.config.mts ] && vitest run --config vitest.extension-api.config.mts || echo 'SKIP: vitest.extension-api.config.mts not found'",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json",
|
||||
"typecheck:desktop": "nx run @comfyorg/desktop-ui:typecheck",
|
||||
|
||||
2
packages/extension-api/.gitignore
vendored
Normal file
2
packages/extension-api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
docs-build/
|
||||
node_modules/
|
||||
9
packages/extension-api/.npmignore
Normal file
9
packages/extension-api/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
src/
|
||||
scripts/
|
||||
tsconfig*.json
|
||||
typedoc.json
|
||||
docs-build/
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
__tests__/
|
||||
node_modules/
|
||||
50
packages/extension-api/README.md
Normal file
50
packages/extension-api/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# @comfyorg/extension-api
|
||||
|
||||
> **Status**: scaffolded. Package implementation pending PKG3 — see
|
||||
> `../../../plans/P2-extension-api-package.md` and
|
||||
> `../../../plans/prompts/PKG3-npm-package.md` in the workspace root.
|
||||
|
||||
The official TypeScript declaration package for ComfyUI extensions. This
|
||||
package replaces the practice of vendoring `comfy.d.ts` files in custom
|
||||
node repos.
|
||||
|
||||
## Install (post-publish)
|
||||
|
||||
```bash
|
||||
pnpm add -D @comfyorg/extension-api
|
||||
```
|
||||
|
||||
```ts
|
||||
import { defineExtension } from '@comfyorg/extension-api'
|
||||
|
||||
export default defineExtension({
|
||||
name: 'MyExtension',
|
||||
setup(ctx) {
|
||||
ctx.onNodeMounted((node) => {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
This package is built from the source-of-truth folder
|
||||
`../../src/extension-api/`. Do not edit the package's `build/` output
|
||||
directly.
|
||||
|
||||
## Versioning
|
||||
|
||||
- `0.x.y` — experimental during parallel-paths transition (D6 Phase A).
|
||||
- `1.0.0` — first stable release once D5/D6/D7/D8 are accepted and the
|
||||
surface has stabilized.
|
||||
- Breaking changes follow semver strictly from `1.0.0` onward.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- `decisions/D6-parallel-paths-migration.md` — versioning rationale
|
||||
- `plans/P2-extension-api-package.md` — package structure plan
|
||||
- `plans/prompts/PKG3-npm-package.md` — implementation prompt
|
||||
- `plans/prompts/PKG4-ci-workflows.md` — publish workflow
|
||||
- `plans/prompts/PKG5-docgen-mdx.md` — docgen pipeline
|
||||
- `plans/prompts/PKG6-docs-comfy-org.md` — docs.comfy.org integration
|
||||
2
packages/extension-api/build/.gitignore
vendored
Normal file
2
packages/extension-api/build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
index.js
|
||||
index.js.map
|
||||
1254
packages/extension-api/build/index.d.ts
vendored
Normal file
1254
packages/extension-api/build/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
41
packages/extension-api/package.json
Normal file
41
packages/extension-api/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@comfyorg/extension-api",
|
||||
"version": "0.1.0",
|
||||
"description": "Official TypeScript extension API for ComfyUI custom nodes",
|
||||
"files": [
|
||||
"build",
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./build/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/index.d.ts",
|
||||
"import": "./build/index.js",
|
||||
"default": "./build/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "vite build --logLevel warn",
|
||||
"build": "vite build",
|
||||
"docs:build": "tsx scripts/build-docs.ts",
|
||||
"docs:watch": "tsx scripts/build-docs.ts --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "catalog:",
|
||||
"typedoc": "0.28.19",
|
||||
"typedoc-plugin-markdown": "^4.6.3",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-dts": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:api"
|
||||
]
|
||||
}
|
||||
}
|
||||
495
packages/extension-api/scripts/build-docs.ts
Normal file
495
packages/extension-api/scripts/build-docs.ts
Normal file
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console -- CLI build script; stdout progress is intentional */
|
||||
|
||||
/**
|
||||
* PKG5 docgen pipeline: TypeDoc → Mintlify MDX
|
||||
*
|
||||
* Steps:
|
||||
* 1. Run TypeDoc with typedoc-plugin-markdown to emit raw markdown into docs-build/raw/
|
||||
* 2. Post-process each markdown file:
|
||||
* - Add Mintlify frontmatter (title, description, sidebarTitle, icon)
|
||||
* - Convert ``` fences without lang tag → ```ts
|
||||
* - Replace raw [TypeName] cross-refs with MDX relative links
|
||||
* - Wrap @example blocks in proper code fences
|
||||
* 3. Write final .mdx files to docs-build/mintlify/
|
||||
* 4. Emit docs-build/mintlify/nav-snippet.json — merges into docs.comfy.org mint.json
|
||||
*
|
||||
* Run: pnpm --filter @comfyorg/extension-api docs:build
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pkgRoot = path.resolve(__dirname, '..')
|
||||
const rawDir = path.join(pkgRoot, 'docs-build', 'raw')
|
||||
const mintlifyDir = path.join(pkgRoot, 'docs-build', 'mintlify')
|
||||
const watchMode = process.argv.includes('--watch')
|
||||
|
||||
// ── Page metadata ────────────────────────────────────────────────────────────
|
||||
// Controls frontmatter for each generated page. Key = TypeDoc output filename
|
||||
// stem (lowercased). Unrecognised files get generic metadata.
|
||||
|
||||
interface PageMeta {
|
||||
title: string
|
||||
sidebarTitle?: string
|
||||
description: string
|
||||
icon?: string
|
||||
group: 'core' | 'handles' | 'events' | 'shell' | 'identity' | 'root'
|
||||
order: number
|
||||
}
|
||||
|
||||
const PAGE_META: Record<string, PageMeta> = {
|
||||
// Top-level overview
|
||||
index: {
|
||||
title: 'Extension API Overview',
|
||||
description: 'TypeScript API reference for ComfyUI custom node extensions.',
|
||||
icon: 'puzzle-piece',
|
||||
group: 'root',
|
||||
order: 0
|
||||
},
|
||||
// Lifecycle / registration
|
||||
defineextension: {
|
||||
title: 'defineExtension',
|
||||
description:
|
||||
'Register an app-scoped extension for init, setup, and shell UI contributions.',
|
||||
icon: 'code',
|
||||
group: 'core',
|
||||
order: 1
|
||||
},
|
||||
definenodeextension: {
|
||||
title: 'defineNodeExtension',
|
||||
description:
|
||||
'Register a node-scoped extension reacting to node lifecycle events.',
|
||||
icon: 'code',
|
||||
group: 'core',
|
||||
order: 2
|
||||
},
|
||||
definewidgetextension: {
|
||||
title: 'defineWidgetExtension',
|
||||
description: 'Register a custom widget type with its own DOM rendering.',
|
||||
icon: 'code',
|
||||
group: 'core',
|
||||
order: 3
|
||||
},
|
||||
extensionoptions: {
|
||||
title: 'ExtensionOptions',
|
||||
description:
|
||||
'Options object for defineExtension — app-wide lifecycle and shell UI.',
|
||||
group: 'core',
|
||||
order: 4
|
||||
},
|
||||
nodeextensionoptions: {
|
||||
title: 'NodeExtensionOptions',
|
||||
description:
|
||||
'Options object for defineNodeExtension — node lifecycle hooks.',
|
||||
group: 'core',
|
||||
order: 5
|
||||
},
|
||||
widgetextensionoptions: {
|
||||
title: 'WidgetExtensionOptions',
|
||||
description:
|
||||
'Options object for defineWidgetExtension — custom widget rendering.',
|
||||
group: 'core',
|
||||
order: 6
|
||||
},
|
||||
onnoderemoved: {
|
||||
title: 'onNodeRemoved',
|
||||
sidebarTitle: 'onNodeRemoved',
|
||||
description:
|
||||
'Implicit-context lifecycle hook: fires when a node is removed from the graph.',
|
||||
group: 'core',
|
||||
order: 7
|
||||
},
|
||||
onnodemounted: {
|
||||
title: 'onNodeMounted',
|
||||
sidebarTitle: 'onNodeMounted',
|
||||
description:
|
||||
'Implicit-context lifecycle hook: fires when a node is fully mounted.',
|
||||
group: 'core',
|
||||
order: 8
|
||||
},
|
||||
// Handles
|
||||
nodehandle: {
|
||||
title: 'NodeHandle',
|
||||
description:
|
||||
'Controlled access to node state, mutations, slots, and events.',
|
||||
icon: 'circle-nodes',
|
||||
group: 'handles',
|
||||
order: 10
|
||||
},
|
||||
widgethandle: {
|
||||
title: 'WidgetHandle',
|
||||
description: 'Controlled access to widget state, mutations, and events.',
|
||||
icon: 'sliders',
|
||||
group: 'handles',
|
||||
order: 11
|
||||
},
|
||||
slotinfo: {
|
||||
title: 'SlotInfo',
|
||||
description: 'Read-only snapshot of a node slot (input or output).',
|
||||
group: 'handles',
|
||||
order: 12
|
||||
},
|
||||
// Events
|
||||
nodeexecutedevent: {
|
||||
title: 'NodeExecutedEvent',
|
||||
description: 'Payload fired when a node finishes execution.',
|
||||
group: 'events',
|
||||
order: 20
|
||||
},
|
||||
nodeconnectedevent: {
|
||||
title: 'NodeConnectedEvent',
|
||||
description: 'Payload fired when a slot connection is made.',
|
||||
group: 'events',
|
||||
order: 21
|
||||
},
|
||||
nodedisconnectedevent: {
|
||||
title: 'NodeDisconnectedEvent',
|
||||
description: 'Payload fired when a slot connection is removed.',
|
||||
group: 'events',
|
||||
order: 22
|
||||
},
|
||||
nodepositionchangedevent: {
|
||||
title: 'NodePositionChangedEvent',
|
||||
description: 'Payload fired when a node is moved on the canvas.',
|
||||
group: 'events',
|
||||
order: 23
|
||||
},
|
||||
nodesizechangedevent: {
|
||||
title: 'NodeSizeChangedEvent',
|
||||
description: 'Payload fired when a node is resized.',
|
||||
group: 'events',
|
||||
order: 24
|
||||
},
|
||||
nodemodechangedevent: {
|
||||
title: 'NodeModeChangedEvent',
|
||||
description: 'Payload fired when a node execution mode changes.',
|
||||
group: 'events',
|
||||
order: 25
|
||||
},
|
||||
nodebeforeserializeevent: {
|
||||
title: 'NodeBeforeSerializeEvent',
|
||||
description: 'Pre-serialization hook payload — override or skip node data.',
|
||||
group: 'events',
|
||||
order: 26
|
||||
},
|
||||
widgetvaluechangeevent: {
|
||||
title: 'WidgetValueChangeEvent',
|
||||
description: 'Payload fired when a widget value changes.',
|
||||
group: 'events',
|
||||
order: 27
|
||||
},
|
||||
widgetbeforeserializeevent: {
|
||||
title: 'WidgetBeforeSerializeEvent',
|
||||
description:
|
||||
'Pre-serialization hook payload — override or skip widget value.',
|
||||
group: 'events',
|
||||
order: 28
|
||||
},
|
||||
widgetbeforequeueevent: {
|
||||
title: 'WidgetBeforeQueueEvent',
|
||||
description:
|
||||
'Pre-queue validation payload — call reject() to cancel queue.',
|
||||
group: 'events',
|
||||
order: 29
|
||||
},
|
||||
// Shell UI
|
||||
sidebartabextension: {
|
||||
title: 'SidebarTabExtension',
|
||||
description: 'Register a custom sidebar tab.',
|
||||
group: 'shell',
|
||||
order: 40
|
||||
},
|
||||
bottompanelextension: {
|
||||
title: 'BottomPanelExtension',
|
||||
description: 'Register a custom bottom panel tab.',
|
||||
group: 'shell',
|
||||
order: 41
|
||||
},
|
||||
toastmanager: {
|
||||
title: 'ToastManager',
|
||||
description: 'Show toast notifications to the user.',
|
||||
group: 'shell',
|
||||
order: 42
|
||||
},
|
||||
commandmanager: {
|
||||
title: 'CommandManager',
|
||||
description: 'Register keyboard shortcuts and command palette entries.',
|
||||
group: 'shell',
|
||||
order: 43
|
||||
},
|
||||
extensionmanager: {
|
||||
title: 'ExtensionManager',
|
||||
description: 'Access shell UI registration APIs.',
|
||||
group: 'shell',
|
||||
order: 44
|
||||
},
|
||||
// Identity
|
||||
nodelocatorid: {
|
||||
title: 'NodeLocatorId',
|
||||
description:
|
||||
'Branded string ID that uniquely locates a node across graph snapshots.',
|
||||
group: 'identity',
|
||||
order: 50
|
||||
},
|
||||
nodeexecutionid: {
|
||||
title: 'NodeExecutionId',
|
||||
description: 'Branded string ID for a specific node execution run.',
|
||||
group: 'identity',
|
||||
order: 51
|
||||
}
|
||||
}
|
||||
|
||||
const GROUP_LABELS: Record<PageMeta['group'], string> = {
|
||||
root: 'Extensions API',
|
||||
core: 'Registration',
|
||||
handles: 'Handles',
|
||||
events: 'Events',
|
||||
shell: 'Shell UI',
|
||||
identity: 'Identity'
|
||||
}
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────────
|
||||
|
||||
function slug(stem: string): string {
|
||||
return stem
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
|
||||
function metaFor(stem: string): PageMeta {
|
||||
const key = stem.toLowerCase().replace(/[^a-z]/g, '')
|
||||
return (
|
||||
PAGE_META[key] ?? {
|
||||
title: stem,
|
||||
description: `API reference for ${stem}.`,
|
||||
group: 'core',
|
||||
order: 99
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Convert TypeDoc raw markdown to Mintlify-compatible MDX. */
|
||||
function toMintlifyMdx(raw: string, stem: string): string {
|
||||
const meta = metaFor(stem)
|
||||
|
||||
// Build frontmatter
|
||||
const fm: string[] = [
|
||||
`---`,
|
||||
`title: "${meta.title}"`,
|
||||
...(meta.sidebarTitle ? [`sidebarTitle: "${meta.sidebarTitle}"`] : []),
|
||||
`description: "${meta.description}"`,
|
||||
...(meta.icon ? [`icon: "${meta.icon}"`] : []),
|
||||
`---`
|
||||
]
|
||||
|
||||
let body = raw
|
||||
|
||||
// Strip TypeDoc breadcrumb header lines (e.g. "[**@comfyorg/...**](../index.md)\n\n***\n\n[@comfyorg...]...")
|
||||
body = body.replace(/^\[.*?\]\(\.\.\/index\.md\)\n+\*+\n+/gm, '')
|
||||
body = body.replace(/^\[.*?\]\(\.\.\/index\.md\).*\n+/gm, '')
|
||||
|
||||
// Remove the TypeDoc-generated H1 (we use frontmatter title instead)
|
||||
body = body.replace(/^# .+\n+/, '')
|
||||
|
||||
// Ensure opening code fences that have no lang tag get `ts`
|
||||
// Only match a ``` that is immediately followed by a newline (opening fence),
|
||||
// not a closing fence (which also has just ``` + newline but we can detect
|
||||
// by context: opening fences follow non-fence lines; closing fences follow content).
|
||||
// Simpler heuristic: replace ``` at start of line only when not already closing a block.
|
||||
// We track state via a flag pass instead of a single regex.
|
||||
let inBlock = false
|
||||
body = body
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (inBlock) {
|
||||
if (line.trim() === '```') {
|
||||
inBlock = false
|
||||
return line
|
||||
}
|
||||
return line
|
||||
}
|
||||
if (line.startsWith('```')) {
|
||||
if (line.trim() === '```') {
|
||||
// bare opening fence → add ts
|
||||
inBlock = true
|
||||
return '```ts'
|
||||
}
|
||||
// has a lang tag already
|
||||
inBlock = true
|
||||
return line
|
||||
}
|
||||
return line
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// TypeDoc emits `typescript` lang tag; normalize to `ts`
|
||||
body = body.replace(/^```typescript\b/gm, '```ts')
|
||||
|
||||
// Fix TypeDoc cross-ref links: [TypeName](../type-alias/TypeName.md) → relative MDX paths
|
||||
// Pattern: [Label](../category/FileName.md) → [Label](./filename)
|
||||
body = body.replace(
|
||||
/\[([^\]]+)\]\(\.\.\/([\w-]+)\/([\w-]+)\.md\)/g,
|
||||
(_match, label, _category, file) => `[${label}](./${slug(file)})`
|
||||
)
|
||||
// Same-dir links
|
||||
body = body.replace(
|
||||
/\[([^\]]+)\]\(([\w-]+)\.md\)/g,
|
||||
(_match, label, file) => `[${label}](./${slug(file)})`
|
||||
)
|
||||
|
||||
// TypeDoc wraps @example content in a "## Example" heading; Mintlify prefers
|
||||
// code examples to be directly under prose without a sub-heading.
|
||||
// Flatten "## Example\n\n```ts" → "```ts"
|
||||
body = body.replace(/^## Example\s*\n+/gm, '')
|
||||
|
||||
// Stability tags: render as a <Tip> callout
|
||||
body = body.replace(
|
||||
/\*\*Stability\*\*: `(stable|experimental|deprecated)`/g,
|
||||
(_match, level) => {
|
||||
const label =
|
||||
level === 'stable'
|
||||
? '<Tip>**Stability:** Stable — part of the public API contract.</Tip>'
|
||||
: level === 'experimental'
|
||||
? '<Warning>**Stability:** Experimental — may change before 1.0.</Warning>'
|
||||
: '<Warning>**Stability:** Deprecated — will be removed. See migration guide.</Warning>'
|
||||
return label
|
||||
}
|
||||
)
|
||||
|
||||
// @stability TSDoc tag (appears as plain text after TypeDoc strips tags)
|
||||
body = body.replace(
|
||||
/^Stability: (stable|experimental|deprecated)\s*$/gm,
|
||||
(_match, level) => {
|
||||
if (level === 'stable') return '<Tip>**Stability:** Stable</Tip>'
|
||||
if (level === 'experimental')
|
||||
return '<Warning>**Stability:** Experimental</Warning>'
|
||||
return '<Warning>**Stability:** Deprecated</Warning>'
|
||||
}
|
||||
)
|
||||
|
||||
return [...fm, '', body.trim(), ''].join('\n')
|
||||
}
|
||||
|
||||
// ── Nav snippet builder ───────────────────────────────────────────────────────
|
||||
|
||||
interface NavPage {
|
||||
group?: string
|
||||
pages: (string | NavPage)[]
|
||||
}
|
||||
|
||||
function buildNavSnippet(stems: string[]): NavPage {
|
||||
// Sort stems by order then group by category
|
||||
const sortedStems = stems
|
||||
.slice()
|
||||
.sort((a, b) => metaFor(a).order - metaFor(b).order)
|
||||
const sortedByGroup: Record<string, string[]> = {}
|
||||
for (const stem of sortedStems) {
|
||||
const group = metaFor(stem).group
|
||||
if (!sortedByGroup[group]) sortedByGroup[group] = []
|
||||
sortedByGroup[group].push(`extensions/api/${slug(stem)}`)
|
||||
}
|
||||
|
||||
const groupOrder: PageMeta['group'][] = [
|
||||
'root',
|
||||
'core',
|
||||
'handles',
|
||||
'events',
|
||||
'shell',
|
||||
'identity'
|
||||
]
|
||||
|
||||
const pages: (string | NavPage)[] = []
|
||||
|
||||
// Overview at top level
|
||||
if (sortedByGroup['root']) {
|
||||
for (const p of sortedByGroup['root']) pages.push(p)
|
||||
}
|
||||
|
||||
for (const grp of groupOrder) {
|
||||
if (grp === 'root') continue
|
||||
const grpPages = sortedByGroup[grp]
|
||||
if (!grpPages?.length) continue
|
||||
pages.push({ group: GROUP_LABELS[grp], pages: grpPages })
|
||||
}
|
||||
|
||||
return { group: 'Extensions API', pages }
|
||||
}
|
||||
|
||||
// ── Main pipeline ────────────────────────────────────────────────────────────
|
||||
|
||||
function runTypedoc(): void {
|
||||
console.log('▶ Running TypeDoc...')
|
||||
execSync(
|
||||
`pnpm exec typedoc --options ${path.join(pkgRoot, 'typedoc.json')} --out ${rawDir}`,
|
||||
{ cwd: pkgRoot, stdio: 'inherit' }
|
||||
)
|
||||
}
|
||||
|
||||
function processFiles(): void {
|
||||
if (!fs.existsSync(rawDir)) {
|
||||
throw new Error(`TypeDoc output directory not found: ${rawDir}`)
|
||||
}
|
||||
|
||||
fs.mkdirSync(mintlifyDir, { recursive: true })
|
||||
|
||||
const mdFiles = fs
|
||||
.readdirSync(rawDir, { recursive: true })
|
||||
.filter((f): f is string => typeof f === 'string' && f.endsWith('.md'))
|
||||
|
||||
const stems: string[] = []
|
||||
|
||||
for (const relPath of mdFiles) {
|
||||
const src = path.join(rawDir, relPath)
|
||||
const stem = path.basename(relPath, '.md')
|
||||
const raw = fs.readFileSync(src, 'utf8')
|
||||
const mdx = toMintlifyMdx(raw, stem)
|
||||
|
||||
const destName = slug(stem) + '.mdx'
|
||||
const dest = path.join(mintlifyDir, destName)
|
||||
fs.writeFileSync(dest, mdx)
|
||||
console.log(` ✔ ${relPath} → mintlify/${destName}`)
|
||||
stems.push(stem)
|
||||
}
|
||||
|
||||
// Write nav snippet
|
||||
const nav = buildNavSnippet(stems)
|
||||
const navDest = path.join(mintlifyDir, 'nav-snippet.json')
|
||||
fs.writeFileSync(navDest, JSON.stringify(nav, null, 2) + '\n')
|
||||
console.log(` ✔ nav-snippet.json`)
|
||||
|
||||
console.log(`\n✅ Mintlify MDX written to: ${mintlifyDir}`)
|
||||
console.log(` ${stems.length} pages + nav-snippet.json`)
|
||||
}
|
||||
|
||||
function run(): void {
|
||||
runTypedoc()
|
||||
processFiles()
|
||||
}
|
||||
|
||||
if (watchMode) {
|
||||
// Simple watch: re-run on change to source files
|
||||
console.log('👁 Watch mode — watching src/extension-api/**')
|
||||
const srcDir = path.resolve(pkgRoot, '../../src/extension-api')
|
||||
let debounce: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
run()
|
||||
|
||||
fs.watch(srcDir, { recursive: true }, () => {
|
||||
if (debounce) clearTimeout(debounce)
|
||||
debounce = setTimeout(() => {
|
||||
console.log('\n🔄 Source changed — rebuilding...')
|
||||
try {
|
||||
run()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
} else {
|
||||
run()
|
||||
}
|
||||
17
packages/extension-api/src/index.ts
Normal file
17
packages/extension-api/src/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @comfyorg/extension-api — Public Extension API for ComfyUI
|
||||
*
|
||||
* This is the package entry point compiled to `build/index.js` + `build/index.d.ts`.
|
||||
* It is a single re-export of the canonical surface defined in
|
||||
* `src/extension-api/index.ts` in the main app — that file is the one source
|
||||
* of truth for what is part of the stable, semver-versioned public contract.
|
||||
*
|
||||
* Do NOT add exports here. Add them to `src/extension-api/index.ts` and they
|
||||
* will flow through this barrel automatically.
|
||||
*
|
||||
* The tsconfig.json `paths` alias `@/*` → `../../src/*` resolves the import
|
||||
* below at both typecheck and build time.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export * from '@/extension-api/index'
|
||||
41
packages/extension-api/tsconfig.build.json
Normal file
41
packages/extension-api/tsconfig.build.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023", "ES2023.Array", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"noEmit": false,
|
||||
"outDir": "./build",
|
||||
"paths": {
|
||||
"@/*": ["../../src/*"],
|
||||
"@/utils/formatUtil": [
|
||||
"../../packages/shared-frontend-utils/src/formatUtil.ts"
|
||||
],
|
||||
"@/utils/networkUtil": [
|
||||
"../../packages/shared-frontend-utils/src/networkUtil.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"../../src/**/*.ts",
|
||||
"../../src/types/litegraph-augmentation.d.ts",
|
||||
"../../global.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"../../src/**/*.test.ts",
|
||||
"../../src/**/*.spec.ts",
|
||||
"../../src/**/*.vue",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"scripts/**"
|
||||
]
|
||||
}
|
||||
19
packages/extension-api/tsconfig.docs.json
Normal file
19
packages/extension-api/tsconfig.docs.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": ["../../src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["../../src/extension-api/**/*.ts"],
|
||||
"exclude": [
|
||||
"../../src/**/*.test.ts",
|
||||
"../../src/**/*.spec.ts",
|
||||
"../../src/**/*.vue"
|
||||
]
|
||||
}
|
||||
8
packages/extension-api/tsconfig.json
Normal file
8
packages/extension-api/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false
|
||||
}
|
||||
}
|
||||
45
packages/extension-api/typedoc.json
Normal file
45
packages/extension-api/typedoc.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"entryPoints": ["../../src/extension-api/index.ts"],
|
||||
"tsconfig": "./tsconfig.docs.json",
|
||||
"out": "./docs-build/raw",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"excludeInternal": true,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"readme": "none",
|
||||
"skipErrorChecking": true,
|
||||
"githubPages": false,
|
||||
"blockTags": [
|
||||
"@stability",
|
||||
"@packageDocumentation",
|
||||
"@example",
|
||||
"@typeParam",
|
||||
"@returns",
|
||||
"@deprecated",
|
||||
"@remarks"
|
||||
],
|
||||
"hideGenerator": true,
|
||||
"useCodeBlocks": true,
|
||||
"flattenOutputFiles": false,
|
||||
"entryFileName": "index",
|
||||
"fileExtension": ".md",
|
||||
"outputFileStrategy": "members",
|
||||
"hidePageHeader": false,
|
||||
"hideBreadcrumbs": false,
|
||||
"useHTMLAnchors": false,
|
||||
"sanitizeComments": true,
|
||||
"expandObjects": false,
|
||||
"parametersFormat": "table",
|
||||
"propertiesFormat": "table",
|
||||
"typeDeclarationFormat": "table",
|
||||
"indexFormat": "table",
|
||||
"tableColumnSettings": {
|
||||
"hideDefaults": false,
|
||||
"hideInherited": false,
|
||||
"hideModifiers": false,
|
||||
"hideOverrides": false,
|
||||
"hideSources": true,
|
||||
"hideValues": false,
|
||||
"leftAlignHeaders": false
|
||||
}
|
||||
}
|
||||
74
packages/extension-api/vite.config.mts
Normal file
74
packages/extension-api/vite.config.mts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import dts from 'vite-plugin-dts'
|
||||
|
||||
const here = fileURLToPath(new URL('.', import.meta.url))
|
||||
const repoRoot = resolve(here, '..', '..')
|
||||
const repoSrc = resolve(repoRoot, 'src')
|
||||
const surfaceRoot = resolve(repoSrc, 'extension-api')
|
||||
|
||||
/**
|
||||
* Library build for `@comfyorg/extension-api`.
|
||||
*
|
||||
* Per ADR D17 (PKG2 build strategy), the package is built from the canonical
|
||||
* surface defined in the main app at `src/extension-api/index.ts`. Vite
|
||||
* resolves the `@/*` aliases against the main app's `src/` directory and
|
||||
* emits a single bundled `index.js` plus a single bundled `index.d.ts`.
|
||||
*
|
||||
* The package barrel at `packages/extension-api/src/index.ts` is the
|
||||
* Vite entry point and re-exports `@/extension-api/index` — preserving
|
||||
* "the barrel is the source of truth in main app `src/extension-api/`"
|
||||
* intent in `packages/extension-api/AGENTS.md`.
|
||||
*
|
||||
* Vue is externalized as a peer dependency (per D6.1 Phase A — extension
|
||||
* authors share the host app's Vue runtime).
|
||||
*/
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@/utils/formatUtil': resolve(
|
||||
repoRoot,
|
||||
'packages/shared-frontend-utils/src/formatUtil.ts'
|
||||
),
|
||||
'@/utils/networkUtil': resolve(
|
||||
repoRoot,
|
||||
'packages/shared-frontend-utils/src/networkUtil.ts'
|
||||
),
|
||||
'@': repoSrc
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: resolve(here, 'build'),
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
target: 'es2022',
|
||||
minify: false,
|
||||
lib: {
|
||||
// Build directly from the canonical surface in the main app — the
|
||||
// package's own `src/index.ts` exists only as a documented entry
|
||||
// point that re-exports the same surface, but we point Vite at the
|
||||
// canonical file so dts paths line up cleanly with the JS bundle.
|
||||
entry: resolve(surfaceRoot, 'index.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'index.js'
|
||||
},
|
||||
rollupOptions: {
|
||||
// Vue is provided by the host app at runtime.
|
||||
external: ['vue', /^@vue\//]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
// Bundle all types into a single index.d.ts. This ensures the package
|
||||
// is self-contained and doesn't reference paths outside build/.
|
||||
rollupTypes: true,
|
||||
outDir: resolve(here, 'build'),
|
||||
tsconfigPath: resolve(here, 'tsconfig.build.json'),
|
||||
logLevel: 'warn',
|
||||
// Only include the extension-api surface, not the entire app
|
||||
include: [resolve(surfaceRoot, '**/*.ts')]
|
||||
})
|
||||
]
|
||||
})
|
||||
420
pnpm-lock.yaml
generated
420
pnpm-lock.yaml
generated
@@ -650,7 +650,7 @@ importers:
|
||||
version: 22.6.1(@babel/traverse@7.29.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.6.1)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||
'@nx/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)
|
||||
version: 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)
|
||||
'@pinia/testing':
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))
|
||||
@@ -662,7 +662,7 @@ importers:
|
||||
version: 4.6.0
|
||||
'@storybook/addon-docs':
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
version: 10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/addon-mcp':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.6(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||
@@ -671,10 +671,10 @@ importers:
|
||||
version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vue@3.5.13(typescript@5.9.3))
|
||||
'@storybook/vue3-vite':
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
version: 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
version: 4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: 'catalog:'
|
||||
version: 6.9.1
|
||||
@@ -704,7 +704,7 @@ importers:
|
||||
version: 0.170.0
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
version: 6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.16(vitest@4.0.16)
|
||||
@@ -842,19 +842,19 @@ importers:
|
||||
version: 11.1.0
|
||||
vite:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
version: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-dts:
|
||||
specifier: 'catalog:'
|
||||
version: 4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
version: 4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-html:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
version: 3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
version: 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue-component-type-helpers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.6
|
||||
@@ -912,10 +912,10 @@ importers:
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
version: 6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
dotenv:
|
||||
specifier: 'catalog:'
|
||||
version: 16.6.1
|
||||
@@ -927,13 +927,13 @@ importers:
|
||||
version: 30.0.0(@babel/parser@7.29.0)(vue@3.5.13(typescript@5.9.3))
|
||||
vite:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-html:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
version: 3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
version: 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.5(typescript@5.9.3)
|
||||
@@ -982,16 +982,16 @@ importers:
|
||||
version: 0.9.8(prettier@3.7.4)(typescript@5.9.3)
|
||||
'@astrojs/vue':
|
||||
specifier: 'catalog:'
|
||||
version: 5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.8.2)
|
||||
version: 5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.9.0)
|
||||
'@playwright/test':
|
||||
specifier: 'catalog:'
|
||||
version: 1.58.1
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
astro:
|
||||
specifier: 'catalog:'
|
||||
version: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2)
|
||||
version: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0)
|
||||
tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.0
|
||||
@@ -1003,7 +1003,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
packages/design-system:
|
||||
dependencies:
|
||||
@@ -1030,6 +1030,31 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
|
||||
packages/extension-api:
|
||||
dependencies:
|
||||
vue:
|
||||
specifier: 'catalog:'
|
||||
version: 3.5.13(typescript@5.9.3)
|
||||
devDependencies:
|
||||
tsx:
|
||||
specifier: 'catalog:'
|
||||
version: 4.19.4
|
||||
typedoc:
|
||||
specifier: 0.28.19
|
||||
version: 0.28.19(typescript@5.9.3)
|
||||
typedoc-plugin-markdown:
|
||||
specifier: ^4.6.3
|
||||
version: 4.11.0(typedoc@0.28.19(typescript@5.9.3))
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-dts:
|
||||
specifier: 'catalog:'
|
||||
version: 4.5.4(@types/node@25.0.3)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
|
||||
packages/ingest-types:
|
||||
dependencies:
|
||||
zod:
|
||||
@@ -2431,6 +2456,9 @@ packages:
|
||||
'@formkit/auto-animate@0.9.0':
|
||||
resolution: {integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==}
|
||||
|
||||
'@gerrit0/mini-shiki@3.23.0':
|
||||
resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==}
|
||||
|
||||
'@grpc/grpc-js@1.9.15':
|
||||
resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==}
|
||||
engines: {node: ^8.13.0 || >=10.10.0}
|
||||
@@ -5453,6 +5481,10 @@ packages:
|
||||
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
brace-expansion@5.0.6:
|
||||
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7624,6 +7656,9 @@ packages:
|
||||
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
|
||||
engines: {node: '>=16.14'}
|
||||
|
||||
lunr@2.3.9:
|
||||
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
|
||||
|
||||
lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
@@ -7864,6 +7899,10 @@ packages:
|
||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
minimatch@3.1.5:
|
||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||
|
||||
@@ -9343,6 +9382,19 @@ packages:
|
||||
typed-binary@4.3.2:
|
||||
resolution: {integrity: sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ==}
|
||||
|
||||
typedoc-plugin-markdown@4.11.0:
|
||||
resolution: {integrity: sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
typedoc: 0.28.x
|
||||
|
||||
typedoc@0.28.19:
|
||||
resolution: {integrity: sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==}
|
||||
engines: {node: '>= 18', pnpm: '>= 10'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x
|
||||
|
||||
typegpu@0.8.2:
|
||||
resolution: {integrity: sha512-wkMJWhJE0pSkw2G/FesjqjbtHkREyOKu1Zmyj19xfmaX5+65YFwgfQNKSK8CxqN4kJkP7JFelLDJTSYY536TYg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -10186,6 +10238,11 @@ packages:
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.9.0:
|
||||
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10467,14 +10524,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@astrojs/vue@5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.8.2)':
|
||||
'@astrojs/vue@5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.9.0)':
|
||||
dependencies:
|
||||
'@vitejs/plugin-vue': 5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx': 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue': 5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx': 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vue/compiler-sfc': 3.5.28
|
||||
astro: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-plugin-vue-devtools: 7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
astro: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-vue-devtools: 7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
@@ -11863,6 +11920,14 @@ snapshots:
|
||||
|
||||
'@formkit/auto-animate@0.9.0': {}
|
||||
|
||||
'@gerrit0/mini-shiki@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/engine-oniguruma': 3.23.0
|
||||
'@shikijs/langs': 3.23.0
|
||||
'@shikijs/themes': 3.23.0
|
||||
'@shikijs/types': 3.23.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@grpc/grpc-js@1.9.15':
|
||||
dependencies:
|
||||
'@grpc/proto-loader': 0.7.13
|
||||
@@ -12251,6 +12316,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/api-extractor-model@7.33.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
'@microsoft/tsdoc-config': 0.18.0
|
||||
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/api-extractor@7.57.2(@types/node@24.10.4)':
|
||||
dependencies:
|
||||
'@microsoft/api-extractor-model': 7.33.1(@types/node@24.10.4)
|
||||
@@ -12270,6 +12343,25 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/api-extractor@7.57.2(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@microsoft/api-extractor-model': 7.33.1(@types/node@25.0.3)
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
'@microsoft/tsdoc-config': 0.18.0
|
||||
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
|
||||
'@rushstack/rig-package': 0.7.1
|
||||
'@rushstack/terminal': 0.22.1(@types/node@25.0.3)
|
||||
'@rushstack/ts-command-line': 5.3.1(@types/node@25.0.3)
|
||||
diff: 8.0.3
|
||||
lodash: 4.17.23
|
||||
minimatch: 10.2.1
|
||||
resolve: 1.22.11
|
||||
semver: 7.5.4
|
||||
source-map: 0.6.1
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/tsdoc-config@0.18.0':
|
||||
dependencies:
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
@@ -12495,11 +12587,11 @@ snapshots:
|
||||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nx/vite@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)':
|
||||
'@nx/vite@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)':
|
||||
dependencies:
|
||||
'@nx/devkit': 22.6.1(nx@22.6.1)
|
||||
'@nx/js': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)
|
||||
'@nx/vitest': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)
|
||||
'@nx/vitest': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)
|
||||
'@phenomnomnominal/tsquery': 6.1.4(typescript@5.9.3)
|
||||
ajv: 8.18.0
|
||||
enquirer: 2.3.6
|
||||
@@ -12507,8 +12599,8 @@ snapshots:
|
||||
semver: 7.7.4
|
||||
tsconfig-paths: 4.2.0
|
||||
tslib: 2.8.1
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -12519,7 +12611,7 @@ snapshots:
|
||||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nx/vitest@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)':
|
||||
'@nx/vitest@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)':
|
||||
dependencies:
|
||||
'@nx/devkit': 22.6.1(nx@22.6.1)
|
||||
'@nx/js': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)
|
||||
@@ -12527,8 +12619,8 @@ snapshots:
|
||||
semver: 7.7.4
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -13142,10 +13234,27 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@rushstack/node-core-library@5.20.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
ajv: 8.13.0
|
||||
ajv-draft-04: 1.0.0(ajv@8.13.0)
|
||||
ajv-formats: 3.0.1(ajv@8.13.0)
|
||||
fs-extra: 11.3.2
|
||||
import-lazy: 4.0.0
|
||||
jju: 1.4.0
|
||||
resolve: 1.22.11
|
||||
semver: 7.5.4
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@rushstack/problem-matcher@0.2.1(@types/node@24.10.4)':
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@rushstack/problem-matcher@0.2.1(@types/node@25.0.3)':
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@rushstack/rig-package@0.7.1':
|
||||
dependencies:
|
||||
resolve: 1.22.11
|
||||
@@ -13159,6 +13268,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@rushstack/terminal@0.22.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
|
||||
'@rushstack/problem-matcher': 0.2.1(@types/node@25.0.3)
|
||||
supports-color: 8.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@rushstack/ts-command-line@5.3.1(@types/node@24.10.4)':
|
||||
dependencies:
|
||||
'@rushstack/terminal': 0.22.1(@types/node@24.10.4)
|
||||
@@ -13168,6 +13285,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@rushstack/ts-command-line@5.3.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@rushstack/terminal': 0.22.1(@types/node@25.0.3)
|
||||
'@types/argparse': 1.0.38
|
||||
argparse: 1.0.10
|
||||
string-argv: 0.3.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@sentry-internal/browser-utils@10.32.1':
|
||||
@@ -13317,10 +13443,10 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@storybook/addon-docs@10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
'@storybook/addon-docs@10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.1.9)(react@19.2.4)
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@storybook/react-dom-shim': 10.2.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
|
||||
react: 19.2.4
|
||||
@@ -13346,25 +13472,25 @@ snapshots:
|
||||
- '@tmcp/auth'
|
||||
- typescript
|
||||
|
||||
'@storybook/builder-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
'@storybook/builder-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- esbuild
|
||||
- rollup
|
||||
- webpack
|
||||
|
||||
'@storybook/csf-plugin@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
'@storybook/csf-plugin@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.3
|
||||
rollup: 4.53.5
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
|
||||
@@ -13389,14 +13515,14 @@ snapshots:
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
||||
'@storybook/vue3-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@storybook/vue3-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@storybook/builder-vite': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
'@storybook/builder-vite': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@storybook/vue3': 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vue@3.5.13(typescript@5.9.3))
|
||||
magic-string: 0.30.21
|
||||
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
typescript: 5.9.3
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue-component-meta: 2.2.12(typescript@5.9.3)
|
||||
vue-docgen-api: 4.79.2(vue@3.5.13(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
@@ -13478,19 +13604,19 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.0
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.0
|
||||
|
||||
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.2.0
|
||||
'@tailwindcss/oxide': 4.2.0
|
||||
tailwindcss: 4.2.0
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.2.0
|
||||
'@tailwindcss/oxide': 4.2.0
|
||||
tailwindcss: 4.2.0
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@tanstack/virtual-core@3.13.12': {}
|
||||
|
||||
@@ -14083,32 +14209,32 @@ snapshots:
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
vue-router: 4.4.3(vue@3.5.13(typescript@5.9.3))
|
||||
|
||||
'@vitejs/plugin-vue-jsx@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue-jsx@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
||||
'@rolldown/pluginutils': 1.0.0-rc.9
|
||||
'@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.29.0)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue@5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vitest/coverage-v8@4.0.16(vitest@4.0.16)':
|
||||
@@ -14124,7 +14250,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 3.10.0
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -14145,21 +14271,21 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.16
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.16
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -14195,7 +14321,7 @@ snapshots:
|
||||
sirv: 3.0.2
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
@@ -14370,38 +14496,38 @@ snapshots:
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
|
||||
'@vue/devtools-core@7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vue/devtools-core@7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
'@vue/devtools-shared': 7.7.9
|
||||
mitt: 3.0.1
|
||||
nanoid: 5.1.5
|
||||
pathe: 2.0.3
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
mitt: 3.0.1
|
||||
nanoid: 5.1.5
|
||||
pathe: 2.0.3
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
mitt: 3.0.1
|
||||
nanoid: 5.1.5
|
||||
pathe: 2.0.3
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
@@ -14809,7 +14935,7 @@ snapshots:
|
||||
|
||||
astral-regex@2.0.0: {}
|
||||
|
||||
astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2):
|
||||
astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.13.1
|
||||
'@astrojs/internal-helpers': 0.7.6
|
||||
@@ -14866,8 +14992,8 @@ snapshots:
|
||||
unist-util-visit: 5.1.0
|
||||
unstorage: 1.17.4
|
||||
vfile: 6.0.3
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vitefu: 1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vitefu: 1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
yocto-spinner: 0.2.3
|
||||
@@ -15060,6 +15186,10 @@ snapshots:
|
||||
dependencies:
|
||||
balanced-match: 4.0.3
|
||||
|
||||
brace-expansion@5.0.6:
|
||||
dependencies:
|
||||
balanced-match: 4.0.3
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
@@ -17463,6 +17593,8 @@ snapshots:
|
||||
|
||||
lru-cache@8.0.5: {}
|
||||
|
||||
lunr@2.3.9: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
lz-utils@2.1.0: {}
|
||||
@@ -17898,6 +18030,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.2
|
||||
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.6
|
||||
|
||||
minimatch@3.1.5:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
@@ -19827,6 +19963,19 @@ snapshots:
|
||||
|
||||
typed-binary@4.3.2: {}
|
||||
|
||||
typedoc-plugin-markdown@4.11.0(typedoc@0.28.19(typescript@5.9.3)):
|
||||
dependencies:
|
||||
typedoc: 0.28.19(typescript@5.9.3)
|
||||
|
||||
typedoc@0.28.19(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@gerrit0/mini-shiki': 3.23.0
|
||||
lunr: 2.3.9
|
||||
markdown-it: 14.1.1
|
||||
minimatch: 10.2.5
|
||||
typescript: 5.9.3
|
||||
yaml: 2.9.0
|
||||
|
||||
typegpu@0.8.2:
|
||||
dependencies:
|
||||
tinyest: 0.1.2
|
||||
@@ -20111,27 +20260,27 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-hot-client@2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-hot-client@2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-plugin-dts@4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-dts@4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.57.2(@types/node@24.10.4)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
@@ -20144,13 +20293,32 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
typescript: 5.9.3
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-dts@4.5.4(@types/node@25.0.3)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.57.2(@types/node@25.0.3)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
'@volar/typescript': 2.4.28
|
||||
'@vue/language-core': 2.2.0(typescript@5.9.3)
|
||||
compare-versions: 6.1.1
|
||||
debug: 4.4.3
|
||||
kolorist: 1.8.0
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.21
|
||||
typescript: 5.9.3
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -20164,9 +20332,9 @@ snapshots:
|
||||
html-minifier-terser: 6.1.0
|
||||
node-html-parser: 5.4.2
|
||||
pathe: 0.2.0
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -20180,9 +20348,9 @@ snapshots:
|
||||
html-minifier-terser: 6.1.0
|
||||
node-html-parser: 5.4.2
|
||||
pathe: 0.2.0
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vite-plugin-inspect@0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-inspect@0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
@@ -20193,12 +20361,12 @@ snapshots:
|
||||
perfect-debounce: 1.0.0
|
||||
picocolors: 1.1.1
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
@@ -20208,12 +20376,12 @@ snapshots:
|
||||
perfect-debounce: 2.0.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
@@ -20223,56 +20391,56 @@ snapshots:
|
||||
perfect-debounce: 2.0.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-devtools@7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)):
|
||||
vite-plugin-vue-devtools@7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vue/devtools-core': 7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 7.7.9
|
||||
'@vue/devtools-shared': 7.7.9
|
||||
execa: 9.6.1
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-plugin-inspect: 0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-inspect: 0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- rollup
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)):
|
||||
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)):
|
||||
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.0.5
|
||||
'@vue/devtools-shared': 8.0.5
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||
@@ -20283,11 +20451,11 @@ snapshots:
|
||||
'@vue/compiler-dom': 3.5.28
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||
@@ -20298,11 +20466,11 @@ snapshots:
|
||||
'@vue/compiler-dom': 3.5.28
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
|
||||
vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.115.0
|
||||
lightningcss: 1.32.0
|
||||
@@ -20317,9 +20485,9 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
terser: 5.39.2
|
||||
tsx: 4.19.4
|
||||
yaml: 2.8.2
|
||||
yaml: 2.9.0
|
||||
|
||||
vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
|
||||
vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.115.0
|
||||
lightningcss: 1.32.0
|
||||
@@ -20334,16 +20502,16 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
terser: 5.39.2
|
||||
tsx: 4.19.4
|
||||
yaml: 2.8.2
|
||||
yaml: 2.9.0
|
||||
|
||||
vitefu@1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
vitefu@1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
|
||||
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
|
||||
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.16
|
||||
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@vitest/pretty-format': 4.0.16
|
||||
'@vitest/runner': 4.0.16
|
||||
'@vitest/snapshot': 4.0.16
|
||||
@@ -20360,7 +20528,7 @@ snapshots:
|
||||
tinyexec: 1.0.4
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -20382,10 +20550,10 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
|
||||
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.16
|
||||
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
'@vitest/pretty-format': 4.0.16
|
||||
'@vitest/runner': 4.0.16
|
||||
'@vitest/snapshot': 4.0.16
|
||||
@@ -20402,7 +20570,7 @@ snapshots:
|
||||
tinyexec: 1.0.4
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.0.3
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -20812,7 +20980,7 @@ snapshots:
|
||||
yaml-eslint-parser@1.3.0:
|
||||
dependencies:
|
||||
eslint-visitor-keys: 3.4.3
|
||||
yaml: 2.8.2
|
||||
yaml: 2.9.0
|
||||
|
||||
yaml-language-server@1.20.0:
|
||||
dependencies:
|
||||
@@ -20834,6 +21002,8 @@ snapshots:
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
yaml@2.9.0: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
|
||||
915
research/touch-points/behavior-categories.yaml
Normal file
915
research/touch-points/behavior-categories.yaml
Normal file
@@ -0,0 +1,915 @@
|
||||
meta:
|
||||
schema_version: 1
|
||||
generated_from:
|
||||
- database.yaml
|
||||
- rollup.yaml
|
||||
- star-cache.yaml
|
||||
generated_by: scripts/build-behavior-categories.py (I-TF.1)
|
||||
source_pattern_count: 62
|
||||
category_count: 37
|
||||
usage_weight_formula: sum_over_members(blast_radius * occurrences)
|
||||
exemplar_ranking: repo_stars desc, then pattern blast_radius desc; distinct (repo, pattern_id)
|
||||
notes:
|
||||
- Categories cluster by intent, not by surface_family. S2 is split into creation / teardown / hydration / interaction /
|
||||
drawing / connection / serialization / properties.
|
||||
- S1 hooks are merged with their prototype-patching equivalents where intent matches (BC.20 node-type reg, BC.22 menus,
|
||||
BC.03 hydration).
|
||||
- S8.P1 isVirtualNode is a registration-time flag, so it lives in BC.20 alongside the node-type registration hooks.
|
||||
- S10.D3 setSize and S15.OS1 dynamic outputs join S10.D1 in BC.09 dynamic-slot-mutation since they all describe runtime
|
||||
topology mutation.
|
||||
- S14.ID1 NodeLocatorId joins S11.G2 graph enumeration in BC.29 because both are about cross-scope node identity/resolution.
|
||||
- S11.G1/G3/G4 (version, batching, setDirtyCanvas) collapse into BC.30 graph change-tracking — the v2 reactivity story replaces
|
||||
all three.
|
||||
- BC.21 (S1.H2 getCustomWidgets) has only 2 evidence rows in database.yaml; this is the 'small family — 2 + 1 minor variant'
|
||||
acceptance carve-out. The two exemplars are kept as-is, no synthetic third row.
|
||||
- BC.31 and BC.32 added 2026-05-08 from Notion API usage research (notion-api-research-evidence.yaml staging).
|
||||
S16 is a new surface family (DOM injection) not previously tracked. S16.VUE1 grouped with BC.32 (embedded runtimes).
|
||||
S3.C2 (ContextMenu replacement) added to BC.06 member list.
|
||||
- Notion source also upgrades occurrence signal on BC.01/BC.02/BC.04/BC.06/BC.07/BC.09/BC.26/BC.29/BC.30 — reflected
|
||||
in staging file; usage_weight values below are NOT yet updated (need re-run of rollup-blast-radius.py after merge).
|
||||
- BC.33 (cross-ext DOM widget obs), BC.34 (settings dialog), BC.35 (pre-queue validation) added 2026-05-08 from Notion COM-3668.
|
||||
- BC.36 (PrimeVue widget API surface) added 2026-05-08 from Notion Widget Component APIs page; was erroneously numbered BC.33 — corrected.
|
||||
- BC.37 (VueNode bridge timing) added 2026-05-08 from Notion Frontend Architecture page (3536d73d). Captures the
|
||||
nodeCreated→VueNode-not-yet-mounted hazard and the waitForLoad3d deferral pattern as a concrete test fixture.
|
||||
categories:
|
||||
- category_id: BC.01
|
||||
name: 'Node lifecycle: creation'
|
||||
intent: Hooks fired when a node is constructed or attached to the graph (per-instance setup).
|
||||
notes: >-
|
||||
nodeCreated fires BEFORE the VueNode Vue component mounts. Extensions that need to access
|
||||
VueNode-backed state (DOM widgets, Three.js renderers, etc.) must defer to onNodeMounted
|
||||
(v2) or waitForLoad3d-style callbacks (v1). See BC.37 for the deferred-mount bridge pattern.
|
||||
Source: Notion Frontend Architecture page (2026-05-08).
|
||||
member_pattern_ids:
|
||||
- S2.N1
|
||||
- S2.N8
|
||||
usage_weight: 37.56
|
||||
exemplars:
|
||||
- pattern_id: S2.N1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/saveImageExtraOutput.ts#L31
|
||||
stars: 1787
|
||||
- pattern_id: S2.N8
|
||||
repo: Azornes/Comfyui-LayerForge
|
||||
url: https://github.com/Azornes/Comfyui-LayerForge/blob/main/src/CanvasView.ts#L1401
|
||||
stars: 313
|
||||
- pattern_id: S2.N1
|
||||
repo: SKBv0/ComfyUI_SpideyReroute
|
||||
url: https://github.com/SKBv0/ComfyUI_SpideyReroute/blob/main/js/SpideyReroute.js#L41
|
||||
stars: 13
|
||||
- category_id: BC.02
|
||||
name: 'Node lifecycle: teardown'
|
||||
intent: Single de-facto teardown surface for cleaning up DOM widgets, intervals, and observers when a node is removed.
|
||||
member_pattern_ids:
|
||||
- S2.N4
|
||||
usage_weight: 29.35
|
||||
exemplars:
|
||||
- pattern_id: S2.N4
|
||||
repo: Lightricks/ComfyUI-LTXVideo
|
||||
url: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L137
|
||||
stars: 3581
|
||||
- pattern_id: S2.N4
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/help_popup.js#L348
|
||||
stars: 2568
|
||||
- pattern_id: S2.N4
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/docs/architecture/ecs-migration-plan.md#L587
|
||||
stars: 1787
|
||||
- category_id: BC.03
|
||||
name: 'Node lifecycle: hydration from saved workflows'
|
||||
intent: React when a node is rehydrated from a stored workflow; the working replacement for the unused loadedGraphNode hook.
|
||||
member_pattern_ids:
|
||||
- S1.H1
|
||||
- S2.N7
|
||||
usage_weight: 15.42
|
||||
exemplars:
|
||||
- pattern_id: S1.H1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H1
|
||||
repo: sofakid/dandy
|
||||
url: https://github.com/sofakid/dandy/blob/main/web/main.js#L114
|
||||
stars: 54
|
||||
- pattern_id: S2.N7
|
||||
repo: akawana/ComfyUI-Folded-Prompts
|
||||
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L1265
|
||||
stars: 4
|
||||
- category_id: BC.04
|
||||
name: 'Node interaction: pointer, selection, resize'
|
||||
intent: 'User-driven per-node events: mouse down for custom click regions, selection focus, and resize feedback for relayout.'
|
||||
member_pattern_ids:
|
||||
- S2.N10
|
||||
- S2.N17
|
||||
- S2.N19
|
||||
usage_weight: 38.07
|
||||
exemplars:
|
||||
- pattern_id: S2.N10
|
||||
repo: diodiogod/TTS-Audio-Suite
|
||||
url: https://github.com/diodiogod/TTS-Audio-Suite/blob/main/web/chatterbox_voice_capture.js#L202
|
||||
stars: 906
|
||||
- pattern_id: S2.N10
|
||||
repo: melMass/comfy_mtb
|
||||
url: https://github.com/melMass/comfy_mtb/blob/main/web/comfy_shared.js#L1047
|
||||
stars: 702
|
||||
- pattern_id: S2.N10
|
||||
repo: pixaroma/ComfyUI-Pixaroma
|
||||
url: https://github.com/pixaroma/ComfyUI-Pixaroma/blob/main/js/compare/index.js#L360
|
||||
stars: 137
|
||||
- category_id: BC.05
|
||||
name: Custom DOM widgets and node sizing
|
||||
intent: Contribute DOM-backed widgets and override computeSize so the node reserves the right area for them.
|
||||
member_pattern_ids:
|
||||
- S4.W2
|
||||
- S2.N11
|
||||
usage_weight: 33.35
|
||||
exemplars:
|
||||
- pattern_id: S4.W2
|
||||
repo: Lightricks/ComfyUI-LTXVideo
|
||||
url: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218
|
||||
stars: 3581
|
||||
- pattern_id: S4.W2
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/editors/editor_base.js#L511
|
||||
stars: 2568
|
||||
- pattern_id: S2.N11
|
||||
repo: o-l-l-i/ComfyUI-Olm-ImageAdjust
|
||||
url: https://github.com/o-l-l-i/ComfyUI-Olm-ImageAdjust/blob/main/web/olm_imageadjust.js#L319
|
||||
stars: 45
|
||||
- category_id: BC.06
|
||||
name: Custom canvas drawing (per-node and canvas-level)
|
||||
intent:
|
||||
Per-node onDrawForeground and full LGraphCanvas.prototype overrides for badges, indicators, keyboard, and custom
|
||||
render passes. Includes global ContextMenu replacement (S3.C2) as the most destructive canvas-level override.
|
||||
v1_scope_note: >-
|
||||
Simon Tranter (COM-3668, 2025-05-12) explicitly vetoed canvas drawing overrides as "too hacky/specific
|
||||
to implement APIs for". Confirmed out of v2 v1 scope. S3.C* patterns remain in DB for blast-radius
|
||||
tracking and strangler-fig planning but v2 need not replace them 1:1. Supports D9 Phase C deferral.
|
||||
member_pattern_ids:
|
||||
- S2.N9
|
||||
- S3.C1
|
||||
- S3.C2
|
||||
usage_weight: 58.97
|
||||
exemplars:
|
||||
- pattern_id: S3.C1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1256
|
||||
stars: 2568
|
||||
- pattern_id: S3.C1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/simpleTouchSupport.ts#L174
|
||||
stars: 1787
|
||||
- pattern_id: S3.C1
|
||||
repo: melMass/comfy_mtb
|
||||
url: https://github.com/melMass/comfy_mtb/blob/main/web/note_plus.js#L1
|
||||
stars: 702
|
||||
- category_id: BC.07
|
||||
name: Connection observation, intercept, and veto
|
||||
intent:
|
||||
Subscribe to link connect/disconnect events on a node and intercept incoming/outgoing connections before they are
|
||||
wired to refuse them, mutate slots, or coerce types.
|
||||
member_pattern_ids:
|
||||
- S2.N3
|
||||
- S2.N12
|
||||
- S2.N13
|
||||
usage_weight: 51.08
|
||||
exemplars:
|
||||
- pattern_id: S2.N13
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/node_mode_relay.js#L90
|
||||
stars: 3049
|
||||
- pattern_id: S2.N12
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/jsnodes.js#L152
|
||||
stars: 2568
|
||||
- pattern_id: S2.N12
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/core/graph/widgets/dynamicWidgets.ts#L539
|
||||
stars: 1787
|
||||
- category_id: BC.08
|
||||
name: Programmatic linking
|
||||
intent: Extensions wire connections from code (workflow templates, auto-routing).
|
||||
member_pattern_ids:
|
||||
- S10.D2
|
||||
usage_weight: 11.81
|
||||
exemplars:
|
||||
- pattern_id: S10.D2
|
||||
repo: MockbaTheBorg/ComfyUI-Mockba
|
||||
url: https://github.com/MockbaTheBorg/ComfyUI-Mockba/blob/main/js/slider.js#L1
|
||||
stars: 1
|
||||
- pattern_id: S10.D2
|
||||
repo: vjumpkung/comfyui-infinitetalk-native-sampler
|
||||
url: https://github.com/vjumpkung/comfyui-infinitetalk-native-sampler/blob/main/README.md#L1
|
||||
stars: 1
|
||||
- pattern_id: S10.D2
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts/blob/main/web/js/quickNodes.js#L138
|
||||
stars: 0
|
||||
- category_id: BC.09
|
||||
name: Dynamic slot and output mutation
|
||||
intent: Grow/shrink inputs and outputs at runtime, with the obligatory computeSize+setSize reflow that follows.
|
||||
member_pattern_ids:
|
||||
- S10.D1
|
||||
- S10.D3
|
||||
- S15.OS1
|
||||
usage_weight: 38.63
|
||||
exemplars:
|
||||
- pattern_id: S10.D1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts#L121
|
||||
stars: 1787
|
||||
- pattern_id: S10.D1
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-mode-nodes.js#L42
|
||||
stars: 19
|
||||
- pattern_id: S15.OS1
|
||||
repo: yorkane/ComfyUI-KYNode
|
||||
url: https://github.com/yorkane/ComfyUI-KYNode/blob/main/web/python-editor.js#L243
|
||||
stars: 10
|
||||
- category_id: BC.10
|
||||
name: Widget value subscription
|
||||
intent: Subscribe to widget value changes either at the widget (callback chain) or node (onWidgetChanged) level.
|
||||
member_pattern_ids:
|
||||
- S4.W1
|
||||
- S2.N14
|
||||
usage_weight: 32.22
|
||||
exemplars:
|
||||
- pattern_id: S2.N14
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/widgetInputs.ts#L317
|
||||
stars: 1787
|
||||
- pattern_id: S4.W1
|
||||
repo: crom8505/ComfyUI-Dynamic-Sigmas
|
||||
url: https://github.com/crom8505/ComfyUI-Dynamic-Sigmas/blob/main/web/js/graph_sigmas.js#L79
|
||||
stars: 8
|
||||
- pattern_id: S4.W1
|
||||
repo: 834t/ComfyUI_834t_scene_composer
|
||||
url: https://github.com/834t/ComfyUI_834t_scene_composer/blob/main/js/b34t_scene_composer.js#L148
|
||||
stars: 5
|
||||
- category_id: BC.11
|
||||
name: Widget imperative state writes
|
||||
intent: Imperatively mutate widget value, COMBO option lists, or the node.widgets array (insert/remove/reorder).
|
||||
member_pattern_ids:
|
||||
- S4.W4
|
||||
- S4.W5
|
||||
- S2.N16
|
||||
usage_weight: 28.42
|
||||
exemplars:
|
||||
- pattern_id: S2.N16
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9
|
||||
stars: 19
|
||||
- pattern_id: S4.W4
|
||||
repo: EnragedAntelope/EA_LMStudio
|
||||
url: https://github.com/EnragedAntelope/EA_LMStudio/blob/main/web/ea_lmstudio.js#L11
|
||||
stars: 7
|
||||
- pattern_id: S4.W4
|
||||
repo: zzggi2024/shaobkj
|
||||
url: https://github.com/zzggi2024/shaobkj/blob/main/js/dynamic_inputs.js#L374
|
||||
stars: 1
|
||||
- category_id: BC.12
|
||||
name: Per-widget serialization transform
|
||||
intent: Transform a widget's value at workflow-serialization time (dynamic prompts, hidden state, expand-on-save).
|
||||
notes: >-
|
||||
widget.options.serialize===false widgets (e.g. control_after_generate) still occupy a widgets_values
|
||||
slot and still fire serializeValue — excluded only from the backend prompt by graphToPrompt(). Test
|
||||
triple must cover this case explicitly. PR #10392 widgets_values_named is the v2 migration path;
|
||||
WidgetHandle identity must be by name not position. See research/architecture/widget-serialization-historical-analysis.md.
|
||||
member_pattern_ids:
|
||||
- S4.W3
|
||||
usage_weight: 27.94
|
||||
exemplars:
|
||||
- pattern_id: S4.W3
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/helpers/painter.ts#L70
|
||||
stars: 1787
|
||||
- pattern_id: S4.W3
|
||||
repo: Raykosan/ComfyUI_RaykoStudio
|
||||
url: https://github.com/Raykosan/ComfyUI_RaykoStudio/blob/main/web/rayko_lora_widget.js#L31
|
||||
stars: 45
|
||||
- pattern_id: S4.W3
|
||||
repo: 834t/ComfyUI_834t_scene_composer
|
||||
url: https://github.com/834t/ComfyUI_834t_scene_composer/blob/main/js/b34t_scene_composer.js#L135
|
||||
stars: 5
|
||||
- category_id: BC.13
|
||||
name: Per-node serialization interception
|
||||
intent: Intercept node-level serialize/onSerialize to inject custom workflow JSON fields.
|
||||
notes: >-
|
||||
Root cause: widgets_values is positional — prototype.serialize patchers consume/produce this array
|
||||
directly. Three index-drift sources: control_after_generate slot occupancy, extension-injected
|
||||
widgets, V3 IO.MultiType topology-dependent widget count. NaN→null pipeline produces silent
|
||||
corruption (backend crash is first visible symptom). Test triple must cover: (a) positional v1
|
||||
compat, (b) named-map v2 round-trip parity, (c) null-in-numeric-widget logs warning + substitutes
|
||||
default. PR #11884 guard, PR #10392 named map. See research/architecture/widget-serialization-historical-analysis.md.
|
||||
member_pattern_ids:
|
||||
- S2.N6
|
||||
- S2.N15
|
||||
usage_weight: 47.07
|
||||
exemplars:
|
||||
- pattern_id: S2.N15
|
||||
repo: Azornes/Comfyui-LayerForge
|
||||
url: https://github.com/Azornes/Comfyui-LayerForge/blob/main/js/CanvasView.js#L1438
|
||||
stars: 313
|
||||
- pattern_id: S2.N15
|
||||
repo: IAMCCS/IAMCCS-nodes
|
||||
url: https://github.com/IAMCCS/IAMCCS-nodes/blob/main/web/iamccs_wan_motion_presets.js#L598
|
||||
stars: 92
|
||||
- pattern_id: S2.N15
|
||||
repo: DazzleNodes/ComfyUI-Smart-Resolution-Calc
|
||||
url: https://github.com/DazzleNodes/ComfyUI-Smart-Resolution-Calc/blob/main/web/utils/serialization.js#L32
|
||||
stars: 7
|
||||
- category_id: BC.14
|
||||
name: Workflow → API serialization interception (graphToPrompt)
|
||||
intent: Patch app.graphToPrompt to resolve virtual nodes, inject custom metadata, or rewrite the API payload before submit.
|
||||
member_pattern_ids:
|
||||
- S6.A1
|
||||
usage_weight: 46.66
|
||||
exemplars:
|
||||
- pattern_id: S6.A1
|
||||
repo: Comfy-Org/ComfyUI-Manager
|
||||
url: https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/js/components-manager.js#L781
|
||||
stars: 14554
|
||||
- pattern_id: S6.A1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1406
|
||||
stars: 2568
|
||||
- pattern_id: S6.A1
|
||||
repo: m3rr/h4_Live
|
||||
url: https://github.com/m3rr/h4_Live/blob/main/js/h4_datastream.js#L23
|
||||
stars: 2
|
||||
- category_id: BC.15
|
||||
name: Workflow loading into the editor
|
||||
intent: External/embed scenario where a workflow JSON is pushed into the running editor via app.loadGraphData.
|
||||
member_pattern_ids:
|
||||
- S6.A2
|
||||
usage_weight: 20.31
|
||||
exemplars:
|
||||
- pattern_id: S6.A2
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/fixtures/helpers/WorkflowHelper.ts#L215
|
||||
stars: 1787
|
||||
- pattern_id: S6.A2
|
||||
repo: BennyKok/comfyui-deploy
|
||||
url: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/workflow-list.js#L456
|
||||
stars: 1507
|
||||
- pattern_id: S6.A2
|
||||
repo: ketle-man/ComfyUI-Workflow-Studio
|
||||
url: https://github.com/ketle-man/ComfyUI-Workflow-Studio/blob/main/static/js/workflow-tab.js#L67
|
||||
stars: 2
|
||||
- category_id: BC.16
|
||||
name: Execution output consumption (per-node)
|
||||
intent: Consume backend execution output on a specific node (text, JSON, image) to drive display.
|
||||
member_pattern_ids:
|
||||
- S2.N2
|
||||
usage_weight: 5.74
|
||||
exemplars:
|
||||
- pattern_id: S2.N2
|
||||
repo: andreszs/ComfyUI-Ultralytics-Studio
|
||||
url: https://github.com/andreszs/ComfyUI-Ultralytics-Studio/blob/main/js/show_string.js#L9
|
||||
stars: 3
|
||||
- pattern_id: S2.N2
|
||||
repo: AlexZ1967/ComfyUI_ALEXZ_tools
|
||||
url: https://github.com/AlexZ1967/ComfyUI_ALEXZ_tools/blob/main/web/show_json.js#L49
|
||||
stars: 0
|
||||
- pattern_id: S2.N2
|
||||
repo: becky3/comfyui-workspace
|
||||
url: https://github.com/becky3/comfyui-workspace/blob/main/custom_nodes/ComfyUI-Becky3-Common/js/show_text.js#L33
|
||||
stars: 0
|
||||
- category_id: BC.17
|
||||
name: Backend execution lifecycle and progress events
|
||||
intent: Subscribe to api.addEventListener for execution_*, progress, status, and reconnecting events.
|
||||
member_pattern_ids:
|
||||
- S5.A1
|
||||
- S5.A2
|
||||
- S5.A3
|
||||
usage_weight: 51.25
|
||||
exemplars:
|
||||
- pattern_id: S5.A2
|
||||
repo: AIGODLIKE/AIGODLIKE-ComfyUI-Studio
|
||||
url: https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Studio/blob/main/loader/components/public/iconRenderer.js#L39
|
||||
stars: 405
|
||||
- pattern_id: S5.A3
|
||||
repo: kyuz0/amd-strix-halo-comfyui-toolboxes
|
||||
url: https://github.com/kyuz0/amd-strix-halo-comfyui-toolboxes/blob/main/scripts/benchmark_workflows.py#L52
|
||||
stars: 109
|
||||
- pattern_id: S5.A1
|
||||
repo: ShakerSmith/ShakerNodesSuite
|
||||
url: https://github.com/ShakerSmith/ShakerNodesSuite/blob/main/js/shaker_preview_ui.js#L58
|
||||
stars: 8
|
||||
- category_id: BC.18
|
||||
name: Backend HTTP calls
|
||||
intent: Call ComfyAPI.fetchApi as the canonical authenticated path to backend HTTP endpoints.
|
||||
member_pattern_ids:
|
||||
- S6.A3
|
||||
usage_weight: 22.74
|
||||
exemplars:
|
||||
- pattern_id: S6.A3
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/components/common/BackgroundImageUpload.vue#L61
|
||||
stars: 1787
|
||||
- pattern_id: S6.A3
|
||||
repo: akawana/ComfyUI-Folded-Prompts
|
||||
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L1227
|
||||
stars: 4
|
||||
- pattern_id: S6.A3
|
||||
repo: zhupeter010903/ComfyUI-XYZ-prompt-library
|
||||
url: https://github.com/zhupeter010903/ComfyUI-XYZ-prompt-library/blob/main/js/prompt_library_window.js#L1379
|
||||
stars: 1
|
||||
- category_id: BC.19
|
||||
name: Workflow execution trigger
|
||||
intent: Trigger or intercept queuePrompt for sidebar Run buttons, auth tokens, or payload mutation.
|
||||
member_pattern_ids:
|
||||
- S6.A4
|
||||
usage_weight: 12.65
|
||||
exemplars:
|
||||
- pattern_id: S6.A4
|
||||
repo: MajoorWaldi/ComfyUI-Majoor-AssetsManager
|
||||
url: https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/blob/main/js/features/viewer/workflowSidebar/sidebarRunButton.js#L317
|
||||
stars: 97
|
||||
- pattern_id: S6.A4
|
||||
repo: gigici/ComfyUI_BlendPack
|
||||
url: https://github.com/gigici/ComfyUI_BlendPack/blob/main/js/ui/NodeUI.js#L99
|
||||
stars: 1
|
||||
- pattern_id: S6.A4
|
||||
repo: rohapa/comfyui-replay
|
||||
url: https://github.com/rohapa/comfyui-replay/blob/main/README.md#L497
|
||||
stars: 0
|
||||
- category_id: BC.20
|
||||
name: Custom node-type registration (frontend-only / virtual)
|
||||
intent: Register pure-frontend or fully virtual node types and mark them with isVirtualNode so the backend ignores them.
|
||||
member_pattern_ids:
|
||||
- S1.H5
|
||||
- S1.H6
|
||||
- S8.P1
|
||||
usage_weight: 27.49
|
||||
exemplars:
|
||||
- pattern_id: S1.H6
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/rerouteNode.ts
|
||||
stars: 1787
|
||||
- pattern_id: S1.H5
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H6
|
||||
repo: sofakid/dandy
|
||||
url: https://github.com/sofakid/dandy/blob/main/web/main.js#L111
|
||||
stars: 54
|
||||
- category_id: BC.21
|
||||
name: Custom widget-type registration
|
||||
intent: Register new widget types (color picker, file uploader, custom inputs) via getCustomWidgets.
|
||||
member_pattern_ids:
|
||||
- S1.H2
|
||||
usage_weight: 7.17
|
||||
exemplars:
|
||||
- pattern_id: S1.H2
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H2
|
||||
repo: haohaocreates/PR-rk-comfy-nodes-36d8f0a5
|
||||
url: https://github.com/haohaocreates/PR-rk-comfy-nodes-36d8f0a5/blob/main/web/rk_nodes.ts#L22
|
||||
stars: 0
|
||||
- category_id: BC.22
|
||||
name: Context menu contributions (node and canvas)
|
||||
intent:
|
||||
Contribute right-click menu items at both the node and canvas scope, including legacy prototype patches and the
|
||||
supported v1 hooks.
|
||||
member_pattern_ids:
|
||||
- S2.N5
|
||||
- S1.H3
|
||||
- S1.H4
|
||||
usage_weight: 19.53
|
||||
exemplars:
|
||||
- pattern_id: S1.H3
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H4
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H3
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-canvas-utils.js#L2
|
||||
stars: 19
|
||||
- category_id: BC.23
|
||||
name: Node property bag mutations
|
||||
intent: React to mutations of node.properties — the persistent property bag that survives serialization.
|
||||
member_pattern_ids:
|
||||
- S2.N18
|
||||
usage_weight: 14.42
|
||||
exemplars:
|
||||
- pattern_id: S2.N18
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/src_web/comfyui/seed.ts#L78
|
||||
stars: 3049
|
||||
- pattern_id: S2.N18
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/seed.js#L26
|
||||
stars: 3049
|
||||
- pattern_id: S2.N18
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/power_primitive.js#L142
|
||||
stars: 3049
|
||||
- category_id: BC.24
|
||||
name: Node-def schema inspection
|
||||
intent: Branch on ComfyNodeDef shape (input.required/optional/hidden, output, output_node, category) to drive UI.
|
||||
member_pattern_ids:
|
||||
- S13.SC1
|
||||
usage_weight: 22.43
|
||||
exemplars:
|
||||
- pattern_id: S13.SC1
|
||||
repo: BennyKok/comfyui-deploy
|
||||
url: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/index.js#L1
|
||||
stars: 1507
|
||||
- pattern_id: S13.SC1
|
||||
repo: StableLlama/ComfyUI-basic_data_handling
|
||||
url: https://github.com/StableLlama/ComfyUI-basic_data_handling/blob/main/web/js/dynamicnode.js#L1
|
||||
stars: 43
|
||||
- pattern_id: S13.SC1
|
||||
repo: xeinherjer-dev/ComfyUI-XENodes
|
||||
url: https://github.com/xeinherjer-dev/ComfyUI-XENodes/blob/main/web/js/combo_selector.js#L1
|
||||
stars: 1
|
||||
- category_id: BC.25
|
||||
name: Shell UI registration (commands, sidebars, toasts)
|
||||
intent: Declarative shell-UI contributions through extensionManager / commandManager / sidebarTab / bottomPanel.
|
||||
member_pattern_ids:
|
||||
- S12.UI1
|
||||
usage_weight: 10.98
|
||||
exemplars:
|
||||
- pattern_id: S12.UI1
|
||||
repo: robertvoy/ComfyUI-Distributed
|
||||
url: https://github.com/robertvoy/ComfyUI-Distributed/blob/main/web/main.js#L269
|
||||
stars: 544
|
||||
- pattern_id: S12.UI1
|
||||
repo: maxi45274/ComfyUI_LinkFX
|
||||
url: https://github.com/maxi45274/ComfyUI_LinkFX/blob/main/js/LinkFX.js#L707
|
||||
stars: 3
|
||||
- pattern_id: S12.UI1
|
||||
repo: criskb/Comfypencil
|
||||
url: https://github.com/criskb/Comfypencil/blob/main/web/comfy_pencil_extension.js#L955
|
||||
stars: 0
|
||||
- category_id: BC.26
|
||||
name: Globals as ABI (window.LiteGraph, window.comfyAPI)
|
||||
intent: Reach into the global namespace for LiteGraph constructors/enums or the module-as-global comfyAPI registry.
|
||||
member_pattern_ids:
|
||||
- S7.G1
|
||||
usage_weight: 27.0
|
||||
exemplars:
|
||||
- pattern_id: S7.G1
|
||||
repo: ryanontheinside/ComfyUI_RyanOnTheInside
|
||||
url: https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside/blob/main/web/js/index.js#L1
|
||||
stars: 801
|
||||
- pattern_id: S7.G1
|
||||
repo: ArtHommage/HommageTools
|
||||
url: https://github.com/ArtHommage/HommageTools/blob/main/web/js/index.js#L1
|
||||
stars: 4
|
||||
- pattern_id: S7.G1
|
||||
repo: PROJECTMAD/PROJECT-MAD-NODES
|
||||
url: https://github.com/PROJECTMAD/PROJECT-MAD-NODES/blob/main/web/js/index.js#L1
|
||||
stars: 4
|
||||
- category_id: BC.27
|
||||
name: LiteGraph entity direct manipulation (reroute, group, link, slot)
|
||||
intent: Direct read/mutation of reroutes, groups, links, and slots — no public extension API exists today.
|
||||
member_pattern_ids:
|
||||
- S9.R1
|
||||
- S9.G1
|
||||
- S9.L1
|
||||
- S9.S1
|
||||
usage_weight: 39.37
|
||||
exemplars:
|
||||
- pattern_id: S9.R1
|
||||
repo: nodetool-ai/nodetool
|
||||
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
|
||||
stars: 330
|
||||
- pattern_id: S9.S1
|
||||
repo: nodetool-ai/nodetool
|
||||
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L267
|
||||
stars: 330
|
||||
- pattern_id: S9.S1
|
||||
repo: Stibo/comfyui-nifty-nodes
|
||||
url: https://github.com/Stibo/comfyui-nifty-nodes/blob/main/js/nifty_nodes.js#L112
|
||||
stars: 3
|
||||
- category_id: BC.28
|
||||
name: Subgraph fan-out via set/get virtual nodes
|
||||
intent: Fan out a single named value across the graph without explicit links (KJNodes-style Set/Get nodes).
|
||||
member_pattern_ids:
|
||||
- S9.SG1
|
||||
usage_weight: 16.89
|
||||
exemplars:
|
||||
- pattern_id: S9.SG1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1406
|
||||
stars: 2568
|
||||
- pattern_id: S9.SG1
|
||||
repo: krismasdev/ComfyUI-Flux-Continuum
|
||||
url: https://github.com/krismasdev/ComfyUI-Flux-Continuum/blob/main/web/hint.js#L1
|
||||
stars: 0
|
||||
- pattern_id: S9.SG1
|
||||
repo: SpaceWarpStudio/ComfyUI-SetInputGetOutput
|
||||
url: https://github.com/SpaceWarpStudio/ComfyUI-SetInputGetOutput/blob/main/web/js/setinputgetoutput.js#L1
|
||||
stars: 0
|
||||
- category_id: BC.29
|
||||
name: Graph enumeration, mutation, and cross-scope identity
|
||||
intent:
|
||||
Enumerate or mutate the node set (graph.add/remove/findNodesByType/serialize/configure) and resolve cross-subgraph
|
||||
references via NodeLocatorId / NodeExecutionId.
|
||||
member_pattern_ids:
|
||||
- S11.G2
|
||||
- S14.ID1
|
||||
usage_weight: 23.56
|
||||
exemplars:
|
||||
- pattern_id: S11.G2
|
||||
repo: yolain/ComfyUI-Easy-Use
|
||||
url: https://github.com/yolain/ComfyUI-Easy-Use/blob/main/web_version/v1/js/easy/easyExtraMenu.js#L439
|
||||
stars: 2503
|
||||
- pattern_id: S11.G2
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/tests/workflowPersistence.spec.ts#L351
|
||||
stars: 1787
|
||||
- pattern_id: S11.G2
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-ui-enhancements.js#L29
|
||||
stars: 19
|
||||
- category_id: BC.30
|
||||
name: Graph change tracking, batching, and reactivity flush
|
||||
intent:
|
||||
'Coordinate graph-level change: graph._version monotonic counter, beforeChange/afterChange batching, and the imperative
|
||||
setDirtyCanvas redraw flush.'
|
||||
member_pattern_ids:
|
||||
- S11.G1
|
||||
- S11.G3
|
||||
- S11.G4
|
||||
usage_weight: 34.38
|
||||
exemplars:
|
||||
- pattern_id: S11.G3
|
||||
repo: nodetool-ai/nodetool
|
||||
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
|
||||
stars: 330
|
||||
- pattern_id: S11.G4
|
||||
repo: akawana/ComfyUI-Folded-Prompts
|
||||
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L776
|
||||
stars: 4
|
||||
- pattern_id: S11.G3
|
||||
repo: linjm8780860/ljm_comfyui
|
||||
url: https://github.com/linjm8780860/ljm_comfyui/blob/main/src/utils/vintageClipboard.ts#L1
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.31
|
||||
name: DOM injection and style management
|
||||
intent:
|
||||
Extensions add UI chrome, toolbars, and style overrides directly into the document outside any provided API —
|
||||
style tags into head, arbitrary elements into body, innerHTML rendering, and external script loading.
|
||||
member_pattern_ids:
|
||||
- S16.DOM1
|
||||
- S16.DOM2
|
||||
- S16.DOM3
|
||||
- S16.DOM4
|
||||
usage_weight: 0.0
|
||||
notes:
|
||||
'usage_weight pending rollup-blast-radius.py re-run after database.yaml merge (I-N4.1). Notion counts: DOM1=354
|
||||
occ, DOM2=364 occ, DOM3=443 occ, DOM4=232 occ across ~81 packages — among the highest raw occurrence counts in
|
||||
the entire dataset. v2 replacements: injectStyles(), addPanel(), addToolbarItem(), safe HTML rendering API.'
|
||||
exemplars:
|
||||
- pattern_id: S16.DOM1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/help_popup.js
|
||||
stars: 2568
|
||||
- pattern_id: S16.DOM2
|
||||
repo: yolain/ComfyUI-Easy-Use
|
||||
url: https://github.com/yolain/ComfyUI-Easy-Use/blob/main/web_version/v1/js/easy/easy.js
|
||||
stars: 2503
|
||||
- pattern_id: S16.DOM3
|
||||
repo: '(aggregate — Notion §2.3)'
|
||||
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.32
|
||||
name: Embedded framework runtimes and Vue widget bundling
|
||||
intent:
|
||||
Extensions bundle their own copy of Vue (or another framework) inside a DOM widget, bypassing the host app
|
||||
instance and losing access to shared stores, i18n, and theme.
|
||||
member_pattern_ids:
|
||||
- S16.VUE1
|
||||
usage_weight: 0.0
|
||||
notes:
|
||||
'usage_weight pending rollup-blast-radius.py re-run. 9 packages confirmed (Notion §2.9). v2 replacement:
|
||||
registerVueWidget(nodeType, name, Component) sharing host Vue instance — already in plans/P1 §5 Custom widget type.
|
||||
This BC provides the evidence base for that P1 design decision.'
|
||||
exemplars:
|
||||
- pattern_id: S16.VUE1
|
||||
repo: ComfyUI-NKD-Sigmas-Curve
|
||||
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
|
||||
stars: 0
|
||||
- pattern_id: S16.VUE1
|
||||
repo: '(aggregate — 9 packages, Notion §2.9)'
|
||||
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
|
||||
stars: 0
|
||||
|
||||
# ── Categories added 2026-05-08 from Notion COM-3668 (Simon Tranter, Custom Scripts API requirements) ──
|
||||
|
||||
- category_id: BC.33
|
||||
name: Cross-extension DOM widget creation observation
|
||||
intent:
|
||||
An extension observes when *any* DOM widget is created (by any other extension) so it can attach its own
|
||||
listeners — the mechanism the Autocomplete extension needs to wire its input handler to every text widget.
|
||||
member_pattern_ids:
|
||||
- S4.W6
|
||||
usage_weight: 0.0
|
||||
notes: >-
|
||||
Identified from COM-3668. Distinct from BC.05 (creating DOM widgets) and BC.10 (subscribing to value changes).
|
||||
Gap: no v1 hook fires for cross-extension widget creation observation. v2 shape: onDOMWidgetCreated(handler)
|
||||
in defineExtension setup context. usage_weight pending blast-radius re-run.
|
||||
source: notion-COM-3668
|
||||
exemplars:
|
||||
- pattern_id: S4.W6
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.34
|
||||
name: Settings-panel custom dialog integration
|
||||
intent: Extensions open custom modal dialogs triggered from the settings panel, rather than injecting raw DOM.
|
||||
member_pattern_ids:
|
||||
- S12.UI3
|
||||
usage_weight: 0.0
|
||||
notes: >-
|
||||
Identified from COM-3668. Currently worked around via S16.DOM3 innerHTML injection. Distinct from S12.UI1
|
||||
(sidebar/command registration) — this is about dialog lifecycle tied to settings entries. v2 shape:
|
||||
app.ui.openDialog(Component) or settings entry type 'dialog-trigger'. usage_weight pending blast-radius re-run.
|
||||
source: notion-COM-3668
|
||||
exemplars:
|
||||
- pattern_id: S12.UI3
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.35
|
||||
name: Pre-queue widget validation
|
||||
intent:
|
||||
Validate widget values before a workflow is submitted and surface typed errors to the user — rejecting
|
||||
the queue rather than silently mutating or failing.
|
||||
member_pattern_ids:
|
||||
- S6.A5
|
||||
usage_weight: 0.0
|
||||
notes: >-
|
||||
Identified from COM-3668. Currently worked around via S6.A4 queuePrompt monkey-patching (silent_breakage=true
|
||||
when multiple extensions patch). Distinct from D5 beforeSerialize (transforms values) and BC.19 (triggers
|
||||
execution). v2 needs explicit beforeQueue event with event.reject(message). usage_weight pending re-run.
|
||||
source: notion-COM-3668
|
||||
exemplars:
|
||||
- pattern_id: S6.A5
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.36
|
||||
name: PrimeVue widget component API surface
|
||||
intent: >-
|
||||
Custom node authors configuring widget behavior via per-component prop subsets — the v2 replacement
|
||||
for direct widget.options mutation (S4.W4, S4.W1) and DOM widget construction (S4.W5, S4.W6).
|
||||
15 PrimeVue components are the authoritative widget-kind enumeration for v2.
|
||||
member_pattern_ids:
|
||||
- S4.W1
|
||||
- S4.W4
|
||||
- S4.W5
|
||||
notes: >-
|
||||
Source: Notion page "Widget Component APIs" (2026-05-08). 15 components: Button, InputText, Select,
|
||||
ColorPicker, MultiSelect, SelectButton, Slider, Textarea, ToggleSwitch, Chart, Image, ImageCompare,
|
||||
Galleria, FileUpload, TreeSelect. Exclusion rule (Pablo): strip style/class/dt/pt/*Class/*Style.
|
||||
ToggleSwitch is the only component with completed Pick<> types so far (WIP).
|
||||
Informs: D7 typed options bags (future pivot), I-TF.2 widget-kind test triples,
|
||||
PKG2 WidgetHandle.getOption key surface. disabled/readonly map to D7 first-class fields,
|
||||
not options bag.
|
||||
usage_weight: 0.0
|
||||
exemplars:
|
||||
- pattern_id: S4.W4
|
||||
repo: '(see database.yaml S4.W4 exemplars — widget.options.values mutation)'
|
||||
url: https://www.notion.so/comfy-org/Widget-Component-APIs-2126d73d365080b0bf30f241c09dd756
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.37
|
||||
name: VueNode bridge timing — deferred mount access
|
||||
intent: >-
|
||||
Extensions that register in nodeCreated but need to access Vue-component-backed state
|
||||
(Three.js renderer, DOM widget, ComponentWidgetImpl value) must defer until the Vue
|
||||
component's onMounted fires. The v1 pattern is waitForLoad3d(node, cb); the v2 pattern
|
||||
is onNodeMounted(() => { ... }) inside defineNodeExtension.
|
||||
member_pattern_ids:
|
||||
- S4.W5
|
||||
notes: >-
|
||||
Source: Notion Frontend Architecture page 3536d73d (2026-05-08). nodeCreated gives the
|
||||
LiteGraph node; the VueNode Vue component has NOT mounted yet. waitForLoad3d in
|
||||
src/extensions/core/Load3D is the canonical v1 fixture. ComponentWidgetImpl dual-identity:
|
||||
LiteGraph side (value/callback/name) vs Vue side (props/emits/lifecycle).
|
||||
v2 contract: onNodeMounted() hook fires after Vue component mount — this is the correct
|
||||
timing for accessing VueNode-backed resources.
|
||||
Informs: I-SR.2.B2 (NodeInstanceScope must not sync-access VueNode at setup time),
|
||||
I-TF.3.C1 (harness must simulate two-phase mount), I-TF.2 test triple for BC.37.
|
||||
D8 relevance: app.rootGraph is not reactive (confirmed by this doc) — the exact gap D8 solves.
|
||||
usage_weight: 0.0
|
||||
source: notion-frontend-architecture-3536d73d
|
||||
exemplars:
|
||||
- pattern_id: S4.W5
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/load3d.ts
|
||||
stars: 1787
|
||||
|
||||
- category_id: BC.38
|
||||
name: Canvas mode observation
|
||||
intent: >-
|
||||
Detect and react to ComfyUI canvas mode transitions (graph / app / builder:inputs /
|
||||
builder:outputs / builder:arrange). Custom nodes that adapt rendering, widget resize
|
||||
behavior, or read-only state across modes need a stable event — not polling or heuristics
|
||||
against internal Pinia store state.
|
||||
member_pattern_ids:
|
||||
- S17.AM1
|
||||
mechanism: absent-api
|
||||
notes: >-
|
||||
appModeStore is a Pinia composable; JS extensions cannot use Vue composables. v2 gap:
|
||||
no node.on('canvasModeChanged') exists yet in node.ts — distinct from NodeModeChangedEvent
|
||||
(execution mode only). v2 contract: app-level or node-level canvasModeChanged event.
|
||||
Flagged: Terry DX walkthrough A.1. Informs: node.ts overloads (add canvasModeChanged
|
||||
or document as known gap), I-TF.2 test triple for BC.38.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.AM1
|
||||
repo: (first-principles assessment — Terry Jia)
|
||||
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.39
|
||||
name: Subgraph boundary event propagation
|
||||
intent: >-
|
||||
Custom node callbacks (onExecuted, MatchType, autogrow onConnectionsChange, promoted widget
|
||||
callbacks) that must propagate across subgraph boundaries. Four distinct silent-failure modes
|
||||
when custom nodes are placed inside subgraphs.
|
||||
member_pattern_ids:
|
||||
- S17.SB1
|
||||
mechanism: absent-api
|
||||
notes: >-
|
||||
Requires D9 Phase B (post-Alex rebase on #11939). ECS substrate must forward SubgraphNode
|
||||
execution events from internal nodes. MatchType and autogrow propagation require subgraph
|
||||
boundary awareness in World dispatcher. Blocked: I-PG.B1. Short-term: @experimental on
|
||||
affected NodeHandle events; subgraphCompatible flag in NodeExtensionOptions.
|
||||
Intersects: ADR 0006 (I-NEW.1), Austin's fix-linked-widget-promotion.
|
||||
Flagged: Terry DX walkthrough A.2.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.SB1
|
||||
repo: (first-principles assessment — Terry Jia)
|
||||
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.40
|
||||
name: File upload and asset URL construction
|
||||
intent: >-
|
||||
Upload files to ComfyUI backend and construct retrieval URLs. 32+ packages duplicate this
|
||||
pattern from scratch — FormData construction, fetchApi('/upload/image'), /view?filename URL
|
||||
assembly. A helper API would collapse this to comfyAPI.uploadFile() + comfyAPI.getFileUrl().
|
||||
member_pattern_ids:
|
||||
- S17.FA1
|
||||
mechanism: absent-api
|
||||
notes: >-
|
||||
Out of scope for @comfyorg/extension-api (node extension surface). Belongs in future
|
||||
@comfyorg/comfy-api package. 32+ packages affected; 9 implement video upload variants.
|
||||
Upload timeout hardcoded 120s; large 3D/video fail silently. No temp file lifecycle.
|
||||
Document as known gap in src/extension-api/README.md.
|
||||
Flagged: Terry DX walkthrough A.3.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.FA1
|
||||
repo: (first-principles assessment — Terry Jia)
|
||||
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.41
|
||||
name: Widget values positional serialization fragility
|
||||
intent: >-
|
||||
Widget values serialized as positional array [v1, v2, v3] instead of named dict.
|
||||
Any input definition change (add, reorder, rename, remove, required→optional) silently
|
||||
misaligns values when loading existing workflows. Root cause of #1 user complaint:
|
||||
"my workflow broke after I updated the custom node."
|
||||
member_pattern_ids:
|
||||
- S17.WV1
|
||||
mechanism: positional-array
|
||||
notes: >-
|
||||
Blocked on workflow-schema-migration (out of v2 surface scope). D7 Part 4 (4→2
|
||||
serialization collapse) + beforeSerialize as partial mitigation. Long-term fix: named dict
|
||||
format { widgetName: value } — breaking JSON schema change requiring versioning +
|
||||
migrateWidgetValues() callback. PR #10392 added widgets_values_named opt-in; PR #11884
|
||||
null guard. v2 contract: name-keyed identity (WidgetHandle by name not position).
|
||||
Intersects: ADR 0006, widget-serialization-historical-analysis.md, Austin's work.
|
||||
Flagged: Terry DX walkthrough A.4.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.WV1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/utils/nodeDefOrderingUtil.ts
|
||||
stars: 1787
|
||||
1185
research/touch-points/rollup.yaml
Normal file
1185
research/touch-points/rollup.yaml
Normal file
File diff suppressed because it is too large
Load Diff
117
scripts/check-compat-floor.py
Normal file
117
scripts/check-compat-floor.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Compat-floor gate: Verify all high-impact behavior categories have test triples.
|
||||
|
||||
Per PLAN.md §Compat-floor: "Every blast_radius ≥ 2.0 pattern MUST pass v1 + v2 +
|
||||
migration tests before v2 ships."
|
||||
|
||||
This script:
|
||||
1. Reads research/touch-points/behavior-categories.yaml
|
||||
2. Finds all categories with usage_weight >= 2.0 (blast_radius threshold)
|
||||
3. Checks that each has all three test files: bc-XX.v1.test.ts, bc-XX.v2.test.ts, bc-XX.migration.test.ts
|
||||
4. Exits 0 if all present, exits 1 if any missing (fails CI)
|
||||
|
||||
Usage: python3 scripts/check-compat-floor.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("ERROR: PyYAML not installed. Run: pip install pyyaml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
COMPAT_FLOOR_THRESHOLD = 2.0
|
||||
BEHAVIOR_CATEGORIES_PATH = Path("research/touch-points/behavior-categories.yaml")
|
||||
TESTS_DIR = Path("src/extension-api-v2/__tests__")
|
||||
|
||||
def main():
|
||||
# Check that behavior-categories.yaml exists
|
||||
if not BEHAVIOR_CATEGORIES_PATH.exists():
|
||||
print(f"ERROR: {BEHAVIOR_CATEGORIES_PATH} not found", file=sys.stderr)
|
||||
print(" Run scripts/build-behavior-categories.py first or copy from workspace", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Skip check if tests directory doesn't exist (tests only in tf branch)
|
||||
if not TESTS_DIR.exists():
|
||||
print(f"SKIP: {TESTS_DIR} not found — compat-floor tests not yet added to this branch")
|
||||
print(" The compat-floor gate only enforces on branches with extension-api-v2 tests.")
|
||||
sys.exit(0)
|
||||
|
||||
# Load categories
|
||||
with open(BEHAVIOR_CATEGORIES_PATH, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
categories = data.get("categories", [])
|
||||
|
||||
# Find categories above compat floor
|
||||
above_floor = []
|
||||
for cat in categories:
|
||||
cat_id = cat.get("category_id", "")
|
||||
usage_weight = cat.get("usage_weight", 0)
|
||||
if usage_weight >= COMPAT_FLOOR_THRESHOLD:
|
||||
above_floor.append({
|
||||
"id": cat_id,
|
||||
"name": cat.get("name", ""),
|
||||
"usage_weight": usage_weight
|
||||
})
|
||||
|
||||
print(f"Compat-floor check: {len(above_floor)} categories with usage_weight >= {COMPAT_FLOOR_THRESHOLD}")
|
||||
print()
|
||||
|
||||
# Check each category for test triples
|
||||
missing = []
|
||||
for cat in above_floor:
|
||||
cat_id = cat["id"]
|
||||
# Extract number from BC.XX
|
||||
num_str = cat_id.replace("BC.", "").zfill(2)
|
||||
|
||||
required_files = [
|
||||
f"bc-{num_str}.v1.test.ts",
|
||||
f"bc-{num_str}.v2.test.ts",
|
||||
f"bc-{num_str}.migration.test.ts"
|
||||
]
|
||||
|
||||
cat_missing = []
|
||||
for fname in required_files:
|
||||
fpath = TESTS_DIR / fname
|
||||
if not fpath.exists():
|
||||
cat_missing.append(fname)
|
||||
|
||||
if cat_missing:
|
||||
missing.append({
|
||||
"category": cat_id,
|
||||
"name": cat["name"],
|
||||
"usage_weight": cat["usage_weight"],
|
||||
"missing": cat_missing
|
||||
})
|
||||
status = "❌ MISSING"
|
||||
else:
|
||||
status = "✅"
|
||||
|
||||
print(f" {cat_id} ({cat['usage_weight']:.2f}) {cat['name'][:40]:<40} {status}")
|
||||
if cat_missing:
|
||||
for m in cat_missing:
|
||||
print(f" └─ {m}")
|
||||
|
||||
print()
|
||||
|
||||
if missing:
|
||||
print(f"FAIL: {len(missing)} categories missing test files", file=sys.stderr)
|
||||
print()
|
||||
print("Per PLAN.md §Compat-floor, all blast_radius >= 2.0 categories", file=sys.stderr)
|
||||
print("must have complete test triples (v1, v2, migration) before v2 ships.", file=sys.stderr)
|
||||
print()
|
||||
print("Missing files:", file=sys.stderr)
|
||||
for m in missing:
|
||||
for f in m["missing"]:
|
||||
print(f" - {TESTS_DIR / f}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"PASS: All {len(above_floor)} compat-floor categories have test triples")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
scripts/generate-docs.sh
Executable file
28
scripts/generate-docs.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# PKG5.D6 — Generate TypeDoc → Mintlify MDX for @comfyorg/extension-api
|
||||
#
|
||||
# Output: packages/extension-api/docs-build/mintlify/*.mdx
|
||||
# packages/extension-api/docs-build/mintlify/nav-snippet.json
|
||||
#
|
||||
# Prerequisites: pnpm install must have been run (typedoc, tsx)
|
||||
# Usage: ./scripts/generate-docs.sh [--watch]
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PKG_DIR="$REPO_ROOT/packages/extension-api"
|
||||
|
||||
if [ ! -f "$PKG_DIR/package.json" ]; then
|
||||
echo "ERROR: $PKG_DIR/package.json not found — run from repo root or ensure packages/extension-api exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--watch" ]; then
|
||||
echo "Starting docs watch mode..."
|
||||
pnpm --filter @comfyorg/extension-api docs:watch
|
||||
else
|
||||
echo "Generating extension API docs..."
|
||||
pnpm --filter @comfyorg/extension-api docs:build
|
||||
echo ""
|
||||
echo "Done. MDX files written to: $PKG_DIR/docs-build/mintlify/"
|
||||
echo "Copy to Comfy-Org/docs: cp -r $PKG_DIR/docs-build/mintlify/* <docs-repo>/extensions/api/"
|
||||
fi
|
||||
68
src/extension-api/README.md
Normal file
68
src/extension-api/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Extension API — Public Source of Truth
|
||||
|
||||
> **Status**: Implemented (Phase A). Runtime backed by stub ECS components;
|
||||
> full ECS integration lands with #11939.
|
||||
|
||||
This folder is the single source of truth for the public ComfyUI extension
|
||||
API. Every file here is part of the published `@comfyorg/extension-api`
|
||||
npm package. Do not re-export from `/src` — this barrel is the **published
|
||||
package entry point**, which is the explicit exception to the project's
|
||||
"no barrel files in /src" rule (root AGENTS.md rule #19).
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
extension-api/
|
||||
├── index.ts ← barrel — package entry point
|
||||
├── node.ts ← NodeHandle interface + node event payload types
|
||||
├── widget.ts ← WidgetHandle interface + widget event payload types
|
||||
├── types.ts ← ExtensionOptions, NodeExtensionOptions, WidgetExtensionOptions
|
||||
├── events.ts ← Handler<E>, AsyncHandler<E>, Unsubscribe
|
||||
├── lifecycle.ts ← onNodeMounted, onNodeRemoved hooks + rationale docs
|
||||
├── shell.ts ← SidebarTabExtension, BottomPanelExtension, CommandManager, etc.
|
||||
├── identifiers.ts ← NodeLocatorId, NodeExecutionId + parsers/type guards
|
||||
└── README.md ← this file
|
||||
```
|
||||
|
||||
## What about v1?
|
||||
|
||||
v1 (`ComfyExtension` interface in `../types/comfy.ts`, `app.registerExtension(...)`
|
||||
runtime entry point in `../scripts/app.ts`) **stays in its current locations**.
|
||||
Custom extensions in the wild consume the runtime entry point, not the type
|
||||
file — moving the type file would churn ~30 internal imports for zero runtime
|
||||
benefit. The v1↔v2 distinction is at the entry point, not the folder.
|
||||
|
||||
## Authoring rules
|
||||
|
||||
1. **Hand-authored**, not generated. This is a public API; we own the shape.
|
||||
2. **No `any`, no `as any`, no `@ts-expect-error`.** If you need an escape
|
||||
hatch, the type is wrong.
|
||||
3. Every public type has a TSDoc block with at minimum:
|
||||
- 1-line summary
|
||||
- `@stability` tag (`stable` | `experimental` | `deprecated`)
|
||||
- `@example` block (where applicable)
|
||||
4. Naming follows conventions:
|
||||
- Read-only invariants (set at construction): `readonly` property
|
||||
- Read-only state (changes over time): method (`getValue()`)
|
||||
- Mutating actions: method (`setValue(v)`)
|
||||
- Boolean predicates: method (`isHidden()`)
|
||||
5. Events: typed payloads, no `Function`, split-channel events
|
||||
(`valueChange` / `optionChange` / `propertyChange`).
|
||||
6. No internal types (`World`, `Component<T>`, branded `EntityId` internals)
|
||||
leak through this barrel.
|
||||
|
||||
## Key design decisions
|
||||
|
||||
| ADR | Decision |
|
||||
| ----------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [ADR-0008](../../docs/adr/0008-entity-component-system.md) | Entity Component System architecture |
|
||||
| [ADR-0010](../../docs/adr/0010-deprecate-node-level-serialization-control.md) | Deprecate `node.on('beforeSerialize')` — use widget-level |
|
||||
| [ADR-0011](../../docs/adr/0011-immutability-via-fresh-copies.md) | Return fresh copies from collection methods |
|
||||
| [ADR-0012](../../docs/adr/0012-pure-function-loader-pattern.md) | Pure function registration + loader activation |
|
||||
|
||||
## Related research
|
||||
|
||||
- [Identity encapsulation](../../docs/research/identity-encapsulation.md) — when extensions need raw entity IDs
|
||||
- [Coordinate systems](../../docs/research/coordinate-systems.md) — canvas vs screen coordinates
|
||||
- [Widget state categories](../../docs/research/widget-state-categories.md) — value/properties/options/DOM
|
||||
- [Serialization context](../../docs/research/serialization-context.md) — workflow/prompt/clone/subgraph-promote
|
||||
61
src/extension-api/brand.ts
Normal file
61
src/extension-api/brand.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* D18 Phase 1 — brand symbols for `define*` outputs.
|
||||
*
|
||||
* Per D18, `defineNode` / `defineWidget` / `defineExtension` will become
|
||||
* pure functions whose return values are recognized at registration time
|
||||
* by a loader that walks module exports and dispatches based on the brand.
|
||||
*
|
||||
* Phase 1 (this file) introduces the brand symbol and the `isBrandedExtension`
|
||||
* type-guard. The `define*` functions stamp the brand on their returned
|
||||
* options so a future loader can identify them. Side-effect registration
|
||||
* remains unchanged in Phase 1; Phase 2 removes it.
|
||||
*
|
||||
* The brand is a `Symbol.for(...)` so HMR + duplicate-package scenarios still
|
||||
* resolve to the same identity (per realm / per JS context).
|
||||
*
|
||||
* @internal — not re-exported from `@comfyorg/extension-api/index.ts`. The
|
||||
* loader lives inside the runtime, not in the published package.
|
||||
*/
|
||||
|
||||
export const EXTENSION_BRAND = Symbol.for('@comfyorg/extension-api:brand')
|
||||
|
||||
export type ExtensionKind = 'node' | 'widget' | 'app'
|
||||
|
||||
export interface Branded {
|
||||
readonly [EXTENSION_BRAND]: ExtensionKind
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp a brand on an options object and freeze it. Returned reference is
|
||||
* the same object — branding is non-enumerable so JSON serialization,
|
||||
* spread operations, and shallow-equal comparisons are unaffected.
|
||||
*/
|
||||
export function stampBrand<T extends object>(
|
||||
options: T,
|
||||
kind: ExtensionKind
|
||||
): T & Branded {
|
||||
Object.defineProperty(options, EXTENSION_BRAND, {
|
||||
value: kind,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: false
|
||||
})
|
||||
return Object.freeze(options) as T & Branded
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-guard for branded extension options. The loader uses this to
|
||||
* decide whether a module export is a `defineX(...)` result.
|
||||
*
|
||||
* Unbranded values (utility exports, constants, helper functions) return
|
||||
* `false` and are silently ignored by the loader.
|
||||
*/
|
||||
export function isBrandedExtension(value: unknown): value is Branded {
|
||||
if (value === null || typeof value !== 'object') return false
|
||||
const kind = (value as Record<symbol, unknown>)[EXTENSION_BRAND]
|
||||
return kind === 'node' || kind === 'widget' || kind === 'app'
|
||||
}
|
||||
|
||||
export function getBrandKind(value: Branded): ExtensionKind {
|
||||
return value[EXTENSION_BRAND]
|
||||
}
|
||||
277
src/extension-api/events.ts
Normal file
277
src/extension-api/events.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Shared event infrastructure for the ComfyUI extension API.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
/**
|
||||
* A typed event handler function.
|
||||
*
|
||||
* @typeParam E - The event payload type.
|
||||
* @example
|
||||
* ```ts
|
||||
* const handler: Handler<WidgetValueChangeEvent<number>> = (e) => {
|
||||
* console.log(e.oldValue, '->', e.newValue)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type Handler<E> = (event: E) => void
|
||||
|
||||
/**
|
||||
* A typed async-capable event handler. Only valid for events that explicitly
|
||||
* support async handling (currently only `beforeSerialize`).
|
||||
*
|
||||
* @typeParam E - The event payload type.
|
||||
* @example
|
||||
* ```ts
|
||||
* import type { AsyncHandler, WidgetBeforeSerializeEvent } from '@comfyorg/extension-api'
|
||||
*
|
||||
* const handler: AsyncHandler<WidgetBeforeSerializeEvent> = async (e) => {
|
||||
* const frame = await captureFrame()
|
||||
* e.setSerializedValue(frame)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type AsyncHandler<E> = (event: E) => void | Promise<void>
|
||||
|
||||
/**
|
||||
* Cleanup function returned by `on()` — call to remove the listener.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const off = node.on('executed', handler)
|
||||
* // later:
|
||||
* off()
|
||||
* ```
|
||||
*/
|
||||
export type Unsubscribe = () => void
|
||||
|
||||
// Event-namespace facades
|
||||
//
|
||||
// Four typed event-namespace handles (`graph` / `execution` / `server` /
|
||||
// `workbench`) replace the ad-hoc `api.addEventListener('execution_start', ...)`
|
||||
// pattern documented in 360+ ecosystem call sites. Each namespace is a
|
||||
// module-level singleton (SD-4 (a), handoff-11) — call from any setup() body
|
||||
// or hook closure. Subscriptions registered inside a setup context auto-dispose
|
||||
// when the surrounding instance is unmounted (Vue-style; subscription is added
|
||||
// to the context's unmountHooks). Outside a setup context, the returned
|
||||
// `Unsubscribe` is the caller's responsibility.
|
||||
//
|
||||
// Payload typing (SD-5 (b)): each `on()` accepts a string event name and
|
||||
// returns `Handler<EventPayloadMap[ns][evt]>`. The maps default to `unknown`
|
||||
// today and are tightened by D5 module augmentation in a follow-on PR. Authors
|
||||
// get autocomplete on canonical event names; payload narrowing arrives when
|
||||
// D5 lands.
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { getCurrentExtensionInstance } from '@/services/extension-api-service'
|
||||
|
||||
/**
|
||||
* Per-namespace event payload map. **Augment via TS module augmentation** to
|
||||
* narrow payloads for canonical events. Until D5 ships, all payloads default
|
||||
* to `unknown`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* declare module '@comfyorg/extension-api' {
|
||||
* interface ExecutionEventPayloads {
|
||||
* start: { promptId: string }
|
||||
* progress: { value: number; max: number }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface GraphEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
/**
|
||||
* See {@link GraphEventPayloads | the augmentation example} —
|
||||
* augment this interface the same way to narrow `execution.*` payloads.
|
||||
*/
|
||||
export interface ExecutionEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
/**
|
||||
* See {@link GraphEventPayloads | the augmentation example} —
|
||||
* augment this interface the same way to narrow `server.*` payloads.
|
||||
*/
|
||||
export interface ServerEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
/**
|
||||
* See {@link GraphEventPayloads | the augmentation example} —
|
||||
* augment this interface the same way to narrow `workbench.*` payloads.
|
||||
*/
|
||||
export interface WorkbenchEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
|
||||
interface EventNamespace<M> {
|
||||
/**
|
||||
* Subscribe to an event. Returns an {@link Unsubscribe} function.
|
||||
*
|
||||
* Inside a `setup()` body the subscription is also added to the
|
||||
* surrounding instance's `onUnmounted` queue and auto-disposes when the
|
||||
* extension/tab/panel is unmounted.
|
||||
*/
|
||||
on<K extends keyof M & string>(event: K, handler: Handler<M[K]>): Unsubscribe
|
||||
|
||||
/**
|
||||
* Remove a previously registered handler. Same as the {@link Unsubscribe}
|
||||
* returned by `on()`. Exposed for symmetry with `addEventListener`/`removeEventListener`.
|
||||
*/
|
||||
off<K extends keyof M & string>(event: K, handler: Handler<M[K]>): void
|
||||
}
|
||||
|
||||
function makeNamespace<M>(rename: (evt: string) => string): EventNamespace<M> {
|
||||
// ComfyApi extends EventTarget but its addEventListener is strictly typed
|
||||
// against the validated ApiCalls union. The bootstrap-hooks facade
|
||||
// accepts any string (custom-node events ride server.on with arbitrary
|
||||
// names per ADR), so we widen via EventTarget to get the generic overload.
|
||||
const target = api as unknown as EventTarget
|
||||
return {
|
||||
on<K extends keyof M & string>(
|
||||
event: K,
|
||||
handler: Handler<M[K]>
|
||||
): Unsubscribe {
|
||||
const wireName = rename(event)
|
||||
// payload arrives as CustomEvent.detail.
|
||||
const adapter = (e: Event): void => {
|
||||
const detail = (e as CustomEvent).detail as M[K]
|
||||
handler(detail)
|
||||
}
|
||||
target.addEventListener(wireName, adapter)
|
||||
const unsubscribe: Unsubscribe = () => {
|
||||
target.removeEventListener(wireName, adapter)
|
||||
}
|
||||
// Auto-dispose inside a setup() context (mirrors Vue's onScopeDispose).
|
||||
const ctx = getCurrentExtensionInstance()
|
||||
if (ctx) {
|
||||
ctx.unmountHooks.push(unsubscribe)
|
||||
}
|
||||
return unsubscribe
|
||||
},
|
||||
off<K extends keyof M & string>(event: K, handler: Handler<M[K]>): void {
|
||||
// Note: off() with a raw handler only matches if the caller saved the
|
||||
// exact adapter reference returned from on(). The recommended path is
|
||||
// to call the Unsubscribe returned by on(). This off() is retained for
|
||||
// API symmetry but does NOT round-trip with on() handlers — they wrap
|
||||
// the user fn in an adapter for CustomEvent unwrap. Authors that need
|
||||
// explicit off() should use the Unsubscribe handle.
|
||||
target.removeEventListener(
|
||||
rename(event),
|
||||
handler as unknown as EventListener
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph-mutation events (frontend-dispatched).
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { onMounted, graph } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* graph.on('changed', (e) => console.log('graph changed', e))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const graph: EventNamespace<GraphEventPayloads> = makeNamespace(
|
||||
(evt) => `graph:${evt}`
|
||||
)
|
||||
|
||||
/**
|
||||
* Prompt-run lifecycle events (backend-dispatched).
|
||||
*
|
||||
* Canonical events: `'start'`, `'end'`, `'error'`, `'interrupted'`, `'cached'`,
|
||||
* `'executing'`, `'progress'`, `'preview'`. The wire-name mapping rewrites
|
||||
* `'start'` → `'execution_start'`, etc., matching the legacy
|
||||
* `api.addEventListener('execution_start', ...)` shape.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension, onMounted, execution } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* execution.on('start', (e) => console.log('run started', e))
|
||||
* execution.on('progress', (e) => console.log('progress', e))
|
||||
* execution.on('end', () => console.log('run done'))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const execution: EventNamespace<ExecutionEventPayloads> = makeNamespace(
|
||||
(evt) => `execution_${evt}`
|
||||
)
|
||||
|
||||
/**
|
||||
* Non-execution backend events + custom-node events.
|
||||
*
|
||||
* Canonical events: `'status'`, `'logs'`, `'reconnected'`, `'feature_flags'`,
|
||||
* `'assets'`. Custom-node events ride this channel with arbitrary string
|
||||
* (e.g. `server.on('rayko.inspline.show', ...)`). Module-augment
|
||||
* `ServerEventPayloads` to type custom events.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension, onMounted, server } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* server.on('reconnected', () => console.log('server back online'))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const server: EventNamespace<ServerEventPayloads> = makeNamespace(
|
||||
(evt) => evt
|
||||
)
|
||||
|
||||
/**
|
||||
* UI shell events.
|
||||
*
|
||||
* Canonical events today: `'notification'`. Future: `'themeChanged'`,
|
||||
* `'panelToggled'`, `'commandInvoked'`. NOT a DI container — see
|
||||
* the bootstrap-hooks design for the "thin event-namespace handle only"
|
||||
* scope-back.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension, onMounted, workbench } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* workbench.on('notification', (e) => console.log('workbench notif', e))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const workbench: EventNamespace<WorkbenchEventPayloads> = makeNamespace(
|
||||
(evt) => `workbench:${evt}`
|
||||
)
|
||||
54
src/extension-api/identifiers.ts
Normal file
54
src/extension-api/identifiers.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Node identity helpers — re-exported from internal `nodeIdentification.ts`.
|
||||
*
|
||||
* `NodeLocatorId` and `NodeExecutionId` are the two stable node identity
|
||||
* primitives in the public API. All extension-facing code that needs to
|
||||
* reference a node across subgraph boundaries or execution runs should use
|
||||
* these rather than raw LiteGraph integer node IDs.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export type { NodeLocatorId, NodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
/**
|
||||
* Node identity round-trip helpers. Create/parse branded `NodeLocatorId` and
|
||||
* `NodeExecutionId` values, or narrow an `unknown` to one with the type
|
||||
* guards. Use these instead of raw string manipulation so future changes to
|
||||
* the identity scheme stay transparent.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* createNodeLocatorId,
|
||||
* parseNodeLocatorId,
|
||||
* isNodeLocatorId,
|
||||
* createNodeExecutionId,
|
||||
* parseNodeExecutionId,
|
||||
* isNodeExecutionId
|
||||
* } from '@comfyorg/extension-api'
|
||||
*
|
||||
* // Construct
|
||||
* const locator = createNodeLocatorId(graphUuid, localId)
|
||||
* const execId = createNodeExecutionId(locator, runTag)
|
||||
*
|
||||
* // Narrow
|
||||
* if (isNodeLocatorId(maybe)) {
|
||||
* const parts = parseNodeLocatorId(maybe)
|
||||
* console.log(parts.graphUuid, parts.localId)
|
||||
* }
|
||||
*
|
||||
* if (isNodeExecutionId(maybe)) {
|
||||
* const parts = parseNodeExecutionId(maybe)
|
||||
* console.log(parts.locator, parts.runTag)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export {
|
||||
isNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
parseNodeLocatorId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
createNodeExecutionId
|
||||
} from '@/types/nodeIdentification'
|
||||
154
src/extension-api/imperatives.ts
Normal file
154
src/extension-api/imperatives.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Inline imperative shell APIs — `toast` + `notify`
|
||||
*.
|
||||
*
|
||||
* Per the ACCEPTED PICK (option (ii) "separate entries" with an
|
||||
* inline-imperative carve-out), `toast` and `notify` are NOT exposed as
|
||||
* `defineToast` / `defineNotify` `defineX` entries — the R1+R2+R3 evidence
|
||||
* showed both are 100% imperative in the ecosystem today (166 toast hits /
|
||||
* 16 repos, zero use as a registration target). Forcing a `defineX` wrapper
|
||||
* would invent a registration concept that doesn't exist in user mental
|
||||
* models.
|
||||
*
|
||||
* Authors call these directly from any setup body or hook closure:
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* defineExtension,
|
||||
* onMounted,
|
||||
* toast
|
||||
* } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* toast.show({ severity: 'info', summary: 'Ready' })
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Both APIs are fire-and-forget; there is no handle to dispose. The toast
|
||||
* component manages its own lifetime (auto-dismiss via the `life` option).
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import type { ToastMessageOptions } from '@/types/extensionTypes'
|
||||
|
||||
/**
|
||||
* Optional shape for {@link notify} — a thinner convenience API over
|
||||
* {@link toast}. `kind` maps onto PrimeVue toast severities; `message` maps
|
||||
* to `summary`; `detail` is optional supplementary text.
|
||||
*
|
||||
* @deprecated Use {@link ToastMessageOptions} via `toast.show(...)`. See
|
||||
* D-notify-toast-consolidation.
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
*/
|
||||
export interface NotifyOptions {
|
||||
/** Severity kind. Default `'info'`. */
|
||||
kind?: 'success' | 'info' | 'warn' | 'error'
|
||||
/** Primary message text. */
|
||||
message: string
|
||||
/** Optional supplementary detail line. */
|
||||
detail?: string
|
||||
/** Auto-dismiss delay in ms. Defaults to PrimeVue's `life`. */
|
||||
life?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast surface — call `toast.show(...)` from any setup body or hook
|
||||
* closure.
|
||||
*
|
||||
* Fire-and-forget. The toast is rendered by the global Toast component
|
||||
* mounted at app root; the call is a no-op if the app is not yet mounted
|
||||
* (the message is silently dropped rather than queued).
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* toast.show({ severity: 'info', summary: 'Saved', life: 2000 })
|
||||
* ```
|
||||
*/
|
||||
export const toast: {
|
||||
/** Show a toast. Severity defaults to `'info'`. */
|
||||
show(opts: ToastMessageOptions): void
|
||||
/** Remove a previously-shown toast (matches by reference). */
|
||||
remove(opts: ToastMessageOptions): void
|
||||
/** Clear every toast currently visible. */
|
||||
removeAll(): void
|
||||
} = {
|
||||
show(opts: ToastMessageOptions): void {
|
||||
void _withToastStore((store) => store.add(opts))
|
||||
},
|
||||
remove(opts: ToastMessageOptions): void {
|
||||
void _withToastStore((store) => store.remove(opts))
|
||||
},
|
||||
removeAll(): void {
|
||||
void _withToastStore((store) => store.removeAll())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience notification API — a thinner wrapper over {@link toast} that
|
||||
* accepts a `{ kind, message, detail }` shape closer to OS notification
|
||||
* vocabulary. Use whichever shape you prefer; they share the same
|
||||
* underlying transport.
|
||||
*
|
||||
* Fire-and-forget.
|
||||
*
|
||||
* @deprecated Use {@link toast.show} — `notify` is a 1:1 wrapper sharing the
|
||||
* same transport. See D-notify-toast-consolidation.
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* // `notify` is deprecated — prefer `toast.show` directly. See
|
||||
* // D-notify-toast-consolidation.
|
||||
* import { toast } from '@comfyorg/extension-api'
|
||||
*
|
||||
* toast.show({
|
||||
* severity: 'error',
|
||||
* summary: 'Workflow failed',
|
||||
* detail: err.message
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function notify(opts: NotifyOptions): void {
|
||||
const severity = opts.kind ?? 'info'
|
||||
toast.show({
|
||||
severity,
|
||||
summary: opts.message,
|
||||
detail: opts.detail,
|
||||
life: opts.life
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the toast store on demand. Lazy-imported so this module is safe
|
||||
* to evaluate at module-init time (before Pinia is ready). Errors are
|
||||
* surfaced loudly in dev and swallowed in prod — toasts are non-critical.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
async function _withToastStore(
|
||||
fn: (store: {
|
||||
add(opts: ToastMessageOptions): void
|
||||
remove(opts: ToastMessageOptions): void
|
||||
removeAll(): void
|
||||
}) => void
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { useToastStore } =
|
||||
await import('@/platform/updates/common/toastStore')
|
||||
const store = useToastStore()
|
||||
fn(store)
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[extension-api] toast call failed:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
184
src/extension-api/index.ts
Normal file
184
src/extension-api/index.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @comfyorg/extension-api — Public Extension API for ComfyUI
|
||||
*
|
||||
* This barrel is the published package entry point. Every export here is
|
||||
* part of the public contract that extension authors depend on.
|
||||
*
|
||||
* Import directly — no dependency on `window.app` at module evaluation time:
|
||||
*
|
||||
* ```ts
|
||||
* import { defineNode, defineExtension } from '@comfyorg/extension-api'
|
||||
* ```
|
||||
*
|
||||
* ## API surface overview
|
||||
*
|
||||
* | Export | Purpose |
|
||||
* |--------|---------|
|
||||
* | `defineNode` | Register a node-scoped extension (the primary entry point) |
|
||||
* | `defineExtension` | Register an app-scoped extension (init, setup, shell UI) |
|
||||
* | `onNodeMounted`, `onNodeRemoved` | Implicit-context lifecycle hooks (call inside nodeCreated) |
|
||||
* | `NodeHandle` | Controlled access to node state and events |
|
||||
* | `WidgetHandle` | Controlled access to widget state and events |
|
||||
* | `WidgetBeforeQueueEvent` | Pre-queue validation event — call `reject(msg)` to cancel |
|
||||
* | `SlotInfo` | Read-only slot snapshot |
|
||||
* | Shell UI types | `SidebarTabExtension`, `BottomPanelExtension`, `CommandManager`, etc. |
|
||||
* | Identity helpers | `NodeLocatorId`, `NodeExecutionId`, parsers, type guards |
|
||||
*
|
||||
* ## Identity (D20)
|
||||
*
|
||||
* Handles expose `id: string` and `equals(other)` — the 90% case never needs
|
||||
* a branded type. The runtime `*EntityId` brands (`NodeEntityId`,
|
||||
* `WidgetEntityId`, `SlotEntityId`) are an internal storage concern and are
|
||||
* NOT re-exported from this barrel. Protocol-boundary identifiers
|
||||
* (`NodeLocatorId` from workflow JSON, `NodeExecutionId` from websocket
|
||||
* frames) remain public because authors **receive** them from event
|
||||
* payloads.
|
||||
*
|
||||
* ## API style
|
||||
*
|
||||
* The public API is **event + getter/setter**, not signals. Vue reactivity is
|
||||
* the internal engine; extension authors never import from Vue or use
|
||||
* `ref`/`computed`/`effect` directly. State is read via methods (`getValue()`,
|
||||
* `getPosition()`), mutated via command-dispatch methods (`setValue()`,
|
||||
* `setPosition()`), and observed via typed event subscriptions (`on('executed', fn)`).
|
||||
* Read-only invariants (set at construction, never change) are exposed as
|
||||
* accessors (`get entityId`, `get type`).
|
||||
*
|
||||
* ## Barrel-file rule exception
|
||||
*
|
||||
* ComfyUI_frontend AGENTS.md rule #19 normally forbids barrel files in `/src`.
|
||||
* This barrel is the **published package entry point** — not an internal
|
||||
* re-export — and is the explicit exception documented in AGENTS.md.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export type {
|
||||
ExtensionOptions,
|
||||
NodeExtensionOptions,
|
||||
WidgetExtensionOptions
|
||||
} from './types'
|
||||
|
||||
// Runtime implementations live in the service; the types above are the
|
||||
// public contract. The barrel re-exports the concrete fns from the service
|
||||
// so `import { defineNode } from '@comfyorg/extension-api'` works
|
||||
// at both typecheck and runtime.
|
||||
//
|
||||
// Note: startExtensionSystem is intentionally NOT exported here — it's an
|
||||
// internal boot function, not part of the extension author API. App wiring
|
||||
// imports it directly from @/services/extension-api-service.
|
||||
export {
|
||||
defineExtension,
|
||||
defineNode,
|
||||
defineWidget
|
||||
} from '@/services/extension-api-service'
|
||||
|
||||
export { onNodeMounted, onNodeRemoved } from './lifecycle'
|
||||
|
||||
// context-scoped Vue-idiomatic lifecycle hooks
|
||||
// usable inside `defineExtension.setup` / `defineSidebarTab.setup` /
|
||||
// `defineBottomPanelTab.setup` bodies.
|
||||
export {
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onActivated,
|
||||
onDeactivated
|
||||
} from './lifecycle'
|
||||
|
||||
// four typed event-namespace handles.
|
||||
// Payload types default to `unknown` and are tightened via D5 module
|
||||
// augmentation in a follow-on PR. Custom-node events ride `server.on(...)`.
|
||||
export { graph, execution, server, workbench } from './events'
|
||||
export type {
|
||||
GraphEventPayloads,
|
||||
ExecutionEventPayloads,
|
||||
ServerEventPayloads,
|
||||
WorkbenchEventPayloads
|
||||
} from './events'
|
||||
|
||||
export type {
|
||||
NodeHandle,
|
||||
SlotInfo,
|
||||
SlotDirection,
|
||||
NodeMode,
|
||||
Point,
|
||||
Size,
|
||||
NodeExecutedEvent,
|
||||
NodeConnectedEvent,
|
||||
NodeDisconnectedEvent,
|
||||
NodePositionChangedEvent,
|
||||
NodeSizeChangedEvent,
|
||||
NodeModeChangedEvent,
|
||||
NodeBeforeSerializeEvent
|
||||
} from './node'
|
||||
|
||||
export type {
|
||||
WidgetHandle,
|
||||
WidgetValue,
|
||||
WidgetOptions,
|
||||
WidgetValueChangeEvent,
|
||||
WidgetOptionChangeEvent,
|
||||
// WidgetPropertyChangeEvent removed per A16 (D-widget-serialization-simplification, wave-9)
|
||||
WidgetBeforeSerializeEvent,
|
||||
WidgetBeforeQueueEvent,
|
||||
// Mount-lifecycle surface per D-widget-converge / Axiom A12 ────────────
|
||||
WidgetCleanup,
|
||||
WidgetMountContext,
|
||||
WidgetMountFn
|
||||
} from './widget'
|
||||
|
||||
export type { Handler, AsyncHandler, Unsubscribe } from './events'
|
||||
|
||||
// Per D19 — VueExtension and CustomExtension are discriminated-union
|
||||
// ingredients of SidebarTabExtension / BottomPanelExtension and are NOT
|
||||
// part of the public surface.
|
||||
//
|
||||
// Note: ExtensionManager + CommandManager are
|
||||
// DROPPED from the public surface — the v2 model uses per-surface defineX
|
||||
// entries (defineSidebarTab, defineCommand, …) each returning a disposable,
|
||||
// not a centralized umbrella handle. Internal callers continue importing
|
||||
// the legacy umbrella types directly from '@/types/extensionTypes'.
|
||||
export type {
|
||||
// Pre-existing
|
||||
SidebarTabExtension,
|
||||
BottomPanelExtension,
|
||||
ToastMessageOptions,
|
||||
ToastManager,
|
||||
// Shell-UI arg types
|
||||
CommandDefinition,
|
||||
HotkeyExtension,
|
||||
AboutBadgeExtension,
|
||||
SettingDefinition,
|
||||
ToolbarButtonExtension
|
||||
} from './shell'
|
||||
|
||||
// per-surface defineX entries. Each
|
||||
// returns a DisposableHandle; carve-out: toast + notify remain inline
|
||||
// imperative (exported below from ./imperatives), NOT as defineX wrappers.
|
||||
export {
|
||||
defineSidebarTab,
|
||||
defineBottomPanelTab,
|
||||
defineCommand,
|
||||
defineHotkey,
|
||||
defineSetting,
|
||||
defineAboutBadge,
|
||||
defineToolbarButton
|
||||
} from './registrations'
|
||||
export type { DisposableHandle } from './registrations'
|
||||
|
||||
// inline imperative carve-out. Fire-and-forget;
|
||||
// no defineX wrapper, no DisposableHandle. Call from any setup() body or
|
||||
// hook closure.
|
||||
export { toast, notify } from './imperatives'
|
||||
export type { NotifyOptions } from './imperatives'
|
||||
|
||||
export type { NodeLocatorId, NodeExecutionId } from './identifiers'
|
||||
export {
|
||||
isNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
parseNodeLocatorId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
createNodeExecutionId
|
||||
} from './identifiers'
|
||||
293
src/extension-api/lifecycle.ts
Normal file
293
src/extension-api/lifecycle.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Extension lifecycle — `defineExtension`, `defineNode`, and
|
||||
* the implicit-context lifecycle hooks (`onNodeMounted`, `onNodeRemoved`).
|
||||
*
|
||||
* Key behaviors:
|
||||
* - Hook firing order = registration order with lexicographic tie-break
|
||||
* on extension name.
|
||||
* - `setup()` is synchronous. `async setup` throws in dev, emits
|
||||
* console.error in prod.
|
||||
* - The object returned by `setup()` is wrapped with `proxyRefs()` so
|
||||
* callers read `entity.extensionState['my-ext'].count` without `.value`.
|
||||
*
|
||||
* Module-level import only. Extensions do NOT depend on `window.app` being
|
||||
* initialized at registration time.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
//
|
||||
// The option-type contracts live in ./types so that both this module and the
|
||||
// runtime service (`@/services/extension-api-service`) can depend on them
|
||||
// without forming a circular import. This module re-exports them so the
|
||||
// existing public path `@/extension-api/lifecycle` keeps working.
|
||||
|
||||
/**
|
||||
* @publicAPI
|
||||
* Back-compat re-exports of the extension option contracts. Prefer importing
|
||||
* from `@comfyorg/extension-api` (or `@/extension-api`); the
|
||||
* `@/extension-api/lifecycle` path is preserved for downstream code that
|
||||
* imported these types from the original module.
|
||||
*/
|
||||
export type {
|
||||
NodeExtensionOptions,
|
||||
ExtensionOptions,
|
||||
WidgetExtensionOptions
|
||||
} from './types'
|
||||
|
||||
import type {
|
||||
NodeExtensionOptions,
|
||||
ExtensionOptions,
|
||||
WidgetExtensionOptions
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* Register a node extension. The runtime calls `nodeCreated` or
|
||||
* `loadedGraphNode` once per node entity matching `nodeTypes`.
|
||||
*
|
||||
* This is the primary entry point for extensions that interact with nodes and
|
||||
* widgets. Import directly from `@comfyorg/extension-api` — no dependency on
|
||||
* `window.app` at module evaluation time.
|
||||
*
|
||||
* Hook firing order across multiple extensions on the same entity follows
|
||||
* extension registration order with a lexicographic tie-break on `name`.
|
||||
*
|
||||
* @publicAPI
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineNode } from '@comfyorg/extension-api'
|
||||
*
|
||||
* // Per AXIOMS.md §A1, nodes cannot enumerate widgets. To attach
|
||||
* // per-widget behavior, register a widget type via `defineWidget` and
|
||||
* // use the mount context's `ctx.widget` handle. This example reacts to
|
||||
* // node-level execution only.
|
||||
* export default defineNode({
|
||||
* name: 'my-org.executed-logger',
|
||||
* nodeTypes: ['KSampler'],
|
||||
*
|
||||
* nodeCreated(node) {
|
||||
* node.on('executed', (e) => {
|
||||
* console.log('node executed:', e.output)
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export declare function defineNode(
|
||||
options: NodeExtensionOptions
|
||||
): NodeExtensionOptions
|
||||
|
||||
/**
|
||||
* Register an extension for app-wide lifecycle and shell UI contributions.
|
||||
*
|
||||
* Use `defineNode` for node/widget interactions. Use this for
|
||||
* `init`, `setup`, sidebar tabs, commands, and other app-level concerns.
|
||||
*
|
||||
* @publicAPI
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineExtension({
|
||||
* name: 'my-org.my-extension',
|
||||
* setup() {
|
||||
* console.log('Extension ready')
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export declare function defineExtension(
|
||||
options: ExtensionOptions
|
||||
): ExtensionOptions
|
||||
|
||||
/**
|
||||
* Register a custom widget type. Called once at module load time to declare
|
||||
* a new widget kind.
|
||||
*
|
||||
* @stability experimental
|
||||
* @publicAPI
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineWidget } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineWidget({
|
||||
* name: 'my-org.color-picker',
|
||||
* type: 'COLOR_PICKER'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export declare function defineWidget(
|
||||
options: WidgetExtensionOptions
|
||||
): WidgetExtensionOptions
|
||||
|
||||
/**
|
||||
* ## Implicit-Context Lifecycle Hooks
|
||||
*
|
||||
* `onNodeMounted` and `onNodeRemoved` use Vue-style implicit context to
|
||||
* associate callbacks with the current node's cleanup scope. This pattern
|
||||
* provides automatic cleanup without manual unsubscribe bookkeeping.
|
||||
*
|
||||
* ### How it works
|
||||
*
|
||||
* 1. The runtime sets a global scope slot before calling `nodeCreated()`
|
||||
* 2. Lifecycle hooks read this slot to register callbacks in the node's scope
|
||||
* 3. When the node is removed, the scope auto-disposes all registered callbacks
|
||||
*
|
||||
* ### Why synchronous-only?
|
||||
*
|
||||
* These hooks **must** be called synchronously inside `nodeCreated` or
|
||||
* `loadedGraphNode`. After an `await`, the call stack has unwound and the
|
||||
* implicit scope context is gone — the same constraint as Vue's `onMounted`.
|
||||
*
|
||||
* ```ts
|
||||
* // ✅ CORRECT — synchronous call
|
||||
* nodeCreated(node) {
|
||||
* onNodeMounted(() => console.log('mounted'))
|
||||
* }
|
||||
*
|
||||
* // ❌ WRONG — after await, scope context is lost
|
||||
* async nodeCreated(node) {
|
||||
* await fetch('/api')
|
||||
* onNodeMounted(() => {}) // Throws in dev, silent no-op in prod
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Benefits
|
||||
*
|
||||
* - **Automatic cleanup**: no manual `unsubscribe()` calls needed
|
||||
* - **Memory-safe**: callbacks are garbage-collected with the node
|
||||
* - **Familiar pattern**: mirrors Vue Composition API (`onMounted`, `onUnmounted`)
|
||||
*
|
||||
* @see {@link onNodeMounted} — fires after node is fully mounted
|
||||
* @see {@link onNodeRemoved} — fires before node cleanup (not on subgraph moves)
|
||||
*/
|
||||
|
||||
/**
|
||||
* ## Context-Scoped Bootstrap Hooks
|
||||
*
|
||||
* In addition to the node-level `onNodeMounted` / `onNodeRemoved` hooks above,
|
||||
* the v2 API exposes Vue-idiomatic context-scoped hooks for
|
||||
* `defineExtension.setup` / `defineSidebarTab.setup` / `defineBottomPanelTab.setup`
|
||||
* bodies:
|
||||
*
|
||||
* - `onBeforeMount` — before the surrounding instance is mounted
|
||||
* - `onMounted` — after the surrounding instance is mounted
|
||||
* - `onUnmounted` — when the surrounding instance is unmounted
|
||||
* - `onActivated` — when a sidebar tab / bottom panel is shown (tab/panel only)
|
||||
* - `onDeactivated` — when a sidebar tab / bottom panel is hidden (tab/panel only)
|
||||
*
|
||||
* Like Vue's `onMounted`, these must be called synchronously inside the
|
||||
* surrounding `setup()` body. Calling them after an `await` (or outside any
|
||||
* setup context) throws in development and silently no-ops in production.
|
||||
*
|
||||
* See {@link onMounted} for full usage examples.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* defineSidebarTab,
|
||||
* onMounted,
|
||||
* onUnmounted,
|
||||
* onActivated,
|
||||
* onDeactivated
|
||||
* } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineSidebarTab({
|
||||
* id: 'my-tab',
|
||||
* title: 'My Tab',
|
||||
* type: 'vue',
|
||||
* component: MyTab,
|
||||
* setup() {
|
||||
* onMounted(() => console.log('tab mounted'))
|
||||
* onActivated(() => console.log('tab shown'))
|
||||
* onDeactivated(() => console.log('tab hidden'))
|
||||
* onUnmounted(() => console.log('tab unmounted'))
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export {
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onActivated,
|
||||
onDeactivated
|
||||
} from '@/services/extension-api-service'
|
||||
|
||||
export {
|
||||
/**
|
||||
* Register a callback to fire when the node is fully mounted to the graph.
|
||||
*
|
||||
* "Mounted" means: the reactive mount watcher has run, the node's scope is
|
||||
* active, and any `setup()` return value has been captured. Safe to access
|
||||
* DOM widgets, canvas elements, and other post-mount resources.
|
||||
*
|
||||
* **Must be called synchronously** inside `nodeCreated` or `loadedGraphNode`.
|
||||
* Calling after an `await` throws in development and silently no-ops in
|
||||
* production (see module docs for rationale).
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineNode, onNodeMounted } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineNode({
|
||||
* name: 'my-ext',
|
||||
* nodeTypes: ['MyNode'],
|
||||
*
|
||||
* nodeCreated(node) {
|
||||
* // Register mount callback synchronously
|
||||
* onNodeMounted(() => {
|
||||
* console.log('Node fully mounted, DOM ready')
|
||||
* // Safe to query DOM widgets, measure sizes, etc.
|
||||
* })
|
||||
*
|
||||
* // Can register multiple callbacks
|
||||
* onNodeMounted(() => {
|
||||
* node.setSize([300, 200]) // Resize after mount
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
onNodeMounted,
|
||||
/**
|
||||
* Register a callback to fire when the node is removed from the graph.
|
||||
*
|
||||
* Use for cleanup: close connections, abort fetches, release resources.
|
||||
* Does NOT fire on subgraph promotion (which is a DOM move, not removal) —
|
||||
* the node's entity ID is preserved across promotion.
|
||||
*
|
||||
* Replaces the v1 `nodeType.prototype.onRemoved` patching pattern.
|
||||
*
|
||||
* **Must be called synchronously** inside `nodeCreated` or `loadedGraphNode`.
|
||||
* Calling after an `await` throws in development and silently no-ops in
|
||||
* production (see module docs for rationale).
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineNode, onNodeRemoved } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineNode({
|
||||
* name: 'my-ext',
|
||||
* nodeTypes: ['MyNode'],
|
||||
*
|
||||
* nodeCreated(node) {
|
||||
* const controller = new AbortController()
|
||||
*
|
||||
* // Start a long-running fetch
|
||||
* fetch('/api/stream', { signal: controller.signal })
|
||||
* .then(res => processStream(res))
|
||||
*
|
||||
* // Clean up when node is deleted
|
||||
* onNodeRemoved(() => {
|
||||
* controller.abort()
|
||||
* console.log('Cleanup complete')
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
onNodeRemoved
|
||||
} from '@/services/extension-api-service'
|
||||
408
src/extension-api/node.ts
Normal file
408
src/extension-api/node.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* NodeHandle — the controlled surface for node access in v2 extensions.
|
||||
*
|
||||
* Reads query ECS World components directly. Writes dispatch commands
|
||||
* (undo-able, serializable, validatable). Events are backed by Vue
|
||||
* reactivity watching World component changes.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import type { AsyncHandler, Handler, Unsubscribe } from './events'
|
||||
|
||||
import type { NodeEntityId } from '@/world/entityIds'
|
||||
/**
|
||||
* Branded entity ID for nodes. Prevents mixing node IDs with widget IDs
|
||||
* at compile time. Re-exported from the world layer so the entire codebase
|
||||
* shares a single brand. The underlying value is `string` in Phase A
|
||||
* (e.g. `node:<graphUuid>:<localId>`).
|
||||
*
|
||||
* @internal Per D20 — extension authors use `node.id: string` and
|
||||
* `node.equals(other)`. The branded type is reserved for internal package
|
||||
* modules and is intentionally absent from the published barrel.
|
||||
*/
|
||||
export type { NodeEntityId }
|
||||
|
||||
/**
|
||||
* A 2D point as `[x, y]`.
|
||||
*
|
||||
* **Immutable tuple.** Attempts to
|
||||
* mutate via `node.getPosition()[0] = X` raise a TypeScript error. Use
|
||||
* {@link NodeHandle.setPosition} to move the node.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import type { Point } from '@comfyorg/extension-api'
|
||||
*
|
||||
* // Per-pixel mouse coordinate from a canvas event
|
||||
* const cursor: Point = [event.canvasX, event.canvasY]
|
||||
* ```
|
||||
*/
|
||||
export type Point = readonly [x: number, y: number]
|
||||
|
||||
/**
|
||||
* A 2D size as `[width, height]`.
|
||||
*
|
||||
* **Immutable tuple.** Attempts to
|
||||
* mutate via `node.getSize()[0] = X` raise a TypeScript error. Use
|
||||
* {@link NodeHandle.setSize} to resize the node.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import type { Size } from '@comfyorg/extension-api'
|
||||
*
|
||||
* const target: Size = [320, 240]
|
||||
* ```
|
||||
*/
|
||||
export type Size = readonly [width: number, height: number]
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: nodeMode has "egregious" use patterns.
|
||||
// export type NodeMode = 'always' | 'never' | 'bypass' | 'once' | 'onTrigger'
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Slot/connection hooks deferred.
|
||||
// export type SlotDirection = 'input' | 'output'
|
||||
// export interface SlotInfo { ... }
|
||||
// export type SlotEntityId = string & { readonly __brand: 'SlotEntityId' }
|
||||
|
||||
/**
|
||||
* Payload for `node.on('executed', handler)`.
|
||||
*
|
||||
* Replaces the v1 `nodeType.prototype.onExecuted` patching pattern.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* node.on('executed', (e) => {
|
||||
* const text = e.output['text'] as string[]
|
||||
* previewWidget.setValue(text.join('\n'))
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface NodeExecutedEvent {
|
||||
/** The backend execution output for this node. Shape varies by node type. */
|
||||
readonly output: Record<string, unknown>
|
||||
}
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Slot/connection hooks deferred.
|
||||
// export interface NodeConnectedEvent { slot: SlotInfo; remote: SlotInfo }
|
||||
// export interface NodeDisconnectedEvent { slot: SlotInfo }
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Spatial events deferred.
|
||||
// export interface NodePositionChangedEvent { pos: Point }
|
||||
// export interface NodeSizeChangedEvent { size: Size }
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: nodeMode has "egregious" use patterns.
|
||||
// export interface NodeModeChangedEvent { mode: NodeMode }
|
||||
|
||||
/**
|
||||
* Payload for `node.on('beforeSerialize', handler)`.
|
||||
*
|
||||
* @deprecated Node-level serialization control will be removed in v1.0.
|
||||
* Use widget-level `widget.on('beforeSerialize')` instead. Store extension
|
||||
* state in widgets rather than arbitrary node fields.
|
||||
*
|
||||
* **Why widget-level is better:**
|
||||
* - Widget values are visible at predictable locations in workflow JSON
|
||||
* - Cleaner separation between framework and extension concerns
|
||||
* - Widget serialization hooks support async operations
|
||||
*
|
||||
* See ADR-0010 for full migration guidance.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
export interface NodeBeforeSerializeEvent {
|
||||
/** Which serialization path triggered this. */
|
||||
readonly context: 'workflow' | 'prompt' | 'clone' | 'subgraph-promote'
|
||||
|
||||
/**
|
||||
* The mutable serialized node object. Mutate in place to append fields.
|
||||
* Type intentionally loose — the exact shape is `ISerialisedNode`.
|
||||
*/
|
||||
readonly data: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Replace the serialized object by providing a transform function.
|
||||
* `fn` receives the current `data` and should return the replacement.
|
||||
* Calling this multiple times chains: each call's `fn` receives the
|
||||
* previous call's output.
|
||||
*/
|
||||
replace(fn: (orig: Record<string, unknown>) => Record<string, unknown>): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Controlled surface for node access. Reads query the ECS World; writes
|
||||
* dispatch commands. Events are Vue-reactive watches on World components.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineNode } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineNode({
|
||||
* name: 'my-size-enforcer',
|
||||
* nodeTypes: ['MyCustomNode'],
|
||||
*
|
||||
* nodeCreated(node) {
|
||||
* const [w, h] = node.getSize()
|
||||
* node.setSize([Math.max(w, 300), Math.max(h, 200)])
|
||||
*
|
||||
* node.on('executed', (e) => {
|
||||
* console.log('output:', e.output)
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface NodeHandle {
|
||||
/**
|
||||
* Opaque identifier for this node. Stable for the lifetime of the node
|
||||
* entity. Treat as a string token: do not parse, slice, or compare its
|
||||
* internal structure. Use {@link NodeHandle.equals} to compare with
|
||||
* another handle.
|
||||
*
|
||||
* @remarks
|
||||
* The underlying value is a branded `NodeEntityId` at runtime
|
||||
* but is narrowed to `string` on the public surface so authors never
|
||||
* need to import a brand to type a local variable.
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
* Returns `true` if `other` represents the same node entity as this one.
|
||||
* Equivalent to `this.id === other.id` but the canonical comparator —
|
||||
* prefer `equals` over manual string comparison so future changes to the
|
||||
* identity scheme remain transparent.
|
||||
*/
|
||||
equals(other: NodeHandle): boolean
|
||||
|
||||
/**
|
||||
* The LiteGraph node type string (e.g. `'KSampler'`).
|
||||
* Read-only invariant: set at construction, never changes.
|
||||
*
|
||||
*/
|
||||
readonly type: string
|
||||
|
||||
/**
|
||||
* The ComfyUI backend class name (e.g. `'KSampler'`).
|
||||
* Equal to `type` for most nodes; differs for reroute/virtual nodes.
|
||||
* Read-only invariant.
|
||||
*
|
||||
*/
|
||||
readonly comfyClass: string
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Deferred pending A13 coord-space stabilization.
|
||||
// getPosition(): Point
|
||||
// setPosition(pos: Point): void
|
||||
// getSize(): Size
|
||||
// setSize(size: Size): void
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Uncertain use case.
|
||||
// getTitle(): string
|
||||
// setTitle(title: string): void
|
||||
// isSelected(): boolean
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: nodeMode has "egregious" use patterns.
|
||||
// getMode(): NodeMode
|
||||
// setMode(mode: NodeMode): void
|
||||
|
||||
/**
|
||||
* Returns a per-node-instance property by key.
|
||||
*
|
||||
* In v2, prefer routing persistent state through widget values or
|
||||
* `beforeSerialize` events. `node.properties` is kept as a migration shim
|
||||
* for v1 extensions that used it for per-instance widget config (e.g. min/max).
|
||||
*
|
||||
*/
|
||||
getProperty<T = unknown>(key: string): T | undefined
|
||||
|
||||
/**
|
||||
* Returns a copy of all per-node-instance properties.
|
||||
*
|
||||
*/
|
||||
getProperties(): Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Sets a per-node-instance property. Dispatches a `SetNodeProperty` command.
|
||||
*
|
||||
* In v2, prefer `widget.setOption(key, value)` for widget-scoped per-instance
|
||||
* config (it persists to the `widget_options` sidecar in the workflow JSON).
|
||||
*
|
||||
*/
|
||||
setProperty(key: string, value: unknown): void
|
||||
|
||||
// COMMENTED OUT per AXIOMS.md A1 + A2:
|
||||
// Nodes cannot reference or enumerate their widgets. Bilateral (node→widget)
|
||||
// direction is closed; the widget→node direction (`widget.parentNode`)
|
||||
// remains the sole channel. Extensions needing per-widget coordination
|
||||
// register via `defineWidget({mount, ...})` and share state through Vue
|
||||
// `provide()` / `inject()` or the World event bus. Restoration criteria
|
||||
// in AXIOMS.md §A14.
|
||||
//
|
||||
// /**
|
||||
// * Returns a `WidgetHandle` for the named widget, or `undefined` if no such
|
||||
// * widget exists on this node.
|
||||
// *
|
||||
// * @example
|
||||
// * ```ts
|
||||
// * const steps = node.getWidget('steps')
|
||||
// * if (steps) steps.setValue(20)
|
||||
// * ```
|
||||
// */
|
||||
// getWidget(name: string): WidgetHandle | undefined
|
||||
//
|
||||
// /**
|
||||
// * Returns all widgets on this node as `WidgetHandle` instances.
|
||||
// *
|
||||
// * **Immutable view.** The returned
|
||||
// * array cannot be mutated (`push`, `splice`, `length =`, index assignment
|
||||
// * all raise TS errors). Each `WidgetHandle` is also surface-frozen — use
|
||||
// * the `WidgetHandle` setter methods (`setValue`, `setHidden`, etc.) to
|
||||
// * mutate widget state.
|
||||
// *
|
||||
// * @example
|
||||
// * ```ts
|
||||
// * // ❌ TS-ERR — readonly array; v1 patterns no longer compile
|
||||
// * node.getWidgets().push(newWidget)
|
||||
// * node.getWidgets()[0] = newWidget
|
||||
// *
|
||||
// * // ✅ Iterate / read freely
|
||||
// * for (const w of node.getWidgets()) console.log(w.name)
|
||||
// * const labels = node.getWidgets().map((w) => w.label)
|
||||
// * ```
|
||||
// */
|
||||
// getWidgets(): ReadonlyArray<Readonly<WidgetHandle>>
|
||||
|
||||
// REMOVED per AXIOMS.md A14: Widgets are defined in Python node schema,
|
||||
// not created at frontend runtime. Node->widget mutation violates A1.
|
||||
// addWidget(type, name, defaultValue, options?): WidgetHandle
|
||||
|
||||
// NOTE: `addDOMWidget(opts)` was removed per D-widget-converge / Axiom A12.
|
||||
// Custom DOM widgets are now registered via `defineWidget({type, mount})`
|
||||
// and instantiated through the same `addWidget(type, name, …)` call as
|
||||
// every other widget. The runtime invokes the registered `mount(host, ctx)`
|
||||
// hook against a per-widget host `<div>` it owns. See `WidgetMountFn` and
|
||||
// `WidgetMountContext` in `./widget` for the lifecycle contract.
|
||||
|
||||
/**
|
||||
* Returns all input slots on this node.
|
||||
*
|
||||
* **Immutable view.** The returned
|
||||
* array and each slot are `Readonly` — `node.getInputs().push(...)`,
|
||||
* `node.getInputs()[i] = X`, and `node.getInputs()[i].name = "x"` all raise
|
||||
* TypeScript errors at compile time. Per-slot mutators (`setInputName`,
|
||||
* `replaceInput`, bulk field setters) are tracked under
|
||||
* *
|
||||
* @example
|
||||
* ```ts
|
||||
* // ❌ TS-ERR — readonly array; v1 patterns no longer compile
|
||||
* node.getInputs().push({ name: 'x', type: 'INT' })
|
||||
* node.getInputs()[0].name = 'renamed'
|
||||
*
|
||||
* // ✅ Read / iterate freely
|
||||
* const types = node.getInputs().map((s) => s.type)
|
||||
* ```
|
||||
*/
|
||||
getInputs(): ReadonlyArray<Readonly<SlotInfo>>
|
||||
|
||||
/**
|
||||
* Returns all output slots on this node.
|
||||
*
|
||||
* **Immutable view.** Same
|
||||
* read-only semantics as {@link NodeHandle.getInputs}. Per-slot mutators
|
||||
* tracked separately.
|
||||
*/
|
||||
getOutputs(): ReadonlyArray<Readonly<SlotInfo>>
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link NodeHandle.getInputs} instead. Renamed to align
|
||||
* with the `getX()` accessor convention.
|
||||
* Will be removed in v1.0.
|
||||
*/
|
||||
inputs(): ReadonlyArray<Readonly<SlotInfo>>
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link NodeHandle.getOutputs} instead. Renamed to align
|
||||
* with the `getX()` accessor convention.
|
||||
* Will be removed in v1.0.
|
||||
*/
|
||||
outputs(): ReadonlyArray<Readonly<SlotInfo>>
|
||||
|
||||
/**
|
||||
* Subscribe to node removal (graph deletion, not subgraph promotion).
|
||||
*
|
||||
* Replaces the v1 `nodeType.prototype.onRemoved` patching pattern.
|
||||
* Does NOT fire on subgraph promotion — the node's entity ID is preserved
|
||||
* across promotion.
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
*/
|
||||
on(event: 'removed', handler: Handler<void>): Unsubscribe
|
||||
|
||||
/**
|
||||
* Subscribe to backend execution completion for this node.
|
||||
*
|
||||
* Replaces the v1 `nodeType.prototype.onExecuted` patching pattern (the
|
||||
* most widely used anti-pattern per R4-P3; 5+ confirmed repos).
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
*/
|
||||
on(event: 'executed', handler: Handler<NodeExecutedEvent>): Unsubscribe
|
||||
|
||||
/**
|
||||
* Subscribe to workflow hydration (node loaded from a saved workflow).
|
||||
*
|
||||
* Replaces the v1 `nodeType.prototype.onConfigure` / `loadedGraphNode`
|
||||
* patterns. Fires after all widget values are restored from the workflow JSON.
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
*/
|
||||
on(event: 'configured', handler: Handler<void>): Unsubscribe
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Slot/connection hooks deferred.
|
||||
// on(event: 'connected', handler: Handler<NodeConnectedEvent>): Unsubscribe
|
||||
// on(event: 'disconnected', handler: Handler<NodeDisconnectedEvent>): Unsubscribe
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Spatial events deferred pending A13.
|
||||
// on(event: 'positionChanged', handler: Handler<NodePositionChangedEvent>): Unsubscribe
|
||||
// on(event: 'sizeChanged', handler: Handler<NodeSizeChangedEvent>): Unsubscribe
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: nodeMode has "egregious" use patterns.
|
||||
// on(event: 'modeChanged', handler: Handler<NodeModeChangedEvent>): Unsubscribe
|
||||
|
||||
/**
|
||||
* Subscribe to node serialization. Async-capable.
|
||||
*
|
||||
* @deprecated Node-level serialization control will be removed in v1.0.
|
||||
* Use widget-level `widget.on('beforeSerialize')` instead — store extension
|
||||
* state in widgets rather than arbitrary node fields. See ADR-0010.
|
||||
*
|
||||
* **Migration example:**
|
||||
* ```ts
|
||||
* // BEFORE (deprecated — node-level)
|
||||
* node.on('beforeSerialize', (e) => {
|
||||
* e.data['my_extension_state'] = computeState()
|
||||
* })
|
||||
*
|
||||
* // AFTER (recommended — widget-level, schema-declared)
|
||||
* // Declare `_my_state` in the Python node's INPUT_TYPES as a hidden
|
||||
* // STRING input. Then attach the serialization transform inside
|
||||
* // `defineWidget({mount})` — `ctx.widget` is the only legal handle:
|
||||
* import { defineWidget } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineWidget({
|
||||
* name: 'my-org.state-shim',
|
||||
* type: 'STRING',
|
||||
* mount(_host, ctx) {
|
||||
* ctx.widget.on('beforeSerialize', (e) => {
|
||||
* e.setSerializedValue(JSON.stringify(computeState()))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
* @stability experimental
|
||||
*/
|
||||
on(
|
||||
event: 'beforeSerialize',
|
||||
handler: AsyncHandler<NodeBeforeSerializeEvent>
|
||||
): Unsubscribe
|
||||
}
|
||||
536
src/extension-api/registrations.ts
Normal file
536
src/extension-api/registrations.ts
Normal file
@@ -0,0 +1,536 @@
|
||||
/**
|
||||
* Per-surface shell UI registration entry points
|
||||
*.
|
||||
*
|
||||
* Each `defineX` function in this module is the v2 replacement for one slot
|
||||
* of the v1 `app.registerExtension({ commands, keybindings, settings, … })`
|
||||
* mega-call. Per the ACCEPTED PICK (option (ii) "separate entries" with an
|
||||
* inline-imperative carve-out for `toast` / `notify`), each surface gets its
|
||||
* own typed, per-import-testable, independently disposable entry point.
|
||||
*
|
||||
* ## Dispose contract
|
||||
*
|
||||
* Every `defineX` returns a `{ dispose(): void }` handle:
|
||||
*
|
||||
* - **Idempotent**: calling `dispose()` more than once is safe and is a no-op
|
||||
* after the first call.
|
||||
* - **Synchronous teardown**: the actual unregister-from-store happens
|
||||
* synchronously inside `dispose()` (no async cleanup).
|
||||
* - **Ordering on multi-surface extensions**: dispose handles are
|
||||
* independent. If you hold handles A, B, C from three `defineX` calls and
|
||||
* call `A.dispose()`, B and C are unaffected. Callers that need ordered
|
||||
* teardown (e.g. drop the hotkey before its command) should sequence
|
||||
* `dispose()` calls explicitly.
|
||||
* - **Pre-mount dispose**: if `dispose()` is called before the runtime has
|
||||
* started the extension system (and thus before the spec has been wired
|
||||
* into the underlying store), the spec is removed from the pending queue
|
||||
* and never reaches the store.
|
||||
*
|
||||
* ## Lazy mount + queue-then-flush pattern
|
||||
*
|
||||
* Each `defineX` is safe to call at module-evaluation time — i.e. before
|
||||
* Pinia is initialized and before any store is reachable. The spec is
|
||||
* pushed onto a per-surface pending queue; the runtime flushes the queue
|
||||
* via {@link _flushShellRegistrations} once `startExtensionSystem()` runs
|
||||
* during app bootstrap. After flush, subsequent `defineX` calls mount
|
||||
* immediately.
|
||||
*
|
||||
* This mirrors the existing `defineExtension` / `defineNode` / `defineWidget`
|
||||
* pattern in `@/services/extension-api-service` (they also push onto
|
||||
* module-level arrays that bootstrap drains later).
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import type {
|
||||
AboutBadgeExtension,
|
||||
BottomPanelExtension,
|
||||
CommandDefinition,
|
||||
HotkeyExtension,
|
||||
SettingDefinition,
|
||||
SidebarTabExtension,
|
||||
ToolbarButtonExtension
|
||||
} from '@/types/extensionTypes'
|
||||
|
||||
/**
|
||||
* Handle returned by every `defineX`. Call `dispose()` to remove the
|
||||
* registration. Idempotent and synchronous — see module-level "Dispose
|
||||
* contract" notes.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineSidebarTab, type DisposableHandle } from '@comfyorg/extension-api'
|
||||
*
|
||||
* const handle: DisposableHandle = defineSidebarTab({
|
||||
* id: 'my-tab',
|
||||
* title: 'My Tab',
|
||||
* type: 'vue',
|
||||
* component: MyTab
|
||||
* })
|
||||
*
|
||||
* // Later: tear down
|
||||
* handle.dispose()
|
||||
* ```
|
||||
*/
|
||||
export interface DisposableHandle {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
// Internal mount-queue infrastructure
|
||||
/**
|
||||
* Registration side effect: registers something into a store and returns a
|
||||
* cleanup. May be async (most registrations dynamic-import their store at
|
||||
* runtime to defer Pinia coupling to bootstrap time).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
type CleanupFn = () => void
|
||||
type Mounter = () => CleanupFn | void | Promise<CleanupFn | void>
|
||||
|
||||
interface PendingEntry {
|
||||
mount: Mounter
|
||||
cleanup: CleanupFn | null
|
||||
disposed: boolean
|
||||
}
|
||||
|
||||
const pendingEntries: PendingEntry[] = []
|
||||
let _systemStarted = false
|
||||
|
||||
/**
|
||||
* Run a mounter and capture its cleanup (sync or async). Async failures are
|
||||
* surfaced loudly in dev, swallowed in prod.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function runMounter(entry: PendingEntry): void {
|
||||
try {
|
||||
const result = entry.mount()
|
||||
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
||||
void (result as Promise<CleanupFn | void>).then(
|
||||
(cleanup) => {
|
||||
if (entry.disposed) {
|
||||
// Already disposed before async mount resolved — invoke the cleanup
|
||||
// immediately to avoid a leak.
|
||||
if (cleanup) {
|
||||
try {
|
||||
cleanup()
|
||||
} catch {
|
||||
/* swallow */
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
entry.cleanup = cleanup ?? null
|
||||
},
|
||||
(err) => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[extension-api] defineX mount failed:', err)
|
||||
} else {
|
||||
console.warn('[extension-api] defineX mount failed:', err)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
entry.cleanup = (result as CleanupFn | void) ?? null
|
||||
}
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[extension-api] defineX mount failed:', err)
|
||||
} else {
|
||||
console.warn('[extension-api] defineX mount failed:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a mount-fn onto the pending queue, returning a disposable handle.
|
||||
* If the system is already started, mount immediately.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function register(mount: Mounter): DisposableHandle {
|
||||
const entry: PendingEntry = { mount, cleanup: null, disposed: false }
|
||||
pendingEntries.push(entry)
|
||||
|
||||
if (_systemStarted) {
|
||||
runMounter(entry)
|
||||
}
|
||||
|
||||
return {
|
||||
dispose() {
|
||||
if (entry.disposed) return
|
||||
entry.disposed = true
|
||||
if (entry.cleanup) {
|
||||
try {
|
||||
entry.cleanup()
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[extension-api] defineX dispose failed:', err)
|
||||
}
|
||||
}
|
||||
entry.cleanup = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush all pending shell-UI registrations. Called by
|
||||
* `startExtensionSystem()` once Pinia and the underlying stores are ready.
|
||||
*
|
||||
* Idempotent: subsequent calls re-flush only entries that have not yet
|
||||
* been mounted (allowing `defineX` calls made after start to mount lazily
|
||||
* via the `_systemStarted` flag in {@link register}).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function _flushShellRegistrations(): void {
|
||||
_systemStarted = true
|
||||
for (const entry of pendingEntries) {
|
||||
if (entry.disposed || entry.cleanup) continue
|
||||
runMounter(entry)
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal Test-only: drop all pending registrations and reset state. */
|
||||
export function _clearShellRegistrationsForTesting(): void {
|
||||
for (const entry of pendingEntries) {
|
||||
if (entry.cleanup) {
|
||||
try {
|
||||
entry.cleanup()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingEntries.length = 0
|
||||
_systemStarted = false
|
||||
}
|
||||
|
||||
// Public defineX entry points
|
||||
/**
|
||||
* Register a sidebar tab. Returns a {@link DisposableHandle} — call
|
||||
* `handle.dispose()` to remove the tab.
|
||||
*
|
||||
* The tab spec is a thin POJO (see {@link SidebarTabExtension}); the runtime
|
||||
* mounts it via the workspace sidebar-tab store at app bootstrap time. May
|
||||
* be called at module-evaluation time; mount is deferred until the system
|
||||
* starts.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineSidebarTab } from '@comfyorg/extension-api'
|
||||
*
|
||||
* const tab = defineSidebarTab({
|
||||
* id: 'my-tab',
|
||||
* title: 'My Tab',
|
||||
* type: 'vue',
|
||||
* component: MyTabComponent
|
||||
* })
|
||||
*
|
||||
* // Later: remove the tab
|
||||
* tab.dispose()
|
||||
* ```
|
||||
*/
|
||||
export function defineSidebarTab(opts: SidebarTabExtension): DisposableHandle {
|
||||
return register(async () => {
|
||||
const { useSidebarTabStore } =
|
||||
await import('@/stores/workspace/sidebarTabStore')
|
||||
const store = useSidebarTabStore()
|
||||
store.registerSidebarTab(opts)
|
||||
return () => store.unregisterSidebarTab(opts.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a bottom-panel tab. Returns a {@link DisposableHandle} — call
|
||||
* `handle.dispose()` to remove the tab.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineBottomPanelTab } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineBottomPanelTab({
|
||||
* id: 'my-logs',
|
||||
* title: 'My Logs',
|
||||
* type: 'vue',
|
||||
* component: MyLogsComponent
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineBottomPanelTab(
|
||||
opts: BottomPanelExtension
|
||||
): DisposableHandle {
|
||||
return register(async () => {
|
||||
const { useBottomPanelStore } =
|
||||
await import('@/stores/workspace/bottomPanelStore')
|
||||
const store = useBottomPanelStore()
|
||||
store.registerBottomPanelTab(opts)
|
||||
// bottomPanelStore exposes no unregister API today; remove from the
|
||||
// reactive list manually. Filed as follow-up when the store gains a
|
||||
// first-class `unregisterBottomPanelTab(id)`.
|
||||
return () => {
|
||||
const idx = store.bottomPanelTabs.findIndex((t) => t.id === opts.id)
|
||||
if (idx >= 0) store.bottomPanelTabs.splice(idx, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a command. Returns a {@link DisposableHandle} — call
|
||||
* `handle.dispose()` to unregister the command.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineCommand } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineCommand({
|
||||
* id: 'my-org.my-command',
|
||||
* label: 'Do The Thing',
|
||||
* function: () => { console.log('doing the thing') }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineCommand(opts: CommandDefinition): DisposableHandle {
|
||||
return register(async () => {
|
||||
const { useCommandStore } = await import('@/stores/commandStore')
|
||||
const store = useCommandStore()
|
||||
store.registerCommand(opts)
|
||||
// commandStore lacks an `unregisterCommand` API today; delete the entry
|
||||
// from its internal map via best-effort. Follow-up to add a first-class
|
||||
// unregister method.
|
||||
return () => {
|
||||
const cmds = store.commands as unknown as Array<{ id: string }>
|
||||
const idx = cmds.findIndex((c) => c.id === opts.id)
|
||||
if (idx >= 0) cmds.splice(idx, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a hotkey binding. Returns a {@link DisposableHandle} — call
|
||||
* `handle.dispose()` to unbind.
|
||||
*
|
||||
* The hotkey targets a command id; register the command separately via
|
||||
* {@link defineCommand}. The `keys` string is parsed via the standard
|
||||
* key-combo grammar (`mod+k`, `ctrl+shift+f`, etc.).
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineCommand, defineHotkey } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineCommand({ id: 'my.cmd', function: () => {} })
|
||||
* defineHotkey({ keys: 'mod+k', commandId: 'my.cmd' })
|
||||
* ```
|
||||
*/
|
||||
export function defineHotkey(opts: HotkeyExtension): DisposableHandle {
|
||||
return register(async () => {
|
||||
const [{ useKeybindingStore }, { KeybindingImpl }] = await Promise.all([
|
||||
import('@/platform/keybindings/keybindingStore'),
|
||||
import('@/platform/keybindings/keybinding')
|
||||
])
|
||||
const store = useKeybindingStore()
|
||||
const combo = parseKeyComboString(opts.keys)
|
||||
const kb = new KeybindingImpl({
|
||||
commandId: opts.commandId,
|
||||
combo,
|
||||
targetElementId: opts.targetElementId
|
||||
})
|
||||
store.addDefaultKeybinding(kb)
|
||||
// keybindingStore has no first-class unbind for default keybindings;
|
||||
// remove the combo from the reactive map directly.
|
||||
return () => {
|
||||
const target = (
|
||||
store as unknown as {
|
||||
defaultKeybindings: { value: Record<string, unknown> }
|
||||
}
|
||||
).defaultKeybindings
|
||||
if (target?.value) {
|
||||
const key = kb.combo.serialize()
|
||||
const next = { ...target.value }
|
||||
delete next[key]
|
||||
target.value = next
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `'mod+shift+k'` → `{ key: 'k', ctrl: true (or meta on mac), shift: true }`.
|
||||
* Matches the convention used by the keybinding store.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function parseKeyComboString(input: string): {
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
alt?: boolean
|
||||
shift?: boolean
|
||||
meta?: boolean
|
||||
} {
|
||||
const parts = input.split('+').map((p) => p.trim().toLowerCase())
|
||||
const isMac =
|
||||
typeof navigator !== 'undefined' &&
|
||||
/mac|iphone|ipad|ipod/i.test(navigator.platform)
|
||||
const result: {
|
||||
key: string
|
||||
ctrl?: boolean
|
||||
alt?: boolean
|
||||
shift?: boolean
|
||||
meta?: boolean
|
||||
} = { key: '' }
|
||||
for (const part of parts) {
|
||||
if (part === 'ctrl' || part === 'control') result.ctrl = true
|
||||
else if (part === 'alt' || part === 'option') result.alt = true
|
||||
else if (part === 'shift') result.shift = true
|
||||
else if (part === 'meta' || part === 'cmd' || part === 'command')
|
||||
result.meta = true
|
||||
else if (part === 'mod') {
|
||||
if (isMac) result.meta = true
|
||||
else result.ctrl = true
|
||||
} else {
|
||||
result.key = part
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a setting. Returns a {@link DisposableHandle} — call
|
||||
* `handle.dispose()` to remove the setting from the settings menu.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineSetting } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineSetting({
|
||||
* id: 'my.option' as never, // widen until Settings is augmented
|
||||
* name: 'My Option',
|
||||
* type: 'boolean',
|
||||
* defaultValue: false
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineSetting<TValue = unknown>(
|
||||
opts: SettingDefinition<TValue>
|
||||
): DisposableHandle {
|
||||
// The underlying SettingParams<TValue> is invariant in TValue inside the
|
||||
// store; widen to `unknown` at the boundary so the typed public arg accepts
|
||||
// any TValue without forcing every internal store to be generic.
|
||||
const widened = opts as unknown as SettingDefinition<unknown>
|
||||
return register(async () => {
|
||||
const { useSettingStore } = await import('@/platform/settings/settingStore')
|
||||
const store = useSettingStore()
|
||||
store.addSetting(widened)
|
||||
// settingStore has no removeSetting API today; clear via direct map
|
||||
// mutation. Follow-up to add a first-class remove.
|
||||
return () => {
|
||||
const settingsById = (
|
||||
store as unknown as { settingsById?: Record<string, unknown> }
|
||||
).settingsById
|
||||
if (settingsById && opts.id in settingsById) {
|
||||
delete settingsById[opts.id]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an About-page badge. Returns a {@link DisposableHandle} — call
|
||||
* `handle.dispose()` to remove the badge.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineAboutBadge } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineAboutBadge({
|
||||
* label: 'GitHub',
|
||||
* url: 'https://github.com/me/my-ext',
|
||||
* icon: 'pi-github'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineAboutBadge(opts: AboutBadgeExtension): DisposableHandle {
|
||||
// The aboutPanelStore today computes its badge list reactively from
|
||||
// `extensionStore.extensions.flatMap(e => e.aboutPageBadges ?? [])`. There
|
||||
// is no direct register API yet. We push into a module-level array that
|
||||
// the aboutPanelStore will consume after a follow-up wires it in (P5.E).
|
||||
// Until then, the call still registers a dispose-aware entry so author
|
||||
// code is forward-compatible.
|
||||
const entry = { ...opts }
|
||||
_aboutBadgeRegistry.push(entry)
|
||||
return register(() => {
|
||||
return () => {
|
||||
const idx = _aboutBadgeRegistry.indexOf(entry)
|
||||
if (idx >= 0) _aboutBadgeRegistry.splice(idx, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal registry for badges registered via {@link defineAboutBadge}.
|
||||
* Consumed by the about-panel store wiring (P5.E follow-up).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const _aboutBadgeRegistry: AboutBadgeExtension[] = []
|
||||
|
||||
/**
|
||||
* Register a toolbar button (action-bar button). Returns a
|
||||
* {@link DisposableHandle} — call `handle.dispose()` to remove the button.
|
||||
*
|
||||
* **Net-new surface**: no v1 registration path existed. Authors using this
|
||||
* are first-movers; the API may evolve before stabilizing.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineToolbarButton } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineToolbarButton({
|
||||
* id: 'my.help',
|
||||
* icon: 'pi-question-circle',
|
||||
* tooltip: 'Get help',
|
||||
* onClick: () => openHelp()
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineToolbarButton(
|
||||
opts: ToolbarButtonExtension
|
||||
): DisposableHandle {
|
||||
// The action-bar today renders from `extensionStore.extensions.flatMap(e =>
|
||||
// e.actionBarButtons ?? [])`. As with defineAboutBadge, the v2 path uses a
|
||||
// module-level registry that the action-bar component will consume after
|
||||
// a follow-up wires it in (P5.E).
|
||||
const entry = { ...opts }
|
||||
_toolbarButtonRegistry.push(entry)
|
||||
return register(() => {
|
||||
return () => {
|
||||
const idx = _toolbarButtonRegistry.indexOf(entry)
|
||||
if (idx >= 0) _toolbarButtonRegistry.splice(idx, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal registry for buttons registered via {@link defineToolbarButton}.
|
||||
* Consumed by the action-bar component wiring (P5.E follow-up).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const _toolbarButtonRegistry: ToolbarButtonExtension[] = []
|
||||
66
src/extension-api/shell.ts
Normal file
66
src/extension-api/shell.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Shell UI extension types — sidebar tabs, bottom panels, commands, hotkeys,
|
||||
* settings, about badges, toolbar buttons, toasts.
|
||||
*
|
||||
* Re-exported from `src/types/extensionTypes.ts` with no shape changes. Each
|
||||
* registerable shell UI surface has its own `defineX` entry point in
|
||||
* `@/extension-api/registrations`; the arg types for those entries are
|
||||
* re-exported here. Toast + notification surfaces remain inline-imperative
|
||||
* (see `@/extension-api/imperatives`).
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// VueExtension and CustomExtension are intentionally NOT re-exported — they
|
||||
// are discriminated-union ingredients of SidebarTabExtension /
|
||||
// BottomPanelExtension, not author-facing entry points. Internal callers
|
||||
// (ExtensionSlot.vue) import them directly from '@/types/extensionTypes'.
|
||||
export type {
|
||||
// Pre-existing types (unchanged shape)
|
||||
/**
|
||||
* Options bag for {@link defineSidebarTab}.
|
||||
* @see {@link defineSidebarTab} for a usage example.
|
||||
*/
|
||||
SidebarTabExtension,
|
||||
/**
|
||||
* Options bag for {@link defineBottomPanelTab}.
|
||||
* @see {@link defineBottomPanelTab} for a usage example.
|
||||
*/
|
||||
BottomPanelExtension,
|
||||
/**
|
||||
* Options bag for {@link toast.show} / {@link toast.remove}.
|
||||
* @see {@link toast} for a usage example.
|
||||
*/
|
||||
ToastMessageOptions,
|
||||
/**
|
||||
* Manager interface backing the {@link toast} surface.
|
||||
* @see {@link toast} for a usage example.
|
||||
*/
|
||||
ToastManager,
|
||||
// Net-new shell-UI arg types
|
||||
/**
|
||||
* Options bag for {@link defineCommand}.
|
||||
* @see {@link defineCommand} for a usage example.
|
||||
*/
|
||||
CommandDefinition,
|
||||
/**
|
||||
* Options bag for {@link defineHotkey}.
|
||||
* @see {@link defineHotkey} for a usage example.
|
||||
*/
|
||||
HotkeyExtension,
|
||||
/**
|
||||
* Options bag for {@link defineAboutBadge}.
|
||||
* @see {@link defineAboutBadge} for a usage example.
|
||||
*/
|
||||
AboutBadgeExtension,
|
||||
/**
|
||||
* Options bag for {@link defineSetting}.
|
||||
* @see {@link defineSetting} for a usage example.
|
||||
*/
|
||||
SettingDefinition,
|
||||
/**
|
||||
* Options bag for {@link defineToolbarButton}.
|
||||
* @see {@link defineToolbarButton} for a usage example.
|
||||
*/
|
||||
ToolbarButtonExtension
|
||||
} from '@/types/extensionTypes'
|
||||
204
src/extension-api/types.ts
Normal file
204
src/extension-api/types.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Extension option interfaces — the type contracts for `defineNode`,
|
||||
* `defineExtension`, and `defineWidget`.
|
||||
*
|
||||
* Lives in its own module so the runtime service (`@/services/extension-api-service`)
|
||||
* and the public lifecycle barrel (`@/extension-api/lifecycle`) can both depend on
|
||||
* these types without forming a circular import (the service implements the
|
||||
* `defineXxx` functions and the `onNodeMounted` / `onNodeRemoved` hooks that the
|
||||
* lifecycle module re-exports).
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import type { NodeHandle } from './node'
|
||||
import type { WidgetMountFn } from './widget'
|
||||
|
||||
/**
|
||||
* Options for `defineNode`. Describes an extension that reacts to
|
||||
* node lifecycle events.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineNode } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineNode({
|
||||
* name: 'my-org.my-extension',
|
||||
* nodeTypes: ['KSampler'],
|
||||
*
|
||||
* nodeCreated(node) {
|
||||
* node.on('executed', (e) => console.log('done', e.output))
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface NodeExtensionOptions {
|
||||
/**
|
||||
* Globally unique extension name. Used for scope registry keying, hook
|
||||
* ordering (lexicographic tie-break), and debug messages.
|
||||
*
|
||||
* Convention: `'org.extension-name'` or `'Comfy.ExtensionName'`.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Filter to specific `comfyClass` names. When omitted, the extension
|
||||
* receives `nodeCreated` / `loadedGraphNode` for every node type.
|
||||
*
|
||||
* Replaces the v1 `beforeRegisterNodeDef` filtering pattern.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* nodeTypes: ['KSampler', 'KSamplerAdvanced']
|
||||
* ```
|
||||
*/
|
||||
nodeTypes?: string[]
|
||||
|
||||
/**
|
||||
* Called once per node instance when the node is first created (typed in,
|
||||
* pasted from clipboard, duplicated, or loaded without an existing workflow).
|
||||
*
|
||||
* - Runs inside a Vue `EffectScope`. All `watch` / `computed` / `onNodeMounted`
|
||||
* calls made here are captured and disposed automatically on node removal.
|
||||
* - Must be synchronous. Kick off async work inside the body; use
|
||||
* `loading: ref(true)` for async-dependent state.
|
||||
* - Called only once per entity ID lifetime. Copy/paste creates a fresh entity
|
||||
* and fires `nodeCreated` again on the new entity (reset-to-fresh).
|
||||
*/
|
||||
nodeCreated?(node: NodeHandle): void
|
||||
|
||||
/**
|
||||
* Called once per node instance when the node is restored from a saved
|
||||
* workflow. Widget values are already populated when this fires.
|
||||
*
|
||||
* Same rules as `nodeCreated`. Exactly one of `nodeCreated` or
|
||||
* `loadedGraphNode` fires per node entity, never both.
|
||||
*
|
||||
* Replaces the v1 `loadedGraphNode` hook and `nodeType.prototype.onConfigure`
|
||||
* patching.
|
||||
*/
|
||||
loadedGraphNode?(node: NodeHandle): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the global `defineExtension` entry point. Covers extension-wide
|
||||
* lifecycle and shell UI contributions.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension, onMounted } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineExtension({
|
||||
* name: 'my-org.my-extension',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* // App is ready; register commands, sidebar tabs, etc.
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface ExtensionOptions {
|
||||
/**
|
||||
* Globally unique extension name. Matches the format of
|
||||
* `NodeExtensionOptions.name`.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Runs once during app initialization (after the app is mounted but before
|
||||
* the first workflow is loaded). Equivalent to the v1 `ComfyExtension.init`.
|
||||
*
|
||||
* @deprecated move the
|
||||
* `init` body into `setup()`. The body of `setup()` runs at the same point
|
||||
* `init` used to run (early lifecycle); use `onMounted(() => ...)` inside
|
||||
* `setup()` for what `init` did via late-lifecycle assumptions. A codemod
|
||||
* ships in `@comfyorg/extension-api` to perform the rewrite mechanically.
|
||||
* The v1 hook is retained for back-compat during the deprecation window.
|
||||
*/
|
||||
init?(): void | Promise<void>
|
||||
|
||||
/**
|
||||
* Runs once after the app and all core extensions are initialized. Equivalent
|
||||
* to the v1 `ComfyExtension.setup`. Safe to call shell UI registration APIs
|
||||
* (`ExtensionManager`, `CommandManager`) here.
|
||||
*
|
||||
* @deprecated the
|
||||
* `setup` property name is retained, but the v1 semantic "fires after all
|
||||
* core extensions ready" now lives in `onMounted(() => ...)` *inside* the
|
||||
* `setup()` body. The `setup()` body itself now runs at the earlier
|
||||
* registration-equivalent point (where v1 `init` used to run). Use
|
||||
* `onBeforeMount` / `onMounted` / `onUnmounted` / `onActivated` /
|
||||
* `onDeactivated` for fine-grained lifecycle hooks.
|
||||
*
|
||||
* Migration:
|
||||
* ```ts
|
||||
* // v1
|
||||
* setup() { api.addEventListener('execution_start', fn) }
|
||||
* // v2
|
||||
* setup() { onMounted(() => execution.on('start', fn)) }
|
||||
* ```
|
||||
*
|
||||
* A codemod ships in `@comfyorg/extension-api`.
|
||||
*/
|
||||
setup?(): void | Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `defineWidget`. Registers a custom widget type that renders
|
||||
* through the mount-lifecycle seam (Axiom A12 / D-widget-converge).
|
||||
*
|
||||
* Once registered, the widget type can be referenced from Python
|
||||
* `INPUT_TYPES` schema declarations. The runtime allocates a per-widget
|
||||
* host `<div>` and invokes the registered `mount(host, ctx)` hook against
|
||||
* it. The widget's mount body captures the host (and any DOM it
|
||||
* constructs) via closure — there is no `widget.element` accessor on the
|
||||
* handle.
|
||||
*
|
||||
* Runtime widget addition (`node.addWidget(...)`) is forbidden per
|
||||
* AXIOMS.md A15 / `decisions/D-ban-runtime-addwidget.md` — widgets are
|
||||
* schema-declared, never created at runtime by extensions.
|
||||
*
|
||||
* `mount` is optional: omit it for value-only widgets (numeric, combo, etc.)
|
||||
* that render through the native widget renderer with no custom DOM.
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineWidget } from '@comfyorg/extension-api'
|
||||
*
|
||||
* export default defineWidget({
|
||||
* name: 'my-org.color-picker',
|
||||
* type: 'COLOR_PICKER',
|
||||
*
|
||||
* mount(host, ctx) {
|
||||
* const input = document.createElement('input')
|
||||
* input.type = 'color'
|
||||
* input.value = String(ctx.widget.getValue() ?? '#000000')
|
||||
* input.addEventListener('input', () => ctx.widget.setValue(input.value))
|
||||
* host.appendChild(input)
|
||||
* // Optional cleanup — fires once on widget destruction.
|
||||
* return () => input.remove()
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface WidgetExtensionOptions {
|
||||
/** Globally unique extension name. */
|
||||
name: string
|
||||
/** Widget type string this extension provides (e.g. `'COLOR_PICKER'`). */
|
||||
type: string
|
||||
|
||||
/**
|
||||
* Mount lifecycle hook — the **sole** DOM seam per Axiom A12. Called once
|
||||
* per widget instance when the widget is first attached to its node host
|
||||
* in the DOM. May return a `WidgetCleanup` function that fires on widget
|
||||
* destruction (host remount does NOT fire cleanup; see
|
||||
* `WidgetMountContext.onBeforeRemount` / `onAfterRemount`).
|
||||
*
|
||||
* Omit entirely for value-only widgets that need no custom DOM.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
mount?: WidgetMountFn
|
||||
}
|
||||
658
src/extension-api/widget.ts
Normal file
658
src/extension-api/widget.ts
Normal file
@@ -0,0 +1,658 @@
|
||||
/**
|
||||
* WidgetHandle — the controlled surface for widget access in v2 extensions.
|
||||
*
|
||||
* All state reads and writes go through this interface. Internal ECS
|
||||
* components and World references are never exposed.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import type { AsyncHandler, Handler, Unsubscribe } from './events'
|
||||
import type { NodeHandle } from './node'
|
||||
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
/**
|
||||
* Branded entity ID for widgets. Prevents mixing widget IDs with node IDs
|
||||
* at compile time. Re-exported from the world layer so the entire codebase
|
||||
* shares a single brand. The underlying value is `string` in Phase A.
|
||||
*
|
||||
* @internal Per D20 — extension authors use `widget.id: string` and
|
||||
* `widget.equals(other)`. The branded type is reserved for internal package
|
||||
* modules and is intentionally absent from the published barrel.
|
||||
*/
|
||||
export type { WidgetEntityId }
|
||||
|
||||
/**
|
||||
* The union of all legal widget scalar values. Complex widgets (DOM, canvas)
|
||||
* may return their own serializable shapes.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import type { WidgetValue } from '@comfyorg/extension-api'
|
||||
*
|
||||
* // `WidgetValue` is `string | number | boolean | null` — the four
|
||||
* // primitive-widget value shapes.
|
||||
* const val: WidgetValue = 42
|
||||
* ```
|
||||
*/
|
||||
export type WidgetValue = string | number | boolean | null
|
||||
|
||||
/**
|
||||
* Payload for `widget.on('valueChange', handler)`.
|
||||
*
|
||||
* Replaces the v1 `widget.callback` pattern.
|
||||
*
|
||||
* @typeParam T - The widget's value type.
|
||||
* @example
|
||||
* ```ts
|
||||
* widget.on('valueChange', (e) => {
|
||||
* console.log('changed from', e.oldValue, 'to', e.newValue)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface WidgetValueChangeEvent<T = WidgetValue> {
|
||||
/** Value before the change. */
|
||||
readonly oldValue: T
|
||||
/** Value after the change. */
|
||||
readonly newValue: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for `widget.on('optionChange', handler)`.
|
||||
*
|
||||
* Fires when a type-specific option is mutated via `setOption(key, value)`.
|
||||
* The exact set of observable option keys is type-dependent (e.g. `min`,
|
||||
* `max`, `step` for numeric widgets; `multiline` for strings).
|
||||
*
|
||||
* This event covers the options-bag tier (type-specific, not every-widget).
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* widget.on('optionChange', (e) => {
|
||||
* if (e.key === 'min') clampValue(e.newValue as number)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface WidgetOptionChangeEvent {
|
||||
/** The option key that changed (e.g. `'min'`, `'max'`, `'multiline'`). */
|
||||
readonly key: string
|
||||
/** Value before the change. */
|
||||
readonly oldValue: unknown
|
||||
/** Value after the change. */
|
||||
readonly newValue: unknown
|
||||
}
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14 + A16:
|
||||
// `WidgetPropertyChangeEvent` is vacuous after `'serialize'` was removed from
|
||||
// the property union (A16: authors cannot disable serialization). The other
|
||||
// historical members ('hidden', 'disabled') were already A14-deferred.
|
||||
// Restoration requires a new first-class property to surface that satisfies
|
||||
// A1–A16 and an axiom amendment.
|
||||
//
|
||||
// export interface WidgetPropertyChangeEvent {
|
||||
// readonly property: 'serialize'
|
||||
// readonly oldValue: boolean
|
||||
// readonly newValue: boolean
|
||||
// }
|
||||
|
||||
/**
|
||||
* Payload for `widget.on('beforeSerialize', handler)`.
|
||||
*
|
||||
* This is the **only async-allowed event** in the API and, per AXIOMS.md
|
||||
* §A16 (Unified Serialization Target), the **sole** extension-author
|
||||
* interface to serialization. Replaces every v1 serialization hook:
|
||||
* `widget.serializeValue = fn`, `widget.options.serialize = false`,
|
||||
* `nodeType.prototype.serialize`.
|
||||
*
|
||||
* The hook fires **once per serialization**. The framework writes the
|
||||
* resulting payload to every transport (workflow JSON `widgets_values[i]`,
|
||||
* API prompt, clone target, subgraph promotion). Extensions do not see and
|
||||
* cannot branch on the transport — that is a framework concern (A16).
|
||||
*
|
||||
* Call `event.setSerializedValue(v)` to override what is written. Do not
|
||||
* call it to pass through the widget's current `getValue()` unchanged.
|
||||
*
|
||||
* Per A16 there is no `skip()` and no `context` discriminator. Per A15
|
||||
* (Widget Declarativity) there is no way to exclude a widget from
|
||||
* serialization — if a widget should not contribute to the payload, it
|
||||
* should not be a widget (use a boxed widget, a non-widget UI primitive,
|
||||
* or a schema input).
|
||||
*
|
||||
* @typeParam T - The widget's value type.
|
||||
* @example
|
||||
* ```ts
|
||||
* // Dynamic prompts: replace value at serialize time
|
||||
* widget.on('beforeSerialize', (e) => {
|
||||
* e.setSerializedValue(processDynamicPrompt(widget.getValue()))
|
||||
* })
|
||||
*
|
||||
* // Async: webcam capture — materialize frame before serialization
|
||||
* widget.on('beforeSerialize', async (e) => {
|
||||
* const frame = await captureFrame()
|
||||
* e.setSerializedValue(frame)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface WidgetBeforeSerializeEvent<T = WidgetValue> {
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14 + A16:
|
||||
// The 4-way transport discriminator inverted the direction of knowledge
|
||||
// flow — framework owns transport, extensions own value. Workflow JSON
|
||||
// and API prompt converge to a single serialized payload; clone and
|
||||
// subgraph-promote are framework concerns. Restoration requires an
|
||||
// axiom amendment to A16.
|
||||
//
|
||||
// readonly context: 'workflow' | 'prompt' | 'clone' | 'subgraph-promote'
|
||||
|
||||
/**
|
||||
* The widget's current value at the time of serialization (before any override).
|
||||
* Equivalent to calling `widget.getValue()`.
|
||||
*/
|
||||
readonly value: T
|
||||
|
||||
/**
|
||||
* Override the serialized value. The provided value is written to every
|
||||
* transport (workflow JSON `widgets_values[i]`, API prompt, clone target,
|
||||
* subgraph promotion). Calling this multiple times keeps the last call's
|
||||
* value.
|
||||
*
|
||||
* @param v - The value to serialize. Must be JSON-serializable.
|
||||
*/
|
||||
setSerializedValue(v: unknown): void
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14 + A16:
|
||||
// `skip()` IS a per-call disable — authors cannot disable serialization
|
||||
// (A16). If a widget should not contribute to the payload, it should not
|
||||
// be a widget (A15). Restoration requires axiom amendments to A15 + A16.
|
||||
//
|
||||
// skip(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for `widget.on('beforeQueue', handler)`.
|
||||
*
|
||||
* Fires when the user triggers a prompt queue (before `graphToPrompt` runs).
|
||||
* Call `event.reject(message)` to cancel the queue attempt with a user-visible
|
||||
* error. Do not call `reject` to allow the queue to proceed.
|
||||
*
|
||||
* Replaces the v1 `app.queuePrompt` monkey-patching pattern (S6.A4/S6.A5)
|
||||
* for per-widget validation (e.g. required field empty, value out of range).
|
||||
* For cross-node/graph-wide rejection, see the app-level `beforePrompt` event
|
||||
* (I-UWF.4 — not yet in the API).
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* // Reject if a required field is empty
|
||||
* widget.on('beforeQueue', (e) => {
|
||||
* if (!widget.getValue()) {
|
||||
* e.reject('Prompt text is required before queueing.')
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* // Reject with a dynamic message
|
||||
* widget.on('beforeQueue', (e) => {
|
||||
* const val = widget.getValue<number>()
|
||||
* const min = widget.getOption<number>('min') ?? 0
|
||||
* if (val < min) {
|
||||
* e.reject(`Value ${val} is below the minimum of ${min}.`)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface WidgetBeforeQueueEvent {
|
||||
/**
|
||||
* Reject the queue attempt, showing `message` to the user.
|
||||
* Once any handler calls `reject`, the queue is cancelled — subsequent
|
||||
* handlers still run but their `reject` calls are no-ops.
|
||||
*
|
||||
* @param message - Human-readable reason shown in the UI toast.
|
||||
*/
|
||||
reject(message: string): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Controlled surface for widget access. Backed by ECS `WidgetValue` and
|
||||
* `WidgetIdentity` components in the World. Reads query components directly;
|
||||
* writes dispatch commands (undo-able, serializable, validatable).
|
||||
*
|
||||
* All views (node, properties panel, promoted copy) share the same backing
|
||||
* `WidgetEntityId`, so mutations from any source trigger `valueChange`.
|
||||
*
|
||||
* @typeParam T - The type of `getValue()` / `setValue()`. Defaults to `WidgetValue`.
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineWidget } from '@comfyorg/extension-api'
|
||||
*
|
||||
* // Per AXIOMS.md A1, nodes cannot
|
||||
* // enumerate or reference widgets — `node.getWidget(name)` was removed.
|
||||
* // To react to a specific widget's lifecycle and value changes, register
|
||||
* // a widget type and use the `mount` context's `ctx.widget` handle:
|
||||
*
|
||||
* export default defineWidget({
|
||||
* name: 'my-extension',
|
||||
* type: 'INT',
|
||||
* mount(_host, { widget }) {
|
||||
* widget.on('valueChange', (e) => console.log(widget.name, '=', e.newValue))
|
||||
* widget.setOption('min', 1)
|
||||
* widget.setOption('max', 150)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface WidgetHandle<T = WidgetValue> {
|
||||
/**
|
||||
* Opaque identifier for this widget. Stable for the lifetime of the
|
||||
* widget entity. Treat as a string token: do not parse, slice, or compare
|
||||
* its internal structure. Use {@link WidgetHandle.equals} to compare with
|
||||
* another handle.
|
||||
*
|
||||
* @remarks
|
||||
* The underlying value is a branded `WidgetEntityId` at runtime
|
||||
* but is narrowed to `string` on the public surface so authors never need
|
||||
* to import a brand to type a local variable.
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
* Returns `true` if `other` represents the same widget entity as this
|
||||
* one. Equivalent to `this.id === other.id` but the canonical comparator
|
||||
* — prefer `equals` over manual string comparison so future changes to
|
||||
* the identity scheme remain transparent.
|
||||
*/
|
||||
equals(other: WidgetHandle): boolean
|
||||
|
||||
/**
|
||||
* The widget's name as registered in the node's `INPUT_TYPES` schema.
|
||||
* Stable for the lifetime of the node; never changes after creation.
|
||||
*
|
||||
*/
|
||||
readonly name: string
|
||||
|
||||
/**
|
||||
* The widget's type string (e.g. `'INT'`, `'STRING'`, `'COMBO'`,
|
||||
* `'MARKDOWN'`). Read-only invariant set at creation.
|
||||
*
|
||||
*/
|
||||
readonly widgetType: string
|
||||
|
||||
/**
|
||||
* Returns the widget's current user-edited value.
|
||||
*
|
||||
* @typeParam T - Narrows the return type when you know the widget type.
|
||||
* @example
|
||||
* ```ts
|
||||
* // Inside `defineWidget({mount})` — `ctx.widget` is the only legal
|
||||
* // path to a `WidgetHandle` (nodes cannot enumerate widgets per A1).
|
||||
* const value = widget.getValue<number>()
|
||||
* ```
|
||||
*/
|
||||
getValue(): T
|
||||
|
||||
/**
|
||||
* Sets the widget's value. Dispatches a `SetWidgetValue` command (undo-able).
|
||||
* Triggers `valueChange` handlers on all views.
|
||||
*
|
||||
*/
|
||||
setValue(value: T): void
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Deferred pending serialization convergence.
|
||||
// isHidden(): boolean
|
||||
// setHidden(hidden: boolean): void
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14: Deferred pending serialization convergence.
|
||||
// isDisabled(): boolean
|
||||
// setDisabled(disabled: boolean): void
|
||||
|
||||
/**
|
||||
* The widget's display label shown to the user. Defaults to the widget name.
|
||||
* Read-only invariant (set at creation, never changes after).
|
||||
*
|
||||
* The label is set by the Python node's `INPUT_TYPES` schema (e.g. via
|
||||
* the `label` key on the input options dict).
|
||||
*/
|
||||
readonly label: string
|
||||
|
||||
/**
|
||||
* Updates the reserved height for this widget and triggers a node relayout.
|
||||
*
|
||||
* Meaningful for widgets registered via {@link defineWidget} with a
|
||||
* {@link WidgetMountFn} `mount()` body — the reserved height bounds the
|
||||
* runtime-owned host `<div>` that the mount body renders into. For widgets
|
||||
* that render through the native widget renderer (no `mount`), this is a
|
||||
* no-op.
|
||||
*
|
||||
* Replaces the v1 pattern of re-assigning `node.computeSize` to return a new
|
||||
* height whenever the embedded element resizes.
|
||||
*
|
||||
* @param px - New reserved height in pixels.
|
||||
* @stability experimental
|
||||
*/
|
||||
setHeight(px: number): void
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14 + A16:
|
||||
// Authors cannot disable serialization at the widget level (A16). If a
|
||||
// widget should not contribute to the serialized payload, it should not
|
||||
// be a widget (A15) — use a boxed/composed widget (BBOX-style), a
|
||||
// non-widget UI primitive, or a schema input. The sole serialization
|
||||
// interface is `widget.on('beforeSerialize', handler)`. Restoration
|
||||
// requires axiom amendments to A15 + A16 + a validated ecosystem use
|
||||
// case that no boxed/composed pattern can serve.
|
||||
//
|
||||
// isSerializeEnabled(): boolean
|
||||
// setSerializeEnabled(enabled: boolean): void
|
||||
|
||||
/**
|
||||
* Read-only snapshot of the full options bag for this widget.
|
||||
*
|
||||
* **Immutable.** The returned
|
||||
* object is `Readonly<WidgetOptions>` — `widget.options.min = 0`,
|
||||
* `widget.options = {...}`, and `widget.options.values = [...]` all raise
|
||||
* TypeScript errors at compile time. To mutate, use
|
||||
* {@link WidgetHandle.setOption} per-key.
|
||||
*
|
||||
* Note: this is an accessor pair on the v2 surface. Reading is free; the
|
||||
* setter intentionally does not exist on the public type. Per AXIOMS.md
|
||||
* §A16, `serialize` is no longer a writable option (and no longer a key
|
||||
* on this bag) — there is no widget-level serialization disable.
|
||||
* `widget.options.values = [...]` (combo refresh) migrates to a future
|
||||
* `setValues` mutator.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // ❌ TS-ERR — every option write raises a compile-time error
|
||||
* widget.options.min = 0
|
||||
* widget.options = { min: 0, max: 100 }
|
||||
*
|
||||
* // ✅ Read freely
|
||||
* const min = widget.options.min ?? 0
|
||||
*
|
||||
* // ✅ Mutate via typed setters
|
||||
* widget.setOption('min', 0)
|
||||
* ```
|
||||
*/
|
||||
readonly options: Readonly<WidgetOptions>
|
||||
|
||||
/**
|
||||
* Returns the per-instance override for `key`, or the class-default value
|
||||
* from `INPUT_TYPES` if no override has been set, or `undefined` if the key
|
||||
* is unknown for this widget type.
|
||||
*
|
||||
* Type-specific option names: `min`, `max`, `step` (INT/FLOAT); `multiline`,
|
||||
* `dynamicPrompts` (STRING); `image_folder`, `upload_to` (upload widgets).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const min = widget.getOption<number>('min') ?? 0
|
||||
* ```
|
||||
*/
|
||||
getOption<K = unknown>(key: string): K | undefined
|
||||
|
||||
/**
|
||||
* Set a per-instance option override. Persisted as a `widget_options` sidecar
|
||||
* in the workflow JSON (additive, backward-compatible). Does not change the
|
||||
* backend prompt schema unless the extension explicitly opts in via
|
||||
* `beforeSerialize`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Primitive Int/Float per-instance config (replaces node.properties anti-pattern)
|
||||
* widget.setOption('min', 0)
|
||||
* widget.setOption('max', 100)
|
||||
* widget.setOption('step', 1)
|
||||
* ```
|
||||
*/
|
||||
setOption(key: string, value: unknown): void
|
||||
|
||||
/**
|
||||
* The widget's current `serializeValue` function (or `undefined` if none is
|
||||
* registered).
|
||||
*
|
||||
* **Accessor-only.** The setter
|
||||
* intentionally does not exist on the public type — assignment
|
||||
* (`widget.serializeValue = fn`) raises a TypeScript error. The v2
|
||||
* migration target is the {@link WidgetHandle.on | `on('beforeSerialize', fn)`}
|
||||
* event, which is typed, async-capable, and composable across
|
||||
* multiple extensions on the same widget.
|
||||
*
|
||||
* @deprecated v1 callers reading `widget.serializeValue` to invoke the
|
||||
* function directly should subscribe to `'beforeSerialize'` instead. This
|
||||
* read-only accessor exists for debugging / introspection only and may be
|
||||
* removed once the v1 surface is fully retired.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // ❌ TS-ERR — direct assignment no longer compiles
|
||||
* widget.serializeValue = () => 'static value'
|
||||
*
|
||||
* // ✅ Subscribe to the typed event (D5). Per A16 the hook fires once
|
||||
* // and the framework writes the resulting payload to every transport;
|
||||
* // there is no transport discriminator.
|
||||
* widget.on('beforeSerialize', (e) => {
|
||||
* e.setSerializedValue('static value')
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
readonly serializeValue: ((...args: unknown[]) => unknown) | undefined
|
||||
|
||||
/**
|
||||
* Subscribe to the widget's value changes.
|
||||
*
|
||||
* Replaces the v1 `widget.callback` pattern.
|
||||
* Fires synchronously after the value is committed.
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
*/
|
||||
on(
|
||||
event: 'valueChange',
|
||||
handler: Handler<WidgetValueChangeEvent<T>>
|
||||
): Unsubscribe
|
||||
|
||||
/**
|
||||
* Subscribe to type-specific option mutations (`setOption(key, value)`).
|
||||
*
|
||||
* Fires for options-bag changes (e.g. `min`, `max`, `step`, `multiline`).
|
||||
* Does NOT fire for value changes or first-class field changes.
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
* @stability experimental
|
||||
*/
|
||||
on(
|
||||
event: 'optionChange',
|
||||
handler: Handler<WidgetOptionChangeEvent>
|
||||
): Unsubscribe
|
||||
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14 + A16:
|
||||
// `WidgetPropertyChangeEvent` is vacuous — the only property the event
|
||||
// ever surfaced was `'serialize'`, which is gone per A16. `setHidden` /
|
||||
// `setDisabled` were already A14-deferred. Restoration requires a new
|
||||
// first-class property to surface that satisfies A1–A16.
|
||||
//
|
||||
// on(
|
||||
// event: 'propertyChange',
|
||||
// handler: Handler<WidgetPropertyChangeEvent>
|
||||
// ): Unsubscribe
|
||||
|
||||
/**
|
||||
* Subscribe to widget serialization. The only async-allowed event.
|
||||
*
|
||||
* Per AXIOMS.md §A16 this is the **sole** extension-author interface
|
||||
* to serialization. The hook fires once per serialization and the
|
||||
* framework writes the resulting payload to every transport (workflow
|
||||
* JSON `widgets_values`, API prompt, clone target, subgraph promotion).
|
||||
* Replaces the v1 `widget.serializeValue = fn` /
|
||||
* `widget.options.serialize` patterns.
|
||||
*
|
||||
* The handler may be sync or async; async handlers are awaited before
|
||||
* the serialization payload is sent.
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
*/
|
||||
on(
|
||||
event: 'beforeSerialize',
|
||||
handler: AsyncHandler<WidgetBeforeSerializeEvent<T>>
|
||||
): Unsubscribe
|
||||
|
||||
/**
|
||||
* Subscribe to pre-queue validation. Fires before `graphToPrompt` runs.
|
||||
*
|
||||
* Call `event.reject(message)` to cancel the queue with a user-visible error.
|
||||
* Replaces the v1 `app.queuePrompt` monkey-patching pattern (S6.A4/S6.A5)
|
||||
* for per-widget validation use cases.
|
||||
*
|
||||
* Handlers are sync-only — use for validation logic only, not I/O.
|
||||
*
|
||||
* @returns A cleanup function to remove the listener.
|
||||
* @stability experimental
|
||||
*/
|
||||
on(
|
||||
event: 'beforeQueue',
|
||||
handler: Handler<WidgetBeforeQueueEvent>
|
||||
): Unsubscribe
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup function returned from a widget's `mount()`. Fires exactly once,
|
||||
* when the widget entity is destroyed. **Does NOT fire on host remount**
|
||||
* (graph↔app mode, subgraph promotion, `<KeepAlive>` shuffle) — use
|
||||
* {@link WidgetMountContext.onBeforeRemount} / {@link WidgetMountContext.onAfterRemount}
|
||||
* for those.
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineWidget, type WidgetCleanup } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineWidget({
|
||||
* name: 'my-ext',
|
||||
* type: 'STRING',
|
||||
* mount(host): WidgetCleanup {
|
||||
* const input = document.createElement('input')
|
||||
* host.appendChild(input)
|
||||
* return () => input.remove()
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export type WidgetCleanup = () => void
|
||||
|
||||
/**
|
||||
* Context passed to a widget's `mount()` function.
|
||||
*
|
||||
* Per **Axiom A12** (Mount-Lifecycle as the Sole DOM Seam), this is the only
|
||||
* surface through which DOM enters a widget. Authors capture the host element
|
||||
* and any constructed DOM via closure inside `mount()` — there is no
|
||||
* `widget.element` / `widget.inputEl` accessor on the handle.
|
||||
*
|
||||
* @stability experimental
|
||||
*/
|
||||
export interface WidgetMountContext {
|
||||
/** The widget being mounted. Use for `getValue` / `setValue` / `on(...)`. */
|
||||
readonly widget: WidgetHandle
|
||||
/** The node hosting this widget. */
|
||||
readonly node: NodeHandle
|
||||
|
||||
/**
|
||||
* Register a callback that fires when the widget entity is destroyed.
|
||||
* Equivalent to returning a cleanup function from `mount()`; provided as
|
||||
* a hook for composition (e.g. inside helpers that own their own
|
||||
* sub-resources).
|
||||
*/
|
||||
onUnmount(fn: () => void): void
|
||||
|
||||
/**
|
||||
* Register a callback that fires immediately **before** the widget's host
|
||||
* `<div>` is moved to a new location (graph↔app mode, subgraph promotion,
|
||||
* Vue `<KeepAlive>` shuffle). Use to detach observers, pause animations,
|
||||
* or capture scroll position before the move.
|
||||
*
|
||||
* The widget's mount body is NOT re-invoked across a remount; only
|
||||
* `onBeforeRemount` then `onAfterRemount` fire.
|
||||
*/
|
||||
onBeforeRemount(fn: () => void): void
|
||||
|
||||
/**
|
||||
* Register a callback that fires immediately **after** the widget's host
|
||||
* `<div>` has been moved to a new location. Receives the new host element
|
||||
* so authors can re-attach observers, restore scroll position, etc.
|
||||
*/
|
||||
onAfterRemount(fn: (newHost: HTMLElement) => void): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount function for a widget. Called once when the widget is first attached
|
||||
* to a node host in the DOM. Returns an optional cleanup function that fires
|
||||
* on widget destruction.
|
||||
*
|
||||
* @param host - A runtime-owned empty `<div>` for the widget to mount into.
|
||||
* The widget MAY append children, set inline styles, attach event listeners,
|
||||
* etc. It MUST NOT replace or remove the host itself.
|
||||
* @param ctx - Mount context with the widget/node handles and remount hooks.
|
||||
* @returns Optional cleanup function called on widget destruction. Host
|
||||
* remount fires `ctx.onBeforeRemount` / `ctx.onAfterRemount` instead.
|
||||
*
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import type { WidgetMountFn } from '@comfyorg/extension-api'
|
||||
*
|
||||
* const mount: WidgetMountFn = (host, ctx) => {
|
||||
* const el = document.createElement('div')
|
||||
* el.textContent = String(ctx.widget.getValue() ?? '')
|
||||
* host.appendChild(el)
|
||||
* return () => el.remove()
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type WidgetMountFn = (
|
||||
host: HTMLElement,
|
||||
ctx: WidgetMountContext
|
||||
) => void | WidgetCleanup
|
||||
|
||||
/**
|
||||
* Options surfaced on each widget instance. Type-specific keys (e.g. `min`,
|
||||
* `max`, `step` for numeric widgets; `multiline`, `dynamicPrompts` for
|
||||
* strings) are passed through from the node's `INPUT_TYPES` schema as-is.
|
||||
*
|
||||
* Runtime widget addition is forbidden per AXIOMS.md A15 (Widget
|
||||
* Declarativity) / `decisions/D-ban-runtime-addwidget.md` — every widget
|
||||
* originates from the Python `INPUT_TYPES` declaration; this type
|
||||
* describes the options surfaced on the resulting `WidgetHandle`, not a
|
||||
* constructor argument bag.
|
||||
*/
|
||||
export interface WidgetOptions {
|
||||
/** If `true`, the widget is hidden from the node UI on creation. */
|
||||
hidden?: boolean
|
||||
/** If `true`, the widget is rendered read-only (no user editing). */
|
||||
readonly?: boolean
|
||||
// PHASE_A_EXCLUDED per AXIOMS.md A14 + A16:
|
||||
// `serialize` contradicted A16 even as a read-only key — there is no
|
||||
// widget-level serialization disable. Removed from the type entirely.
|
||||
//
|
||||
// serialize?: boolean
|
||||
/** Display label override. Defaults to the widget `name`. */
|
||||
label?: string
|
||||
/** Toggle label shown when value is `true` (BOOLEAN widgets). */
|
||||
labelOn?: string
|
||||
/** Toggle label shown when value is `false` (BOOLEAN widgets). */
|
||||
labelOff?: string
|
||||
/** Multiline text input (STRING widgets). */
|
||||
multiline?: boolean
|
||||
/**
|
||||
* When `true`, the widget value is processed for dynamic prompt syntax
|
||||
* at serialize time. (STRING widgets with `dynamicPrompts: true`.)
|
||||
*/
|
||||
dynamicPrompts?: boolean
|
||||
/** Min value for numeric widgets (INT, FLOAT). */
|
||||
min?: number
|
||||
/** Max value for numeric widgets. */
|
||||
max?: number
|
||||
/** Step size for numeric widgets. */
|
||||
step?: number
|
||||
/** Default value at construction time. */
|
||||
default?: unknown
|
||||
/** Any additional type-specific option. */
|
||||
[key: string]: unknown
|
||||
}
|
||||
76
src/extensions/core/coordSpaceDemo.v2.ts
Normal file
76
src/extensions/core/coordSpaceDemo.v2.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* CoordSpaceDemo — canary example for the D-coord-space PICK
|
||||
* (W6.P4 ACCEPTED 2026-05-18, Axiom A13 Single Coordinate Space).
|
||||
*
|
||||
* Shows three things:
|
||||
*
|
||||
* 1. **The default — canvas units everywhere.** `node.getPosition()` /
|
||||
* `getSize()` / `setPosition()` / `setSize()` all speak canvas units.
|
||||
* Zoom and pan don't perturb the numbers; devicePixelRatio is
|
||||
* invisible. This is the path 96%+ of extensions should ever take.
|
||||
*
|
||||
* 2. **The escape-hatch — explicit + annotated.** For the legitimate
|
||||
* cases that need screen-space coords (custom GPU canvas, floating
|
||||
* overlay anchored to absolute browser coords, hi-DPI export math),
|
||||
* drop to `window.app.canvas.{ds,canvas}` + `window.devicePixelRatio`.
|
||||
* Every escape-hatch use site MUST carry the
|
||||
* `// escape-hatch — see D-coord-space.md` comment so reviewers
|
||||
* (human or AI) can see the dependency is deliberate.
|
||||
*
|
||||
* 3. **The cliff — what's NOT on the public surface.** No
|
||||
* `node.getScreenPosition()`, no `node.getCSSPosition()`, no
|
||||
* `space: 'client' | 'css'` parameter, no branded `ClientPoint`
|
||||
* type. Reaching for any of those is a sign the author wants the
|
||||
* escape-hatch.
|
||||
*/
|
||||
|
||||
import { defineExtension, defineNode, type NodeHandle } from '@/extension-api'
|
||||
|
||||
defineNode({
|
||||
name: 'Comfy.CoordSpaceDemo.V2',
|
||||
|
||||
nodeCreated(node: NodeHandle) {
|
||||
// ── (1) Default path: canvas units, no conversion needed ──────────
|
||||
const [x, y] = node.getPosition() // canvas units
|
||||
const [w, h] = node.getSize() // canvas units
|
||||
|
||||
// Move a node 16 canvas units down-and-right of its current spot.
|
||||
// No /scale, no *scale, no devicePixelRatio — the runtime owns it.
|
||||
node.setPosition([x + 16, y + 16]) // canvas units
|
||||
|
||||
// Reserve a minimum size — also canvas units; zoom doesn't matter.
|
||||
if (w < 200) node.setSize([200, h])
|
||||
}
|
||||
})
|
||||
|
||||
defineExtension({
|
||||
name: 'Comfy.CoordSpaceDemo.V2.Escape',
|
||||
|
||||
setup() {
|
||||
// ── (2) Escape-hatch path — explicit + annotated ──────────────────
|
||||
//
|
||||
// Use case: extension wants to draw a 2x-pixel-density preview
|
||||
// thumbnail PNG of the visible viewport. PNG export needs *device
|
||||
// pixels* (so the saved image is crisp on hi-DPI displays); the v2
|
||||
// public surface does not expose dpr or screen-pixel sizing.
|
||||
//
|
||||
// The escape-hatch is the same shape extension authors are already
|
||||
// using today (213 dpr hits across 27 repos in the W6.P4.R1 sweep).
|
||||
|
||||
// escape-hatch — see D-coord-space.md § Documentation contract
|
||||
const dpr = window.devicePixelRatio
|
||||
// escape-hatch — see D-coord-space.md § Documentation contract
|
||||
const canvas = globalThis.app?.canvas
|
||||
if (!canvas) return
|
||||
// escape-hatch — see D-coord-space.md § Documentation contract
|
||||
const { scale } = canvas.ds
|
||||
// escape-hatch — see D-coord-space.md § Documentation contract
|
||||
const rect = canvas.canvas.getBoundingClientRect()
|
||||
|
||||
// From here, the author owns dpr math + canvas↔screen conversions.
|
||||
// The runtime makes no stability promise about ds.scale or the
|
||||
// shape of window.app.canvas — escape-hatch is intentionally
|
||||
// fragile per Axiom A13.
|
||||
void { dpr, scale, rect }
|
||||
}
|
||||
})
|
||||
@@ -44,7 +44,9 @@ function onCustomComboCreated(this: LGraphNode) {
|
||||
if (!node.widgets) return
|
||||
const newCount = node.widgets.length - 1
|
||||
const widgetName = `option${newCount}`
|
||||
const widget = node.addWidget('string', widgetName, '', () => {})
|
||||
const widget = node.addWidget('string', widgetName, '', () => {}, {
|
||||
__suppressDeprecationWarning: true
|
||||
})
|
||||
if (!widget) return
|
||||
let localValue = `${widget.value ?? ''}`
|
||||
|
||||
|
||||
32
src/extensions/core/dynamicPrompts.v2.ts
Normal file
32
src/extensions/core/dynamicPrompts.v2.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* DynamicPrompts — v2 extension API.
|
||||
*
|
||||
* STUBBED PENDING NEW PUBLIC API per D-no-node-widget-access (2026-05-19).
|
||||
*
|
||||
* The previous v2 implementation iterated `node.getWidgets()` inside
|
||||
* `defineNode.nodeCreated` to attach `beforeSerialize` handlers to every
|
||||
* widget with `options.dynamicPrompts === true`. That pattern is now
|
||||
* forbidden by the bilateral A1 closure: nodes cannot enumerate or
|
||||
* reference their widgets.
|
||||
*
|
||||
* The clean v2 path for "augment all widgets matching predicate P with
|
||||
* behavior B" does not yet exist on the public surface. `defineWidget`
|
||||
* registers a NEW widget type with a `mount` lifecycle; it does not
|
||||
* augment existing types' instances.
|
||||
*
|
||||
* Restoration plan (follow-up issue to file):
|
||||
* - Add a `defineWidgetAugmenter({ matches, setup })` (or per-widget
|
||||
* `setup(widget)` hook on `defineWidget`) so extensions can attach
|
||||
* behavior to existing-typed widgets opted in via `options`.
|
||||
* - Migrate this stub to that API once it ships.
|
||||
* - Update D-no-node-widget-access "Restoration criteria" if the
|
||||
* augmenter API requires loosening A1 (it shouldn't — the augmenter
|
||||
* hands the extension a `WidgetHandle` directly, never via a node).
|
||||
*
|
||||
* v1 (`Comfy.DynamicPrompts`) continues to work — this is a v2 surface
|
||||
* gap only, not a user-visible regression.
|
||||
*/
|
||||
|
||||
// Intentionally empty — no `defineNode` / `defineWidget` registration.
|
||||
// See block comment above for context.
|
||||
export {}
|
||||
18
src/extensions/core/imageCrop.v2.ts
Normal file
18
src/extensions/core/imageCrop.v2.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ImageCrop — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1: 13 lines, accesses node.size and node.constructor.comfyClass directly
|
||||
* v2: 12 lines, uses NodeHandle — type filtering via nodeTypes option
|
||||
*/
|
||||
|
||||
import { defineNode, type NodeHandle } from '@/extension-api'
|
||||
|
||||
defineNode({
|
||||
name: 'Comfy.ImageCrop.V2',
|
||||
nodeTypes: ['ImageCropV2'],
|
||||
|
||||
nodeCreated(node: NodeHandle) {
|
||||
const [w, h] = node.getSize()
|
||||
node.setSize([Math.max(w, 300), Math.max(h, 450)])
|
||||
}
|
||||
})
|
||||
@@ -4,6 +4,10 @@ import './clipspace'
|
||||
import './contextMenuFilter'
|
||||
import './customWidgets'
|
||||
import './dynamicPrompts'
|
||||
// v1 and v2 conversions are loaded side-by-side during the migration window
|
||||
// (D6 parallel paths). v2 extensions register under distinct names
|
||||
// (e.g. `Comfy.DynamicPrompts.V2`), so no idempotent guard is needed.
|
||||
import './dynamicPrompts.v2'
|
||||
import './editAttention'
|
||||
import './electronAdapter'
|
||||
import './groupNode'
|
||||
@@ -11,6 +15,7 @@ import './groupNodeManage'
|
||||
import './groupOptions'
|
||||
import './imageCompare'
|
||||
import './imageCrop'
|
||||
import './imageCrop.v2'
|
||||
// load3d and saveMesh are loaded on-demand to defer THREE.js (~1.8MB)
|
||||
// The lazy loader triggers loading when a 3D node is used
|
||||
import './load3dLazy'
|
||||
@@ -21,6 +26,7 @@ if (!isCloud) {
|
||||
import './noteNode'
|
||||
import './painter'
|
||||
import './previewAny'
|
||||
import './previewAny.v2'
|
||||
import './rerouteNode'
|
||||
import './saveImageExtraOutput'
|
||||
// saveMesh is loaded on-demand with load3d (see load3dLazy.ts)
|
||||
|
||||
@@ -272,9 +272,15 @@ useExtensionService().registerExtension({
|
||||
await handleModelUpload(fileInput.files!, node)
|
||||
}
|
||||
|
||||
node.addWidget('button', 'upload 3d model', 'upload3dmodel', () => {
|
||||
fileInput.click()
|
||||
})
|
||||
node.addWidget(
|
||||
'button',
|
||||
'upload 3d model',
|
||||
'upload3dmodel',
|
||||
() => {
|
||||
fileInput.click()
|
||||
},
|
||||
{ __suppressDeprecationWarning: true }
|
||||
)
|
||||
|
||||
const resourcesInput = createFileInput('*', true)
|
||||
|
||||
@@ -289,15 +295,24 @@ useExtensionService().registerExtension({
|
||||
'uploadExtraResources',
|
||||
() => {
|
||||
resourcesInput.click()
|
||||
}
|
||||
},
|
||||
{ __suppressDeprecationWarning: true }
|
||||
)
|
||||
|
||||
node.addWidget('button', 'clear', 'clear', () => {
|
||||
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
|
||||
if (modelWidget) {
|
||||
modelWidget.value = LOAD3D_NONE_MODEL
|
||||
}
|
||||
})
|
||||
node.addWidget(
|
||||
'button',
|
||||
'clear',
|
||||
'clear',
|
||||
() => {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model_file'
|
||||
)
|
||||
if (modelWidget) {
|
||||
modelWidget.value = LOAD3D_NONE_MODEL
|
||||
}
|
||||
},
|
||||
{ __suppressDeprecationWarning: true }
|
||||
)
|
||||
|
||||
const widget = new ComponentWidgetImpl({
|
||||
node: node,
|
||||
|
||||
124
src/extensions/core/noteNode.v2.ts
Normal file
124
src/extensions/core/noteNode.v2.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* NoteNode + MarkdownNoteNode — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1 used `registerCustomNodes` to call `LiteGraph.registerNodeType()` directly.
|
||||
* v2 does NOT yet have a `registerNodeType` hook on `defineExtension`. The
|
||||
* custom-node-type registration surface is a planned addition (gap tracked in
|
||||
* the inline comment below).
|
||||
*
|
||||
* What this file demonstrates to Simon/Austin:
|
||||
* 1. Pure app-level extensions use `defineExtension({ setup() })`.
|
||||
* 2. Shell settings are accessed via the `ExtensionManager` passed to `setup`.
|
||||
* 3. Custom LiteGraph node type registration has NO v2 equivalent yet.
|
||||
* The v2 API surface covers node *instance* hooks (nodeCreated, executed,
|
||||
* etc.) but not node *type* registration, which today still requires
|
||||
* LiteGraph.registerNodeType(). That gap will be addressed in PKG4 /
|
||||
* the ComfyNodeRegistry design.
|
||||
*
|
||||
* Compare with noteNode.ts (v1):
|
||||
* v1: registerCustomNodes() callback, direct LiteGraph + ComfyWidgets calls
|
||||
* v2: setup() callback, custom-node-type registration still needs v1 bridge
|
||||
*
|
||||
* API GAPS (feedback items for Simon/Austin):
|
||||
* GAP-1: No `registerNodeTypes` hook on `ExtensionOptions` — can't replace
|
||||
* `registerCustomNodes` in pure v2. Need a `NodeTypeRegistry` surface
|
||||
* or a first-class "custom node type" abstraction in the v2 API.
|
||||
* GAP-2: No `addWidget` for node *type* construction time (before any
|
||||
* instance exists) — `ComfyWidgets.STRING(this, ...)` has no analog.
|
||||
* GAP-3: Node colour + visual styling (`this.color`, `this.bgcolor`,
|
||||
* `this.groupcolor`) has no API surface; would need NodeHandle setter.
|
||||
*
|
||||
* Interim bridge: call LiteGraph directly inside `setup()` to register the
|
||||
* types, then rely on `defineNodeExtension({ nodeTypes: ['Note'] })` for any
|
||||
* per-instance extension logic. This hybrid is the least-bad option until
|
||||
* GAP-1 is closed.
|
||||
*/
|
||||
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { ComfyWidgets } from '../../scripts/widgets'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
|
||||
// ── GAP-1: Interim bridge for custom node type registration ──────────────────
|
||||
// We still call LiteGraph.registerNodeType() directly because there is no v2
|
||||
// `registerNodeTypes` hook. This is intentionally non-ideal — the explicit goal
|
||||
// is to surface this gap for the Simon/Austin design discussion.
|
||||
|
||||
function registerNoteTypes() {
|
||||
class NoteNode extends LGraphNode {
|
||||
static override category: string
|
||||
static collapsable: boolean
|
||||
static title_mode: number
|
||||
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
override isVirtualNode: boolean
|
||||
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
// GAP-3: node colour should be settable via NodeHandle in nodeCreated.
|
||||
this.color = LGraphCanvas.node_colors.yellow.color
|
||||
this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
if (!this.properties) this.properties = { text: '' }
|
||||
// GAP-2: no v2 analog for widget addition at type-construction time.
|
||||
ComfyWidgets.STRING(
|
||||
this,
|
||||
'text',
|
||||
['STRING', { default: this.properties.text, multiline: true }],
|
||||
// @ts-expect-error app not available at this layer
|
||||
undefined
|
||||
)
|
||||
this.serialize_widgets = true
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
'Note',
|
||||
Object.assign(NoteNode, {
|
||||
title_mode: LiteGraph.NORMAL_TITLE,
|
||||
title: 'Note',
|
||||
collapsable: true
|
||||
})
|
||||
)
|
||||
NoteNode.category = 'utils'
|
||||
|
||||
class MarkdownNoteNode extends LGraphNode {
|
||||
static override title = 'Markdown Note'
|
||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor
|
||||
|
||||
constructor(title: string) {
|
||||
super(title)
|
||||
this.color = LGraphCanvas.node_colors.yellow.color
|
||||
this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor
|
||||
if (!this.properties) this.properties = { text: '' }
|
||||
ComfyWidgets.MARKDOWN(
|
||||
this,
|
||||
'text',
|
||||
['STRING', { default: this.properties.text }],
|
||||
// @ts-expect-error app not available at this layer
|
||||
undefined
|
||||
)
|
||||
this.serialize_widgets = true
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType('MarkdownNote', MarkdownNoteNode)
|
||||
MarkdownNoteNode.category = 'utils'
|
||||
}
|
||||
|
||||
// ── v2 registration ──────────────────────────────────────────────────────────
|
||||
|
||||
defineExtension({
|
||||
name: 'Comfy.NoteNode.V2',
|
||||
|
||||
setup() {
|
||||
// GAP-1: Custom node types must be registered here via LiteGraph directly.
|
||||
// In the intended v2 design this would be a `registerNodeTypes(registry)`
|
||||
// hook on ExtensionOptions where `registry.add('Note', NoteNodeDef)`.
|
||||
registerNoteTypes()
|
||||
}
|
||||
})
|
||||
54
src/extensions/core/previewAny.v2.ts
Normal file
54
src/extensions/core/previewAny.v2.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* PreviewAny — rewritten with the v2 extension API.
|
||||
*
|
||||
* Compare with previewAny.ts (v1) which uses beforeRegisterNodeDef +
|
||||
* prototype patching + manual callback chaining.
|
||||
*
|
||||
* v1: 90 lines, prototype.onNodeCreated override, prototype.onExecuted override
|
||||
* v2: 35 lines, no prototype access, no manual chaining
|
||||
*/
|
||||
|
||||
import {
|
||||
defineNode,
|
||||
type NodeHandle,
|
||||
type NodeExecutedEvent,
|
||||
type WidgetValueChangeEvent
|
||||
} from '@/extension-api'
|
||||
|
||||
defineNode({
|
||||
name: 'Comfy.PreviewAny.V2',
|
||||
nodeTypes: ['PreviewAny'],
|
||||
|
||||
nodeCreated(node: NodeHandle) {
|
||||
const markdown = node.addWidget('MARKDOWN', 'preview_markdown', '', {
|
||||
hidden: true,
|
||||
readonly: true,
|
||||
serialize: false,
|
||||
label: 'Preview'
|
||||
})
|
||||
|
||||
const plaintext = node.addWidget('STRING', 'preview_text', '', {
|
||||
multiline: true,
|
||||
readonly: true,
|
||||
serialize: false,
|
||||
label: 'Preview'
|
||||
})
|
||||
|
||||
const toggle = node.addWidget('BOOLEAN', 'previewMode', false, {
|
||||
labelOn: 'Markdown',
|
||||
labelOff: 'Plaintext'
|
||||
})
|
||||
|
||||
toggle.on('valueChange', (e: WidgetValueChangeEvent) => {
|
||||
markdown.setHidden(!e.newValue)
|
||||
plaintext.setHidden(e.newValue as boolean)
|
||||
})
|
||||
|
||||
node.on('executed', (e: NodeExecutedEvent) => {
|
||||
const text = (e.output['text'] as string | string[]) ?? ''
|
||||
const content = Array.isArray(text) ? text.join('\n\n') : text
|
||||
markdown.setValue(content)
|
||||
plaintext.setValue(content)
|
||||
})
|
||||
}
|
||||
})
|
||||
353
src/extensions/core/rerouteNode.v2.ts
Normal file
353
src/extensions/core/rerouteNode.v2.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* RerouteNode — annotated port to the v2 extension API.
|
||||
*
|
||||
* v1 used `registerCustomNodes` to call `LiteGraph.registerNodeType()` with
|
||||
* a class that heavily overrides LiteGraph node behaviour (`onConnectionsChange`,
|
||||
* `clone`, `computeSize`, `getExtraMenuOptions`).
|
||||
*
|
||||
* RerouteNode is the *most v1-coupled* core extension: its entire value lives
|
||||
* in LiteGraph prototype methods. It is the intentional hard case for this
|
||||
* conversion exercise.
|
||||
*
|
||||
* What this file demonstrates to Simon/Austin:
|
||||
* 1. The `defineNodeExtension` pattern works only for *per-instance hooks*
|
||||
* — events that fire after a node exists. LiteGraph prototype overrides
|
||||
* (`onConnectionsChange`, `computeSize`, `clone`) fire synchronously
|
||||
* inside LiteGraph's own rendering loop and have no v2 equivalent.
|
||||
* 2. Custom context-menu contributions (`getExtraMenuOptions`) have no v2
|
||||
* surface. This is intentionally out of scope for the initial API.
|
||||
* 3. `localStorage` / settings persistence (`defaultVisibility`) works the
|
||||
* same in v2 — no v2 API involvement needed.
|
||||
*
|
||||
* API GAPS (feedback items for Simon/Austin):
|
||||
* GAP-1: (same as noteNode.v2) No `registerNodeTypes` hook — custom LiteGraph
|
||||
* node types cannot be registered via the v2 API.
|
||||
* GAP-7: No v2 hook for `onConnectionsChange`. This is a hot-path LiteGraph
|
||||
* callback that fires during canvas interaction. Mapping it to the v2
|
||||
* model would require an `NodeConnectedEvent` / `NodeDisconnectedEvent`
|
||||
* that fires SYNCHRONOUSLY and allows the handler to mutate outputs
|
||||
* and downstream nodes. Current v2 `node.on('connected')` is async-safe
|
||||
* and does not support synchronous output-type mutation.
|
||||
* GAP-8: No v2 surface for `getExtraMenuOptions` (context menu extension).
|
||||
* Would need an `onContextMenu(items)` hook on NodeExtensionOptions
|
||||
* that allows item injection.
|
||||
* GAP-9: `clone()` override. No v2 equivalent. If we want the cloned reroute
|
||||
* node to have its output reset, we'd need a post-copy lifecycle hook
|
||||
* (e.g. `nodeCopied(clone, source)`) which D12 explicitly deferred.
|
||||
* GAP-10: `computeSize()` override. Pure LiteGraph geometry; unlikely to
|
||||
* ever have a v2 equivalent. Extensions that need custom size should
|
||||
* either accept a fixed size or use a separate API.
|
||||
*
|
||||
* Conclusion: RerouteNode cannot be converted to pure v2 in the current API.
|
||||
* It is a LiteGraph-native "virtual node" with synchronous connection-type
|
||||
* propagation logic. The correct long-term path is to make RerouteNode a
|
||||
* first-class feature of the ComfyUI graph engine (not an extension at all)
|
||||
* and expose its behaviour through a higher-level abstraction.
|
||||
*
|
||||
* What *can* be expressed in v2 is shown in the `defineNodeExtension` block
|
||||
* below — the per-instance "user changed show/hide type" preference is a clean
|
||||
* v2 pattern. The rest remains in the v1 bridge.
|
||||
*/
|
||||
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISlotType } from '@/lib/litegraph/src/interfaces'
|
||||
import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
|
||||
// ── GAP-1: Interim bridge — LiteGraph node type registration ─────────────────
|
||||
|
||||
function registerRerouteType() {
|
||||
// Declaration-merging interface so the class gains `__outputType`.
|
||||
interface RerouteNode extends LGraphNode {
|
||||
__outputType?: string | number
|
||||
}
|
||||
|
||||
class RerouteNode extends LGraphNode {
|
||||
static override category: string | undefined
|
||||
static defaultVisibility = false
|
||||
|
||||
constructor(title?: string) {
|
||||
super(title ?? '')
|
||||
if (!this.properties) this.properties = {}
|
||||
this.properties.showOutputText = RerouteNode.defaultVisibility
|
||||
this.properties.horizontal = false
|
||||
this.addInput('', '*')
|
||||
this.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
this.setSize(this.computeSize())
|
||||
this.isVirtualNode = true
|
||||
}
|
||||
|
||||
override onAfterGraphConfigured() {
|
||||
requestAnimationFrame(() => {
|
||||
this.onConnectionsChange(LiteGraph.INPUT, undefined, true)
|
||||
})
|
||||
}
|
||||
|
||||
// GAP-9: This clone() override would need a v2 `nodeCopied` lifecycle hook.
|
||||
override clone(): LGraphNode | null {
|
||||
const cloned = super.clone()
|
||||
if (!cloned) return cloned
|
||||
cloned.removeOutput(0)
|
||||
cloned.addOutput(this.properties.showOutputText ? '*' : '', '*')
|
||||
cloned.setSize(cloned.computeSize())
|
||||
return cloned
|
||||
}
|
||||
|
||||
// GAP-7: onConnectionsChange cannot be expressed in v2 — synchronous
|
||||
// output-type mutation during connection is not supported by v2 event model.
|
||||
override onConnectionsChange(
|
||||
type: ISlotType,
|
||||
_index: number | undefined,
|
||||
connected: boolean
|
||||
) {
|
||||
const { graph } = this
|
||||
if (!graph) return
|
||||
// strangler-bridge:Phase-B — direct ComfyApp access; replace with
|
||||
// dispatch/world signal once the bridge audit lands (D9).
|
||||
// @ts-expect-error ComfyApp
|
||||
if (globalThis.app?.configuringGraph) return
|
||||
|
||||
if (connected && type === LiteGraph.OUTPUT) {
|
||||
const types = new Set(
|
||||
this.outputs[0].links
|
||||
?.map((l) => graph.links[l]?.type)
|
||||
?.filter((t) => t && t !== '*') ?? []
|
||||
)
|
||||
if (types.size > 1) {
|
||||
const linksToDisconnect = []
|
||||
for (const linkId of this.outputs[0].links ?? []) {
|
||||
linksToDisconnect.push(graph.links[linkId])
|
||||
}
|
||||
linksToDisconnect.pop()
|
||||
for (const link of linksToDisconnect) {
|
||||
if (!link) continue
|
||||
const node = graph.getNodeById(link.target_id)
|
||||
node?.disconnectInput(link.target_slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let currentNode: RerouteNode | null = this
|
||||
let updateNodes: RerouteNode[] = []
|
||||
let inputType = null
|
||||
let inputNode = null
|
||||
while (currentNode) {
|
||||
updateNodes.unshift(currentNode)
|
||||
const linkId = currentNode.inputs[0].link
|
||||
if (linkId !== null) {
|
||||
const link = graph.links[linkId]
|
||||
if (!link) return
|
||||
const node = graph.getNodeById(link.origin_id)
|
||||
if (!node) return
|
||||
if (node instanceof RerouteNode) {
|
||||
if (node === this) {
|
||||
currentNode.disconnectInput(link.target_slot)
|
||||
currentNode = null
|
||||
} else {
|
||||
currentNode = node
|
||||
}
|
||||
} else {
|
||||
inputNode = currentNode
|
||||
inputType = node.outputs[link.origin_slot]?.type ?? null
|
||||
break
|
||||
}
|
||||
} else {
|
||||
currentNode = null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: RerouteNode[] = [this]
|
||||
let outputType = null
|
||||
while (nodes.length) {
|
||||
currentNode = nodes.pop()!
|
||||
const outputs = currentNode.outputs?.[0]?.links ?? []
|
||||
for (const linkId of outputs) {
|
||||
const link = graph.links[linkId]
|
||||
if (!link) continue
|
||||
const node = graph.getNodeById(link.target_id)
|
||||
if (!node) continue
|
||||
if (node instanceof RerouteNode) {
|
||||
nodes.push(node)
|
||||
updateNodes.push(node)
|
||||
} else {
|
||||
const nodeInput = node.inputs[link.target_slot]
|
||||
const nodeOutType = nodeInput.type
|
||||
const keep =
|
||||
!inputType ||
|
||||
!nodeOutType ||
|
||||
LiteGraph.isValidConnection(inputType, nodeOutType)
|
||||
if (!keep) {
|
||||
node.disconnectInput(link.target_slot)
|
||||
continue
|
||||
}
|
||||
node.onConnectionsChange?.(
|
||||
LiteGraph.INPUT,
|
||||
link.target_slot,
|
||||
keep,
|
||||
link,
|
||||
nodeInput
|
||||
)
|
||||
outputType = node.inputs[link.target_slot].type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const displayType = inputType || outputType || '*'
|
||||
const color = LGraphCanvas.link_type_colors[displayType]
|
||||
|
||||
let widgetConfig
|
||||
let widgetType
|
||||
for (const node of updateNodes) {
|
||||
node.outputs[0].type = inputType || '*'
|
||||
node.__outputType = displayType
|
||||
node.outputs[0].name = node.properties.showOutputText
|
||||
? `${displayType}`
|
||||
: ''
|
||||
node.setSize(node.computeSize())
|
||||
for (const l of node.outputs[0].links || []) {
|
||||
const link = graph.links[l]
|
||||
if (!link) continue
|
||||
link.color = color
|
||||
// strangler-bridge:Phase-B — direct ComfyApp access; replace with
|
||||
// dispatch/world signal once the bridge audit lands (D9).
|
||||
// @ts-expect-error ComfyApp
|
||||
if (globalThis.app?.configuringGraph) continue
|
||||
const targetNode = graph.getNodeById(link.target_id)
|
||||
if (!targetNode) continue
|
||||
const targetInput = targetNode.inputs?.[link.target_slot]
|
||||
if (targetInput?.widget) {
|
||||
const config = getWidgetConfig(targetInput)
|
||||
if (!widgetConfig) {
|
||||
widgetConfig = config[1] ?? {}
|
||||
widgetType = config[0]
|
||||
}
|
||||
const merged = mergeIfValid(targetInput, [config[0], widgetConfig])
|
||||
if (merged.customConfig) widgetConfig = merged.customConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of updateNodes) {
|
||||
if (widgetConfig && outputType) {
|
||||
node.inputs[0].widget = { name: 'value' }
|
||||
setWidgetConfig(node.inputs[0], [
|
||||
widgetType ?? `${displayType}`,
|
||||
widgetConfig
|
||||
])
|
||||
} else {
|
||||
setWidgetConfig(node.inputs[0], undefined)
|
||||
}
|
||||
}
|
||||
|
||||
if (inputNode?.inputs?.[0]?.link) {
|
||||
const link = graph.links[inputNode.inputs[0].link]
|
||||
if (link) link.color = color
|
||||
}
|
||||
}
|
||||
|
||||
// GAP-8: getExtraMenuOptions has no v2 equivalent.
|
||||
override getExtraMenuOptions(
|
||||
_: unknown,
|
||||
options: (IContextMenuValue | null)[]
|
||||
): IContextMenuValue[] {
|
||||
options.unshift(
|
||||
{
|
||||
content: (this.properties.showOutputText ? 'Hide' : 'Show') + ' Type',
|
||||
callback: () => {
|
||||
this.properties.showOutputText = !this.properties.showOutputText
|
||||
if (this.properties.showOutputText) {
|
||||
this.outputs[0].name = `${this.__outputType || this.outputs[0].type}`
|
||||
} else {
|
||||
this.outputs[0].name = ''
|
||||
}
|
||||
this.setSize(this.computeSize())
|
||||
// strangler-bridge:Phase-B — direct ComfyApp access; replace with
|
||||
// dispatch/world signal once the bridge audit lands (D9).
|
||||
// @ts-expect-error ComfyApp
|
||||
globalThis.app?.canvas?.setDirty(true, true)
|
||||
}
|
||||
},
|
||||
{
|
||||
content:
|
||||
(RerouteNode.defaultVisibility ? 'Hide' : 'Show') +
|
||||
' Type By Default',
|
||||
callback: () => {
|
||||
RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility)
|
||||
}
|
||||
}
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
// GAP-10: computeSize override — no v2 surface.
|
||||
override computeSize(): [number, number] {
|
||||
return [
|
||||
this.properties.showOutputText && this.outputs?.length
|
||||
? Math.max(
|
||||
75,
|
||||
LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40
|
||||
)
|
||||
: 75,
|
||||
26
|
||||
]
|
||||
}
|
||||
|
||||
static setDefaultTextVisibility(visible: boolean) {
|
||||
RerouteNode.defaultVisibility = visible
|
||||
if (visible) {
|
||||
localStorage['Comfy.RerouteNode.DefaultVisibility'] = 'true'
|
||||
} else {
|
||||
delete localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RerouteNode.setDefaultTextVisibility(
|
||||
!!localStorage['Comfy.RerouteNode.DefaultVisibility']
|
||||
)
|
||||
|
||||
LiteGraph.registerNodeType(
|
||||
'Reroute',
|
||||
Object.assign(RerouteNode, {
|
||||
title_mode: LiteGraph.NO_TITLE,
|
||||
title: 'Reroute',
|
||||
collapsable: false
|
||||
})
|
||||
)
|
||||
RerouteNode.category = 'utils'
|
||||
}
|
||||
|
||||
// ── v2: app-level registration (GAP-1 bridge) ─────────────────────────────────
|
||||
|
||||
defineExtension({
|
||||
name: 'Comfy.RerouteNode.V2',
|
||||
setup() {
|
||||
registerRerouteType()
|
||||
}
|
||||
})
|
||||
|
||||
// ── v2: what *can* be expressed cleanly ──────────────────────────────────────
|
||||
// The context-menu "Show/Hide Type" toggle persists a preference to localStorage.
|
||||
// In a fully realized v2 API this would live here. Today it's inside the
|
||||
// LiteGraph class because there's no v2 hook for per-node menu items (GAP-8).
|
||||
//
|
||||
// If GAP-7 (synchronous connection-type propagation) were solved, the
|
||||
// onConnectionsChange logic above could become:
|
||||
//
|
||||
// defineNodeExtension({
|
||||
// name: 'Comfy.RerouteNode.V2',
|
||||
// nodeTypes: ['Reroute'],
|
||||
// nodeCreated(node) {
|
||||
// node.on('connected', (e) => propagateType(node, e))
|
||||
// node.on('disconnected', (e) => propagateType(node, e))
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// That path requires the connected/disconnected events to be synchronous
|
||||
// and to carry a mutable output descriptor — a non-trivial API contract.
|
||||
73
src/extensions/core/slotDefaults.v2.ts
Normal file
73
src/extensions/core/slotDefaults.v2.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* SlotDefaults — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1 used `init` + `beforeRegisterNodeDef` + direct `app.ui.settings.addSetting`.
|
||||
* v2 uses `defineExtension({ setup(ext) })`. The `ExtensionManager` passed to
|
||||
* `setup` exposes `setting.get/set` but NOT `addSetting` — that gap is noted below.
|
||||
*
|
||||
* What this file demonstrates to Simon/Austin:
|
||||
* 1. App-level extensions (init/setup) map cleanly to `defineExtension`.
|
||||
* 2. `beforeRegisterNodeDef` has no v2 equivalent — node type metadata is not
|
||||
* surfaced through the v2 API at registration time.
|
||||
* 3. `app.ui.settings.addSetting` (declares a new setting with slider + label)
|
||||
* has no v2 `ExtensionManager` surface.
|
||||
*
|
||||
* API GAPS (feedback items for Simon/Austin):
|
||||
* GAP-4: No `beforeRegisterNodeDef` hook on `ExtensionOptions`. This hook
|
||||
* fires *once per node type*, before any instance exists, giving access
|
||||
* to `nodeData` (input/output schema). Needed for type-level analysis
|
||||
* (e.g. slot type registry). Candidate: `onNodeTypeRegistered(typeDef)`.
|
||||
* GAP-5: `ExtensionManager.setting` exposes only `get/set`. It does NOT
|
||||
* expose `addSetting` (declare a new setting with UI metadata, type,
|
||||
* default, onChange callback). Needed for extensions that contribute
|
||||
* settings to the settings dialog. Candidate: extend the `setting`
|
||||
* interface with `add(spec: SettingSpec)`.
|
||||
* GAP-6: `LiteGraph.registered_slot_in_types` / `slot_types_out` are
|
||||
* global LiteGraph state mutated here. No v2 abstraction exists for
|
||||
* the "node suggestions" subsystem. Low priority — this is fine to
|
||||
* keep calling LiteGraph directly as an implementation detail.
|
||||
*
|
||||
* Interim strategy: `setup()` falls back to direct LiteGraph manipulation for
|
||||
* slot type data. The settings contribution stays as a TODO annotation until
|
||||
* GAP-5 is resolved.
|
||||
*/
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { defineExtension } from '@/extension-api'
|
||||
|
||||
// ── v2 registration ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @remarks
|
||||
* **DEMO — incomplete migration.** Compared to v1
|
||||
* (`src/extensions/core/slotDefaults.ts`), this v2 port currently only sets
|
||||
* `LiteGraph.search_filter_enabled = true`. The following v1 features are
|
||||
* **not yet ported** and stay as feedback items for Simon/Austin:
|
||||
*
|
||||
* - Node-type metadata accumulation via `beforeRegisterNodeDef` (GAP-4)
|
||||
* - Settings-dialog contribution for the suggestion-count slider (GAP-5)
|
||||
* - LiteGraph slot-type registry mutation (GAP-6, low priority)
|
||||
*
|
||||
* Do not rely on this extension for slot-default behavior in PoC bring-up —
|
||||
* load the v1 `slotDefaults.ts` instead, or wait for the gaps above to land.
|
||||
*/
|
||||
defineExtension({
|
||||
name: 'Comfy.SlotDefaults.V2 (DEMO — incomplete migration)',
|
||||
|
||||
init() {
|
||||
LiteGraph.search_filter_enabled = true
|
||||
},
|
||||
|
||||
setup() {
|
||||
// GAP-5: In v1, `app.ui.settings.addSetting(spec)` declared a user-facing
|
||||
// slider in the settings dialog with an onChange callback. In v2,
|
||||
// `defineExtension({ setup })` takes no arguments — the ExtensionManager
|
||||
// is not yet plumbed into the setup callback. Until GAP-5 is resolved,
|
||||
// we cannot register the user-facing setting from a v2 extension.
|
||||
//
|
||||
// GAP-4: In v1, `beforeRegisterNodeDef(nodeType, nodeData)` processed each
|
||||
// node type's input/output schema. In v2 there is no equivalent hook.
|
||||
// The slot-type accumulator logic from v1 cannot be ported until
|
||||
// `onNodeTypeRegistered(def)` or equivalent is added to the API.
|
||||
}
|
||||
})
|
||||
@@ -121,7 +121,9 @@ app.registerExtension({
|
||||
audio.setAttribute('name', 'media')
|
||||
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement, string> =
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio, {
|
||||
__suppressDeprecationWarning: true
|
||||
})
|
||||
audioUIWidget.serialize = false
|
||||
const { nodeData } = node.constructor
|
||||
if (nodeData == null) throw new TypeError('nodeData is null')
|
||||
@@ -263,7 +265,11 @@ app.registerExtension({
|
||||
inputName,
|
||||
'',
|
||||
openFileSelection,
|
||||
{ serialize: false, canvasOnly: true }
|
||||
{
|
||||
serialize: false,
|
||||
canvasOnly: true,
|
||||
__suppressDeprecationWarning: true
|
||||
}
|
||||
)
|
||||
uploadWidget.label = t('g.choose_file_to_upload')
|
||||
|
||||
@@ -296,7 +302,9 @@ app.registerExtension({
|
||||
audio.classList.add('comfy-audio')
|
||||
audio.setAttribute('name', 'media')
|
||||
const audioUIWidget: DOMWidget<HTMLAudioElement, string> =
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
|
||||
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio, {
|
||||
__suppressDeprecationWarning: true
|
||||
})
|
||||
audioUIWidget.options.canvasOnly = false
|
||||
|
||||
let mediaRecorder: MediaRecorder | null = null
|
||||
@@ -420,7 +428,11 @@ app.registerExtension({
|
||||
mediaRecorder.stop()
|
||||
}
|
||||
},
|
||||
{ serialize: false, canvasOnly: false }
|
||||
{
|
||||
serialize: false,
|
||||
canvasOnly: false,
|
||||
__suppressDeprecationWarning: true
|
||||
}
|
||||
)
|
||||
|
||||
recordWidget.label = t('g.startRecording')
|
||||
|
||||
@@ -64,7 +64,11 @@ app.registerExtension({
|
||||
|
||||
loadVideo()
|
||||
|
||||
return { widget: node.addDOMWidget(inputName, 'WEBCAM', container) }
|
||||
return {
|
||||
widget: node.addDOMWidget(inputName, 'WEBCAM', container, {
|
||||
__suppressDeprecationWarning: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -111,7 +115,7 @@ app.registerExtension({
|
||||
'waiting for camera...',
|
||||
'capture',
|
||||
capture,
|
||||
{}
|
||||
{ __suppressDeprecationWarning: true }
|
||||
)
|
||||
btn.disabled = true
|
||||
btn.serializeValue = () => undefined
|
||||
|
||||
110
src/extensions/core/webcamCapture.v2.ts
Normal file
110
src/extensions/core/webcamCapture.v2.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* WebcamCapture — rewritten with the v2 extension API.
|
||||
*
|
||||
* v1: registers the `WEBCAM` custom widget type via `getCustomWidgets()`
|
||||
* returning `node.addDOMWidget(name, 'WEBCAM', container)`, then a
|
||||
* separate `nodeCreated` reaches into `node.widgets` to wire the
|
||||
* capture button and `serializeValue` override.
|
||||
*
|
||||
* v2: registers the `WEBCAM` widget type via `defineWidget({ type, mount })`
|
||||
* per **Axiom A12** — the mount-lifecycle hook is the sole DOM seam.
|
||||
* The mount body captures `host` (and the constructed `<video>`) via
|
||||
* closure; there is no `widget.element` accessor on `WidgetHandle`.
|
||||
* Cleanup stops the camera stream when the widget is destroyed
|
||||
* (D-widget-converge §Clarification 1: cleanup = destruction-only).
|
||||
*
|
||||
* The `nodeCreated` half of the v1 extension (wiring the capture button +
|
||||
* serializeValue override) surfaces several gaps already tracked under
|
||||
* I-COORD.1 — see GAP comments inline.
|
||||
*/
|
||||
|
||||
import { defineNode, defineWidget, type NodeHandle } from '@/extension-api'
|
||||
|
||||
// ── defineWidget — Axiom A12 mount-lifecycle seam ───────────────────────────
|
||||
|
||||
export default defineWidget({
|
||||
name: 'Comfy.WebcamCapture.V2.Widget',
|
||||
type: 'WEBCAM',
|
||||
|
||||
mount(host, ctx) {
|
||||
const container = document.createElement('div')
|
||||
container.style.background = 'rgba(0,0,0,0.25)'
|
||||
container.style.textAlign = 'center'
|
||||
|
||||
const video = document.createElement('video')
|
||||
video.style.height = video.style.width = '100%'
|
||||
|
||||
let stream: MediaStream | null = null
|
||||
|
||||
const loadVideo = async () => {
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false
|
||||
})
|
||||
container.replaceChildren(video)
|
||||
video.srcObject = stream
|
||||
await video.play()
|
||||
} catch (error) {
|
||||
const label = document.createElement('div')
|
||||
label.style.color = 'red'
|
||||
label.style.overflow = 'auto'
|
||||
label.style.maxHeight = '100%'
|
||||
label.style.whiteSpace = 'pre-wrap'
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
label.textContent = window.isSecureContext
|
||||
? `Unable to load webcam, please ensure access is granted:\n${message}`
|
||||
: `Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n${message}`
|
||||
|
||||
container.replaceChildren(label)
|
||||
}
|
||||
}
|
||||
|
||||
host.appendChild(container)
|
||||
void loadVideo()
|
||||
|
||||
// Re-bind the video element to the new host on remount (graph ↔ app
|
||||
// mode swap, subgraph promotion). Mount body is NOT re-invoked per
|
||||
// D-widget-converge §Clarification 1.
|
||||
ctx.onAfterRemount((newHost) => {
|
||||
newHost.appendChild(container)
|
||||
})
|
||||
|
||||
// Destruction-only cleanup: stop the camera stream + release tracks.
|
||||
return () => {
|
||||
stream?.getTracks().forEach((t) => t.stop())
|
||||
stream = null
|
||||
video.srcObject = null
|
||||
container.remove()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ── Companion defineNode — capture button + serializeValue wiring ──────────
|
||||
//
|
||||
// The `WebcamCapture` node-side logic still has open v2 surface gaps:
|
||||
// GAP-2 (I-COORD.1): no type-construction `addWidget('button', …)` on
|
||||
// NodeHandle — the v1 path adds a button programmatically inside
|
||||
// `nodeCreated` to drive `capture()`.
|
||||
// GAP-11 (new): no `widget.serializeValue = async () => …` setter
|
||||
// on WidgetHandle. The v2 path is `widget.on('beforeSerialize',
|
||||
// e => e.setSerializedValue(…))`, but the v1 override is *async*
|
||||
// (uploads to /upload/image and awaits the response); the
|
||||
// `beforeSerialize` payload shape (D5) does not yet promise async
|
||||
// resolution. Tracked separately — do not unblock here.
|
||||
// Until those land, the node-side companion stays a v1 extension. The
|
||||
// `defineNode` below is a placeholder that registers the type filter so
|
||||
// downstream tooling can correlate the v2 widget registration with the
|
||||
// node that consumes it.
|
||||
|
||||
defineNode({
|
||||
name: 'Comfy.WebcamCapture.V2.Node',
|
||||
nodeTypes: ['WebcamCapture'],
|
||||
|
||||
nodeCreated(_node: NodeHandle) {
|
||||
// Wiring deferred — see GAP-2 / GAP-11 above. The v1 extension's
|
||||
// nodeCreated body remains the authoritative implementation until
|
||||
// those gaps close.
|
||||
}
|
||||
})
|
||||
@@ -224,6 +224,17 @@ export interface LGraphNode {
|
||||
|
||||
// #endregion Types
|
||||
|
||||
/**
|
||||
* Shared deprecation message for the v1 `addWidget` / `addCustomWidget` /
|
||||
* `addDOMWidget` family. Sharing one message string lets `warnDeprecated`
|
||||
* deduplicate naturally — calling `addWidget` warns once per session even
|
||||
* if it internally invokes `addCustomWidget`. See AXIOMS.md A15 and
|
||||
* `decisions/D-ban-runtime-addwidget.md`.
|
||||
*/
|
||||
const ADD_WIDGET_DEPRECATION_MESSAGE =
|
||||
"LGraphNode.addWidget(...) / addCustomWidget(...) / addDOMWidget(...) is deprecated and will be removed in v1.0. Widgets must be declared in the Python node's INPUT_TYPES per AXIOMS.md A15 (Widget Declarativity). Migration paths: (1) boxed widget — declare a single rich-value INPUT_TYPES field (e.g. BBOX [x,y,w,h]) and compose the multi-control UI against it; (2) non-widget UI primitive — mount custom DOM from defineNode/defineExtension setup() via bootstrap-hooks, owned by the extension not the widget system; (3) schema input — add the field to INPUT_TYPES and let the frontend render it normally. See AXIOMS.md A15 and decisions/D-ban-runtime-addwidget.md."
|
||||
export { ADD_WIDGET_DEPRECATION_MESSAGE }
|
||||
|
||||
/**
|
||||
* Base class for all nodes
|
||||
* @param title a name for the node
|
||||
@@ -1930,6 +1941,21 @@ export class LGraphNode
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Runtime widget addition will be removed in v1.0 per
|
||||
* **AXIOMS.md A15 (Widget Declarativity)**. Widgets must be declared in
|
||||
* the Python node's `INPUT_TYPES`. Migration paths:
|
||||
*
|
||||
* 1. **Boxed widget** — declare a single rich-value `INPUT_TYPES` field
|
||||
* (e.g. `BBOX [x,y,w,h]`) and compose the multi-control UI inside one
|
||||
* widget against that one value.
|
||||
* 2. **Non-widget UI primitive** — mount custom DOM from
|
||||
* `defineNode` / `defineExtension` `setup()` via the bootstrap-hooks
|
||||
* lifecycle. Owned by the extension, not the widget system.
|
||||
* 3. **Schema input** — add the field to Python `INPUT_TYPES` and let the
|
||||
* frontend render it normally.
|
||||
*
|
||||
* See `AXIOMS.md` §A15 and `decisions/D-ban-runtime-addwidget.md`.
|
||||
*
|
||||
* Defines a widget inside the node, it will be rendered on top of the node, you can control lots of properties
|
||||
* @param type the widget type
|
||||
* @param name the text to show on the widget
|
||||
@@ -1948,6 +1974,13 @@ export class LGraphNode
|
||||
callback: IBaseWidget['callback'] | string | null,
|
||||
options?: IWidgetOptions | string
|
||||
): WidgetTypeMap[Type] | IBaseWidget {
|
||||
const suppress =
|
||||
typeof options === 'object' &&
|
||||
options !== null &&
|
||||
(options as IWidgetOptions).__suppressDeprecationWarning === true
|
||||
if (!suppress) {
|
||||
warnDeprecated(ADD_WIDGET_DEPRECATION_MESSAGE, this)
|
||||
}
|
||||
this.widgets ||= []
|
||||
|
||||
if (!options && callback && typeof callback === 'object') {
|
||||
@@ -1993,9 +2026,21 @@ export class LGraphNode
|
||||
return widget
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Runtime widget addition will be removed in v1.0 per
|
||||
* **AXIOMS.md A15 (Widget Declarativity)**. See {@link addWidget} for
|
||||
* the three migration paths (boxed widget / non-widget UI primitive /
|
||||
* schema input). Reference: `decisions/D-ban-runtime-addwidget.md`.
|
||||
*/
|
||||
addCustomWidget<TPlainWidget extends IBaseWidget>(
|
||||
custom_widget: TPlainWidget
|
||||
): TPlainWidget | WidgetTypeMap[TPlainWidget['type']] {
|
||||
const customOptions = (custom_widget as { options?: IWidgetOptions })
|
||||
.options
|
||||
const suppress = customOptions?.__suppressDeprecationWarning === true
|
||||
if (!suppress) {
|
||||
warnDeprecated(ADD_WIDGET_DEPRECATION_MESSAGE, this)
|
||||
}
|
||||
this.widgets ||= []
|
||||
const widget = toConcreteWidget(custom_widget, this, false) ?? custom_widget
|
||||
this.widgets.push(widget)
|
||||
|
||||
@@ -21,6 +21,15 @@ export interface NodeBindable {
|
||||
}
|
||||
|
||||
export interface IWidgetOptions<TValues = unknown> {
|
||||
/**
|
||||
* @internal
|
||||
* Suppresses the `LGraphNode.addWidget` / `addCustomWidget` / `addDOMWidget`
|
||||
* deprecation warning (D-ban-runtime-addwidget / AXIOMS.md A15) for
|
||||
* first-party core call sites that have not yet been migrated to a
|
||||
* schema-declared widget, boxed widget, or non-widget UI primitive.
|
||||
* NOT part of the public surface — stripped before serialization.
|
||||
*/
|
||||
__suppressDeprecationWarning?: boolean
|
||||
on?: string
|
||||
off?: string
|
||||
max?: number
|
||||
|
||||
@@ -59,6 +59,10 @@ import {
|
||||
} from '@/scripts/domWidget'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import {
|
||||
invokeV2AppExtensions,
|
||||
startExtensionSystem
|
||||
} from '@/services/extension-api-service'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useSubgraphService } from '@/services/subgraphService'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
@@ -855,6 +859,10 @@ export class ComfyApp {
|
||||
//Doesn't need to block. Blueprints will load async
|
||||
void useSubgraphStore().fetchSubgraphs()
|
||||
await useExtensionService().loadExtensions()
|
||||
// Start the v2 node-extension reactive mount watcher. Must run after
|
||||
// loadExtensions() so all defineNode() calls have pushed into
|
||||
// nodeExtensions[] before the first watcher tick.
|
||||
startExtensionSystem()
|
||||
|
||||
this.addProcessKeyHandler()
|
||||
this.addConfigureHandler()
|
||||
@@ -949,11 +957,13 @@ export class ComfyApp {
|
||||
})
|
||||
|
||||
await useExtensionService().invokeExtensionsAsync('init')
|
||||
await invokeV2AppExtensions('init')
|
||||
await this.registerNodes()
|
||||
|
||||
this.addDropHandler()
|
||||
|
||||
await useExtensionService().invokeExtensionsAsync('setup')
|
||||
await invokeV2AppExtensions('setup')
|
||||
|
||||
this.positionConversion = useCanvasPositionConversion(
|
||||
this.canvasContainer,
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat'
|
||||
import { type Component, toRaw } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { ADD_WIDGET_DEPRECATION_MESSAGE } from '@/lib/litegraph/src/LGraphNode'
|
||||
import {
|
||||
LGraphNode,
|
||||
LegacyWidget,
|
||||
@@ -11,6 +12,7 @@ import type {
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import { warnDeprecated } from '@/lib/litegraph/src/utils/feedback'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
@@ -358,6 +360,14 @@ export const addWidget = <W extends BaseDOMWidget<object | string>>(
|
||||
node: LGraphNode,
|
||||
widget: W
|
||||
) => {
|
||||
// Internal first-party registration path — silence the v1
|
||||
// `addCustomWidget` deprecation warning per
|
||||
// decisions/D-ban-runtime-addwidget.md. Third-party callers of
|
||||
// `LGraphNode.addCustomWidget` still see the warning.
|
||||
;(widget as { options?: IWidgetOptions }).options ||= {}
|
||||
;(
|
||||
widget as { options: IWidgetOptions }
|
||||
).options.__suppressDeprecationWarning = true
|
||||
node.addCustomWidget(widget)
|
||||
|
||||
if (node.graph) {
|
||||
@@ -378,6 +388,13 @@ export const addWidget = <W extends BaseDOMWidget<object | string>>(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Runtime DOM widget addition will be removed in v1.0 per
|
||||
* **AXIOMS.md A15 (Widget Declarativity)**. Custom DOM widgets register
|
||||
* via `defineWidget({ type, mount })` (D-widget-converge / Axiom A12) and
|
||||
* are instantiated through schema declarations in Python `INPUT_TYPES`.
|
||||
* See AXIOMS.md A15 and `decisions/D-ban-runtime-addwidget.md`.
|
||||
*/
|
||||
LGraphNode.prototype.addDOMWidget = function <
|
||||
T extends HTMLElement,
|
||||
V extends object | string
|
||||
@@ -388,6 +405,9 @@ LGraphNode.prototype.addDOMWidget = function <
|
||||
element: T,
|
||||
options: DOMWidgetOptions<V> = {}
|
||||
): DOMWidget<T, V> {
|
||||
if (options.__suppressDeprecationWarning !== true) {
|
||||
warnDeprecated(ADD_WIDGET_DEPRECATION_MESSAGE, this)
|
||||
}
|
||||
const widget = new DOMWidgetImpl({
|
||||
node: this,
|
||||
name,
|
||||
|
||||
@@ -135,7 +135,8 @@ export function addValueControlWidgets(
|
||||
{
|
||||
values: ['fixed', 'increment', 'decrement', 'randomize'],
|
||||
serialize: false, // Don't include this in prompt.
|
||||
canvasOnly: true
|
||||
canvasOnly: true,
|
||||
__suppressDeprecationWarning: true
|
||||
}
|
||||
) as IComboWidget
|
||||
|
||||
@@ -161,7 +162,8 @@ export function addValueControlWidgets(
|
||||
'',
|
||||
function () {},
|
||||
{
|
||||
serialize: false // Don't include this in prompt.
|
||||
serialize: false, // Don't include this in prompt.
|
||||
__suppressDeprecationWarning: true
|
||||
}
|
||||
) as IStringWidget
|
||||
updateControlWidgetLabel(comboFilter)
|
||||
|
||||
420
src/services/__tests__/scope-registry.test.ts
Normal file
420
src/services/__tests__/scope-registry.test.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
// D12 regression: copy/paste must yield reset-to-fresh extensionState, not clone.
|
||||
// Decision: decisions/D12-scope-clone-on-copy.md — Option (c) accepted.
|
||||
// Task: I-SR.2.B5
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// ── Mock world ────────────────────────────────────────────────────────────────
|
||||
// The world must be mocked before the service is imported so the import-time
|
||||
// getWorld() call picks up the mock. We fake only what the service exercises:
|
||||
// getComponent() with NodeTypeKey (gates mountExtensionsForNode early-return)
|
||||
// and entitiesWith() (used by startExtensionSystem's watch — not exercised here).
|
||||
|
||||
const mockGetComponent = vi.fn()
|
||||
const mockEntitiesWith = vi.fn(() => [])
|
||||
|
||||
vi.mock('@/world/worldInstance', () => ({
|
||||
getWorld: () => ({
|
||||
getComponent: mockGetComponent,
|
||||
entitiesWith: mockEntitiesWith,
|
||||
setComponent: vi.fn(),
|
||||
removeComponent: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
// Widget/node component modules must exist as mock modules so the service's
|
||||
// top-level imports don't fail. The actual values are opaque keys; we just need
|
||||
// them to be non-null references so `getComponent(id, key)` calls resolve.
|
||||
vi.mock('@/world/widgets/widgetComponents', () => ({
|
||||
WidgetComponentContainer: Symbol('WidgetComponentContainer'),
|
||||
WidgetComponentDisplay: Symbol('WidgetComponentDisplay'),
|
||||
WidgetComponentSchema: Symbol('WidgetComponentSchema'),
|
||||
WidgetComponentSerialize: Symbol('WidgetComponentSerialize'),
|
||||
WidgetComponentValue: Symbol('WidgetComponentValue')
|
||||
}))
|
||||
|
||||
vi.mock('@/world/entityIds', () => ({}))
|
||||
|
||||
// defineComponentKey returns an identity object; tests don't need real ECS queries.
|
||||
vi.mock('@/world/componentKey', () => ({
|
||||
defineComponentKey: (name: string) => ({ name })
|
||||
}))
|
||||
|
||||
// extension-api types: service re-exports from these, mocking prevents import errors.
|
||||
vi.mock('@/extension-api/node', () => ({}))
|
||||
vi.mock('@/extension-api/widget', () => ({}))
|
||||
vi.mock('@/extension-api/lifecycle', () => ({}))
|
||||
|
||||
// ── Import service (after mocks are in place) ────────────────────────────────
|
||||
import {
|
||||
_clearExtensionsForTesting,
|
||||
defineNode,
|
||||
getCurrentScope,
|
||||
getScopeRegistry,
|
||||
mountExtensionsForNode,
|
||||
onNodeMounted,
|
||||
onNodeRemoved,
|
||||
unmountExtensionsForNode
|
||||
} from '../extension-api-service'
|
||||
import type { NodeEntityId } from '@/world/entityIds'
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function makeNodeId(n: number): NodeEntityId {
|
||||
return `node:graph-uuid-test:${n}` as NodeEntityId
|
||||
}
|
||||
|
||||
function stubNodeType(nodeEntityId: NodeEntityId, comfyClass = 'TestNode') {
|
||||
mockGetComponent.mockImplementation((id, key) => {
|
||||
if (id === nodeEntityId && key?.name === 'NodeType')
|
||||
return { type: comfyClass, comfyClass }
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('scope-registry — D12 copy/paste reset semantics', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
_clearExtensionsForTesting()
|
||||
;[1, 2, 3, 4, 100, 101, 102, 103].forEach((n) => {
|
||||
unmountExtensionsForNode(makeNodeId(n))
|
||||
})
|
||||
})
|
||||
|
||||
describe('D12(c): clone gets fresh extensionState, not source value', () => {
|
||||
it('copy-pasted node (new entityId) starts with setup-default extensionState, not mutated source state', () => {
|
||||
const SOURCE_ID = makeNodeId(1)
|
||||
const CLONE_ID = makeNodeId(2)
|
||||
|
||||
// Register an extension that initialises count = 0 every time setup runs.
|
||||
// The extension stores a ref so we can mutate it after mount.
|
||||
const counters = new Map<NodeEntityId, ReturnType<typeof ref>>()
|
||||
|
||||
defineNode({
|
||||
name: 'z-counter',
|
||||
nodeCreated(handle) {
|
||||
const count = ref(0)
|
||||
counters.set(handle.id as NodeEntityId, count)
|
||||
return { count }
|
||||
}
|
||||
})
|
||||
|
||||
// Mount source node.
|
||||
stubNodeType(SOURCE_ID)
|
||||
mountExtensionsForNode(SOURCE_ID)
|
||||
|
||||
// Mutate source extensionState (simulates user interaction driving count up).
|
||||
const sourceCounter = counters.get(SOURCE_ID)!
|
||||
expect(sourceCounter.value).toBe(0)
|
||||
sourceCounter.value = 42
|
||||
|
||||
// Verify the mutation is visible via getScopeRegistry().
|
||||
// proxyRefs unwraps refs, so extensionState.count returns the number directly (D10d).
|
||||
const sourceEntry = getScopeRegistry().get(`z-counter:${SOURCE_ID}`)!
|
||||
expect((sourceEntry.extensionState as { count: number }).count).toBe(42)
|
||||
|
||||
// Simulate copy/paste: new entityId added to the world.
|
||||
// D12(c): mountExtensionsForNode runs fresh setup — no priorState.
|
||||
stubNodeType(CLONE_ID)
|
||||
mountExtensionsForNode(CLONE_ID)
|
||||
|
||||
// Clone's extensionState must be the setup-default (0), not source's (42).
|
||||
const cloneEntry = getScopeRegistry().get(`z-counter:${CLONE_ID}`)!
|
||||
expect(cloneEntry).toBeDefined()
|
||||
expect((cloneEntry.extensionState as { count: number }).count).toBe(0)
|
||||
|
||||
// Source is unaffected.
|
||||
const sourceAfter = getScopeRegistry().get(`z-counter:${SOURCE_ID}`)!
|
||||
expect((sourceAfter.extensionState as { count: number }).count).toBe(42)
|
||||
})
|
||||
|
||||
it('N pastes from the same source all start at setup-default (no shared state)', () => {
|
||||
const SOURCE_ID = makeNodeId(100)
|
||||
const PASTE_IDS = [
|
||||
makeNodeId(101),
|
||||
makeNodeId(102),
|
||||
makeNodeId(103)
|
||||
] as NodeEntityId[]
|
||||
|
||||
let setupCallCount = 0
|
||||
|
||||
defineNode({
|
||||
name: 'a-setup-counter',
|
||||
nodeCreated() {
|
||||
setupCallCount++
|
||||
return { iteration: ref(setupCallCount) }
|
||||
}
|
||||
})
|
||||
|
||||
stubNodeType(SOURCE_ID)
|
||||
mountExtensionsForNode(SOURCE_ID)
|
||||
const countAfterSource = setupCallCount // 1
|
||||
|
||||
for (const pasteId of PASTE_IDS) {
|
||||
stubNodeType(pasteId)
|
||||
mountExtensionsForNode(pasteId)
|
||||
}
|
||||
|
||||
// Each paste ran setup() independently.
|
||||
expect(setupCallCount).toBe(countAfterSource + PASTE_IDS.length)
|
||||
|
||||
// Each paste scope holds its own `iteration` value — no aliasing.
|
||||
// proxyRefs unwraps refs so we access .iteration directly (D10d).
|
||||
const iterations = PASTE_IDS.map((id) => {
|
||||
const entry = getScopeRegistry().get(`a-setup-counter:${id}`)!
|
||||
return (entry.extensionState as { iteration: number }).iteration
|
||||
})
|
||||
const unique = new Set(iterations)
|
||||
expect(unique.size).toBe(PASTE_IDS.length)
|
||||
})
|
||||
|
||||
it('unmounting source does not affect clone scope', () => {
|
||||
const SOURCE_ID = makeNodeId(3)
|
||||
const CLONE_ID = makeNodeId(4)
|
||||
|
||||
defineNode({
|
||||
name: 'b-flag',
|
||||
nodeCreated() {
|
||||
return { flag: ref(true) }
|
||||
}
|
||||
})
|
||||
|
||||
stubNodeType(SOURCE_ID)
|
||||
mountExtensionsForNode(SOURCE_ID)
|
||||
stubNodeType(CLONE_ID)
|
||||
mountExtensionsForNode(CLONE_ID)
|
||||
|
||||
unmountExtensionsForNode(SOURCE_ID)
|
||||
|
||||
// Source scope removed from registry.
|
||||
expect(getScopeRegistry().get(`b-flag:${SOURCE_ID}`)).toBeUndefined()
|
||||
|
||||
// Clone scope survives independently.
|
||||
const cloneEntry = getScopeRegistry().get(`b-flag:${CLONE_ID}`)
|
||||
expect(cloneEntry).toBeDefined()
|
||||
// proxyRefs unwraps refs so .flag returns the boolean directly (D10d).
|
||||
expect((cloneEntry!.extensionState as { flag: boolean }).flag).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── I-SR.2.B3 + I-SR.3: currentExtension slot + lifecycle hooks ───────────────
|
||||
// Tests that _currentScope is set/restored around setup() and that
|
||||
// onNodeMounted/onNodeRemoved read it correctly (D10a).
|
||||
|
||||
describe('currentExtension global slot (D10a) + lifecycle hooks (I-SR.2.B3 / I-SR.3)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
_clearExtensionsForTesting()
|
||||
;[10, 11, 12].forEach((n) => unmountExtensionsForNode(makeNodeId(n)))
|
||||
})
|
||||
|
||||
it('getCurrentScope() returns null outside of setup', () => {
|
||||
expect(getCurrentScope()).toBeNull()
|
||||
})
|
||||
|
||||
it('getCurrentScope() is non-null during nodeCreated setup', () => {
|
||||
const NODE_ID = makeNodeId(10)
|
||||
let scopeDuringSetup: ReturnType<typeof getCurrentScope> = null
|
||||
|
||||
defineNode({
|
||||
name: 'c-slot-check',
|
||||
nodeCreated() {
|
||||
scopeDuringSetup = getCurrentScope()
|
||||
}
|
||||
})
|
||||
|
||||
stubNodeType(NODE_ID)
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
|
||||
expect(scopeDuringSetup).not.toBeNull()
|
||||
expect(scopeDuringSetup!.extensionName).toBe('c-slot-check')
|
||||
expect(scopeDuringSetup!.nodeEntityId).toBe(NODE_ID)
|
||||
})
|
||||
|
||||
it('getCurrentScope() is restored to null after setup completes', () => {
|
||||
const NODE_ID = makeNodeId(11)
|
||||
|
||||
defineNode({
|
||||
name: 'd-slot-restore',
|
||||
nodeCreated() {
|
||||
/* no-op */
|
||||
}
|
||||
})
|
||||
|
||||
stubNodeType(NODE_ID)
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
|
||||
expect(getCurrentScope()).toBeNull()
|
||||
})
|
||||
|
||||
it('onNodeRemoved callback fires when node is unmounted', () => {
|
||||
const NODE_ID = makeNodeId(12)
|
||||
const removedCb = vi.fn()
|
||||
|
||||
defineNode({
|
||||
name: 'e-on-removed',
|
||||
nodeCreated() {
|
||||
onNodeRemoved(removedCb)
|
||||
}
|
||||
})
|
||||
|
||||
stubNodeType(NODE_ID)
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
expect(removedCb).not.toHaveBeenCalled()
|
||||
|
||||
unmountExtensionsForNode(NODE_ID)
|
||||
expect(removedCb).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('onNodeRemoved outside setup context throws in dev', () => {
|
||||
expect(() => onNodeRemoved(() => {})).toThrow(/outside setup context/)
|
||||
})
|
||||
|
||||
it('onNodeMounted outside setup context throws in dev', () => {
|
||||
expect(() => onNodeMounted(() => {})).toThrow(/outside setup context/)
|
||||
})
|
||||
})
|
||||
|
||||
// ── I-SR.6: missing lifecycle invariants ────────────────────────────────────────
|
||||
// (a) setup-runs-once: calling mountExtensionsForNode twice on the same entity
|
||||
// must not re-run setup. The scope registry already has an entry — getOrCreateScope
|
||||
// short-circuits and setup is skipped.
|
||||
// (b) no-dispose-on-subgraph-promotion: scopes survive DOM moves (subgraph promote).
|
||||
// The v2 contract is: scope lifetime = entity lifetime, NOT DOM lifetime.
|
||||
// Subgraph promotion creates a new logical location for the same entityId, but
|
||||
// the scope must survive — only unmountExtensionsForNode destroys it.
|
||||
|
||||
describe('I-SR.6 — scope lifecycle invariants', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
_clearExtensionsForTesting()
|
||||
;[30, 31, 32].forEach((n) => unmountExtensionsForNode(makeNodeId(n)))
|
||||
})
|
||||
|
||||
it('(setup-runs-once) mounting the same node twice does not re-invoke nodeCreated', () => {
|
||||
const NODE_ID = makeNodeId(30)
|
||||
let setupCount = 0
|
||||
|
||||
defineNode({
|
||||
name: 'h-once',
|
||||
nodeCreated() {
|
||||
setupCount++
|
||||
}
|
||||
})
|
||||
|
||||
mockGetComponent.mockImplementation((id, key) => {
|
||||
if (id === NODE_ID && key?.name === 'NodeType')
|
||||
return { type: 'TestNode', comfyClass: 'TestNode' }
|
||||
return undefined
|
||||
})
|
||||
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
// Second call on same entity — must be idempotent
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
|
||||
// setup ran exactly once (getOrCreateScope short-circuits on second call)
|
||||
expect(setupCount).toBe(1)
|
||||
})
|
||||
|
||||
it('(no-dispose-on-subgraph-promotion) scope survives a non-removal remount; only unmount destroys it', () => {
|
||||
const NODE_ID = makeNodeId(31)
|
||||
let setupCount = 0
|
||||
|
||||
defineNode({
|
||||
name: 'i-promotion',
|
||||
nodeCreated() {
|
||||
setupCount++
|
||||
// (In real Phase B, onNodeRemoved would be used; here we verify via
|
||||
// setupCount that setup does not re-run, meaning scope was preserved.)
|
||||
}
|
||||
})
|
||||
|
||||
mockGetComponent.mockImplementation((id, key) => {
|
||||
if (id === NODE_ID && key?.name === 'NodeType')
|
||||
return { type: 'TestNode', comfyClass: 'TestNode' }
|
||||
return undefined
|
||||
})
|
||||
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
expect(setupCount).toBe(1)
|
||||
|
||||
// Simulate subgraph promotion: the runtime calls mountExtensionsForNode again
|
||||
// for the same entity (the node "moved" but the entityId is unchanged).
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
|
||||
// Scope was NOT disposed — setup did not re-run
|
||||
expect(setupCount).toBe(1)
|
||||
|
||||
// Only an explicit unmount destroys the scope
|
||||
unmountExtensionsForNode(NODE_ID)
|
||||
|
||||
// Scope removed from registry
|
||||
const entry = getScopeRegistry().get(`i-promotion:${NODE_ID}`)
|
||||
expect(entry).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ── I-SR.3.B4: reactive dispatch — LoadedFromWorkflow tag ─────────────────────
|
||||
// Tests that LoadedFromWorkflow presence routes to loadedGraphNode hook
|
||||
// (hydration) rather than nodeCreated (fresh creation).
|
||||
|
||||
describe('LoadedFromWorkflow tag routes to correct hook (I-SR.3)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
_clearExtensionsForTesting()
|
||||
;[20, 21].forEach((n) => unmountExtensionsForNode(makeNodeId(n)))
|
||||
})
|
||||
|
||||
it('node without LoadedFromWorkflow tag calls nodeCreated', () => {
|
||||
const NODE_ID = makeNodeId(20)
|
||||
const created = vi.fn()
|
||||
const loaded = vi.fn()
|
||||
|
||||
defineNode({
|
||||
name: 'f-routing',
|
||||
nodeCreated: created,
|
||||
loadedGraphNode: loaded
|
||||
})
|
||||
|
||||
// Stub: no LoadedFromWorkflow component (getComponent returns undefined for it)
|
||||
mockGetComponent.mockImplementation((id, key) => {
|
||||
if (id === NODE_ID && key?.name === 'NodeType')
|
||||
return { type: 'TestNode', comfyClass: 'TestNode' }
|
||||
return undefined // no LoadedFromWorkflow
|
||||
})
|
||||
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
|
||||
expect(created).toHaveBeenCalledOnce()
|
||||
expect(loaded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('node with LoadedFromWorkflow tag calls loadedGraphNode', () => {
|
||||
const NODE_ID = makeNodeId(21)
|
||||
const created = vi.fn()
|
||||
const loaded = vi.fn()
|
||||
|
||||
defineNode({
|
||||
name: 'g-routing',
|
||||
nodeCreated: created,
|
||||
loadedGraphNode: loaded
|
||||
})
|
||||
|
||||
mockGetComponent.mockImplementation((id, key) => {
|
||||
if (id === NODE_ID && key?.name === 'NodeType')
|
||||
return { type: 'TestNode', comfyClass: 'TestNode' }
|
||||
if (id === NODE_ID && key?.name === 'LoadedFromWorkflow')
|
||||
return { _tag: 'LoadedFromWorkflow' }
|
||||
return undefined
|
||||
})
|
||||
|
||||
mountExtensionsForNode(NODE_ID)
|
||||
|
||||
expect(loaded).toHaveBeenCalledOnce()
|
||||
expect(created).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
1250
src/services/extension-api-service.ts
Normal file
1250
src/services/extension-api-service.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,9 @@ import type { AuthUserInfo } from '@/types/authTypes'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
// Tracks which extensions have already received the beforeRegisterNodeDef deprecation warning
|
||||
const _warnedBeforeRegisterNodeDef = new Set<string>()
|
||||
|
||||
export const useExtensionService = () => {
|
||||
const extensionStore = useExtensionStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -209,6 +212,19 @@ export const useExtensionService = () => {
|
||||
legacyMenuCompat.setCurrentExtension(ext.name)
|
||||
}
|
||||
|
||||
// DEP1: warn once per extension that uses beforeRegisterNodeDef
|
||||
if (
|
||||
method === 'beforeRegisterNodeDef' &&
|
||||
!_warnedBeforeRegisterNodeDef.has(ext.name)
|
||||
) {
|
||||
_warnedBeforeRegisterNodeDef.add(ext.name)
|
||||
console.warn(
|
||||
`[ComfyUI] Extension "${ext.name}" uses deprecated hook "beforeRegisterNodeDef". ` +
|
||||
'Use defineNode({ nodeCreated(handle) { ... } }) with a nodeTypes filter instead. ' +
|
||||
'See https://docs.comfy.org/extensions/api for the v2 API.'
|
||||
)
|
||||
}
|
||||
|
||||
const result = await fn.call(ext, ...args, app)
|
||||
|
||||
// Clear current extension after setup
|
||||
|
||||
18
src/services/registries/README.md
Normal file
18
src/services/registries/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# `src/services/registries/`
|
||||
|
||||
D18 Phase 1 scaffolding — empty registry modules that the loader will
|
||||
populate in Phase 2 once side-effect registration is removed from
|
||||
`@/services/extension-api-service`.
|
||||
|
||||
Each module owns one extension kind:
|
||||
|
||||
- `nodeExtensionRegistry.ts` — outputs of `defineNode(...)`
|
||||
- `widgetExtensionRegistry.ts` — outputs of `defineWidget(...)`
|
||||
- `appExtensionRegistry.ts` — outputs of `defineExtension(...)`
|
||||
|
||||
These modules are intentionally minimal in Phase 1. They expose the
|
||||
`register / getAll / clearForTesting` shape the future loader will call,
|
||||
and a stub adapter the existing service will switch to in Phase 2.
|
||||
|
||||
See `decisions/D18-pure-functions-loader-registration.md` for the full
|
||||
plan and rationale.
|
||||
25
src/services/registries/appExtensionRegistry.ts
Normal file
25
src/services/registries/appExtensionRegistry.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Registry for `defineExtension(...)` outputs (D18 Phase 1 scaffolding).
|
||||
*
|
||||
* See `nodeExtensionRegistry.ts` for the rollout plan and
|
||||
* `decisions/D18-pure-functions-loader-registration.md` for rationale.
|
||||
*
|
||||
* @internal — runtime-only; not part of `@comfyorg/extension-api`.
|
||||
*/
|
||||
|
||||
import type { ExtensionOptions } from '@/extension-api/types'
|
||||
|
||||
const _appExtensions: ExtensionOptions[] = []
|
||||
|
||||
export function register(options: ExtensionOptions): void {
|
||||
_appExtensions.push(options)
|
||||
}
|
||||
|
||||
export function getAll(): readonly ExtensionOptions[] {
|
||||
return _appExtensions
|
||||
}
|
||||
|
||||
/** @internal Test-only. */
|
||||
export function _clearForTesting(): void {
|
||||
_appExtensions.length = 0
|
||||
}
|
||||
30
src/services/registries/nodeExtensionRegistry.ts
Normal file
30
src/services/registries/nodeExtensionRegistry.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Registry for `defineNode(...)` outputs (D18 Phase 1 scaffolding).
|
||||
*
|
||||
* Phase 1: this module is empty / unused. The existing
|
||||
* `extension-api-service.ts` continues to push into its module-local
|
||||
* `nodeExtensions` array on import-time side effect.
|
||||
*
|
||||
* Phase 2: the side-effect path is removed, the loader walks every
|
||||
* imported extension module and calls `register(...)` on each branded
|
||||
* `defineNode` result.
|
||||
*
|
||||
* @internal — runtime-only; not part of `@comfyorg/extension-api`.
|
||||
*/
|
||||
|
||||
import type { NodeExtensionOptions } from '@/extension-api/types'
|
||||
|
||||
const _nodeExtensions: NodeExtensionOptions[] = []
|
||||
|
||||
export function register(options: NodeExtensionOptions): void {
|
||||
_nodeExtensions.push(options)
|
||||
}
|
||||
|
||||
export function getAll(): readonly NodeExtensionOptions[] {
|
||||
return _nodeExtensions
|
||||
}
|
||||
|
||||
/** @internal Test-only. */
|
||||
export function _clearForTesting(): void {
|
||||
_nodeExtensions.length = 0
|
||||
}
|
||||
25
src/services/registries/widgetExtensionRegistry.ts
Normal file
25
src/services/registries/widgetExtensionRegistry.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Registry for `defineWidget(...)` outputs (D18 Phase 1 scaffolding).
|
||||
*
|
||||
* See `nodeExtensionRegistry.ts` for the rollout plan and
|
||||
* `decisions/D18-pure-functions-loader-registration.md` for rationale.
|
||||
*
|
||||
* @internal — runtime-only; not part of `@comfyorg/extension-api`.
|
||||
*/
|
||||
|
||||
import type { WidgetExtensionOptions } from '@/extension-api/types'
|
||||
|
||||
const _widgetExtensions: WidgetExtensionOptions[] = []
|
||||
|
||||
export function register(options: WidgetExtensionOptions): void {
|
||||
_widgetExtensions.push(options)
|
||||
}
|
||||
|
||||
export function getAll(): readonly WidgetExtensionOptions[] {
|
||||
return _widgetExtensions
|
||||
}
|
||||
|
||||
/** @internal Test-only. */
|
||||
export function _clearForTesting(): void {
|
||||
_widgetExtensions.length = 0
|
||||
}
|
||||
@@ -108,34 +108,75 @@ export interface ComfyExtension {
|
||||
name: string
|
||||
/**
|
||||
* The commands defined by the extension
|
||||
*
|
||||
* @deprecated Per D-shell-ui-entrypoints (W6.P5.C, ACCEPTED 2026-05-18): use
|
||||
* the per-surface `defineCommand(...)` entry point from
|
||||
* `@comfyorg/extension-api` instead. Each call returns a `DisposableHandle`
|
||||
* for independent unregister. A codemod in `@comfyorg/extension-api` ships
|
||||
* to perform the v1→v2 rewrite mechanically. The v1 slot is retained for
|
||||
* back-compat during the deprecation window.
|
||||
*/
|
||||
commands?: ComfyCommand[]
|
||||
/**
|
||||
* The keybindings defined by the extension
|
||||
*
|
||||
* @deprecated Per D-shell-ui-entrypoints (W6.P5.C, ACCEPTED 2026-05-18): use
|
||||
* `defineHotkey(...)` from `@comfyorg/extension-api`. Each call returns a
|
||||
* `DisposableHandle`. The v1 slot is retained for back-compat during the
|
||||
* deprecation window.
|
||||
*/
|
||||
keybindings?: Keybinding[]
|
||||
/**
|
||||
* Menu commands to add to the menu bar
|
||||
*
|
||||
* @deprecated Menu surface is out of scope for D-shell-ui-entrypoints
|
||||
* (W6.P5.C) and will be addressed in a follow-on ADR (the prototype-patch
|
||||
* `getExtraMenuOptions` tax is the load-bearing migration question and
|
||||
* needs its own treatment). The v1 slot remains supported until that
|
||||
* ADR ships.
|
||||
*/
|
||||
menuCommands?: MenuCommandGroup[]
|
||||
/**
|
||||
* Settings to add to the settings menu
|
||||
*
|
||||
* @deprecated Per D-shell-ui-entrypoints (W6.P5.C, ACCEPTED 2026-05-18): use
|
||||
* `defineSetting(...)` from `@comfyorg/extension-api`. The v1 slot is
|
||||
* retained for back-compat during the deprecation window.
|
||||
*/
|
||||
settings?: SettingParams[]
|
||||
/**
|
||||
* Bottom panel tabs to add to the bottom panel
|
||||
*
|
||||
* @deprecated Per D-shell-ui-entrypoints (W6.P5.C, ACCEPTED 2026-05-18): use
|
||||
* `defineBottomPanelTab(...)` from `@comfyorg/extension-api`. The v1 slot
|
||||
* is retained for back-compat during the deprecation window.
|
||||
*/
|
||||
bottomPanelTabs?: BottomPanelExtension[]
|
||||
/**
|
||||
* Badges to add to the about page
|
||||
*
|
||||
* @deprecated Per D-shell-ui-entrypoints (W6.P5.C, ACCEPTED 2026-05-18): use
|
||||
* `defineAboutBadge(...)` from `@comfyorg/extension-api`. The v1 slot is
|
||||
* retained for back-compat during the deprecation window.
|
||||
*/
|
||||
aboutPageBadges?: AboutPageBadge[]
|
||||
/**
|
||||
* Badges to add to the top bar
|
||||
*
|
||||
* @deprecated Per D-shell-ui-entrypoints (W6.P5.C, ACCEPTED 2026-05-18):
|
||||
* topbar badges are not part of the W6.P5 scope (the R3 evidence sweep
|
||||
* found this surface had effectively zero ecosystem use and was treated as
|
||||
* an internal-only concern). A `defineTopbarBadge` entry may be added in
|
||||
* a follow-on PR; the v1 slot remains supported until then.
|
||||
*/
|
||||
topbarBadges?: TopbarBadge[]
|
||||
/**
|
||||
* Buttons to add to the action bar
|
||||
*
|
||||
* @deprecated Per D-shell-ui-entrypoints (W6.P5.C, ACCEPTED 2026-05-18): use
|
||||
* `defineToolbarButton(...)` from `@comfyorg/extension-api`. Note this is
|
||||
* a net-new surface (0 hits in the v1 R3 evidence sweep) — the v1 slot
|
||||
* existed but was undocumented; any first-mover usage is greenfield.
|
||||
*/
|
||||
actionBarButtons?: ActionBarButton[]
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import type { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ExecutionErrorWsMessage, NodeError } from '@/schemas/apiSchema'
|
||||
@@ -48,7 +49,17 @@ export type BottomPanelExtension =
|
||||
| CustomBottomPanelExtension
|
||||
|
||||
/**
|
||||
* Defines message options in Toast component.
|
||||
* Defines message options in Toast component. Passed to {@link toast.show} /
|
||||
* {@link toast.remove} to surface a transient message to the user.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { toast } from '@comfyorg/extension-api'
|
||||
*
|
||||
* toast.show({ severity: 'info', summary: 'Saved', life: 2000 })
|
||||
* ```
|
||||
*/
|
||||
export interface ToastMessageOptions {
|
||||
/**
|
||||
@@ -139,3 +150,154 @@ export interface CommandManager {
|
||||
}
|
||||
): void
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// D-shell-ui-entrypoints (W6.P5.C) — per-surface registration arg types
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Each `defineX` entry point in `@comfyorg/extension-api` accepts one of the
|
||||
// types below. The shapes are deliberately thin POJOs (not class instances) so
|
||||
// they are JSON-friendly, tree-shake-friendly, and easy to author/test in
|
||||
// isolation. The runtime wraps each one into the appropriate store registration
|
||||
// at mount time (see `src/extension-api/registrations.ts`).
|
||||
//
|
||||
// New types added under (ii) "separate entries" per W6.P5.B reconciliation:
|
||||
// - HotkeyExtension
|
||||
// - AboutBadgeExtension
|
||||
// - SettingDefinition
|
||||
// - ToolbarButtonExtension
|
||||
// - CommandDefinition (alias of v1 ComfyCommand for the public surface)
|
||||
|
||||
/**
|
||||
* Public arg type for {@link defineCommand}. Alias of the v1 `ComfyCommand`
|
||||
* shape exported from `@/stores/commandStore`, re-surfaced under a public name
|
||||
* so authors do not import from the internal `stores/` path.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
*/
|
||||
export type CommandDefinition = ComfyCommand
|
||||
|
||||
/**
|
||||
* Public arg type for {@link defineHotkey}. A hotkey binds a key combination
|
||||
* (already-registered command) to fire on press. Mirrors v1
|
||||
* `extension.keybindings[]` which used the internal `Keybinding` shape.
|
||||
*
|
||||
* `keys` accepts a human-readable combo string (e.g. `'mod+k'`, `'ctrl+shift+f'`)
|
||||
* matching the Vue/PrimeVue keybinding convention. `mod` resolves to `cmd` on
|
||||
* macOS and `ctrl` elsewhere. The runtime parses this into the underlying
|
||||
* `KeyCombo` shape used by the keybinding store.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineCommand, defineHotkey } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineCommand({ id: 'my.cmd', function: () => {} })
|
||||
* defineHotkey({ keys: 'mod+k', commandId: 'my.cmd' })
|
||||
* ```
|
||||
*/
|
||||
export interface HotkeyExtension {
|
||||
/**
|
||||
* Key combination to listen for. Examples: `'mod+k'`, `'ctrl+shift+f'`,
|
||||
* `'alt+enter'`. `mod` resolves to `cmd` on macOS and `ctrl` elsewhere.
|
||||
*/
|
||||
keys: string
|
||||
/**
|
||||
* The id of an already-registered command. Use {@link defineCommand} to
|
||||
* register the command before (or alongside) the hotkey.
|
||||
*/
|
||||
commandId: string
|
||||
/**
|
||||
* Optional id of the DOM element that must be focused for the hotkey to
|
||||
* fire. When omitted, the hotkey is global.
|
||||
*/
|
||||
targetElementId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Public arg type for {@link defineAboutBadge}. A badge that appears on the
|
||||
* About page (linked label + icon + optional severity). Mirrors v1
|
||||
* `extension.aboutPageBadges[]`.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineAboutBadge } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineAboutBadge({
|
||||
* label: 'GitHub',
|
||||
* url: 'https://github.com/me/my-ext',
|
||||
* icon: 'pi-github'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface AboutBadgeExtension {
|
||||
/** Display label for the badge. */
|
||||
label: string
|
||||
/** URL the badge links to on click. */
|
||||
url: string
|
||||
/** Icon class (e.g. `'pi-github'`) shown next to the label. */
|
||||
icon: string
|
||||
/** Optional severity tint; defaults to neutral. */
|
||||
severity?: 'danger' | 'warn'
|
||||
}
|
||||
|
||||
/**
|
||||
* Public arg type for {@link defineSetting}. Re-surfaces the existing
|
||||
* `SettingParams` shape (from `@/platform/settings/types`) under a public name
|
||||
* so authors do not import from the internal `platform/` path.
|
||||
*
|
||||
* Note: the underlying `SettingParams.id` is typed against the `Settings`
|
||||
* keymap; for ecosystem extension settings, authors widen the id type via
|
||||
* `as keyof Settings` or rely on TS module augmentation of `Settings` (a
|
||||
* follow-on RFC will formalize the augmentation path).
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
*/
|
||||
export type SettingDefinition<TValue = unknown> = SettingParams<TValue>
|
||||
|
||||
/**
|
||||
* Public arg type for {@link defineToolbarButton}. A button that appears in
|
||||
* the action bar (top of the canvas area). Wraps the v1
|
||||
* `extension.actionBarButtons[]` shape with an `id` so each registration is
|
||||
* independently disposable.
|
||||
*
|
||||
* **Net-new surface**: no v1 registration path existed for toolbar buttons
|
||||
* (action-bar buttons were possible but undocumented and never had a
|
||||
* `defineToolbarButton` equivalent). Per W6.P5 evidence sweep this surface
|
||||
* had 0 hits across the 138-repo corpus — any first-mover use is greenfield.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineToolbarButton } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineToolbarButton({
|
||||
* id: 'my.help',
|
||||
* icon: 'pi-question-circle',
|
||||
* tooltip: 'Get help',
|
||||
* onClick: () => openHelp()
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export interface ToolbarButtonExtension {
|
||||
/** Stable id for the button — used by `dispose()` to unregister. */
|
||||
id: string
|
||||
/**
|
||||
* Icon class to display (e.g. `'icon-[lucide--message-circle-question-mark]'`).
|
||||
*/
|
||||
icon: string
|
||||
/** Optional label text shown next to the icon. */
|
||||
label?: string
|
||||
/** Optional tooltip shown on hover. */
|
||||
tooltip?: string
|
||||
/** Optional CSS class string applied to the button element. */
|
||||
class?: string
|
||||
/** Click handler invoked when the button is pressed. */
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
24
src/types/extensionV2.ts
Normal file
24
src/types/extensionV2.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @deprecated Import from `@comfyorg/extension-api` (or `@/extension-api`)
|
||||
* instead. This stub will be removed in the next release after PKG2 lands.
|
||||
*
|
||||
* See `src/extension-api/` for the new source of truth.
|
||||
*/
|
||||
|
||||
// NodeEntityId/WidgetEntityId/SlotEntityId removed from this re-export per D20
|
||||
// (id-type-convergence) — they are now @internal. Use `node.id` / `widget.id`
|
||||
// (string) and `node.equals(other)` for the public surface.
|
||||
export type {
|
||||
Point,
|
||||
Size,
|
||||
NodeMode,
|
||||
SlotDirection,
|
||||
SlotInfo,
|
||||
WidgetHandle,
|
||||
WidgetOptions,
|
||||
NodeHandle
|
||||
} from '@/extension-api'
|
||||
|
||||
export type { NodeExtensionOptions, ExtensionOptions } from '@/extension-api'
|
||||
|
||||
export { defineNode, defineExtension } from '@/extension-api'
|
||||
12
src/world/componentKey.ts
Normal file
12
src/world/componentKey.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Phase A stub — replaced by real ECS componentKey when PR #11939 lands.
|
||||
// Tests mock this module via vi.mock('@/world/componentKey').
|
||||
|
||||
export interface ComponentKey<_TData, _TEntity> {
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
export function defineComponentKey<TData, TEntity>(
|
||||
name: string
|
||||
): ComponentKey<TData, TEntity> {
|
||||
return { name }
|
||||
}
|
||||
8
src/world/entityIds.ts
Normal file
8
src/world/entityIds.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Phase A stub — replaced by real ECS entityIds when PR #11939 lands.
|
||||
// Tests mock this module via vi.mock('@/world/entityIds').
|
||||
|
||||
export type Brand<T, B extends string> = T & { readonly __brand: B }
|
||||
|
||||
export type NodeEntityId = Brand<string, 'NodeEntityId'>
|
||||
export type WidgetEntityId = Brand<string, 'WidgetEntityId'>
|
||||
export type EntityId = NodeEntityId | WidgetEntityId
|
||||
45
src/world/widgets/widgetComponents.ts
Normal file
45
src/world/widgets/widgetComponents.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Phase A stub — replaced by real ECS widget components when PR #11939 lands.
|
||||
// Tests mock this module via vi.mock('@/world/widgets/widgetComponents').
|
||||
|
||||
import { defineComponentKey } from '../componentKey'
|
||||
import type { NodeEntityId, WidgetEntityId } from '../entityIds'
|
||||
|
||||
interface WidgetContainerData {
|
||||
widgetIds: WidgetEntityId[]
|
||||
}
|
||||
interface WidgetDisplayData {
|
||||
label?: string
|
||||
hidden?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
interface WidgetSchemaData {
|
||||
type?: string
|
||||
options?: Record<string, unknown>
|
||||
}
|
||||
interface WidgetSerializeData {
|
||||
serialize?: boolean
|
||||
}
|
||||
interface WidgetValueData {
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
export const WidgetComponentContainer = defineComponentKey<
|
||||
WidgetContainerData,
|
||||
NodeEntityId
|
||||
>('WidgetComponentContainer')
|
||||
export const WidgetComponentDisplay = defineComponentKey<
|
||||
WidgetDisplayData,
|
||||
WidgetEntityId
|
||||
>('WidgetComponentDisplay')
|
||||
export const WidgetComponentSchema = defineComponentKey<
|
||||
WidgetSchemaData,
|
||||
WidgetEntityId
|
||||
>('WidgetComponentSchema')
|
||||
export const WidgetComponentSerialize = defineComponentKey<
|
||||
WidgetSerializeData,
|
||||
WidgetEntityId
|
||||
>('WidgetComponentSerialize')
|
||||
export const WidgetComponentValue = defineComponentKey<
|
||||
WidgetValueData,
|
||||
WidgetEntityId
|
||||
>('WidgetComponentValue')
|
||||
34
src/world/worldInstance.ts
Normal file
34
src/world/worldInstance.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Phase A stub — replaced by real ECS world when PR #11939 lands.
|
||||
// Tests mock this module via vi.mock('@/world/worldInstance').
|
||||
|
||||
import type { ComponentKey } from './componentKey'
|
||||
import type { EntityId } from './entityIds'
|
||||
|
||||
/**
|
||||
* ECS World contract. Internal only — not part of public extension API.
|
||||
* Phase A surface; replaced by Alex's PR #11939 (ECS substrate slice 2).
|
||||
*/
|
||||
interface World {
|
||||
getComponent<TData, TEntity extends EntityId>(
|
||||
entityId: TEntity,
|
||||
key: ComponentKey<TData, TEntity>
|
||||
): TData | undefined
|
||||
setComponent<TData, TEntity extends EntityId>(
|
||||
entityId: TEntity,
|
||||
key: ComponentKey<TData, TEntity>,
|
||||
data: TData
|
||||
): void
|
||||
removeComponent<TData, TEntity extends EntityId>(
|
||||
entityId: TEntity,
|
||||
key: ComponentKey<TData, TEntity>
|
||||
): void
|
||||
entitiesWith<TData, TEntity extends EntityId>(
|
||||
key: ComponentKey<TData, TEntity>
|
||||
): TEntity[]
|
||||
}
|
||||
|
||||
export function getWorld(): World {
|
||||
throw new Error(
|
||||
'[worldInstance] ECS world not yet initialized (Phase A stub)'
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user