mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 05:19:03 +00:00
Compare commits
14 Commits
glary/remo
...
ext-api/i-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa5f3cf550 | ||
|
|
d4b04bac6c | ||
|
|
c5865d3717 | ||
|
|
2ec4fec8c2 | ||
|
|
c614243e36 | ||
|
|
e010c47110 | ||
|
|
192c102c7a | ||
|
|
58d6d2a157 | ||
|
|
e7f642765f | ||
|
|
96addd0e94 | ||
|
|
7200eb0dc4 | ||
|
|
e616a9386a | ||
|
|
fe6d4399c3 | ||
|
|
6dd361bbca |
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@v4
|
||||
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 == 'false'
|
||||
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 == 'true'
|
||||
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@v7
|
||||
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
|
||||
@@ -172,7 +172,7 @@ This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g
|
||||
16. Whenever a new piece of code is written, the author should ask themselves 'is there a simpler way to introduce the same functionality?'. If the answer is yes, the simpler course should be chosen
|
||||
17. [Refactoring](https://refactoring.com/catalog/) should be used to make complex code simpler
|
||||
18. Try to minimize the surface area (exported values) of each module and composable
|
||||
19. Don't use barrel files, e.g. `/some/package/index.ts` to re-export within `/src`
|
||||
19. Don't use barrel files, e.g. `/some/package/index.ts` to re-export within `/src`. **Exception**: `src/extension-api/index.ts` is the published npm package entry point (`@comfyorg/extension-api`) and is explicitly exempt from this rule.
|
||||
20. Keep functions short and functional
|
||||
21. Minimize [nesting](https://wiki.c2.com/?ArrowAntiPattern), e.g. `if () { ... }` or `for () { ... }`
|
||||
22. Avoid mutable state, prefer immutability and assignment at point of declaration
|
||||
|
||||
187
docs/architecture/extension-api-v2/names-appendix.md
Normal file
187
docs/architecture/extension-api-v2/names-appendix.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Names That Must Agree Across Layers
|
||||
|
||||
**Task:** DOC1.E6
|
||||
**Date:** 2026-05-08
|
||||
**Patterns cross-walked:** S2.N16 (widget array access), S13.SC1 (ComfyNodeDef inspection), S15.OS1 (dynamic output mutation)
|
||||
|
||||
This appendix enumerates the terms that span at least two of the four layers — Python backend, v1 frontend (`ComfyExtension`/LiteGraph), v2 extension API (`NodeHandle`/`WidgetHandle`), and ECS World components — and calls out real inconsistencies where the same concept is named differently or the semantics diverge. Future contributors who rename or refactor any of these terms must propagate the change across all layers listed.
|
||||
|
||||
---
|
||||
|
||||
## Layers
|
||||
|
||||
| Layer | Owner | Primary source |
|
||||
|-------|-------|---------------|
|
||||
| **Python backend** | ComfyUI server | `NODE_CLASS_MAPPINGS`, `INPUT_TYPES`, `RETURN_TYPES` |
|
||||
| **v1 frontend** | LiteGraph / ComfyExtension | `src/types/comfy.ts`, `src/schemas/nodeDefSchema.ts`, `LGraphNode.ts` |
|
||||
| **v2 extension API** | This project | `src/extension-api/node.ts`, `src/extension-api/widget.ts` |
|
||||
| **ECS World** | Alex's branch (PR #11939) | `src/services/extension-api-service.ts` (stubs), `@/world/entityIds` |
|
||||
|
||||
---
|
||||
|
||||
## Term 1 — Node class identifier (`class_type` / `type` / `comfyClass`)
|
||||
|
||||
**What it is:** The string that identifies which Python class backs a node. Used to look up `object_info`, serialize the prompt, and match `INPUT_TYPES` definitions.
|
||||
|
||||
| Layer | Name | Value example | Notes |
|
||||
|-------|------|--------------|-------|
|
||||
| Python backend | Python class name | `'KSampler'` | The class registered in `NODE_CLASS_MAPPINGS` |
|
||||
| Execution prompt JSON (API format) | `class_type` | `"class_type": "KSampler"` | Key in the flat prompt dict |
|
||||
| UWF backend spec | `class_type` | `"class_type": "KSampler"` | Unchanged from API format (per `uwf-backend-data-model.md`) |
|
||||
| `ComfyNodeDef` (v1 schema) | `name` | `nodeData.name === 'KSampler'` | The `name` field in the server's `/object_info` response |
|
||||
| `LGraphNode` (v1) | `node.type` | `node.type === 'KSampler'` | LiteGraph's class string; set at registration |
|
||||
| v2 `NodeHandle` | `handle.type` | `handle.type === 'KSampler'` | `readonly type: string` in `src/extension-api/node.ts:260` |
|
||||
| v2 `NodeHandle` | `handle.comfyClass` | `handle.comfyClass === 'KSampler'` | `readonly comfyClass: string` in `src/extension-api/node.ts:269` |
|
||||
| ECS `NodeTypeData` component | `type` + `comfyClass` | both fields | Stub in `extension-api-service.ts:61–63` |
|
||||
|
||||
**⚠ Inconsistency — `type` vs `comfyClass`:** v2 `NodeHandle` exposes **both** `type` and `comfyClass` because for most nodes they are equal, but for virtual/reroute nodes `type` is the LiteGraph registration string while `comfyClass` is the actual Python class backing the node. Extensions that compare against ComfyUI node names should always use `comfyClass`. Extensions that filter by LiteGraph registration (for `nodeTypes:` filter in `defineNodeExtension`) should use `type`. The distinction must be preserved — collapsing them back to one field would break reroute/virtual-node detection.
|
||||
|
||||
**Rule:** `class_type` (wire format, Python, UWF) = `comfyClass` (v2 API) = `name` in `ComfyNodeDef` = `node.type` in LiteGraph for non-virtual nodes.
|
||||
|
||||
---
|
||||
|
||||
## Term 2 — Node display name (`display_name` / `title`)
|
||||
|
||||
**What it is:** The human-readable label shown in the node header and search results. Distinct from the class identifier.
|
||||
|
||||
| Layer | Name | Notes |
|
||||
|-------|------|-------|
|
||||
| Python backend | `NODE_DISPLAY_NAME_MAPPINGS[class]` (optional) | If absent, falls back to the class name |
|
||||
| `ComfyNodeDef` (v1 schema) | `display_name` | `nodeData.display_name` — always a string per zod schema (`src/schemas/nodeDefSchema.ts:279`) |
|
||||
| `LGraphNode` (v1) | `node.title` | Set during `nodeCreated`; extensions mutate `node.title` directly to rename nodes |
|
||||
| v2 `NodeHandle` | `handle.title` (getter) + `handle.setTitle(s)` | `src/extension-api/node.ts:304–315`; accessor pair per D3.3/D6 hybrid rule |
|
||||
| ECS `NodeVisualData` component | `title` | Field name consistent with LiteGraph |
|
||||
|
||||
**No inconsistency.** `display_name` lives only in the schema/backend layer; `title` is the runtime/frontend term. The two layers don't overlap. Extension authors should use `handle.setTitle()` in v2; `node.title =` is the v1 equivalent. Both are stable.
|
||||
|
||||
---
|
||||
|
||||
## Term 3 — Widget name (`name` / `input_name` / `widget.name`)
|
||||
|
||||
**What it is:** The stable per-node-instance identifier for a widget slot. Used as the key in `widgets_values_named`, `promoted_inputs`, and for `WidgetHandle` lookup.
|
||||
|
||||
| Layer | Name | Notes |
|
||||
|-------|------|-------|
|
||||
| Python backend `INPUT_TYPES` | Dict key (e.g. `'seed'`, `'steps'`) | The name as declared in the Python class |
|
||||
| UWF `promoted_inputs` | `input_name` | `{ node_id, input_name, display_name }` — snake_case to match Python origin |
|
||||
| UWF `spec.nodes.{id}.inputs` | Named key in the inputs object | e.g. `"seed": { "value": 123, "link": null }` |
|
||||
| v1 `LGraphNode.widgets` | `widget.name` | `widget.name === 'seed'` — used in `node.widgets.find(w => w.name === ...)` (S2.N16 pattern) |
|
||||
| v1 `node.widgets` iteration | position index (implicit) | `widgets_values` positional array — the root cause of widget shift bugs (S17.WV1) |
|
||||
| v2 `NodeHandle.widget(name)` | `name` argument | Lookup by name, not position (`src/extension-api/node.ts:383`) |
|
||||
| v2 `NodeHandle.addWidget(type, name, ...)` | `name` parameter | `src/extension-api/node.ts:396–403`; stable key for the widget's lifetime |
|
||||
| v2 `WidgetHandle.name` | `readonly name: string` | `src/extension-api/widget.ts:277` — "stable within the node's lifetime" |
|
||||
| ECS `WidgetComponentSchema` | `name` field (inferred) | Widget schema component expected to carry `name` per `widgetComponents` import |
|
||||
|
||||
**⚠ Inconsistency — positional array vs. named:** v1 serialization stores widget values as a positional array (`widgets_values: [123, 20, 7.5]`). v2 API and UWF both use `name` as the stable key. The bridge is Austin's PR #10392 (`widgets_values_named`). Until that merges, any code that reads `node.widgets[i]` by index is fragile; code that reads `widget.name` is UWF-safe. The v2 `WidgetHandle` **must** be looked up by name, never by index — this is enforced by the API shape.
|
||||
|
||||
**Rule:** Always use the Python-declared input name as the canonical widget identifier. Never use position. `widget.name` (v1) = `name` parameter (v2 addWidget) = `input_name` (UWF wire format).
|
||||
|
||||
---
|
||||
|
||||
## Term 4 — Widget type string (`type` / `widgetType` / widget constructor key)
|
||||
|
||||
**What it is:** The string describing what kind of widget a slot is (e.g. `'INT'`, `'STRING'`, `'COMBO'`, `'IMAGE'`). Controls which widget constructor is used and which validation rules apply.
|
||||
|
||||
| Layer | Name | Notes |
|
||||
|-------|------|-------|
|
||||
| Python backend | Return value of `INPUT_TYPES()` tuple: first element | e.g. `("INT", {"default": 42})` — the type string |
|
||||
| `ComfyNodeDef` / `InputSpec` | Type string as zod-inferred from the schema | `INPUT_TYPES['required']['steps'] = ['INT', {...}]` |
|
||||
| `zBaseInputOptions` | `widgetType` field (optional override) | `src/schemas/nodeDefSchema.ts:33` — overrides the slot type for widget selection; rare |
|
||||
| v1 `getCustomWidgets` return | Record key | `{ MY_WIDGET: constructor }` — extension-registered type strings |
|
||||
| v1 `node.addWidget(type, ...)` | `type` first arg | The LiteGraph widget constructor key |
|
||||
| v2 `NodeHandle.addWidget(type, name, ...)` | `type` first arg | `src/extension-api/node.ts:395, 403` |
|
||||
| v2 `WidgetHandle.type` | `readonly type: string` | `src/extension-api/widget.ts` line ~280 |
|
||||
| ECS `WidgetComponentSchema` | `type` field | Expected to match the Python-declared type string |
|
||||
|
||||
**⚠ Inconsistency — `widgetType` override:** The `widgetType` field in `zBaseInputOptions` is an override that makes the frontend render a different widget than the Python type implies. Extensions that inspect `nodeData.input.required[name][1].widgetType` to determine rendering (S13.SC1 pattern) must check this field **before** using the slot's primary type. The v2 `ComfyNodeDef`-inspection helper (`ctx.inspectNodeDef`) must resolve this correctly — it cannot just return `InputSpec[0]` (the Python type) as `widgetType`.
|
||||
|
||||
**Rule:** Python type string = v1 `widget.type` = v2 `WidgetHandle.type` = `widgetType` override if present (takes precedence).
|
||||
|
||||
---
|
||||
|
||||
## Term 5 — Slot type string (connection type / `'IMAGE'`, `'LATENT'`, etc.)
|
||||
|
||||
**What it is:** The type label on a node's input/output slot that governs which connections are valid. Distinct from widget type (a slot may be a pure connection point with no widget).
|
||||
|
||||
| Layer | Name | Notes |
|
||||
|-------|------|-------|
|
||||
| Python backend | `RETURN_TYPES` tuple element | e.g. `RETURN_TYPES = ('IMAGE', 'MASK')` |
|
||||
| Python backend `INPUT_TYPES` | First element of required/optional tuple | `'IMAGE'` means "must receive an IMAGE connection" |
|
||||
| UWF `spec.nodes.{id}.outputs` | `{ "name": "IMAGE" }` per output | Output type declarations (new in UWF — not in old API format) |
|
||||
| v1 `LGraphNode` slot | `slot.type` | String on the slot object; extensions read/mutate via S15.OS1 |
|
||||
| v1 `node.addInput(name, type)` | `type` second arg | e.g. `node.addInput('mask', 'MASK')` |
|
||||
| v1 `node.addOutput(name, type)` | `type` second arg | S15.OS1 pattern |
|
||||
| v2 `SlotInfo` | `readonly type: string` | `src/extension-api/node.ts:82–83` |
|
||||
| v2 slot events | `event.slot.type` | Available in `onSlotConnected`, `onSlotDisconnected` |
|
||||
|
||||
**⚠ Inconsistency — dynamic mutation (S15.OS1):** v1 allows `slot.type = 'IMAGE'` and `node.outputs[i].type = newType` at runtime. v2 restricts this: output types must be declared in `INPUT_TYPES` / schema; runtime mutation is only via `node.declareOutputs(spec)` (proposed, not yet implemented). This is an intentional breaking change. The UWF spec formalizes this by requiring `spec.nodes.{id}.outputs` to be declared at save time, not derived from runtime state.
|
||||
|
||||
**Rule:** Slot type strings are uppercase by convention (matching Python `RETURN_TYPES`). v2 enforces schema-declaration; mutation-at-runtime is deprecated.
|
||||
|
||||
---
|
||||
|
||||
## Term 6 — Node output name (`output_name` / `RETURN_NAMES`)
|
||||
|
||||
**What it is:** Optional human-readable names for a node's output slots. Not the type — the label shown on the output connector.
|
||||
|
||||
| Layer | Name | Notes |
|
||||
|-------|------|-------|
|
||||
| Python backend | `RETURN_NAMES` class attribute | e.g. `RETURN_NAMES = ('upscaled_image', 'mask')` — optional |
|
||||
| `ComfyNodeDef` schema | `output_name` | `z.array(z.string()).optional()` in `src/schemas/nodeDefSchema.ts:275` |
|
||||
| v1 `LGraphNode` | `output.name` | String on the slot object; `node.outputs[i].name` |
|
||||
| v2 `SlotInfo` | `readonly name: string` | `src/extension-api/node.ts:80–81` — same field name |
|
||||
|
||||
**No inconsistency.** `RETURN_NAMES` → `output_name` in the schema → `slot.name` at runtime. All three refer to the same string. Field name shifts from snake_case (`output_name`) in the schema to camelCase-neutral (`name`) on the slot object — consistent with the rest of the frontend.
|
||||
|
||||
---
|
||||
|
||||
## Term 7 — Extension name (`ComfyExtension.name` / `ExtensionOptions.name`)
|
||||
|
||||
**What it is:** The unique identifier for an extension used for hook ordering (D10b), scope registry keys, deprecation telemetry, and conflict detection.
|
||||
|
||||
| Layer | Name | Notes |
|
||||
|-------|------|-------|
|
||||
| v1 `ComfyExtension` | `name: string` | Required field; `src/types/comfy.ts:108`; typically a dotted namespace like `'Comfy.Sidebar'` |
|
||||
| v2 `ExtensionOptions` | `name: string` | Required field; `src/extension-api/lifecycle.ts`; same semantic |
|
||||
| ECS scope registry | `extensionName` | Key component of `NodeInstanceScope`; used in `${extensionName}:${nodeEntityId}` scope key |
|
||||
| D6 telemetry | `apiVersion` | Separate field added by I-EXT.3 to `ExtensionOptions` for version tracking — not a replacement for `name` |
|
||||
|
||||
**No inconsistency.** Same field name and semantics across v1 and v2. The scope registry key format is `${extensionName}:${nodeEntityId}` — both components must be stable.
|
||||
|
||||
**Rule:** Extension names should follow the dotted-namespace convention (e.g. `'MyPublisher.MyExtension'`) to avoid collisions. This is currently advisory, not enforced.
|
||||
|
||||
---
|
||||
|
||||
## Term 8 — Node input display name (`display_name` / widget label)
|
||||
|
||||
**What it is:** The label shown next to the widget in the UI. Distinct from the internal `name` (key) used for serialization.
|
||||
|
||||
| Layer | Name | Notes |
|
||||
|-------|------|-------|
|
||||
| Python backend | `display_name` in input options dict | `INPUT_TYPES()['required']['steps'] = ['INT', {'display_name': 'Steps'}]` |
|
||||
| `zBaseInputOptions` schema | `display_name: z.string().optional()` | `src/schemas/nodeDefSchema.ts:27` |
|
||||
| UWF `promoted_inputs` | `display_name` field | `{ node_id, input_name, display_name }` — the UI label for app-mode promoted inputs |
|
||||
| v1 `widget` | `widget.label` | Optional; falls back to `widget.name` if absent. Inspected in S2.N16 patterns |
|
||||
| v2 `WidgetHandle` | `label` getter | `src/extension-api/widget.ts:355` — "Defaults to the widget name" |
|
||||
|
||||
**No inconsistency** in naming — all layers call it `display_name` (schema/wire) or `label` (runtime). The two forms are consistent: `display_name` is the static schema-declared label; `label` is the runtime-settable display string. v2 exposes `label` as a settable accessor; the Python-declared `display_name` becomes its initial value.
|
||||
|
||||
---
|
||||
|
||||
## Summary: real inconsistencies to track
|
||||
|
||||
| # | Inconsistency | Risk | Resolution |
|
||||
|---|--------------|------|-----------|
|
||||
| 1 | `type` vs `comfyClass` on `NodeHandle` — two fields, must not collapse | Medium | Document: use `comfyClass` for Python identity, `type` for LiteGraph registration. Enforced by distinct fields. |
|
||||
| 2 | Widget identity: positional index (v1 `widgets_values`) vs name key (v2 / UWF) | **HIGH** | Bridge: Austin's PR #10392 (`widgets_values_named`). v2 `WidgetHandle` is name-only. Never look up by position in v2 code. |
|
||||
| 3 | `widgetType` override in `InputSpec` takes precedence over slot type for rendering | Medium | `ctx.inspectNodeDef` must resolve `widgetType` before returning slot type. Do not skip this field. |
|
||||
| 4 | Slot type mutation (S15.OS1): `slot.type = X` is valid v1, banned in v2/UWF | Medium | v2 must not expose a `setType()` mutator on `SlotInfo`. Schema-declare outputs; UWF enforces at save time. |
|
||||
|
||||
---
|
||||
|
||||
## Cross-references
|
||||
|
||||
- **S2.N16** — widget array iteration/mutation (`node.widgets[i]`, `node.widgets.find(w => w.name === ...)`): the `name` field is the stable key; position is not. v2 forces name-based lookup.
|
||||
- **S13.SC1** — `ComfyNodeDef` inspection: callers must resolve `widgetType` override (Term 4) and understand `display_name` vs runtime `label` (Term 8). The `ctx.inspectNodeDef` typed helper (D4 G1 BLOCKER) wraps this correctly.
|
||||
- **S15.OS1** — dynamic output mutation: slot `type` strings are the agreed layer (Term 5), but v1 allows mutation that v2/UWF forbids. Track in I-PG.B2 as `strangler-bridge` until UWF Phase 3 covers output schema declaration.
|
||||
- **UWF backend data model** — `class_type`, `input_name`, `display_name` snake_case keys mirror Python origin. v2 API uses camelCase (`comfyClass`, widget `name`, widget `label`) per JS convention. No semantic difference; only case convention changes at the API boundary.
|
||||
@@ -47,6 +47,9 @@
|
||||
"test:browser:coverage": "cross-env COLLECT_COVERAGE=true pnpm test:browser",
|
||||
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:extension-api": "vitest run --config vitest.extension-api.config.mts",
|
||||
"test:extension-api:watch": "vitest --config vitest.extension-api.config.mts",
|
||||
"test:extension-api:coverage": "vitest run --config vitest.extension-api.config.mts --coverage",
|
||||
"test:unit": "nx run test",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json",
|
||||
|
||||
3
packages/extension-api/.gitignore
vendored
Normal file
3
packages/extension-api/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
docs-build/
|
||||
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
|
||||
28
packages/extension-api/package.json
Normal file
28
packages/extension-api/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@comfyorg/extension-api",
|
||||
"version": "0.1.0",
|
||||
"description": "Official TypeScript extension API for ComfyUI custom nodes",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./build/index.js"
|
||||
},
|
||||
"types": "./build/index.d.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc --emitDeclarationOnly --outDir 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:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:api"
|
||||
]
|
||||
}
|
||||
}
|
||||
461
packages/extension-api/scripts/build-docs.ts
Normal file
461
packages/extension-api/scripts/build-docs.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
78
packages/extension-api/src/index.ts
Normal file
78
packages/extension-api/src/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @comfyorg/extension-api — Public Extension API for ComfyUI
|
||||
*
|
||||
* This is the package entry point compiled to `build/index.js` + `build/index.d.ts`.
|
||||
* It re-exports the stable public contract from `src/extension-api/` in the main app.
|
||||
*
|
||||
* All types and functions exported here are part of the semver-stable surface.
|
||||
* Do not add internal implementation details to this barrel.
|
||||
*/
|
||||
|
||||
// Re-export everything from the canonical source in the main app tree.
|
||||
// The tsconfig.json paths alias @/* → ../../src/* so these resolve correctly.
|
||||
export type {
|
||||
ExtensionOptions,
|
||||
NodeExtensionOptions,
|
||||
WidgetExtensionOptions
|
||||
} from '@/extension-api/lifecycle'
|
||||
|
||||
export {
|
||||
defineExtension,
|
||||
defineNodeExtension,
|
||||
defineWidgetExtension,
|
||||
onNodeMounted,
|
||||
onNodeRemoved
|
||||
} from '@/extension-api/lifecycle'
|
||||
|
||||
export type {
|
||||
NodeHandle,
|
||||
NodeEntityId,
|
||||
SlotEntityId,
|
||||
SlotInfo,
|
||||
SlotDirection,
|
||||
NodeMode,
|
||||
Point,
|
||||
Size,
|
||||
NodeExecutedEvent,
|
||||
NodeConnectedEvent,
|
||||
NodeDisconnectedEvent,
|
||||
NodePositionChangedEvent,
|
||||
NodeSizeChangedEvent,
|
||||
NodeModeChangedEvent,
|
||||
NodeBeforeSerializeEvent
|
||||
} from '@/extension-api/node'
|
||||
|
||||
export type {
|
||||
WidgetHandle,
|
||||
WidgetEntityId,
|
||||
WidgetValue,
|
||||
WidgetOptions,
|
||||
WidgetValueChangeEvent,
|
||||
WidgetOptionChangeEvent,
|
||||
WidgetPropertyChangeEvent,
|
||||
WidgetBeforeSerializeEvent,
|
||||
WidgetBeforeQueueEvent
|
||||
} from '@/extension-api/widget'
|
||||
|
||||
export type { Handler, AsyncHandler, Unsubscribe } from '@/extension-api/events'
|
||||
|
||||
export type {
|
||||
SidebarTabExtension,
|
||||
BottomPanelExtension,
|
||||
VueExtension,
|
||||
CustomExtension,
|
||||
ToastMessageOptions,
|
||||
ToastManager,
|
||||
ExtensionManager,
|
||||
CommandManager
|
||||
} from '@/extension-api/shell'
|
||||
|
||||
export type { NodeLocatorId, NodeExecutionId } from '@/extension-api/identifiers'
|
||||
export {
|
||||
isNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
parseNodeLocatorId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
createNodeExecutionId
|
||||
} from '@/extension-api/identifiers'
|
||||
4
packages/extension-api/tsconfig.build.json
Normal file
4
packages/extension-api/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["**/*.test.ts", "**/*.spec.ts", "**/__tests__/**", "scripts/**"]
|
||||
}
|
||||
21
packages/extension-api/tsconfig.docs.json
Normal file
21
packages/extension-api/tsconfig.docs.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
19
packages/extension-api/tsconfig.json
Normal file
19
packages/extension-api/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "build",
|
||||
"rootDir": "src",
|
||||
"paths": {
|
||||
"@/*": ["../../src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["**/*.test.ts", "**/*.spec.ts", "scripts/**"]
|
||||
}
|
||||
37
packages/extension-api/typedoc.json
Normal file
37
packages/extension-api/typedoc.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
328
pnpm-lock.yaml
generated
328
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.8.4))(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.8.4))
|
||||
'@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.8.4))(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.8.4))
|
||||
'@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.8.4))(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.8.4)
|
||||
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.8.4))
|
||||
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.8.4))
|
||||
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.8.4))(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.8.4)
|
||||
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.8.4))
|
||||
'@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.8.4))(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.8.4)
|
||||
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.8.4))
|
||||
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.8.4))(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.8.4))(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.4)
|
||||
'@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.8.4))
|
||||
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.8.4)
|
||||
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.8.4)
|
||||
|
||||
packages/design-system:
|
||||
dependencies:
|
||||
@@ -1030,6 +1030,21 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
|
||||
packages/extension-api:
|
||||
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
|
||||
|
||||
packages/ingest-types:
|
||||
dependencies:
|
||||
zod:
|
||||
@@ -2431,6 +2446,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 +5471,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 +7646,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 +7889,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 +9372,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 +10228,11 @@ packages:
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.8.4:
|
||||
resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10467,14 +10514,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.8.4))(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.4)':
|
||||
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.8.4))(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.4))(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.8.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.4)
|
||||
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.4))(vue@3.5.13(typescript@5.9.3))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
@@ -11863,6 +11910,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
|
||||
@@ -12495,11 +12550,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.8.4))(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.8.4))(vitest@4.0.16)
|
||||
'@phenomnomnominal/tsquery': 6.1.4(typescript@5.9.3)
|
||||
ajv: 8.18.0
|
||||
enquirer: 2.3.6
|
||||
@@ -12507,8 +12562,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.8.4)
|
||||
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.4)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -12519,7 +12574,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.8.4))(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 +12582,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.8.4)
|
||||
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.4)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -13317,10 +13372,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.8.4))':
|
||||
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.8.4))
|
||||
'@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 +13401,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.8.4))':
|
||||
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.8.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)
|
||||
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.8.4)
|
||||
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.8.4))':
|
||||
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.8.4)
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
|
||||
@@ -13389,14 +13444,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.8.4))(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.8.4))
|
||||
'@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.8.4)
|
||||
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 +13533,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.8.4))':
|
||||
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.8.4)
|
||||
|
||||
'@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.8.4))':
|
||||
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.8.4)
|
||||
|
||||
'@tanstack/virtual-core@3.13.12': {}
|
||||
|
||||
@@ -14083,32 +14138,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.8.4))(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.8.4)
|
||||
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.8.4))(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.8.4)
|
||||
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.8.4))(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.8.4)
|
||||
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.8.4))(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.8.4)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vitest/coverage-v8@4.0.16(vitest@4.0.16)':
|
||||
@@ -14124,7 +14179,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.8.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -14145,21 +14200,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.8.4))':
|
||||
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.8.4)
|
||||
|
||||
'@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.8.4))':
|
||||
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.8.4)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -14195,7 +14250,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@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.4)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
@@ -14370,38 +14425,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.8.4))(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.8.4))
|
||||
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.8.4))(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.8.4))
|
||||
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.8.4))(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.8.4))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
@@ -14809,7 +14864,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.8.4):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.13.1
|
||||
'@astrojs/internal-helpers': 0.7.6
|
||||
@@ -14866,8 +14921,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.8.4)
|
||||
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.4))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
yocto-spinner: 0.2.3
|
||||
@@ -15060,6 +15115,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 +17522,8 @@ snapshots:
|
||||
|
||||
lru-cache@8.0.5: {}
|
||||
|
||||
lunr@2.3.9: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
lz-utils@2.1.0: {}
|
||||
@@ -17898,6 +17959,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 +19892,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.8.4
|
||||
|
||||
typegpu@0.8.2:
|
||||
dependencies:
|
||||
tinyest: 0.1.2
|
||||
@@ -20111,27 +20189,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.8.4)):
|
||||
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.8.4)
|
||||
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.4))
|
||||
|
||||
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.8.4)):
|
||||
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.8.4)
|
||||
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.4))
|
||||
|
||||
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.8.4)):
|
||||
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.8.4)
|
||||
|
||||
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.8.4)):
|
||||
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.8.4)
|
||||
|
||||
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.8.4)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.57.2(@types/node@24.10.4)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
@@ -20144,13 +20222,13 @@ 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.8.4)
|
||||
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-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.4)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -20164,9 +20242,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.8.4)
|
||||
|
||||
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.8.4)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -20180,9 +20258,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.8.4)
|
||||
|
||||
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.8.4)):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
@@ -20193,12 +20271,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.8.4)
|
||||
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.8.4)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
@@ -20208,12 +20286,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.8.4)
|
||||
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.4))
|
||||
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.8.4)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
@@ -20223,56 +20301,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.8.4)
|
||||
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.4))
|
||||
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.8.4))(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.8.4))(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.8.4)
|
||||
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.4))
|
||||
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.4))
|
||||
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.8.4))(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.8.4))(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.8.4)
|
||||
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.4))
|
||||
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.4))
|
||||
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.8.4))(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.8.4))(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.8.4)
|
||||
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.4))
|
||||
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.4))
|
||||
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.8.4)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||
@@ -20283,11 +20361,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.8.4)
|
||||
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.8.4)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||
@@ -20298,11 +20376,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.8.4)
|
||||
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.8.4):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.115.0
|
||||
lightningcss: 1.32.0
|
||||
@@ -20317,9 +20395,9 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
terser: 5.39.2
|
||||
tsx: 4.19.4
|
||||
yaml: 2.8.2
|
||||
yaml: 2.8.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):
|
||||
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.4):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.115.0
|
||||
lightningcss: 1.32.0
|
||||
@@ -20334,16 +20412,16 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
terser: 5.39.2
|
||||
tsx: 4.19.4
|
||||
yaml: 2.8.2
|
||||
yaml: 2.8.4
|
||||
|
||||
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.8.4)):
|
||||
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.8.4)
|
||||
|
||||
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.8.4):
|
||||
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.8.4))
|
||||
'@vitest/pretty-format': 4.0.16
|
||||
'@vitest/runner': 4.0.16
|
||||
'@vitest/snapshot': 4.0.16
|
||||
@@ -20360,7 +20438,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.8.4)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -20382,10 +20460,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.8.4):
|
||||
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.8.4))
|
||||
'@vitest/pretty-format': 4.0.16
|
||||
'@vitest/runner': 4.0.16
|
||||
'@vitest/snapshot': 4.0.16
|
||||
@@ -20402,7 +20480,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.8.4)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -20834,6 +20912,8 @@ snapshots:
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
yaml@2.8.4: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
|
||||
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
|
||||
39
src/extension-api-v2/__tests__/bc-01.migration.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-01.migration.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.01 — Node lifecycle: creation
|
||||
// DB cross-ref: S2.N1, S2.N8
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/saveImageExtraOutput.ts#L31
|
||||
// compat-floor: blast_radius 4.48 ≥ 2.0 — MUST pass before v2 ships
|
||||
// Migration: v1 nodeCreated(node) + beforeRegisterNodeDef → v2 defineNodeExtension({ nodeCreated(handle) })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.01 migration — node lifecycle: creation', () => {
|
||||
describe('nodeCreated parity (S2.N1)', () => {
|
||||
it.todo(
|
||||
'v1 nodeCreated and v2 nodeCreated are both invoked the same number of times when N nodes are created'
|
||||
)
|
||||
it.todo(
|
||||
'side-effects applied to the node in v1 nodeCreated(node) are reproducible via NodeHandle methods in v2'
|
||||
)
|
||||
it.todo(
|
||||
'v2 nodeCreated fires in the same relative order as v1 for extensions registered in the same order'
|
||||
)
|
||||
})
|
||||
|
||||
describe('beforeRegisterNodeDef → type-scoped defineNodeExtension (S2.N8)', () => {
|
||||
it.todo(
|
||||
'prototype mutation applied in v1 beforeRegisterNodeDef produces the same per-instance behavior as v2 type-scoped nodeCreated'
|
||||
)
|
||||
it.todo(
|
||||
'v2 type-scoped extension does not affect node types that were excluded, matching v1 type-guard behavior'
|
||||
)
|
||||
})
|
||||
|
||||
describe('VueNode mount timing invariant', () => {
|
||||
it.todo(
|
||||
'both v1 and v2 nodeCreated fire before VueNode mounts — extensions relying on this ordering do not need changes'
|
||||
)
|
||||
it.todo(
|
||||
'extensions that deferred DOM work to a callback in v1 can use onNodeMounted in v2 for the same guarantee'
|
||||
)
|
||||
})
|
||||
})
|
||||
45
src/extension-api-v2/__tests__/bc-01.v1.test.ts
Normal file
45
src/extension-api-v2/__tests__/bc-01.v1.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Category: BC.01 — Node lifecycle: creation
|
||||
// DB cross-ref: S2.N1, S2.N8
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/saveImageExtraOutput.ts#L31
|
||||
// Surface: S2.N1 = nodeCreated hook, S2.N8 = beforeRegisterNodeDef
|
||||
// compat-floor: blast_radius 4.48 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: app.registerExtension({ nodeCreated(node) { ... } })
|
||||
// Note: nodeCreated fires BEFORE the VueNode Vue component mounts; extensions needing
|
||||
// VueNode-backed state must defer (see BC.37).
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.01 v1 contract — node lifecycle: creation', () => {
|
||||
describe('S2.N1 — nodeCreated hook', () => {
|
||||
it.todo(
|
||||
'nodeCreated is called once per node instance immediately after the node is constructed'
|
||||
)
|
||||
it.todo(
|
||||
'nodeCreated receives the LGraphNode instance as its first argument'
|
||||
)
|
||||
it.todo(
|
||||
'nodeCreated fires before the node is added to the graph (graph.nodes does not yet contain the node)'
|
||||
)
|
||||
it.todo(
|
||||
'nodeCreated fires before the VueNode Vue component is mounted (vm.$el is null at call time)'
|
||||
)
|
||||
it.todo(
|
||||
'properties set on node inside nodeCreated are accessible in subsequent lifecycle hooks'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N8 — beforeRegisterNodeDef hook', () => {
|
||||
it.todo(
|
||||
'beforeRegisterNodeDef is called once per node type before the type is registered in the node registry'
|
||||
)
|
||||
it.todo(
|
||||
'beforeRegisterNodeDef receives the node constructor and the raw node definition object'
|
||||
)
|
||||
it.todo(
|
||||
'prototype mutations made in beforeRegisterNodeDef affect all subsequently created instances of that type'
|
||||
)
|
||||
it.todo(
|
||||
'beforeRegisterNodeDef is NOT called again on graph reload if the type is already registered'
|
||||
)
|
||||
})
|
||||
})
|
||||
41
src/extension-api-v2/__tests__/bc-01.v2.test.ts
Normal file
41
src/extension-api-v2/__tests__/bc-01.v2.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Category: BC.01 — Node lifecycle: creation
|
||||
// DB cross-ref: S2.N1, S2.N8
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/saveImageExtraOutput.ts#L31
|
||||
// compat-floor: blast_radius 4.48 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: defineNodeExtension({ nodeCreated(handle) { ... } })
|
||||
// Note: v2 nodeCreated receives a NodeHandle, not a raw LGraphNode. VueNode mount
|
||||
// timing guarantee is unchanged — defer to onNodeMounted for Vue-backed state.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.01 v2 contract — node lifecycle: creation', () => {
|
||||
describe('nodeCreated(handle) — per-instance setup', () => {
|
||||
it.todo(
|
||||
'nodeCreated is called once per node instance and receives a NodeHandle wrapping the created node'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.id is stable and matches the underlying LGraphNode id at call time'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.type returns the registered node type string'
|
||||
)
|
||||
it.todo(
|
||||
'state stored via NodeHandle.setState() inside nodeCreated is retrievable in subsequent hooks for the same instance'
|
||||
)
|
||||
it.todo(
|
||||
'nodeCreated fires before VueNode mounts; accessing NodeHandle.vueRef inside nodeCreated returns null'
|
||||
)
|
||||
})
|
||||
|
||||
describe('type-level registration (replacement for S2.N8)', () => {
|
||||
it.todo(
|
||||
'defineNodeExtension({ types: [\"MyNode\"] }) scopes nodeCreated to only instances of the listed types'
|
||||
)
|
||||
it.todo(
|
||||
'omitting types: causes nodeCreated to fire for every node type (global registration)'
|
||||
)
|
||||
it.todo(
|
||||
'type-scoped registration does not receive nodeCreated calls for unregistered node types'
|
||||
)
|
||||
})
|
||||
})
|
||||
36
src/extension-api-v2/__tests__/bc-02.migration.test.ts
Normal file
36
src/extension-api-v2/__tests__/bc-02.migration.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Category: BC.02 — Node lifecycle: teardown
|
||||
// DB cross-ref: S2.N4
|
||||
// Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L137
|
||||
// compat-floor: blast_radius 5.20 ≥ 2.0 — MUST pass before v2 ships
|
||||
// Migration: v1 node.onRemoved assignment → v2 defineNodeExtension({ onRemoved(handle) })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.02 migration — node lifecycle: teardown', () => {
|
||||
describe('invocation parity (S2.N4)', () => {
|
||||
it.todo(
|
||||
'v1 onRemoved and v2 onRemoved are both called the same number of times for the same sequence of node removals'
|
||||
)
|
||||
it.todo(
|
||||
'v2 onRemoved fires at the same point in the removal lifecycle as v1 (after node is detached from graph)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('resource cleanup equivalence', () => {
|
||||
it.todo(
|
||||
'intervals cleared in v1 onRemoved are equally suppressible via NodeHandle.onDispose() in v2 without manual tracking'
|
||||
)
|
||||
it.todo(
|
||||
'DOM elements removed manually in v1 onRemoved are automatically removed by v2 auto-disposal when registered via addDOMWidget()'
|
||||
)
|
||||
it.todo(
|
||||
'observer.disconnect() patterns in v1 can be replaced by NodeHandle.onDispose(() => observer.disconnect()) in v2'
|
||||
)
|
||||
})
|
||||
|
||||
describe('graph clear coverage', () => {
|
||||
it.todo(
|
||||
'both v1 and v2 teardown hooks are invoked for all nodes when graph.clear() is called'
|
||||
)
|
||||
})
|
||||
})
|
||||
135
src/extension-api-v2/__tests__/bc-02.v1.test.ts
Normal file
135
src/extension-api-v2/__tests__/bc-02.v1.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// Category: BC.02 — Node lifecycle: teardown
|
||||
// DB cross-ref: S2.N4
|
||||
// Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L137
|
||||
// Surface: S2.N4 = node.onRemoved
|
||||
// compat-floor: blast_radius 5.20 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.onRemoved = function() { /* cleanup DOM, intervals, observers */ }
|
||||
//
|
||||
// I-TF.3.C3 — proof-of-concept harness wiring.
|
||||
// Phase A harness limitation: MiniGraph.remove() deletes the entity from the World
|
||||
// but does NOT automatically call onRemoved (that requires Phase B eval sandbox +
|
||||
// LiteGraph prototype wiring). The wired tests below call onRemoved explicitly after
|
||||
// graph.remove() to prove the harness mechanics and assertion patterns work.
|
||||
// The TODO stubs below them track what needs Phase B to become real assertions.
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
countEvidenceExcerpts,
|
||||
createHarnessWorld,
|
||||
createMiniComfyApp,
|
||||
loadEvidenceSnippet
|
||||
} from '../harness'
|
||||
|
||||
// ── Proof-of-concept wired tests (I-TF.3.C3) ────────────────────────────────
|
||||
// These pass today. They prove: (a) the harness can model the v1 teardown
|
||||
// pattern, (b) removal is reflected in the World, (c) the cleanup callback
|
||||
// fires when the extension calls it, (d) evidence excerpts load for S2.N4.
|
||||
|
||||
describe('BC.02 v1 contract — node lifecycle: teardown [harness POC]', () => {
|
||||
describe('S2.N4 — onRemoved harness mechanics', () => {
|
||||
it('cleanup callback fires when extension calls it after graph.remove()', () => {
|
||||
const world = createHarnessWorld()
|
||||
const app = createMiniComfyApp(world)
|
||||
|
||||
// v1 pattern: extension patches onRemoved on the node during nodeCreated.
|
||||
// We model this as a plain function stored on a node-shaped object.
|
||||
const cleanupFn = vi.fn()
|
||||
const node = {
|
||||
type: 'LTXVideo',
|
||||
entityId: app.graph.add({ type: 'LTXVideo' }),
|
||||
onRemoved: cleanupFn
|
||||
}
|
||||
|
||||
expect(world.findNode(node.entityId)).toBeDefined()
|
||||
|
||||
// Simulate the LiteGraph removal sequence (Phase A: explicit call).
|
||||
app.graph.remove(node.entityId)
|
||||
node.onRemoved()
|
||||
|
||||
expect(world.findNode(node.entityId)).toBeUndefined()
|
||||
expect(cleanupFn).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('cleanup callback does not fire if remove is never called', () => {
|
||||
const world = createHarnessWorld()
|
||||
const app = createMiniComfyApp(world)
|
||||
const cleanupFn = vi.fn()
|
||||
const entityId = app.graph.add({ type: 'KSampler' })
|
||||
|
||||
// Node exists; no removal; callback should not have been invoked.
|
||||
void entityId
|
||||
expect(cleanupFn).not.toHaveBeenCalled()
|
||||
expect(world.allNodes()).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('multiple nodes — each removal triggers only its own callback', () => {
|
||||
const world = createHarnessWorld()
|
||||
const app = createMiniComfyApp(world)
|
||||
|
||||
const cbA = vi.fn()
|
||||
const cbB = vi.fn()
|
||||
const idA = app.graph.add({ type: 'NodeA' })
|
||||
const idB = app.graph.add({ type: 'NodeB' })
|
||||
|
||||
// Remove only A.
|
||||
app.graph.remove(idA)
|
||||
cbA() // simulate LiteGraph calling onRemoved on the removed node only
|
||||
|
||||
expect(cbA).toHaveBeenCalledOnce()
|
||||
expect(cbB).not.toHaveBeenCalled()
|
||||
expect(world.findNode(idA)).toBeUndefined()
|
||||
expect(world.findNode(idB)).toBeDefined()
|
||||
})
|
||||
|
||||
it('graph.clear() removes all nodes from the World', () => {
|
||||
const world = createHarnessWorld()
|
||||
const app = createMiniComfyApp(world)
|
||||
|
||||
app.graph.add({ type: 'NodeA' })
|
||||
app.graph.add({ type: 'NodeB' })
|
||||
app.graph.add({ type: 'NodeC' })
|
||||
expect(world.allNodes()).toHaveLength(3)
|
||||
|
||||
world.clear()
|
||||
expect(world.allNodes()).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('S2.N4 — evidence excerpt (loadEvidenceSnippet)', () => {
|
||||
it('S2.N4 has at least one evidence excerpt in the snapshot', () => {
|
||||
expect(countEvidenceExcerpts('S2.N4')).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('S2.N4 excerpt contains onRemoved fingerprint', () => {
|
||||
const snippet = loadEvidenceSnippet('S2.N4', 0)
|
||||
expect(snippet.length).toBeGreaterThan(0)
|
||||
expect(snippet).toMatch(/onRemoved/i)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── Phase B stubs — need eval sandbox + LiteGraph prototype wiring ───────────
|
||||
|
||||
describe('BC.02 v1 contract — node lifecycle: teardown [Phase B]', () => {
|
||||
describe('S2.N4 — node.onRemoved', () => {
|
||||
it.todo(
|
||||
'onRemoved is called exactly once when a node is removed from the graph via graph.remove(node)'
|
||||
)
|
||||
it.todo(
|
||||
'onRemoved is called when a node is deleted via the canvas context-menu delete action'
|
||||
)
|
||||
it.todo(
|
||||
'onRemoved is called for every node when the graph is cleared (graph.clear())'
|
||||
)
|
||||
it.todo(
|
||||
'DOM widgets appended by the extension are accessible for cleanup inside onRemoved (not yet garbage-collected)'
|
||||
)
|
||||
it.todo(
|
||||
'setInterval / requestAnimationFrame handles stored on the node instance can be cancelled inside onRemoved'
|
||||
)
|
||||
it.todo(
|
||||
'MutationObserver and ResizeObserver instances stored on the node can be disconnected inside onRemoved'
|
||||
)
|
||||
})
|
||||
})
|
||||
38
src/extension-api-v2/__tests__/bc-02.v2.test.ts
Normal file
38
src/extension-api-v2/__tests__/bc-02.v2.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Category: BC.02 — Node lifecycle: teardown
|
||||
// DB cross-ref: S2.N4
|
||||
// Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L137
|
||||
// compat-floor: blast_radius 5.20 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: defineNodeExtension({ onRemoved(handle) { ... } })
|
||||
// Note: v2 onRemoved runs inside the NodeHandle scope; extension-owned resources
|
||||
// registered via handle APIs are auto-disposed before onRemoved fires.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.02 v2 contract — node lifecycle: teardown', () => {
|
||||
describe('onRemoved(handle) — cleanup hook', () => {
|
||||
it.todo(
|
||||
'onRemoved is called exactly once per node instance when the node is removed from the graph'
|
||||
)
|
||||
it.todo(
|
||||
'onRemoved receives the same NodeHandle that was passed to nodeCreated for the same instance'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.getState() is still readable inside onRemoved (state not yet cleared)'
|
||||
)
|
||||
it.todo(
|
||||
'onRemoved is called for every node when the graph is cleared, in no guaranteed order'
|
||||
)
|
||||
})
|
||||
|
||||
describe('auto-disposal of handle-registered resources', () => {
|
||||
it.todo(
|
||||
'DOM widgets registered via NodeHandle.addDOMWidget() are removed from the DOM before onRemoved fires'
|
||||
)
|
||||
it.todo(
|
||||
'cleanup functions registered via NodeHandle.onDispose() are invoked before onRemoved fires'
|
||||
)
|
||||
it.todo(
|
||||
'extension can still perform additional teardown in onRemoved after auto-disposal completes'
|
||||
)
|
||||
})
|
||||
})
|
||||
36
src/extension-api-v2/__tests__/bc-03.migration.test.ts
Normal file
36
src/extension-api-v2/__tests__/bc-03.migration.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Category: BC.03 — Node lifecycle: hydration from saved workflows
|
||||
// DB cross-ref: S1.H1, S2.N7
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// compat-floor: blast_radius 4.91 ≥ 2.0 — MUST pass before v2 ships
|
||||
// Migration: v1 node.onConfigure / beforeRegisterNodeDef → v2 defineNodeExtension({ onConfigure(handle, data) })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.03 migration — node lifecycle: hydration from saved workflows', () => {
|
||||
describe('onConfigure parity (S2.N7)', () => {
|
||||
it.todo(
|
||||
'v1 node.onConfigure and v2 onConfigure are both called exactly once per node during workflow load'
|
||||
)
|
||||
it.todo(
|
||||
'the serialized data object received in v2 onConfigure contains the same fields as in v1'
|
||||
)
|
||||
it.todo(
|
||||
'custom property restoration logic written for v1 onConfigure is portable to v2 with only handle substitution'
|
||||
)
|
||||
})
|
||||
|
||||
describe('beforeRegisterNodeDef hydration guard → type-scoped extension (S1.H1)', () => {
|
||||
it.todo(
|
||||
'prototype-level onConfigure injected via v1 beforeRegisterNodeDef produces the same hydration result as a v2 type-scoped onConfigure'
|
||||
)
|
||||
it.todo(
|
||||
'v2 type-scoped onConfigure does not fire for node types not listed in types:, matching v1 guard behavior'
|
||||
)
|
||||
})
|
||||
|
||||
describe('fresh-creation exclusion invariant', () => {
|
||||
it.todo(
|
||||
'neither v1 nor v2 onConfigure fires when a node is created fresh (not from a saved workflow)'
|
||||
)
|
||||
})
|
||||
})
|
||||
39
src/extension-api-v2/__tests__/bc-03.v1.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-03.v1.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.03 — Node lifecycle: hydration from saved workflows
|
||||
// DB cross-ref: S1.H1, S2.N7
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// Surface: S1.H1 = beforeRegisterNodeDef (used for hydration guards), S2.N7 = node.onConfigure
|
||||
// compat-floor: blast_radius 4.91 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: S1.H1 = beforeRegisterNodeDef guard; S2.N7 = node.onConfigure = function(data) { ... }
|
||||
// Note: loadedGraphNode hook exists in LiteGraph but is effectively unused in ComfyUI —
|
||||
// onConfigure is the de-facto hydration surface.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.03 v1 contract — node lifecycle: hydration from saved workflows', () => {
|
||||
describe('S2.N7 — node.onConfigure', () => {
|
||||
it.todo(
|
||||
'onConfigure is called when a saved workflow is loaded and the node is rehydrated from serialized data'
|
||||
)
|
||||
it.todo(
|
||||
'onConfigure receives the raw serialized node object (data) as its first argument'
|
||||
)
|
||||
it.todo(
|
||||
'onConfigure is NOT called on freshly created nodes (only on deserialization)'
|
||||
)
|
||||
it.todo(
|
||||
'widget values written to data inside a prior session are accessible via data.widgets_values in onConfigure'
|
||||
)
|
||||
it.todo(
|
||||
'extensions can restore custom properties stored in data.properties inside onConfigure'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S1.H1 — beforeRegisterNodeDef hydration guard', () => {
|
||||
it.todo(
|
||||
'beforeRegisterNodeDef can inject a custom onConfigure override on the node prototype before any instance is created'
|
||||
)
|
||||
it.todo(
|
||||
'prototype-level onConfigure injected in beforeRegisterNodeDef is invoked for all instances during workflow load'
|
||||
)
|
||||
})
|
||||
})
|
||||
36
src/extension-api-v2/__tests__/bc-03.v2.test.ts
Normal file
36
src/extension-api-v2/__tests__/bc-03.v2.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Category: BC.03 — Node lifecycle: hydration from saved workflows
|
||||
// DB cross-ref: S1.H1, S2.N7
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// compat-floor: blast_radius 4.91 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: defineNodeExtension({ onConfigure(handle, data) { ... } })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.03 v2 contract — node lifecycle: hydration from saved workflows', () => {
|
||||
describe('onConfigure(handle, data) — workflow hydration hook', () => {
|
||||
it.todo(
|
||||
'onConfigure is called when a node is rehydrated from a saved workflow and NOT on fresh node creation'
|
||||
)
|
||||
it.todo(
|
||||
'onConfigure receives the NodeHandle as first argument and the raw serialized node object as second argument'
|
||||
)
|
||||
it.todo(
|
||||
'data passed to onConfigure contains widgets_values from the saved workflow'
|
||||
)
|
||||
it.todo(
|
||||
'data passed to onConfigure contains properties from the saved workflow'
|
||||
)
|
||||
it.todo(
|
||||
'state written to NodeHandle inside onConfigure is readable in all subsequent hook calls for that instance'
|
||||
)
|
||||
})
|
||||
|
||||
describe('ordering and idempotency guarantees', () => {
|
||||
it.todo(
|
||||
'onConfigure fires after nodeCreated for the same instance during workflow load'
|
||||
)
|
||||
it.todo(
|
||||
'onConfigure is not called a second time if the same node receives a re-configure (idempotent load)'
|
||||
)
|
||||
})
|
||||
})
|
||||
45
src/extension-api-v2/__tests__/bc-04.migration.test.ts
Normal file
45
src/extension-api-v2/__tests__/bc-04.migration.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Category: BC.04 — Node interaction: pointer, selection, resize
|
||||
// DB cross-ref: S2.N10, S2.N17, S2.N19
|
||||
// Exemplar: https://github.com/diodiogod/TTS-Audio-Suite/blob/main/web/chatterbox_voice_capture.js#L202
|
||||
// compat-floor: blast_radius 4.95 ≥ 2.0 — MUST pass before v2 ships
|
||||
// Migration: v1 node.onMouseDown/onSelected/onResize → v2 handle.on('mousedown'|'selected'|'resize', ...)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.04 migration — node interaction: pointer, selection, resize', () => {
|
||||
describe('mousedown parity (S2.N10)', () => {
|
||||
it.todo(
|
||||
'v1 node.onMouseDown and v2 handle.on("mousedown") are both invoked for the same pointer-down events'
|
||||
)
|
||||
it.todo(
|
||||
'propagation-stop by returning true in v1 is equivalent to event.stopPropagation() in v2 handler'
|
||||
)
|
||||
it.todo(
|
||||
'local coordinates passed to v1 onMouseDown match the x/y in the v2 event object for the same input'
|
||||
)
|
||||
})
|
||||
|
||||
describe('selection parity (S2.N17)', () => {
|
||||
it.todo(
|
||||
'v1 node.onSelected and v2 handle.on("selected") are both invoked when the node is selected'
|
||||
)
|
||||
it.todo(
|
||||
'v2 introduces an explicit deselected event absent in v1; migration must add deselected handler for cleanup that relied on onSelected re-fire'
|
||||
)
|
||||
})
|
||||
|
||||
describe('resize parity (S2.N19)', () => {
|
||||
it.todo(
|
||||
'v1 node.onResize([w,h]) and v2 handle.on("resize", { width, height }) convey the same dimensions for the same resize action'
|
||||
)
|
||||
it.todo(
|
||||
'computeSize overrides that triggered onResize in v1 still trigger the resize event in v2'
|
||||
)
|
||||
})
|
||||
|
||||
describe('listener lifetime', () => {
|
||||
it.todo(
|
||||
'v1 listeners on removed nodes remain registered (leak); v2 handle.on() listeners are auto-removed on node removal'
|
||||
)
|
||||
})
|
||||
})
|
||||
49
src/extension-api-v2/__tests__/bc-04.v1.test.ts
Normal file
49
src/extension-api-v2/__tests__/bc-04.v1.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// Category: BC.04 — Node interaction: pointer, selection, resize
|
||||
// DB cross-ref: S2.N10, S2.N17, S2.N19
|
||||
// Exemplar: https://github.com/diodiogod/TTS-Audio-Suite/blob/main/web/chatterbox_voice_capture.js#L202
|
||||
// Surface: S2.N10 = node.onMouseDown, S2.N17 = node.onSelected, S2.N19 = node.onResize
|
||||
// compat-floor: blast_radius 4.95 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.onMouseDown, node.onSelected, node.onResize prototype method assignments
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.04 v1 contract — node interaction: pointer, selection, resize', () => {
|
||||
describe('S2.N10 — node.onMouseDown', () => {
|
||||
it.todo(
|
||||
'onMouseDown is called when a pointer-down event occurs within the node bounding box on the canvas'
|
||||
)
|
||||
it.todo(
|
||||
'onMouseDown receives the MouseEvent and the local [x, y] position within the node as arguments'
|
||||
)
|
||||
it.todo(
|
||||
'returning true from onMouseDown stops propagation to LiteGraph default mouse handling'
|
||||
)
|
||||
it.todo(
|
||||
'onMouseDown is NOT called when the pointer down is outside the node bounding box'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N17 — node.onSelected', () => {
|
||||
it.todo(
|
||||
'onSelected is called when the node transitions to selected state (single-click or box-select)'
|
||||
)
|
||||
it.todo(
|
||||
'onSelected is called once per selection event even if the node was already selected'
|
||||
)
|
||||
it.todo(
|
||||
'onSelected is not called when a different node is selected and this node is deselected'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N19 — node.onResize', () => {
|
||||
it.todo(
|
||||
'onResize is called after the node dimensions change (user drag-resize or programmatic setSize)'
|
||||
)
|
||||
it.todo(
|
||||
'onResize receives the new [width, height] array as its argument'
|
||||
)
|
||||
it.todo(
|
||||
'onResize is called after the node size is committed, not during the drag'
|
||||
)
|
||||
})
|
||||
})
|
||||
48
src/extension-api-v2/__tests__/bc-04.v2.test.ts
Normal file
48
src/extension-api-v2/__tests__/bc-04.v2.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Category: BC.04 — Node interaction: pointer, selection, resize
|
||||
// DB cross-ref: S2.N10, S2.N17, S2.N19
|
||||
// Exemplar: https://github.com/diodiogod/TTS-Audio-Suite/blob/main/web/chatterbox_voice_capture.js#L202
|
||||
// compat-floor: blast_radius 4.95 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: defineNodeExtension({ on('mousedown', ...), on('selected', ...), on('resize', ...) })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.04 v2 contract — node interaction: pointer, selection, resize', () => {
|
||||
describe('on(\"mousedown\", handler) — pointer events (S2.N10)', () => {
|
||||
it.todo(
|
||||
'handle.on("mousedown", handler) registers a listener called when pointer-down occurs within the node bounding box'
|
||||
)
|
||||
it.todo(
|
||||
'handler receives an event object with local x/y coordinates relative to the node origin'
|
||||
)
|
||||
it.todo(
|
||||
'handler returning true stops propagation to LiteGraph default mouse handling'
|
||||
)
|
||||
it.todo(
|
||||
'listener registered via handle.on() is automatically removed when the node is removed from the graph'
|
||||
)
|
||||
})
|
||||
|
||||
describe('on(\"selected\", handler) — selection focus (S2.N17)', () => {
|
||||
it.todo(
|
||||
'handle.on("selected", handler) is called when the node enters selected state'
|
||||
)
|
||||
it.todo(
|
||||
'handle.on("deselected", handler) is called when the node exits selected state'
|
||||
)
|
||||
it.todo(
|
||||
'selected and deselected events do not fire during programmatic selection with { silent: true } option'
|
||||
)
|
||||
})
|
||||
|
||||
describe('on(\"resize\", handler) — resize feedback (S2.N19)', () => {
|
||||
it.todo(
|
||||
'handle.on("resize", handler) is called after the node dimensions change'
|
||||
)
|
||||
it.todo(
|
||||
'handler receives a { width, height } object matching the new node size'
|
||||
)
|
||||
it.todo(
|
||||
'resize event fires for both user drag-resize and programmatic NodeHandle.setSize() calls'
|
||||
)
|
||||
})
|
||||
})
|
||||
39
src/extension-api-v2/__tests__/bc-05.migration.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-05.migration.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.05 — Custom DOM widgets and node sizing
|
||||
// DB cross-ref: S4.W2, S2.N11
|
||||
// Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218
|
||||
// compat-floor: blast_radius 5.45 ≥ 2.0 — MUST pass before v2 ships
|
||||
// Migration: v1 node.addDOMWidget + node.computeSize → v2 NodeHandle.addDOMWidget + WidgetHandle.setHeight
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.05 migration — custom DOM widgets and node sizing', () => {
|
||||
describe('widget registration parity (S4.W2)', () => {
|
||||
it.todo(
|
||||
'v1 node.addDOMWidget and v2 NodeHandle.addDOMWidget both result in the element being visible inside the node widget area'
|
||||
)
|
||||
it.todo(
|
||||
'the widget is accessible by name in both v1 node.widgets and v2 NodeHandle.widgets after registration'
|
||||
)
|
||||
it.todo(
|
||||
'v1 opts.getHeight() returning N produces the same reserved height as v2 addDOMWidget({ height: N })'
|
||||
)
|
||||
})
|
||||
|
||||
describe('computeSize elimination (S2.N11)', () => {
|
||||
it.todo(
|
||||
'v1 manual computeSize override is unnecessary in v2; equivalent height reservation is achieved via WidgetHandle.setHeight()'
|
||||
)
|
||||
it.todo(
|
||||
'node rendered with v2 auto-computeSize integration has the same final dimensions as v1 with an equivalent manual computeSize override'
|
||||
)
|
||||
})
|
||||
|
||||
describe('cleanup parity', () => {
|
||||
it.todo(
|
||||
'v1 requires manual DOM removal in onRemoved; v2 auto-removes the widget element — both result in the element being absent after node removal'
|
||||
)
|
||||
it.todo(
|
||||
'v2 auto-cleanup does not remove DOM elements that were not registered via addDOMWidget, matching v1 scoping'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
src/extension-api-v2/__tests__/bc-05.v1.test.ts
Normal file
43
src/extension-api-v2/__tests__/bc-05.v1.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Category: BC.05 — Custom DOM widgets and node sizing
|
||||
// DB cross-ref: S4.W2, S2.N11
|
||||
// Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218
|
||||
// Surface: S4.W2 = node.addDOMWidget, S2.N11 = node.computeSize override
|
||||
// compat-floor: blast_radius 5.45 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.addDOMWidget(name, type, element, opts) + node.computeSize = function(out) { ... }
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.05 v1 contract — custom DOM widgets and node sizing', () => {
|
||||
describe('S4.W2 — node.addDOMWidget', () => {
|
||||
it.todo(
|
||||
'addDOMWidget(name, type, element, opts) appends the provided DOM element inside the node widget area'
|
||||
)
|
||||
it.todo(
|
||||
'widget registered via addDOMWidget is accessible via node.widgets array by the given name'
|
||||
)
|
||||
it.todo(
|
||||
'addDOMWidget opts.getHeight() is called during layout to determine the widget reserved height'
|
||||
)
|
||||
it.todo(
|
||||
'addDOMWidget opts.onDraw(ctx) callback is invoked during each canvas render pass'
|
||||
)
|
||||
it.todo(
|
||||
'the DOM element is removed from the document when the node is removed via graph.remove()'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N11 — node.computeSize override', () => {
|
||||
it.todo(
|
||||
'assigning node.computeSize = function(out) { ... } overrides the default size calculation for the node'
|
||||
)
|
||||
it.todo(
|
||||
'overridden computeSize is called by LiteGraph layout engine before rendering'
|
||||
)
|
||||
it.todo(
|
||||
'computeSize can return a [width, height] pair that accounts for the DOM widget reserved height'
|
||||
)
|
||||
it.todo(
|
||||
'computeSize override persists across graph load/reload if set in nodeCreated or beforeRegisterNodeDef'
|
||||
)
|
||||
})
|
||||
})
|
||||
39
src/extension-api-v2/__tests__/bc-05.v2.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-05.v2.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.05 — Custom DOM widgets and node sizing
|
||||
// DB cross-ref: S4.W2, S2.N11
|
||||
// Exemplar: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218
|
||||
// compat-floor: blast_radius 5.45 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: NodeHandle.addDOMWidget(opts) — auto-hooks computeSize via WidgetHandle geometry
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.05 v2 contract — custom DOM widgets and node sizing', () => {
|
||||
describe('NodeHandle.addDOMWidget(opts) — widget registration', () => {
|
||||
it.todo(
|
||||
'NodeHandle.addDOMWidget({ name, element }) appends the element inside the node widget area'
|
||||
)
|
||||
it.todo(
|
||||
'addDOMWidget returns a WidgetHandle that exposes the registered widget for further configuration'
|
||||
)
|
||||
it.todo(
|
||||
'widget registered via addDOMWidget is included in NodeHandle.widgets list under opts.name'
|
||||
)
|
||||
it.todo(
|
||||
'addDOMWidget({ name, element, height }) reserves the specified height without requiring a manual computeSize override'
|
||||
)
|
||||
it.todo(
|
||||
'the DOM element is removed from the document automatically when the node is removed (no manual cleanup)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('WidgetHandle geometry — auto-computeSize integration (S2.N11)', () => {
|
||||
it.todo(
|
||||
'WidgetHandle.setHeight(px) updates the reserved height and triggers a node relayout without a manual computeSize call'
|
||||
)
|
||||
it.todo(
|
||||
'when multiple DOM widgets are registered, the total node height accounts for all widget heights'
|
||||
)
|
||||
it.todo(
|
||||
'calling WidgetHandle.setHeight() after initial mount correctly re-lays out the node on next render frame'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-06.migration.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-06.migration.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.06 — Custom canvas drawing (per-node and canvas-level)
|
||||
// DB cross-ref: S2.N9, S3.C1, S3.C2
|
||||
// Exemplar: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1256
|
||||
// compat-floor: blast_radius 5.25 ≥ 2.0 — MUST pass before v2 ships
|
||||
// Migration: v1 node.onDrawForeground → v2 NodeHandle.onDraw (partial).
|
||||
// S3.C1 / S3.C2 canvas-level overrides: no v2 migration path yet (D9 Phase C).
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.06 migration — custom canvas drawing (per-node and canvas-level)', () => {
|
||||
describe('per-node drawing migration (S2.N9)', () => {
|
||||
it.todo(
|
||||
'v1 node.onDrawForeground and v2 NodeHandle.onDraw both produce visually equivalent output on the canvas for the same drawing operations'
|
||||
)
|
||||
it.todo(
|
||||
'draw callback in v2 fires the same number of times per second as v1 onDrawForeground for a static scene'
|
||||
)
|
||||
it.todo(
|
||||
'v2 DrawContext.ctx is the same CanvasRenderingContext2D state as v1 receives (same transform, same clip)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('auto-deregistration vs manual cleanup', () => {
|
||||
it.todo(
|
||||
'v1 onDrawForeground continues to fire after node removal if the reference is not cleared (leak); v2 onDraw is auto-removed'
|
||||
)
|
||||
it.todo(
|
||||
'v2 auto-deregistration on node removal does not affect onDraw callbacks registered for other nodes'
|
||||
)
|
||||
})
|
||||
|
||||
describe('canvas-level override coexistence (S3.C1, S3.C2)', () => {
|
||||
it.todo(
|
||||
'extensions that replace LGraphCanvas.prototype methods in v1 continue to function alongside v2 NodeHandle.onDraw registrations without conflict'
|
||||
)
|
||||
it.todo(
|
||||
'processContextMenu replacement in v1 is not disrupted by extensions migrated to v2 per-node APIs'
|
||||
)
|
||||
})
|
||||
})
|
||||
55
src/extension-api-v2/__tests__/bc-06.v1.test.ts
Normal file
55
src/extension-api-v2/__tests__/bc-06.v1.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// Category: BC.06 — Custom canvas drawing (per-node and canvas-level)
|
||||
// DB cross-ref: S2.N9, S3.C1, S3.C2
|
||||
// Exemplar: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1256
|
||||
// Surface: S2.N9 = node.onDrawForeground, S3.C1 = LGraphCanvas.prototype overrides, S3.C2 = ContextMenu replacement
|
||||
// compat-floor: blast_radius 5.25 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.onDrawForeground(ctx, area), LGraphCanvas.prototype.processContextMenu = ...,
|
||||
// LGraphCanvas.prototype.drawNodeShape = ... etc.
|
||||
// v1_scope_note: Simon Tranter (COM-3668) vetoed canvas drawing overrides as "too hacky/specific".
|
||||
// S3.C* patterns tracked for blast-radius / strangler-fig planning only.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.06 v1 contract — custom canvas drawing (per-node and canvas-level)', () => {
|
||||
describe('S2.N9 — node.onDrawForeground', () => {
|
||||
it.todo(
|
||||
'onDrawForeground(ctx, visibleArea) is called once per render frame for each visible node'
|
||||
)
|
||||
it.todo(
|
||||
'ctx passed to onDrawForeground is the same CanvasRenderingContext2D used by LiteGraph for the node layer'
|
||||
)
|
||||
it.todo(
|
||||
'drawing operations performed in onDrawForeground appear above the node body and below the selection highlight'
|
||||
)
|
||||
it.todo(
|
||||
'onDrawForeground is NOT called for nodes outside the visible area (culled by LiteGraph)'
|
||||
)
|
||||
it.todo(
|
||||
'canvas transform (scale, translate) is already applied when onDrawForeground fires — coordinates are in graph space'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S3.C1 — LGraphCanvas.prototype method overrides', () => {
|
||||
it.todo(
|
||||
'assigning LGraphCanvas.prototype.drawNodeShape replaces the built-in node shape renderer for all nodes'
|
||||
)
|
||||
it.todo(
|
||||
'prototype override affects all canvas instances sharing the same prototype (global side-effect)'
|
||||
)
|
||||
it.todo(
|
||||
'two extensions both overriding the same LGraphCanvas.prototype method result in last-writer-wins behavior'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S3.C2 — ContextMenu global replacement', () => {
|
||||
it.todo(
|
||||
'reassigning LGraphCanvas.prototype.processContextMenu replaces the context-menu handler for every right-click on the canvas'
|
||||
)
|
||||
it.todo(
|
||||
'extensions replacing processContextMenu must call the original to preserve built-in menu items'
|
||||
)
|
||||
it.todo(
|
||||
'replacing processContextMenu is the most destructive canvas-level override — absence of original call silently drops all built-in menu entries'
|
||||
)
|
||||
})
|
||||
})
|
||||
41
src/extension-api-v2/__tests__/bc-06.v2.test.ts
Normal file
41
src/extension-api-v2/__tests__/bc-06.v2.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Category: BC.06 — Custom canvas drawing (per-node and canvas-level)
|
||||
// DB cross-ref: S2.N9, S3.C1, S3.C2
|
||||
// Exemplar: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1256
|
||||
// compat-floor: blast_radius 5.25 ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: NodeHandle.onDraw(callback) for per-node drawing (S2.N9).
|
||||
// Canvas-level overrides (S3.C1, S3.C2) are OUT OF v2 SCOPE — deferred to D9 Phase C.
|
||||
// S3.C* stubs present for blast-radius tracking and strangler-fig planning.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.06 v2 contract — custom canvas drawing (per-node and canvas-level)', () => {
|
||||
describe('NodeHandle.onDraw(callback) — per-node foreground drawing (S2.N9)', () => {
|
||||
it.todo(
|
||||
'NodeHandle.onDraw(cb) registers cb to be called once per render frame while the node is visible'
|
||||
)
|
||||
it.todo(
|
||||
'callback receives a DrawContext with ctx (CanvasRenderingContext2D) and area (bounding rect) arguments'
|
||||
)
|
||||
it.todo(
|
||||
'drawing operations in the callback appear in the same layer as v1 onDrawForeground (above node body)'
|
||||
)
|
||||
it.todo(
|
||||
'the canvas transform is pre-applied when the callback fires — coordinates are in graph space, matching v1 behavior'
|
||||
)
|
||||
it.todo(
|
||||
'callback registered via NodeHandle.onDraw() is automatically deregistered when the node is removed'
|
||||
)
|
||||
})
|
||||
|
||||
describe('canvas-level overrides — deferred (S3.C1, S3.C2)', () => {
|
||||
it.todo(
|
||||
'[D9 Phase C] v2 exposes no stable API for replacing LGraphCanvas.prototype.drawNodeShape — extensions using this pattern must remain on v1 shim'
|
||||
)
|
||||
it.todo(
|
||||
'[D9 Phase C] v2 exposes no stable API for replacing processContextMenu — context-menu customization is deferred to the ComfyUI menu extension point'
|
||||
)
|
||||
it.todo(
|
||||
'[D9 Phase C] blast-radius tracking: S3.C1 and S3.C2 overrides coexist with v2 per-node drawing without mutual interference'
|
||||
)
|
||||
})
|
||||
})
|
||||
44
src/extension-api-v2/__tests__/bc-07.migration.test.ts
Normal file
44
src/extension-api-v2/__tests__/bc-07.migration.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Category: BC.07 — Connection observation, intercept, and veto
|
||||
// DB cross-ref: S2.N3, S2.N12, S2.N13
|
||||
// Exemplar: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/node_mode_relay.js#L90
|
||||
// Migration: v1 prototype method assignment → v2 NodeHandle.on('connectInput'/'connectOutput'/'connectionChange')
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.07 migration — connection observation, intercept, and veto', () => {
|
||||
describe('onConnectionsChange → on(\'connectionChange\') (S2.N3)', () => {
|
||||
it.todo(
|
||||
'v1 onConnectionsChange and v2 on(\'connectionChange\') both fire for the same link connect event with equivalent payload data'
|
||||
)
|
||||
it.todo(
|
||||
'v2 connectionChange event fires at the same point in the link-wiring sequence as v1 onConnectionsChange'
|
||||
)
|
||||
})
|
||||
|
||||
describe('onConnectInput → on(\'connectInput\') (S2.N12)', () => {
|
||||
it.todo(
|
||||
'v1 onConnectInput returning false and v2 on(\'connectInput\') returning false both result in an unwired graph with no link object created'
|
||||
)
|
||||
it.todo(
|
||||
'type coercion performed inside v1 onConnectInput produces the same wired slot type as equivalent mutation inside v2 on(\'connectInput\')'
|
||||
)
|
||||
})
|
||||
|
||||
describe('onConnectOutput → on(\'connectOutput\') (S2.N13)', () => {
|
||||
it.todo(
|
||||
'v1 onConnectOutput veto and v2 on(\'connectOutput\') veto both prevent connectionChange from firing on either endpoint node'
|
||||
)
|
||||
it.todo(
|
||||
'v2 on(\'connectOutput\') listener receives equivalent data to v1 onConnectOutput arguments for the same connection attempt'
|
||||
)
|
||||
})
|
||||
|
||||
describe('scope and cleanup', () => {
|
||||
it.todo(
|
||||
'v1 prototype method persists after extension unregisters (no cleanup); v2 on() listeners are removed on scope dispose'
|
||||
)
|
||||
it.todo(
|
||||
'v2 cleanup does not affect connection listeners registered by other extensions on the same node'
|
||||
)
|
||||
})
|
||||
})
|
||||
53
src/extension-api-v2/__tests__/bc-07.v1.test.ts
Normal file
53
src/extension-api-v2/__tests__/bc-07.v1.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// Category: BC.07 — Connection observation, intercept, and veto
|
||||
// DB cross-ref: S2.N3, S2.N12, S2.N13
|
||||
// Exemplar: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/node_mode_relay.js#L90
|
||||
// blast_radius: 5.46 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.onConnectInput(slot, type, link, node, fromSlot)
|
||||
// node.onConnectOutput(slot, type, link, node, toSlot)
|
||||
// node.onConnectionsChange(type, slot, connected, link, ioSlot)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.07 v1 contract — connection observation, intercept, and veto', () => {
|
||||
describe('S2.N3 — onConnectionsChange: passive observation', () => {
|
||||
it.todo(
|
||||
'onConnectionsChange is called on the node when any input or output link is connected or disconnected'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectionsChange receives type (INPUT=1/OUTPUT=2), slot index, connected boolean, link info, and ioSlot'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectionsChange fires after the link is already wired into the graph (link is present at call time)'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectionsChange fires for both the source node and the target node on a single link operation'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N12 — onConnectInput: intercept and veto incoming connections', () => {
|
||||
it.todo(
|
||||
'onConnectInput returning false vetoes the connection before it is wired'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectInput returning true (or undefined) allows the connection to proceed'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectInput receives slot index, incoming type, link object, source node, and source slot'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectInput can mutate the slot type to coerce an incompatible type before wiring'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N13 — onConnectOutput: intercept and veto outgoing connections', () => {
|
||||
it.todo(
|
||||
'onConnectOutput returning false vetoes the outgoing connection before it is wired'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectOutput receives slot index, outgoing type, link object, target node, and target slot'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectOutput veto does not trigger onConnectionsChange on either node'
|
||||
)
|
||||
})
|
||||
})
|
||||
51
src/extension-api-v2/__tests__/bc-07.v2.test.ts
Normal file
51
src/extension-api-v2/__tests__/bc-07.v2.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Category: BC.07 — Connection observation, intercept, and veto
|
||||
// DB cross-ref: S2.N3, S2.N12, S2.N13
|
||||
// Exemplar: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/node_mode_relay.js#L90
|
||||
// blast_radius: 5.46 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: NodeHandle.on('connectInput', ...), on('connectOutput', ...), on('connectionChange', ...)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.07 v2 contract — connection observation, intercept, and veto', () => {
|
||||
describe('on(\'connectionChange\', fn) — passive observation', () => {
|
||||
it.todo(
|
||||
'NodeHandle.on(\'connectionChange\', fn) fires fn after any input or output link is connected or disconnected'
|
||||
)
|
||||
it.todo(
|
||||
'connectionChange event payload includes type (\'input\'|\'output\'), slotIndex, connected boolean, and link info'
|
||||
)
|
||||
it.todo(
|
||||
'multiple listeners registered via on(\'connectionChange\') are all invoked in registration order'
|
||||
)
|
||||
it.todo(
|
||||
'listener registered with on() is removed when the extension scope is disposed'
|
||||
)
|
||||
})
|
||||
|
||||
describe('on(\'connectInput\', fn) — intercept and veto incoming connections', () => {
|
||||
it.todo(
|
||||
'fn returning false from on(\'connectInput\') vetoes the connection; graph remains unwired'
|
||||
)
|
||||
it.todo(
|
||||
'fn returning true or undefined from on(\'connectInput\') allows the connection to proceed'
|
||||
)
|
||||
it.todo(
|
||||
'connectInput event payload includes slotIndex, type, link, sourceHandle, and sourceSlot'
|
||||
)
|
||||
it.todo(
|
||||
'fn can mutate event.type to coerce a type mismatch before the connection is wired'
|
||||
)
|
||||
})
|
||||
|
||||
describe('on(\'connectOutput\', fn) — intercept and veto outgoing connections', () => {
|
||||
it.todo(
|
||||
'fn returning false from on(\'connectOutput\') vetoes the outgoing connection; connectionChange does not fire'
|
||||
)
|
||||
it.todo(
|
||||
'connectOutput event payload includes slotIndex, type, link, targetHandle, and targetSlot'
|
||||
)
|
||||
it.todo(
|
||||
'veto from connectOutput does not affect other registered connectOutput listeners on the same node'
|
||||
)
|
||||
})
|
||||
})
|
||||
38
src/extension-api-v2/__tests__/bc-08.migration.test.ts
Normal file
38
src/extension-api-v2/__tests__/bc-08.migration.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Category: BC.08 — Programmatic linking
|
||||
// DB cross-ref: S10.D2
|
||||
// Exemplar: https://github.com/goodtab/ComfyUI-Custom-Scripts/blob/main/web/js/quickNodes.js#L138
|
||||
// Migration: v1 node.connect/disconnectInput → v2 NodeHandle.connect/disconnectInput (typed handles)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.08 migration — programmatic linking', () => {
|
||||
describe('connect() equivalence', () => {
|
||||
it.todo(
|
||||
'v1 node.connect(srcSlot, targetNode, dstSlot) and v2 NodeHandle.connect(srcSlot, targetHandle, dstSlot) produce identical graph link state'
|
||||
)
|
||||
it.todo(
|
||||
'link id returned by v2 connect() matches the id on the underlying LGraph link created by an equivalent v1 call'
|
||||
)
|
||||
it.todo(
|
||||
'v2 connect() with a type-incompatible pair raises a typed error; v1 returns null — callers must handle both forms during migration'
|
||||
)
|
||||
})
|
||||
|
||||
describe('disconnectInput() equivalence', () => {
|
||||
it.todo(
|
||||
'v1 node.disconnectInput(slot) and v2 NodeHandle.disconnectInput(slotIndex) both leave the graph with no link on that slot'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectionsChange (v1) and on(\'connectionChange\') (v2) both fire for the same disconnect operation with equivalent payload data'
|
||||
)
|
||||
})
|
||||
|
||||
describe('handle vs. raw node reference', () => {
|
||||
it.todo(
|
||||
'v2 NodeHandle.connect() accepts a NodeHandle for targetHandle; passing a raw LGraphNode instance throws a deprecation error'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle obtained from v2 nodeCreated correctly wraps the same node that v1 connect() would operate on'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-08.v1.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-08.v1.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.08 — Programmatic linking
|
||||
// DB cross-ref: S10.D2
|
||||
// Exemplar: https://github.com/goodtab/ComfyUI-Custom-Scripts/blob/main/web/js/quickNodes.js#L138
|
||||
// blast_radius: 5.99 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.connect(srcSlot, targetNode, dstSlot)
|
||||
// node.disconnectInput(slot)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.08 v1 contract — programmatic linking', () => {
|
||||
describe('S10.D2 — node.connect(srcSlot, targetNode, dstSlot)', () => {
|
||||
it.todo(
|
||||
'node.connect(srcSlot, targetNode, dstSlot) creates a link between the source output slot and the target input slot'
|
||||
)
|
||||
it.todo(
|
||||
'connect() returns the newly created link object with a stable numeric id'
|
||||
)
|
||||
it.todo(
|
||||
'connect() on an already-occupied input slot replaces the existing link without leaving a dangling reference'
|
||||
)
|
||||
it.todo(
|
||||
'connect() with a type-incompatible slot pair is rejected and returns null without modifying the graph'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectionsChange fires on both the source and target node after a successful connect() call'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S10.D2 — node.disconnectInput(slot)', () => {
|
||||
it.todo(
|
||||
'node.disconnectInput(slot) removes the link on the specified input slot and updates both endpoint nodes'
|
||||
)
|
||||
it.todo(
|
||||
'disconnectInput() on an empty slot is a no-op and does not throw'
|
||||
)
|
||||
it.todo(
|
||||
'onConnectionsChange fires on both the source and target node after disconnectInput() removes a link'
|
||||
)
|
||||
})
|
||||
})
|
||||
39
src/extension-api-v2/__tests__/bc-08.v2.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-08.v2.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.08 — Programmatic linking
|
||||
// DB cross-ref: S10.D2
|
||||
// Exemplar: https://github.com/goodtab/ComfyUI-Custom-Scripts/blob/main/web/js/quickNodes.js#L138
|
||||
// blast_radius: 5.99 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: NodeHandle.connect(slotIndex, targetHandle, dstSlot) — same semantics, typed handles
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.08 v2 contract — programmatic linking', () => {
|
||||
describe('NodeHandle.connect(slotIndex, targetHandle, dstSlot) — create links', () => {
|
||||
it.todo(
|
||||
'NodeHandle.connect(slotIndex, targetHandle, dstSlot) creates a link between the source output slot and the target input slot'
|
||||
)
|
||||
it.todo(
|
||||
'connect() returns a LinkHandle with a stable id that matches the underlying graph link id'
|
||||
)
|
||||
it.todo(
|
||||
'connect() on an already-occupied input slot replaces the existing link and the old LinkHandle becomes invalid'
|
||||
)
|
||||
it.todo(
|
||||
'connect() with a type-incompatible slot pair throws a typed error and leaves the graph unchanged'
|
||||
)
|
||||
it.todo(
|
||||
'on(\'connectionChange\') fires on both NodeHandles after a successful connect() call'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NodeHandle.disconnectInput(slotIndex) — remove links', () => {
|
||||
it.todo(
|
||||
'NodeHandle.disconnectInput(slotIndex) removes the link on the specified input slot and the returned LinkHandle becomes invalid'
|
||||
)
|
||||
it.todo(
|
||||
'disconnectInput() on an empty slot is a no-op and does not throw'
|
||||
)
|
||||
it.todo(
|
||||
'on(\'connectionChange\') fires on both source and target NodeHandles after disconnectInput() removes a link'
|
||||
)
|
||||
})
|
||||
})
|
||||
42
src/extension-api-v2/__tests__/bc-09.migration.test.ts
Normal file
42
src/extension-api-v2/__tests__/bc-09.migration.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// Category: BC.09 — Dynamic slot and output mutation
|
||||
// DB cross-ref: S10.D1, S10.D3, S15.OS1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts#L121
|
||||
// Migration: v1 positional addInput/removeInput/addOutput/removeOutput + manual setSize
|
||||
// → v2 name-based NodeHandle.addInput/removeInput/addOutput/removeOutput with auto-reflow
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.09 migration — dynamic slot and output mutation', () => {
|
||||
describe('addInput / addOutput equivalence (S10.D1, S10.D3)', () => {
|
||||
it.todo(
|
||||
'v1 node.addInput(name, type) and v2 NodeHandle.addInput({ name, type }) both result in an equivalent slot appended to the node'
|
||||
)
|
||||
it.todo(
|
||||
'v1 node.addOutput(name, type) and v2 NodeHandle.addOutput({ name, type }) both result in an equivalent output slot with a matching type'
|
||||
)
|
||||
it.todo(
|
||||
'slot added via v2 addInput() is accessible at the same index position as an equivalent v1 addInput() call (append-only ordering preserved)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('removeInput / removeOutput equivalence', () => {
|
||||
it.todo(
|
||||
'v1 node.removeInput(slotIndex) and v2 NodeHandle.removeInput(name) both remove the slot and detach active links; remaining slots have consistent indices'
|
||||
)
|
||||
it.todo(
|
||||
'v2 removeInput(name) correctly identifies the slot when multiple slots exist, matching by name not by position'
|
||||
)
|
||||
})
|
||||
|
||||
describe('reflow: manual setSize vs. automatic (S15.OS1)', () => {
|
||||
it.todo(
|
||||
'v1 addInput() + setSize([...computeSize()]) and v2 addInput() auto-reflow both produce a node with equal or greater height to display the new slot'
|
||||
)
|
||||
it.todo(
|
||||
'v2 auto-reflow after removeOutput() shrinks the node to the same height as a v1 removeOutput() + manual setSize() sequence'
|
||||
)
|
||||
it.todo(
|
||||
'omitting setSize after a v1 addInput() call causes slot overlap; v2 auto-reflow never produces this condition'
|
||||
)
|
||||
})
|
||||
})
|
||||
50
src/extension-api-v2/__tests__/bc-09.v1.test.ts
Normal file
50
src/extension-api-v2/__tests__/bc-09.v1.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Category: BC.09 — Dynamic slot and output mutation
|
||||
// DB cross-ref: S10.D1, S10.D3, S15.OS1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts#L121
|
||||
// blast_radius: 6.03 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.addInput(name, type), node.removeInput(slot)
|
||||
// node.addOutput(name, type), node.removeOutput(slot)
|
||||
// node.setSize([w, h])
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.09 v1 contract — dynamic slot and output mutation', () => {
|
||||
describe('S10.D1 — addInput / removeInput', () => {
|
||||
it.todo(
|
||||
'node.addInput(name, type) appends a new input slot to node.inputs and increments node.inputs.length'
|
||||
)
|
||||
it.todo(
|
||||
'node.removeInput(slot) removes the slot at the given index and shifts subsequent slots down by one'
|
||||
)
|
||||
it.todo(
|
||||
'removing an input slot that has an active link also removes the corresponding link from the graph'
|
||||
)
|
||||
it.todo(
|
||||
'addInput with a duplicate name appends a second slot without error (v1 allows duplicates)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S10.D3 — addOutput / removeOutput', () => {
|
||||
it.todo(
|
||||
'node.addOutput(name, type) appends a new output slot to node.outputs and increments node.outputs.length'
|
||||
)
|
||||
it.todo(
|
||||
'node.removeOutput(slot) removes the output slot and detaches all outgoing links on that slot'
|
||||
)
|
||||
it.todo(
|
||||
'removing an output slot does not affect links on other output slots of the same node'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S15.OS1 — computeSize / setSize reflow', () => {
|
||||
it.todo(
|
||||
'node.setSize([w, h]) updates node.size to the provided dimensions immediately'
|
||||
)
|
||||
it.todo(
|
||||
'addInput/addOutput followed by node.setSize([...node.computeSize()]) produces a node tall enough to display all slots without overlap'
|
||||
)
|
||||
it.todo(
|
||||
'setSize does not trigger a canvas redraw synchronously; redraw occurs on the next animation frame'
|
||||
)
|
||||
})
|
||||
})
|
||||
50
src/extension-api-v2/__tests__/bc-09.v2.test.ts
Normal file
50
src/extension-api-v2/__tests__/bc-09.v2.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Category: BC.09 — Dynamic slot and output mutation
|
||||
// DB cross-ref: S10.D1, S10.D3, S15.OS1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts#L121
|
||||
// blast_radius: 6.03 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: NodeHandle.addInput(opts), NodeHandle.removeInput(name)
|
||||
// NodeHandle.addOutput(opts), NodeHandle.removeOutput(name)
|
||||
// reflow handled automatically — no manual setSize required
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.09 v2 contract — dynamic slot and output mutation', () => {
|
||||
describe('NodeHandle.addInput / removeInput (S10.D1)', () => {
|
||||
it.todo(
|
||||
'NodeHandle.addInput({ name, type }) appends a new input slot and returns a SlotHandle with a stable name-based identity'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.removeInput(name) removes the named input slot and detaches any active link on that slot'
|
||||
)
|
||||
it.todo(
|
||||
'removeInput(name) on a non-existent slot name throws a typed SlotNotFoundError'
|
||||
)
|
||||
it.todo(
|
||||
'addInput with a duplicate name throws a DuplicateSlotError (v2 enforces uniqueness unlike v1)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NodeHandle.addOutput / removeOutput (S10.D3)', () => {
|
||||
it.todo(
|
||||
'NodeHandle.addOutput({ name, type }) appends a new output slot and returns a SlotHandle'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.removeOutput(name) removes the output slot and detaches all outgoing links on that slot'
|
||||
)
|
||||
it.todo(
|
||||
'removeOutput does not affect slots or links on other output slots of the same node'
|
||||
)
|
||||
})
|
||||
|
||||
describe('automatic reflow (replaces S15.OS1 manual setSize)', () => {
|
||||
it.todo(
|
||||
'after addInput() or addOutput() the node size is automatically reflowed to fit all slots without a manual setSize call'
|
||||
)
|
||||
it.todo(
|
||||
'after removeInput() or removeOutput() the node size is automatically shrunk to remove the vacated slot space'
|
||||
)
|
||||
it.todo(
|
||||
'automatic reflow does not trigger a synchronous canvas redraw; redraw occurs on the next animation frame'
|
||||
)
|
||||
})
|
||||
})
|
||||
39
src/extension-api-v2/__tests__/bc-10.migration.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-10.migration.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.10 — Widget value subscription
|
||||
// DB cross-ref: S4.W1, S2.N14
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/widgetInputs.ts#L317
|
||||
// Migration: v1 widget.callback chain-patching / node.onWidgetChanged
|
||||
// → v2 WidgetHandle.on('change') / NodeHandle.on('widgetChanged')
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.10 migration — widget value subscription', () => {
|
||||
describe('widget.callback → WidgetHandle.on(\'change\') (S4.W1)', () => {
|
||||
it.todo(
|
||||
'v1 widget.callback and v2 WidgetHandle.on(\'change\') both fire with the new value for the same user interaction'
|
||||
)
|
||||
it.todo(
|
||||
'v2 on(\'change\') fires at the same point in the event sequence as the last v1 callback in the chain'
|
||||
)
|
||||
it.todo(
|
||||
'v1 chain-patching does not compose with v2 on(\'change\'): each operates independently; both fire for the same change event'
|
||||
)
|
||||
})
|
||||
|
||||
describe('node.onWidgetChanged → NodeHandle.on(\'widgetChanged\') (S2.N14)', () => {
|
||||
it.todo(
|
||||
'v1 node.onWidgetChanged and v2 NodeHandle.on(\'widgetChanged\') both receive equivalent widget name, value, and oldValue for the same change'
|
||||
)
|
||||
it.todo(
|
||||
'v2 widgetChanged payload includes a WidgetHandle reference instead of a raw widget object; WidgetHandle.name matches the widget name'
|
||||
)
|
||||
})
|
||||
|
||||
describe('ordering and isolation', () => {
|
||||
it.todo(
|
||||
'v2 on(\'change\') listeners from different extensions on the same widget all fire without one suppressing another'
|
||||
)
|
||||
it.todo(
|
||||
'disposing one extension scope removes only its own on(\'change\') listeners; other extensions\' listeners continue to fire'
|
||||
)
|
||||
})
|
||||
})
|
||||
37
src/extension-api-v2/__tests__/bc-10.v1.test.ts
Normal file
37
src/extension-api-v2/__tests__/bc-10.v1.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Category: BC.10 — Widget value subscription
|
||||
// DB cross-ref: S4.W1, S2.N14
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/widgetInputs.ts#L317
|
||||
// blast_radius: 5.09 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: widget.callback = function(value, ...) { ... } (chain-patching)
|
||||
// node.onWidgetChanged = function(name, value, ...) { ... }
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.10 v1 contract — widget value subscription', () => {
|
||||
describe('S4.W1 — widget.callback chain-patching', () => {
|
||||
it.todo(
|
||||
'assigning widget.callback invokes the function with the new value whenever the widget is interacted with'
|
||||
)
|
||||
it.todo(
|
||||
'chain-patching preserves the previous callback: saving the old reference and calling it at the end of the new function'
|
||||
)
|
||||
it.todo(
|
||||
'widget.callback receives (value, app, node, pos, event) in that argument order'
|
||||
)
|
||||
it.todo(
|
||||
'if multiple extensions chain-patch widget.callback, all callbacks are invoked in stack order (last-patched first)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N14 — node.onWidgetChanged', () => {
|
||||
it.todo(
|
||||
'node.onWidgetChanged is called once per widget value change with the widget name, new value, old value, and widget reference'
|
||||
)
|
||||
it.todo(
|
||||
'onWidgetChanged fires for every widget on the node, not only those with an explicit callback'
|
||||
)
|
||||
it.todo(
|
||||
'onWidgetChanged fires after widget.callback has been invoked for the same change event'
|
||||
)
|
||||
})
|
||||
})
|
||||
36
src/extension-api-v2/__tests__/bc-10.v2.test.ts
Normal file
36
src/extension-api-v2/__tests__/bc-10.v2.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Category: BC.10 — Widget value subscription
|
||||
// DB cross-ref: S4.W1, S2.N14
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/widgetInputs.ts#L317
|
||||
// blast_radius: 5.09 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: WidgetHandle.on('change', fn), NodeHandle.on('widgetChanged', fn)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.10 v2 contract — widget value subscription', () => {
|
||||
describe('WidgetHandle.on(\'change\', fn) — per-widget subscription (S4.W1)', () => {
|
||||
it.todo(
|
||||
'WidgetHandle.on(\'change\', fn) fires fn with (newValue, oldValue) whenever the widget value changes'
|
||||
)
|
||||
it.todo(
|
||||
'multiple on(\'change\') listeners on the same WidgetHandle are all invoked in registration order'
|
||||
)
|
||||
it.todo(
|
||||
'on(\'change\') listener is removed when the extension scope is disposed; subsequent changes do not invoke the stale listener'
|
||||
)
|
||||
it.todo(
|
||||
'on(\'change\') listener can call event.preventDefault() to block the value write (unlike v1 callback which cannot veto)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NodeHandle.on(\'widgetChanged\', fn) — node-level subscription (S2.N14)', () => {
|
||||
it.todo(
|
||||
'NodeHandle.on(\'widgetChanged\', fn) fires fn for any widget value change on the node, with payload { name, value, oldValue, widget }'
|
||||
)
|
||||
it.todo(
|
||||
'widgetChanged fires after all per-widget on(\'change\') listeners have been invoked for the same change event'
|
||||
)
|
||||
it.todo(
|
||||
'widgetChanged fires for every widget on the node regardless of whether the widget has individual on(\'change\') listeners'
|
||||
)
|
||||
})
|
||||
})
|
||||
45
src/extension-api-v2/__tests__/bc-11.migration.test.ts
Normal file
45
src/extension-api-v2/__tests__/bc-11.migration.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Category: BC.11 — Widget imperative state writes
|
||||
// DB cross-ref: S4.W4, S4.W5, S2.N16
|
||||
// Exemplar: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9
|
||||
// Migration: v1 direct property mutation (widget.value, widget.options.values, node.widgets.push/splice)
|
||||
// → v2 WidgetHandle.setValue / setOptions / NodeHandle.addWidget / removeWidget
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.11 migration — widget imperative state writes', () => {
|
||||
describe('widget.value → WidgetHandle.setValue() (S4.W4)', () => {
|
||||
it.todo(
|
||||
'v1 widget.value = v and v2 WidgetHandle.setValue(v) both result in the same displayed value on the canvas'
|
||||
)
|
||||
it.todo(
|
||||
'v1 direct assignment does not fire on(\'change\') listeners; v2 setValue() does — callers must not assume silence'
|
||||
)
|
||||
it.todo(
|
||||
'v2 setValue() raises InvalidValueError for out-of-range COMBO values; v1 assignment silently accepts them'
|
||||
)
|
||||
})
|
||||
|
||||
describe('widget.options.values → WidgetHandle.setOptions() (S4.W5)', () => {
|
||||
it.todo(
|
||||
'v1 widget.options.values = [...] and v2 WidgetHandle.setOptions({ values: [...] }) both replace the COMBO option list'
|
||||
)
|
||||
it.todo(
|
||||
'v1 does not auto-reset stale current value; v2 setOptions() does — migration callers must handle the resulting on(\'change\') event'
|
||||
)
|
||||
})
|
||||
|
||||
describe('node.widgets.push/splice → NodeHandle.addWidget/removeWidget (S2.N16)', () => {
|
||||
it.todo(
|
||||
'v1 node.widgets.push(w) and v2 NodeHandle.addWidget(opts) both result in the widget being present in the node\'s widget list after the call'
|
||||
)
|
||||
it.todo(
|
||||
'v1 splice causes widgets_values positional drift; v2 addWidget uses named-map and produces no drift even when inserted mid-list'
|
||||
)
|
||||
it.todo(
|
||||
'v1 push requires a manual setSize reflow; v2 addWidget performs it automatically — do not double-reflow when migrating'
|
||||
)
|
||||
it.todo(
|
||||
'v2 removeWidget(name) correctly finds the widget by name regardless of its position in the list; v1 splice requires the caller to track the index'
|
||||
)
|
||||
})
|
||||
})
|
||||
51
src/extension-api-v2/__tests__/bc-11.v1.test.ts
Normal file
51
src/extension-api-v2/__tests__/bc-11.v1.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Category: BC.11 — Widget imperative state writes
|
||||
// DB cross-ref: S4.W4, S4.W5, S2.N16
|
||||
// Exemplar: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9
|
||||
// blast_radius: 5.81 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: widget.value = newVal
|
||||
// widget.options.values = [...]
|
||||
// node.widgets.splice(i, 0, w)
|
||||
// node.widgets.push(w)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.11 v1 contract — widget imperative state writes', () => {
|
||||
describe('S4.W4 — widget.value direct assignment', () => {
|
||||
it.todo(
|
||||
'assigning widget.value = newVal updates the displayed value on the next canvas redraw without triggering widget.callback'
|
||||
)
|
||||
it.todo(
|
||||
'widget.value assignment to a value outside the COMBO options list does not throw but may display an invalid state'
|
||||
)
|
||||
it.todo(
|
||||
'reading widget.value immediately after assignment returns the assigned value'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S4.W5 — widget.options.values mutation (COMBO options)', () => {
|
||||
it.todo(
|
||||
'assigning widget.options.values = [...] replaces the COMBO dropdown options on the next canvas redraw'
|
||||
)
|
||||
it.todo(
|
||||
'if the current widget.value is absent from the new options list, the widget continues to display the stale value (no auto-reset in v1)'
|
||||
)
|
||||
it.todo(
|
||||
'widget.options.values mutation does not fire widget.callback'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N16 — node.widgets array mutation (insert / push)', () => {
|
||||
it.todo(
|
||||
'node.widgets.push(widget) appends the widget to the node\'s widget list and it renders on the next canvas redraw'
|
||||
)
|
||||
it.todo(
|
||||
'node.widgets.splice(i, 0, widget) inserts a widget at position i and shifts subsequent widgets\' positional indices'
|
||||
)
|
||||
it.todo(
|
||||
'inserting a widget via splice causes widgets_values positional drift if not followed by a node size reflow'
|
||||
)
|
||||
it.todo(
|
||||
'node.widgets.push does not update node.size; calling setSize([...computeSize()]) is required to avoid slot overlap'
|
||||
)
|
||||
})
|
||||
})
|
||||
52
src/extension-api-v2/__tests__/bc-11.v2.test.ts
Normal file
52
src/extension-api-v2/__tests__/bc-11.v2.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Category: BC.11 — Widget imperative state writes
|
||||
// DB cross-ref: S4.W4, S4.W5, S2.N16
|
||||
// Exemplar: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9
|
||||
// blast_radius: 5.81 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: WidgetHandle.setValue(v), WidgetHandle.setOptions({ values: [...] })
|
||||
// NodeHandle.addWidget(opts), NodeHandle.removeWidget(name)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.11 v2 contract — widget imperative state writes', () => {
|
||||
describe('WidgetHandle.setValue(v) — controlled value write (S4.W4)', () => {
|
||||
it.todo(
|
||||
'WidgetHandle.setValue(v) updates the widget\'s current value and triggers a reactive update visible on the next canvas frame'
|
||||
)
|
||||
it.todo(
|
||||
'setValue() fires the on(\'change\') listeners with (newValue, oldValue) in the same tick'
|
||||
)
|
||||
it.todo(
|
||||
'setValue() with a value outside the COMBO options list throws a typed InvalidValueError'
|
||||
)
|
||||
it.todo(
|
||||
'reading WidgetHandle.value immediately after setValue() returns the new value'
|
||||
)
|
||||
})
|
||||
|
||||
describe('WidgetHandle.setOptions({ values }) — COMBO option replacement (S4.W5)', () => {
|
||||
it.todo(
|
||||
'WidgetHandle.setOptions({ values: [...] }) replaces the COMBO options and triggers a reactive update'
|
||||
)
|
||||
it.todo(
|
||||
'if the current value is absent from the new options list, setOptions() resets the value to options[0] automatically'
|
||||
)
|
||||
it.todo(
|
||||
'setOptions() fires on(\'change\') only if the current value was reset due to option list change'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NodeHandle.addWidget / removeWidget — managed widget list mutation (S2.N16)', () => {
|
||||
it.todo(
|
||||
'NodeHandle.addWidget(opts) appends a widget, auto-reflowing node size and updating the named widgets_values map'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.removeWidget(name) removes the named widget, auto-reflowing node size and removing the entry from widgets_values'
|
||||
)
|
||||
it.todo(
|
||||
'addWidget does not cause widgets_values positional drift because v2 uses a named map rather than a positional array'
|
||||
)
|
||||
it.todo(
|
||||
'removeWidget(name) on a non-existent widget name throws a typed WidgetNotFoundError'
|
||||
)
|
||||
})
|
||||
})
|
||||
41
src/extension-api-v2/__tests__/bc-12.migration.test.ts
Normal file
41
src/extension-api-v2/__tests__/bc-12.migration.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Category: BC.12 — Per-widget serialization transform
|
||||
// DB cross-ref: S4.W3
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/helpers/painter.ts#L70
|
||||
// Migration: v1 widget.serializeValue positional index → v2 WidgetHandle.on('serialize') / setSerializeValue name-based
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.12 migration — per-widget serialization transform', () => {
|
||||
describe('serializeValue → on(\'serialize\') round-trip equivalence', () => {
|
||||
it.todo(
|
||||
'a v1 widget.serializeValue that returns a transformed value and a v2 on(\'serialize\') returning the same transformation produce identical output in the serialized workflow JSON'
|
||||
)
|
||||
it.todo(
|
||||
'v1 serializeValue receives a positional index; v2 on(\'serialize\') does not — callers relying on the index for slot lookup must migrate to name-based lookup'
|
||||
)
|
||||
it.todo(
|
||||
'async transforms: both v1 serializeValue and v2 on(\'serialize\') are awaited by graphToPrompt() before the workflow is finalized'
|
||||
)
|
||||
})
|
||||
|
||||
describe('serialize===false widget compat', () => {
|
||||
it.todo(
|
||||
'v1 positional index for a widget after control_after_generate is offset by 1 relative to the backend prompt; v2 named-map has no such offset'
|
||||
)
|
||||
it.todo(
|
||||
'migrate: v1 code that hard-codes an index offset for serialize===false slots must be rewritten to use WidgetHandle identity by name in v2'
|
||||
)
|
||||
it.todo(
|
||||
'widgets_values_named round-trip: a workflow serialized under v2 with an on(\'serialize\') transform deserializes to the same widget values as the equivalent v1 serializeValue workflow'
|
||||
)
|
||||
})
|
||||
|
||||
describe('identity stability', () => {
|
||||
it.todo(
|
||||
'v2 WidgetHandle identity is stable after node.widgets reordering; v1 serializeValue index changes if widgets are reordered — this is the primary reason to migrate'
|
||||
)
|
||||
it.todo(
|
||||
'setSerializeValue(fn) called twice replaces the first registration; widget.serializeValue overwrites also replace — both v1 and v2 are last-write-wins'
|
||||
)
|
||||
})
|
||||
})
|
||||
39
src/extension-api-v2/__tests__/bc-12.v1.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-12.v1.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.12 — Per-widget serialization transform
|
||||
// DB cross-ref: S4.W3
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/helpers/painter.ts#L70
|
||||
// blast_radius: 5.58 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: widget.serializeValue = async function(node, index) { return transformedValue }
|
||||
// Notes: widget.options.serialize===false widgets (e.g. control_after_generate) still occupy a
|
||||
// widgets_values slot and still fire serializeValue — excluded only from backend prompt by
|
||||
// graphToPrompt(). See research/architecture/widget-serialization-historical-analysis.md.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.12 v1 contract — per-widget serialization transform', () => {
|
||||
describe('S4.W3 — widget.serializeValue assignment', () => {
|
||||
it.todo(
|
||||
'assigning widget.serializeValue = async fn(node, index) causes graphToPrompt() to await fn and use its return value in widgets_values'
|
||||
)
|
||||
it.todo(
|
||||
'serializeValue receives the owning node as first argument and the widget\'s positional index in node.widgets as second argument'
|
||||
)
|
||||
it.todo(
|
||||
'if serializeValue is not assigned, graphToPrompt() uses widget.value directly as the serialized value'
|
||||
)
|
||||
it.todo(
|
||||
'serializeValue may return a value of a different type than widget.value (e.g. string expansion of a seed integer)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('serialize===false widgets (control_after_generate)', () => {
|
||||
it.todo(
|
||||
'a widget with options.serialize===false still occupies a slot in the widgets_values positional array during serialization'
|
||||
)
|
||||
it.todo(
|
||||
'serializeValue fires for a serialize===false widget and its return value appears in widgets_values even though graphToPrompt() excludes it from the backend prompt'
|
||||
)
|
||||
it.todo(
|
||||
'the positional index passed to serializeValue for widgets after a serialize===false widget is offset by one relative to the backend prompt widgets_values array'
|
||||
)
|
||||
})
|
||||
})
|
||||
47
src/extension-api-v2/__tests__/bc-12.v2.test.ts
Normal file
47
src/extension-api-v2/__tests__/bc-12.v2.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Category: BC.12 — Per-widget serialization transform
|
||||
// DB cross-ref: S4.W3
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/helpers/painter.ts#L70
|
||||
// blast_radius: 5.58 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: WidgetHandle.on('serialize', fn) or WidgetHandle.setSerializeValue(fn)
|
||||
// Notes: WidgetHandle identity is by name not position (PR #10392 widgets_values_named migration path).
|
||||
// serialize===false widgets still fire the serialize event and still appear in the named map.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.12 v2 contract — per-widget serialization transform', () => {
|
||||
describe('WidgetHandle.on(\'serialize\', fn) — event-based transform', () => {
|
||||
it.todo(
|
||||
'WidgetHandle.on(\'serialize\', fn) fires fn during graphToPrompt(); fn may return a transformed value which replaces the default in the named map'
|
||||
)
|
||||
it.todo(
|
||||
'fn receives a SerializeEvent with { node: NodeHandle, widget: WidgetHandle, value } and can set event.serializedValue to override'
|
||||
)
|
||||
it.todo(
|
||||
'if no on(\'serialize\') listener is registered, graphToPrompt() uses WidgetHandle.value directly'
|
||||
)
|
||||
it.todo(
|
||||
'on(\'serialize\') listener is removed when the extension scope is disposed; subsequent serializations use the raw value'
|
||||
)
|
||||
})
|
||||
|
||||
describe('WidgetHandle.setSerializeValue(fn) — imperative transform assignment', () => {
|
||||
it.todo(
|
||||
'WidgetHandle.setSerializeValue(async fn) registers fn as the sole serialize transform, superseding any prior assignment'
|
||||
)
|
||||
it.todo(
|
||||
'fn passed to setSerializeValue receives (widgetHandle) and its return value is placed in widgets_values_named under the widget name'
|
||||
)
|
||||
})
|
||||
|
||||
describe('serialize===false widgets (control_after_generate)', () => {
|
||||
it.todo(
|
||||
'a widget with serialize===false still appears as a named entry in widgets_values_named during serialization'
|
||||
)
|
||||
it.todo(
|
||||
'on(\'serialize\') fires for a serialize===false WidgetHandle; the returned value is stored in the named map but omitted from the backend prompt'
|
||||
)
|
||||
it.todo(
|
||||
'WidgetHandle identity for serialize===false widgets is stable across slot reordering because it is name-based not position-based'
|
||||
)
|
||||
})
|
||||
})
|
||||
44
src/extension-api-v2/__tests__/bc-13.migration.test.ts
Normal file
44
src/extension-api-v2/__tests__/bc-13.migration.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Category: BC.13 — Per-node serialization interception
|
||||
// DB cross-ref: S2.N6, S2.N15
|
||||
// Exemplar: https://github.com/Azornes/Comfyui-LayerForge/blob/main/js/CanvasView.js#L1438
|
||||
// Migration: v1 prototype.serialize patching / node.onSerialize → v2 NodeHandle.on('serialize') named-map
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.13 migration — per-node serialization interception', () => {
|
||||
describe('(a) positional v1 compat: prototype.serialize / onSerialize parity', () => {
|
||||
it.todo(
|
||||
'custom field injected via v1 prototype.serialize patch and the same field injected via v2 on(\'serialize\') both appear in the serialized workflow JSON under identical keys'
|
||||
)
|
||||
it.todo(
|
||||
'v1 onSerialize and v2 on(\'serialize\') both fire once per graphToPrompt() call with the same node\'s serialization data'
|
||||
)
|
||||
it.todo(
|
||||
'v1 chain of two prototype.serialize patchers produces the same custom-field set as two v2 on(\'serialize\') listeners registered by separate extensions'
|
||||
)
|
||||
})
|
||||
|
||||
describe('(b) named-map v2 round-trip parity', () => {
|
||||
it.todo(
|
||||
'a workflow serialized under v2 with widgets_values_named and deserialized produces the same widget values as the equivalent v1 workflow with a positional widgets_values array'
|
||||
)
|
||||
it.todo(
|
||||
'adding a new widget between two existing widgets does not shift the named-map entries for subsequent widgets (v2); it does shift positional indices in v1 — migration callers must stop relying on hardcoded indices'
|
||||
)
|
||||
it.todo(
|
||||
'serialize===false widget (control_after_generate) occupies a named-map entry in v2 with no positional offset; v1 callers that computed offsets must remove that logic'
|
||||
)
|
||||
})
|
||||
|
||||
describe('(c) null-in-numeric-widget: warning + default substitution', () => {
|
||||
it.todo(
|
||||
'v1 NaN widget value silently becomes null in the workflow JSON; v2 substitutes the declared default and emits a console.warn — the logged message includes the node id and widget name'
|
||||
)
|
||||
it.todo(
|
||||
'a workflow with a null widgets_values entry for a numeric widget loaded under v2 emits a console.warn and restores the declared default rather than loading null'
|
||||
)
|
||||
it.todo(
|
||||
'the NaN guard does not trigger for non-numeric widgets whose value is legitimately null (e.g. unset optional inputs)'
|
||||
)
|
||||
})
|
||||
})
|
||||
53
src/extension-api-v2/__tests__/bc-13.v1.test.ts
Normal file
53
src/extension-api-v2/__tests__/bc-13.v1.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// Category: BC.13 — Per-node serialization interception
|
||||
// DB cross-ref: S2.N6, S2.N15
|
||||
// Exemplar: https://github.com/Azornes/Comfyui-LayerForge/blob/main/js/CanvasView.js#L1438
|
||||
// blast_radius: 6.36 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v1 contract: node.prototype.serialize = function() { const r = origSerialize.call(this); r.myData = ...; return r }
|
||||
// node.onSerialize = function(data) { data.myData = ... }
|
||||
// Notes: widgets_values is positional. 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. Test (a) positional v1 compat, (b) named-map v2 round-trip parity,
|
||||
// (c) null-in-numeric-widget logs warning + substitutes default.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.13 v1 contract — per-node serialization interception', () => {
|
||||
describe('S2.N6 — prototype.serialize patching', () => {
|
||||
it.todo(
|
||||
'patching node.constructor.prototype.serialize and calling origSerialize.call(this) produces the base serialization object which can be extended with custom fields'
|
||||
)
|
||||
it.todo(
|
||||
'custom fields added to the object returned by the patched serialize are present in the workflow JSON written to disk'
|
||||
)
|
||||
it.todo(
|
||||
'multiple extensions each patching prototype.serialize via origSerialize chaining all contribute their custom fields to the final serialized object'
|
||||
)
|
||||
it.todo(
|
||||
'positional widgets_values in the patched serialize output drifts when a serialize===false widget occupies a slot before the target widget'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N15 — node.onSerialize callback', () => {
|
||||
it.todo(
|
||||
'assigning node.onSerialize = fn causes fn to be called with the serialization data object after the base serialize completes'
|
||||
)
|
||||
it.todo(
|
||||
'onSerialize may mutate data.myData in place; the mutation is reflected in the workflow JSON'
|
||||
)
|
||||
it.todo(
|
||||
'NaN values written to widgets_values inside onSerialize are silently coerced to null by JSON.stringify, producing silent corruption'
|
||||
)
|
||||
it.todo(
|
||||
'onSerialize fires once per serialization pass; calling graphToPrompt() twice calls onSerialize twice'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NaN→null silent corruption', () => {
|
||||
it.todo(
|
||||
'a numeric widget whose serializeValue returns NaN causes a null entry in widgets_values after JSON round-trip'
|
||||
)
|
||||
it.todo(
|
||||
'the null entry in widgets_values is loaded back as null on graph restore, not as 0 or the widget default'
|
||||
)
|
||||
})
|
||||
})
|
||||
50
src/extension-api-v2/__tests__/bc-13.v2.test.ts
Normal file
50
src/extension-api-v2/__tests__/bc-13.v2.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Category: BC.13 — Per-node serialization interception
|
||||
// DB cross-ref: S2.N6, S2.N15
|
||||
// Exemplar: https://github.com/Azornes/Comfyui-LayerForge/blob/main/js/CanvasView.js#L1438
|
||||
// blast_radius: 6.36 — compat-floor: blast_radius ≥ 2.0 — MUST pass before v2 ships
|
||||
// v2 replacement: NodeHandle.on('serialize', (data) => { data.myData = ... }) — named map round-trip
|
||||
// Notes: v2 uses widgets_values_named keyed by widget name, eliminating positional drift.
|
||||
// NaN→null pipeline: v2 serializer logs a warning and substitutes the widget's declared default.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.13 v2 contract — per-node serialization interception', () => {
|
||||
describe('NodeHandle.on(\'serialize\', fn) — node-level serialization hook (S2.N6, S2.N15)', () => {
|
||||
it.todo(
|
||||
'NodeHandle.on(\'serialize\', fn) fires fn with the serialization data object during graphToPrompt(); fn may add custom fields'
|
||||
)
|
||||
it.todo(
|
||||
'custom fields added to data inside on(\'serialize\') are present in the workflow JSON under the node\'s entry'
|
||||
)
|
||||
it.todo(
|
||||
'multiple on(\'serialize\') listeners from different extensions all fire and their custom fields coexist without overwriting each other (assuming distinct keys)'
|
||||
)
|
||||
it.todo(
|
||||
'on(\'serialize\') listener is removed when the extension scope is disposed; subsequent serializations omit the custom fields'
|
||||
)
|
||||
})
|
||||
|
||||
describe('named-map round-trip (widgets_values_named)', () => {
|
||||
it.todo(
|
||||
'v2 serialization stores widget values in a named map (widgets_values_named) keyed by widget name; the map survives a JSON round-trip with no null drift'
|
||||
)
|
||||
it.todo(
|
||||
'a workflow serialized with three widgets including one serialize===false widget deserializes with correct values for all three regardless of insertion order'
|
||||
)
|
||||
it.todo(
|
||||
'widgets added or removed between two serialization passes do not corrupt the named-map entries for unaffected widgets'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NaN→null guard (numeric widget safety)', () => {
|
||||
it.todo(
|
||||
'when a numeric widget value resolves to NaN at serialization time, v2 logs a console warning and substitutes the widget\'s declared default value'
|
||||
)
|
||||
it.todo(
|
||||
'the substituted default value round-trips through JSON correctly; the deserialized node shows the default, not null'
|
||||
)
|
||||
it.todo(
|
||||
'NaN guard fires per-widget and does not abort the serialization of the remaining widgets on the same node'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-14.migration.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-14.migration.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.14 — Workflow → API serialization interception (graphToPrompt)
|
||||
// DB cross-ref: S6.A1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/js/components-manager.js#L781
|
||||
// blast_radius: 7.02 (HIGHEST in dataset)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 app.graphToPrompt monkey-patch → v2 app.on('beforeGraphToPrompt', handler)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.14 migration — graphToPrompt interception', () => {
|
||||
describe('payload equivalence', () => {
|
||||
it.todo(
|
||||
'v1 monkey-patch and v2 beforeGraphToPrompt handler both receive equivalent { output, workflow } structures'
|
||||
)
|
||||
it.todo(
|
||||
'custom metadata injected in v1 via return-value mutation is equally injectable via v2 payload mutation'
|
||||
)
|
||||
it.todo(
|
||||
'v1 virtual-node removal logic produces the same serialized output as v2 automatic isVirtual resolution'
|
||||
)
|
||||
})
|
||||
|
||||
describe('execution ordering', () => {
|
||||
it.todo(
|
||||
'v2 handler fires at the same logical point in the queue pipeline as v1 wrapper (before HTTP dispatch)'
|
||||
)
|
||||
it.todo(
|
||||
'v2 cancellation via payload.cancel() has equivalent effect to v1 throwing an error inside the wrapper'
|
||||
)
|
||||
})
|
||||
|
||||
describe('coexistence during migration window', () => {
|
||||
it.todo(
|
||||
'a v1 monkey-patch and a v2 beforeGraphToPrompt handler active simultaneously do not double-mutate the payload'
|
||||
)
|
||||
it.todo(
|
||||
'removing the v1 monkey-patch while keeping the v2 handler produces identical final API payloads'
|
||||
)
|
||||
})
|
||||
})
|
||||
32
src/extension-api-v2/__tests__/bc-14.v1.test.ts
Normal file
32
src/extension-api-v2/__tests__/bc-14.v1.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Category: BC.14 — Workflow → API serialization interception (graphToPrompt)
|
||||
// DB cross-ref: S6.A1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/js/components-manager.js#L781
|
||||
// blast_radius: 7.02 (HIGHEST in dataset)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: monkey-patch app.graphToPrompt — const orig = app.graphToPrompt.bind(app); app.graphToPrompt = async function(...args) { const r = await orig(...args); /* mutate r */ return r }
|
||||
// v2 replacement: app.on('beforeGraphToPrompt', (payload) => { /* mutate payload */ }) event with cancellable/mutable payload
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.14 v1 contract — graphToPrompt monkey-patch', () => {
|
||||
describe('S6.A1 — app.graphToPrompt interception', () => {
|
||||
it.todo(
|
||||
'extension can replace app.graphToPrompt with a wrapper that calls the original and returns the result'
|
||||
)
|
||||
it.todo(
|
||||
'wrapper receives the same positional arguments that the caller passed to app.graphToPrompt'
|
||||
)
|
||||
it.todo(
|
||||
'mutations to the resolved prompt object (output, workflow) are reflected in the final API payload'
|
||||
)
|
||||
it.todo(
|
||||
'virtual nodes resolved by the extension wrapper are absent from the serialized output sent to the backend'
|
||||
)
|
||||
it.todo(
|
||||
'custom metadata injected into prompt.output is preserved through the full queuePrompt call'
|
||||
)
|
||||
it.todo(
|
||||
'multiple extensions wrapping graphToPrompt in sequence each receive and pass through prior mutations'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
src/extension-api-v2/__tests__/bc-14.v2.test.ts
Normal file
43
src/extension-api-v2/__tests__/bc-14.v2.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Category: BC.14 — Workflow → API serialization interception (graphToPrompt)
|
||||
// DB cross-ref: S6.A1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/js/components-manager.js#L781
|
||||
// blast_radius: 7.02 (HIGHEST in dataset)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: app.on('beforeGraphToPrompt', (payload) => { /* mutate payload */ }) event with cancellable/mutable payload
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.14 v2 contract — beforeGraphToPrompt event', () => {
|
||||
describe('event registration and dispatch', () => {
|
||||
it.todo(
|
||||
'app.on("beforeGraphToPrompt", handler) registers a handler that fires before every prompt serialization'
|
||||
)
|
||||
it.todo(
|
||||
'handler receives a mutable payload object containing { output, workflow } matching the v1 return shape'
|
||||
)
|
||||
it.todo(
|
||||
'mutations to payload.output inside the handler are present in the API body sent to the backend'
|
||||
)
|
||||
it.todo(
|
||||
'handler can cancel serialization by calling payload.cancel(), preventing the queue call from proceeding'
|
||||
)
|
||||
})
|
||||
|
||||
describe('virtual node resolution', () => {
|
||||
it.todo(
|
||||
'virtual nodes declared via defineNodeExtension({ isVirtual: true }) are resolved before beforeGraphToPrompt fires'
|
||||
)
|
||||
it.todo(
|
||||
'handler does not need to manually remove virtual nodes; they are absent from payload.output by default'
|
||||
)
|
||||
})
|
||||
|
||||
describe('multiple handlers and ordering', () => {
|
||||
it.todo(
|
||||
'multiple handlers registered with app.on("beforeGraphToPrompt") are called in registration order'
|
||||
)
|
||||
it.todo(
|
||||
'each handler sees mutations made by prior handlers in the same event cycle'
|
||||
)
|
||||
})
|
||||
})
|
||||
37
src/extension-api-v2/__tests__/bc-15.migration.test.ts
Normal file
37
src/extension-api-v2/__tests__/bc-15.migration.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Category: BC.15 — Workflow loading into the editor
|
||||
// DB cross-ref: S6.A2
|
||||
// Exemplar: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/workflow-list.js#L456
|
||||
// blast_radius: 5.05 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 app.loadGraphData(json) → v2 app.loadWorkflow(json) with lifecycle hooks
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.15 migration — workflow loading', () => {
|
||||
describe('graph state equivalence', () => {
|
||||
it.todo(
|
||||
'v1 app.loadGraphData(json) and v2 app.loadWorkflow(json) produce identical node/link graphs for the same input'
|
||||
)
|
||||
it.todo(
|
||||
'node widget values are preserved identically between v1 and v2 load paths'
|
||||
)
|
||||
it.todo(
|
||||
'custom node types registered by extensions are correctly hydrated by both v1 and v2 load paths'
|
||||
)
|
||||
})
|
||||
|
||||
describe('interception migration', () => {
|
||||
it.todo(
|
||||
'v1 monkey-patching app.loadGraphData to mutate json can be replaced by a v2 beforeLoadWorkflow handler with equivalent effect'
|
||||
)
|
||||
it.todo(
|
||||
'v1 post-load logic run synchronously after app.loadGraphData can be moved to a v2 afterLoadWorkflow handler'
|
||||
)
|
||||
})
|
||||
|
||||
describe('coexistence', () => {
|
||||
it.todo(
|
||||
'calling v2 app.loadWorkflow does not break extensions that still listen on the legacy nodeCreated hook'
|
||||
)
|
||||
})
|
||||
})
|
||||
28
src/extension-api-v2/__tests__/bc-15.v1.test.ts
Normal file
28
src/extension-api-v2/__tests__/bc-15.v1.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Category: BC.15 — Workflow loading into the editor
|
||||
// DB cross-ref: S6.A2
|
||||
// Exemplar: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/workflow-list.js#L456
|
||||
// blast_radius: 5.05 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: app.loadGraphData(workflowJson) — direct call, no lifecycle events
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.15 v1 contract — app.loadGraphData', () => {
|
||||
describe('S6.A2 — direct workflow load', () => {
|
||||
it.todo(
|
||||
'app.loadGraphData(json) replaces the current graph with the nodes and links from json'
|
||||
)
|
||||
it.todo(
|
||||
'calling app.loadGraphData clears all existing nodes before deserializing the new workflow'
|
||||
)
|
||||
it.todo(
|
||||
'node IDs in the loaded workflow are preserved as-is in the editor graph'
|
||||
)
|
||||
it.todo(
|
||||
'app.loadGraphData accepts a plain JSON object (not a string) as its argument'
|
||||
)
|
||||
it.todo(
|
||||
'extensions registered with nodeCreated receive each deserialized node after loadGraphData completes'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-15.v2.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-15.v2.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.15 — Workflow loading into the editor
|
||||
// DB cross-ref: S6.A2
|
||||
// Exemplar: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/workflow-list.js#L456
|
||||
// blast_radius: 5.05 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: app.loadWorkflow(json) — stable public API with beforeLoad/afterLoad hooks for intercepting extensions
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.15 v2 contract — app.loadWorkflow', () => {
|
||||
describe('core load API', () => {
|
||||
it.todo(
|
||||
'app.loadWorkflow(json) loads workflow nodes and links into the editor, equivalent to v1 loadGraphData'
|
||||
)
|
||||
it.todo(
|
||||
'app.loadWorkflow returns a Promise that resolves once all nodes are deserialized and rendered'
|
||||
)
|
||||
it.todo(
|
||||
'app.loadWorkflow accepts both plain objects and JSON strings'
|
||||
)
|
||||
})
|
||||
|
||||
describe('beforeLoad hook', () => {
|
||||
it.todo(
|
||||
'app.on("beforeLoadWorkflow", handler) fires before the graph is cleared, allowing cancellation via event.cancel()'
|
||||
)
|
||||
it.todo(
|
||||
'handler can mutate event.workflow to transform the incoming JSON before deserialization'
|
||||
)
|
||||
})
|
||||
|
||||
describe('afterLoad hook', () => {
|
||||
it.todo(
|
||||
'app.on("afterLoadWorkflow", handler) fires after all nodes are created, with the fully hydrated graph accessible'
|
||||
)
|
||||
it.todo(
|
||||
'afterLoad handler receives the original workflow JSON alongside the live graph for cross-referencing'
|
||||
)
|
||||
})
|
||||
})
|
||||
37
src/extension-api-v2/__tests__/bc-16.migration.test.ts
Normal file
37
src/extension-api-v2/__tests__/bc-16.migration.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Category: BC.16 — Execution output consumption (per-node)
|
||||
// DB cross-ref: S2.N2
|
||||
// Exemplar: https://github.com/andreszs/ComfyUI-Ultralytics-Studio/blob/main/js/show_string.js#L9
|
||||
// blast_radius: 4.67 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 node.onExecuted = fn → v2 NodeHandle.on('executed', fn)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.16 migration — per-node execution output', () => {
|
||||
describe('data equivalence', () => {
|
||||
it.todo(
|
||||
'v1 onExecuted data argument and v2 executed event data contain identical fields for the same backend response'
|
||||
)
|
||||
it.todo(
|
||||
'data.text and data.images accessed in v2 handler match the same properties read in v1 onExecuted for the same execution'
|
||||
)
|
||||
})
|
||||
|
||||
describe('timing equivalence', () => {
|
||||
it.todo(
|
||||
'v2 NodeHandle.on("executed") fires at the same point in the WebSocket message processing pipeline as v1 onExecuted'
|
||||
)
|
||||
it.todo(
|
||||
'DOM/widget updates performed in the v2 handler are applied within the same animation frame as equivalent v1 updates'
|
||||
)
|
||||
})
|
||||
|
||||
describe('cleanup behaviour', () => {
|
||||
it.todo(
|
||||
'v1 onExecuted persists after node removal (no automatic cleanup); v2 handler is removed automatically'
|
||||
)
|
||||
it.todo(
|
||||
'explicitly calling the v2 unsubscribe function produces equivalent silence to never assigning v1 onExecuted'
|
||||
)
|
||||
})
|
||||
})
|
||||
31
src/extension-api-v2/__tests__/bc-16.v1.test.ts
Normal file
31
src/extension-api-v2/__tests__/bc-16.v1.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Category: BC.16 — Execution output consumption (per-node)
|
||||
// DB cross-ref: S2.N2
|
||||
// Exemplar: https://github.com/andreszs/ComfyUI-Ultralytics-Studio/blob/main/js/show_string.js#L9
|
||||
// blast_radius: 4.67 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: node.onExecuted = function(data) { /* data.text, data.images etc */ }
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.16 v1 contract — node.onExecuted callback', () => {
|
||||
describe('S2.N2 — per-node execution output', () => {
|
||||
it.todo(
|
||||
'node.onExecuted is called by the runtime when the backend reports output for that node\'s ID'
|
||||
)
|
||||
it.todo(
|
||||
'data.text is an array of strings when the node outputs text-type results'
|
||||
)
|
||||
it.todo(
|
||||
'data.images is an array of image descriptor objects when the node outputs image-type results'
|
||||
)
|
||||
it.todo(
|
||||
'data passed to onExecuted matches the raw output object from the backend executed event for that node'
|
||||
)
|
||||
it.todo(
|
||||
'assigning node.onExecuted after graph load is sufficient; the handler receives subsequent execution outputs'
|
||||
)
|
||||
it.todo(
|
||||
'onExecuted is not called for nodes whose IDs are absent from the execution output'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-16.v2.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-16.v2.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.16 — Execution output consumption (per-node)
|
||||
// DB cross-ref: S2.N2
|
||||
// Exemplar: https://github.com/andreszs/ComfyUI-Ultralytics-Studio/blob/main/js/show_string.js#L9
|
||||
// blast_radius: 4.67 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: NodeHandle.on('executed', (data) => { ... })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.16 v2 contract — NodeHandle executed event', () => {
|
||||
describe('event subscription', () => {
|
||||
it.todo(
|
||||
'nodeHandle.on("executed", handler) registers a handler that fires when backend output arrives for that node'
|
||||
)
|
||||
it.todo(
|
||||
'handler receives a typed data object with text, images, and any other output slots defined by the node\'s schema'
|
||||
)
|
||||
it.todo(
|
||||
'nodeHandle.on("executed", ...) returns an unsubscribe function; calling it stops future invocations'
|
||||
)
|
||||
})
|
||||
|
||||
describe('data shape and typing', () => {
|
||||
it.todo(
|
||||
'data.text is typed as string[] for text-output nodes; accessing it does not require a cast'
|
||||
)
|
||||
it.todo(
|
||||
'data.images is typed as ImageOutput[] for image-output nodes, including filename, subfolder, and type fields'
|
||||
)
|
||||
})
|
||||
|
||||
describe('handler lifecycle', () => {
|
||||
it.todo(
|
||||
'handlers registered via nodeHandle.on("executed") are automatically removed when the node is removed from the graph'
|
||||
)
|
||||
it.todo(
|
||||
'multiple handlers on the same node each fire independently and in registration order'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
src/extension-api-v2/__tests__/bc-17.migration.test.ts
Normal file
43
src/extension-api-v2/__tests__/bc-17.migration.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Category: BC.17 — Backend execution lifecycle and progress events
|
||||
// DB cross-ref: S5.A1, S5.A2, S5.A3
|
||||
// Exemplar: https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Studio/blob/main/loader/components/public/iconRenderer.js#L39
|
||||
// blast_radius: 5.00 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 app.api.addEventListener → v2 comfyApp.on with typed payloads
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.17 migration — execution lifecycle events', () => {
|
||||
describe('event payload equivalence (S5.A1 — executed / execution_error)', () => {
|
||||
it.todo(
|
||||
'v1 "executed" CustomEvent.detail and v2 "executed" payload carry the same node ID and output fields'
|
||||
)
|
||||
it.todo(
|
||||
'v1 "execution_error" detail and v2 "executionError" payload both identify the failing node and provide error text'
|
||||
)
|
||||
})
|
||||
|
||||
describe('progress payload equivalence (S5.A2)', () => {
|
||||
it.todo(
|
||||
'v1 progress detail { value, max } and v2 progress payload { step, totalSteps } encode the same completion fraction'
|
||||
)
|
||||
})
|
||||
|
||||
describe('status and reconnect equivalence (S5.A3)', () => {
|
||||
it.todo(
|
||||
'v1 "status" event and v2 "status" event fire at the same points in the WebSocket message lifecycle'
|
||||
)
|
||||
it.todo(
|
||||
'v1 "reconnecting" event and v2 "reconnecting" event both fire before the first reconnect attempt'
|
||||
)
|
||||
})
|
||||
|
||||
describe('handler removal equivalence', () => {
|
||||
it.todo(
|
||||
'v1 app.api.removeEventListener(name, fn) and v2 unsubscribe() both stop the handler from firing on subsequent events'
|
||||
)
|
||||
it.todo(
|
||||
'removing a v1 listener does not affect a concurrently registered v2 listener for the same logical event'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
src/extension-api-v2/__tests__/bc-17.v1.test.ts
Normal file
43
src/extension-api-v2/__tests__/bc-17.v1.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Category: BC.17 — Backend execution lifecycle and progress events
|
||||
// DB cross-ref: S5.A1, S5.A2, S5.A3
|
||||
// Exemplar: https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Studio/blob/main/loader/components/public/iconRenderer.js#L39
|
||||
// blast_radius: 5.00 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: app.api.addEventListener('executed'|'progress'|'status'|'execution_error'|'reconnecting', fn)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.17 v1 contract — app.api.addEventListener', () => {
|
||||
describe('S5.A1 — execution lifecycle events (executed, execution_error)', () => {
|
||||
it.todo(
|
||||
'app.api.addEventListener("executed", fn) fires fn when a node execution completes with output data'
|
||||
)
|
||||
it.todo(
|
||||
'app.api.addEventListener("execution_error", fn) fires fn with error detail when the backend reports a failure'
|
||||
)
|
||||
it.todo(
|
||||
'the executed event detail includes { node, output } matching the backend WebSocket message structure'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S5.A2 — progress events', () => {
|
||||
it.todo(
|
||||
'app.api.addEventListener("progress", fn) fires fn on each step tick during a running execution'
|
||||
)
|
||||
it.todo(
|
||||
'the progress event detail includes { value, max } allowing accurate percentage calculation'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S5.A3 — status and reconnect events', () => {
|
||||
it.todo(
|
||||
'app.api.addEventListener("status", fn) fires fn when the backend queue status changes'
|
||||
)
|
||||
it.todo(
|
||||
'app.api.addEventListener("reconnecting", fn) fires fn when the WebSocket connection is lost and retrying'
|
||||
)
|
||||
it.todo(
|
||||
'app.api.removeEventListener with the same event name and function reference removes the handler'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
src/extension-api-v2/__tests__/bc-17.v2.test.ts
Normal file
43
src/extension-api-v2/__tests__/bc-17.v2.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Category: BC.17 — Backend execution lifecycle and progress events
|
||||
// DB cross-ref: S5.A1, S5.A2, S5.A3
|
||||
// Exemplar: https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Studio/blob/main/loader/components/public/iconRenderer.js#L39
|
||||
// blast_radius: 5.00 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: comfyApp.on('executed', fn), comfyApp.on('progress', fn) — typed event payloads
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.17 v2 contract — comfyApp event subscriptions', () => {
|
||||
describe('S5.A1 — execution lifecycle events', () => {
|
||||
it.todo(
|
||||
'comfyApp.on("executed", fn) fires fn when a node reports completion, with a typed { nodeId, output } payload'
|
||||
)
|
||||
it.todo(
|
||||
'comfyApp.on("executionError", fn) fires fn with a typed error payload including nodeId and exception detail'
|
||||
)
|
||||
it.todo(
|
||||
'comfyApp.on("executionStart", fn) fires fn when the backend begins processing a new prompt'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S5.A2 — progress events', () => {
|
||||
it.todo(
|
||||
'comfyApp.on("progress", fn) fires fn on each step tick with typed { step, totalSteps, nodeId } fields'
|
||||
)
|
||||
it.todo(
|
||||
'progress percentage derived from v2 payload (step / totalSteps) equals percentage from v1 (value / max)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S5.A3 — status and connectivity events', () => {
|
||||
it.todo(
|
||||
'comfyApp.on("status", fn) fires fn when queue depth or running state changes, with a typed status payload'
|
||||
)
|
||||
it.todo(
|
||||
'comfyApp.on("reconnecting", fn) fires fn when the WebSocket drops and a reconnect attempt begins'
|
||||
)
|
||||
it.todo(
|
||||
'calling the unsubscribe handle returned by comfyApp.on() removes the handler without affecting other subscribers'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-18.migration.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-18.migration.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.18 — Backend HTTP calls
|
||||
// DB cross-ref: S6.A3
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/components/common/BackgroundImageUpload.vue#L61
|
||||
// blast_radius: 5.77 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 app.api.fetchApi → v2 comfyAPI.fetchApi (same signature, stable import)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.18 migration — backend HTTP calls', () => {
|
||||
describe('request equivalence', () => {
|
||||
it.todo(
|
||||
'v1 app.api.fetchApi(path, init) and v2 comfyAPI.fetchApi(path, init) send identical HTTP requests to the backend'
|
||||
)
|
||||
it.todo(
|
||||
'authentication headers attached by v1 and v2 are equivalent; the backend accepts both without reconfiguration'
|
||||
)
|
||||
it.todo(
|
||||
'FormData uploads via v1 and v2 produce the same multipart body on the wire'
|
||||
)
|
||||
})
|
||||
|
||||
describe('response handling equivalence', () => {
|
||||
it.todo(
|
||||
'v1 and v2 both return a native Response object; callers can use .json(), .text(), and .ok identically'
|
||||
)
|
||||
it.todo(
|
||||
'4xx/5xx responses resolve (not reject) in both v1 and v2, so existing error-check patterns remain valid'
|
||||
)
|
||||
})
|
||||
|
||||
describe('import path migration', () => {
|
||||
it.todo(
|
||||
'replacing "app.api.fetchApi" with an import of comfyAPI.fetchApi requires no call-site argument changes'
|
||||
)
|
||||
it.todo(
|
||||
'comfyAPI.fetchApi is available at extension init time without waiting for app.setup() to complete'
|
||||
)
|
||||
})
|
||||
})
|
||||
31
src/extension-api-v2/__tests__/bc-18.v1.test.ts
Normal file
31
src/extension-api-v2/__tests__/bc-18.v1.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Category: BC.18 — Backend HTTP calls
|
||||
// DB cross-ref: S6.A3
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/components/common/BackgroundImageUpload.vue#L61
|
||||
// blast_radius: 5.77 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: app.api.fetchApi('/endpoint', { method: 'POST', body: ... })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.18 v1 contract — app.api.fetchApi', () => {
|
||||
describe('S6.A3 — authenticated HTTP calls via fetchApi', () => {
|
||||
it.todo(
|
||||
'app.api.fetchApi(path, init) returns a Promise<Response> from the ComfyUI backend origin'
|
||||
)
|
||||
it.todo(
|
||||
'fetchApi prepends the configured base URL so callers use relative paths like "/upload/image"'
|
||||
)
|
||||
it.todo(
|
||||
'fetchApi includes authentication headers (e.g. session cookie or Authorization) automatically'
|
||||
)
|
||||
it.todo(
|
||||
'a POST call with a FormData body is forwarded without Content-Type override, allowing multipart to work'
|
||||
)
|
||||
it.todo(
|
||||
'a non-2xx response from the backend is returned as a resolved Promise (not rejected); callers must check response.ok'
|
||||
)
|
||||
it.todo(
|
||||
'concurrent fetchApi calls from different extensions do not share or corrupt each other\'s request state'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-18.v2.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-18.v2.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.18 — Backend HTTP calls
|
||||
// DB cross-ref: S6.A3
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/components/common/BackgroundImageUpload.vue#L61
|
||||
// blast_radius: 5.77 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: comfyAPI.fetchApi(path, opts) — same signature, same authentication, stable import path
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.18 v2 contract — comfyAPI.fetchApi', () => {
|
||||
describe('API surface stability', () => {
|
||||
it.todo(
|
||||
'comfyAPI.fetchApi(path, init) is importable from the stable extension-api-v2 package without accessing app.api'
|
||||
)
|
||||
it.todo(
|
||||
'comfyAPI.fetchApi signature is identical to v1 app.api.fetchApi: (path: string, init?: RequestInit) => Promise<Response>'
|
||||
)
|
||||
it.todo(
|
||||
'comfyAPI.fetchApi uses the same base URL and authentication mechanism as v1 fetchApi'
|
||||
)
|
||||
})
|
||||
|
||||
describe('request handling', () => {
|
||||
it.todo(
|
||||
'POST with FormData body is forwarded correctly, preserving multipart boundary'
|
||||
)
|
||||
it.todo(
|
||||
'JSON body with explicit Content-Type: application/json is sent without modification'
|
||||
)
|
||||
it.todo(
|
||||
'non-2xx responses resolve (not reject) the returned Promise, consistent with v1 behaviour'
|
||||
)
|
||||
})
|
||||
|
||||
describe('extension isolation', () => {
|
||||
it.todo(
|
||||
'comfyAPI.fetchApi does not expose session credentials in a way that allows cross-extension credential theft'
|
||||
)
|
||||
})
|
||||
})
|
||||
40
src/extension-api-v2/__tests__/bc-19.migration.test.ts
Normal file
40
src/extension-api-v2/__tests__/bc-19.migration.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Category: BC.19 — Workflow execution trigger
|
||||
// DB cross-ref: S6.A4
|
||||
// Exemplar: https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/blob/main/js/features/viewer/workflowSidebar/sidebarRunButton.js#L317
|
||||
// blast_radius: 6.09 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 app.queuePrompt monkey-patch → v2 comfyApp.on('beforeQueuePrompt') + comfyApp.queuePrompt(opts)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.19 migration — workflow execution trigger', () => {
|
||||
describe('payload mutation equivalence', () => {
|
||||
it.todo(
|
||||
'v1 wrapper mutation of the serialized prompt body and v2 event.payload mutation produce identical HTTP request bodies'
|
||||
)
|
||||
it.todo(
|
||||
'auth tokens injected via v1 wrapper extra_data and v2 event.payload.extra_data reach the backend identically'
|
||||
)
|
||||
})
|
||||
|
||||
describe('cancellation equivalence', () => {
|
||||
it.todo(
|
||||
'v1 wrapper that does not call orig() and v2 handler that calls event.cancel() both result in zero HTTP calls to /prompt'
|
||||
)
|
||||
})
|
||||
|
||||
describe('programmatic trigger equivalence', () => {
|
||||
it.todo(
|
||||
'v1 app.queuePrompt(0, 1) and v2 comfyApp.queuePrompt({ batchCount: 1 }) both enqueue the same graph payload'
|
||||
)
|
||||
it.todo(
|
||||
'v2 comfyApp.queuePrompt() fires beforeQueuePrompt handlers; v1 programmatic call also triggers any active v1 wrappers'
|
||||
)
|
||||
})
|
||||
|
||||
describe('coexistence', () => {
|
||||
it.todo(
|
||||
'a v1 monkey-patch and a v2 beforeQueuePrompt handler active simultaneously do not double-submit the prompt'
|
||||
)
|
||||
})
|
||||
})
|
||||
31
src/extension-api-v2/__tests__/bc-19.v1.test.ts
Normal file
31
src/extension-api-v2/__tests__/bc-19.v1.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Category: BC.19 — Workflow execution trigger
|
||||
// DB cross-ref: S6.A4
|
||||
// Exemplar: https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/blob/main/js/features/viewer/workflowSidebar/sidebarRunButton.js#L317
|
||||
// blast_radius: 6.09 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: monkey-patch app.queuePrompt — const orig = app.queuePrompt.bind(app); app.queuePrompt = async function(num, batchCount) { /* mutate */ return orig(num, batchCount) }
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.19 v1 contract — app.queuePrompt monkey-patch', () => {
|
||||
describe('S6.A4 — queuePrompt interception', () => {
|
||||
it.todo(
|
||||
'extension can replace app.queuePrompt with a wrapper that calls the original and returns its result'
|
||||
)
|
||||
it.todo(
|
||||
'wrapper receives (number, batchCount) arguments matching the internal call signature'
|
||||
)
|
||||
it.todo(
|
||||
'extension can inject an auth token or extra field into the prompt payload before delegating to orig()'
|
||||
)
|
||||
it.todo(
|
||||
'extension can prevent execution by not calling orig() inside the wrapper'
|
||||
)
|
||||
it.todo(
|
||||
'multiple extensions wrapping queuePrompt in sequence each execute in wrapping order'
|
||||
)
|
||||
it.todo(
|
||||
'programmatic call to app.queuePrompt(0, 1) from an extension correctly enqueues the current graph'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
src/extension-api-v2/__tests__/bc-19.v2.test.ts
Normal file
43
src/extension-api-v2/__tests__/bc-19.v2.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Category: BC.19 — Workflow execution trigger
|
||||
// DB cross-ref: S6.A4
|
||||
// Exemplar: https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/blob/main/js/features/viewer/workflowSidebar/sidebarRunButton.js#L317
|
||||
// blast_radius: 6.09 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: comfyApp.on('beforeQueuePrompt', handler) with event.payload mutation; comfyApp.queuePrompt(opts) for programmatic trigger
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.19 v2 contract — beforeQueuePrompt event and comfyApp.queuePrompt', () => {
|
||||
describe('beforeQueuePrompt event', () => {
|
||||
it.todo(
|
||||
'comfyApp.on("beforeQueuePrompt", handler) fires before every prompt is enqueued, including UI-triggered runs'
|
||||
)
|
||||
it.todo(
|
||||
'handler receives a mutable event.payload containing the prompt body and extra_data fields'
|
||||
)
|
||||
it.todo(
|
||||
'mutating event.payload.extra_data.extra_pnginfo in the handler persists into the queued request'
|
||||
)
|
||||
it.todo(
|
||||
'calling event.cancel() inside the handler prevents the prompt from being submitted to the backend'
|
||||
)
|
||||
})
|
||||
|
||||
describe('programmatic trigger', () => {
|
||||
it.todo(
|
||||
'comfyApp.queuePrompt(opts) programmatically enqueues the current workflow, firing beforeQueuePrompt first'
|
||||
)
|
||||
it.todo(
|
||||
'opts.batchCount defaults to 1 when omitted; the backend receives a single prompt'
|
||||
)
|
||||
})
|
||||
|
||||
describe('multiple handlers', () => {
|
||||
it.todo(
|
||||
'multiple beforeQueuePrompt handlers are called in registration order; each sees prior mutations'
|
||||
)
|
||||
it.todo(
|
||||
'cancellation by any handler short-circuits remaining handlers and suppresses the HTTP call'
|
||||
)
|
||||
})
|
||||
})
|
||||
46
src/extension-api-v2/__tests__/bc-20.migration.test.ts
Normal file
46
src/extension-api-v2/__tests__/bc-20.migration.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Category: BC.20 — Custom node-type registration (frontend-only / virtual)
|
||||
// DB cross-ref: S1.H5, S1.H6, S8.P1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/rerouteNode.ts
|
||||
// blast_radius: 5.49 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 LiteGraph.registerNodeType + isVirtualNode → v2 defineNodeExtension({ isVirtual: true, setup })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.20 migration — custom and virtual node registration', () => {
|
||||
describe('registration equivalence (S1.H5)', () => {
|
||||
it.todo(
|
||||
'v1 LiteGraph.registerNodeType("MyType", MyClass) and v2 defineNodeExtension({ nodeType: "MyType" }) both make the type droppable from the node picker'
|
||||
)
|
||||
it.todo(
|
||||
'v1 MyClass.prototype.isVirtualNode = true and v2 isVirtual: true both exclude the node from the graphToPrompt output'
|
||||
)
|
||||
it.todo(
|
||||
'canvas rendering behaviour of a virtual node is identical between v1 and v2 registration paths'
|
||||
)
|
||||
})
|
||||
|
||||
describe('augmentation equivalence (S1.H6)', () => {
|
||||
it.todo(
|
||||
'v1 beforeRegisterNodeDef prototype mutation and v2 defineNodeExtension setup() widget addition produce equivalent UI on existing backend node types'
|
||||
)
|
||||
it.todo(
|
||||
'widget values set via v2 setup(handle) are serialized identically to those set via v1 prototype augmentation'
|
||||
)
|
||||
})
|
||||
|
||||
describe('serialization equivalence (S8.P1)', () => {
|
||||
it.todo(
|
||||
'a graph with virtual nodes serialized via v1 graphToPrompt and the same graph using v2 produce bit-equivalent backend payloads'
|
||||
)
|
||||
it.todo(
|
||||
'link re-routing through virtual nodes produces the same source→target pairs in both v1 and v2 serialized outputs'
|
||||
)
|
||||
})
|
||||
|
||||
describe('cleanup on unregister', () => {
|
||||
it.todo(
|
||||
'v1 registered types persist in LiteGraph after extension unregisters; v2 types registered via defineNodeExtension are removed'
|
||||
)
|
||||
})
|
||||
})
|
||||
47
src/extension-api-v2/__tests__/bc-20.v1.test.ts
Normal file
47
src/extension-api-v2/__tests__/bc-20.v1.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Category: BC.20 — Custom node-type registration (frontend-only / virtual)
|
||||
// DB cross-ref: S1.H5, S1.H6, S8.P1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/rerouteNode.ts
|
||||
// blast_radius: 5.49 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: app.registerExtension({ registerCustomNodes(app) { LiteGraph.registerNodeType('MyType', MyClass); MyClass.prototype.isVirtualNode = true } })
|
||||
// app.registerExtension({ beforeRegisterNodeDef(nodeType, nodeData) { ... } })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.20 v1 contract — LiteGraph.registerNodeType and isVirtualNode', () => {
|
||||
describe('S1.H5 — registerCustomNodes hook', () => {
|
||||
it.todo(
|
||||
'registerExtension({ registerCustomNodes(app) }) is called during setup before any graph is loaded'
|
||||
)
|
||||
it.todo(
|
||||
'LiteGraph.registerNodeType("MyType", MyClass) inside registerCustomNodes makes the type instantiable in the graph'
|
||||
)
|
||||
it.todo(
|
||||
'setting MyClass.prototype.isVirtualNode = true causes the serializer to omit the node from the backend API payload'
|
||||
)
|
||||
it.todo(
|
||||
'virtual node is still visible and interactive in the LiteGraph canvas'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S1.H6 — beforeRegisterNodeDef hook', () => {
|
||||
it.todo(
|
||||
'registerExtension({ beforeRegisterNodeDef(nodeType, nodeData) }) fires for every backend-defined node type before it is registered'
|
||||
)
|
||||
it.todo(
|
||||
'extension can augment nodeType prototype inside beforeRegisterNodeDef and the change affects all future instances'
|
||||
)
|
||||
it.todo(
|
||||
'mutations to nodeData inside beforeRegisterNodeDef alter the node\'s widget/input schema visible to the graph'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S8.P1 — virtual node payload suppression', () => {
|
||||
it.todo(
|
||||
'graphToPrompt excludes nodes with isVirtualNode === true from the output object sent to the backend'
|
||||
)
|
||||
it.todo(
|
||||
'links connected to a virtual node are re-routed in the serialized output to preserve logical connectivity'
|
||||
)
|
||||
})
|
||||
})
|
||||
46
src/extension-api-v2/__tests__/bc-20.v2.test.ts
Normal file
46
src/extension-api-v2/__tests__/bc-20.v2.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Category: BC.20 — Custom node-type registration (frontend-only / virtual)
|
||||
// DB cross-ref: S1.H5, S1.H6, S8.P1
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/rerouteNode.ts
|
||||
// blast_radius: 5.49 (compat-floor)
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: defineNodeExtension({ nodeType: 'MyType', isVirtual: true, setup(handle) { ... } })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.20 v2 contract — defineNodeExtension', () => {
|
||||
describe('S1.H5 — virtual node registration', () => {
|
||||
it.todo(
|
||||
'defineNodeExtension({ nodeType: "MyType", isVirtual: true, setup }) registers a pure-frontend node type'
|
||||
)
|
||||
it.todo(
|
||||
'nodes registered with isVirtual: true do not appear in the serialized API payload from graphToPrompt'
|
||||
)
|
||||
it.todo(
|
||||
'the virtual node is rendered on the canvas and accepts user interaction normally'
|
||||
)
|
||||
it.todo(
|
||||
'setup(handle) receives a NodeHandle bound to every instance created at graph-load or user-drop time'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S1.H6 — backend node-def augmentation', () => {
|
||||
it.todo(
|
||||
'defineNodeExtension({ nodeType: "ExistingBackendType", setup }) fires setup for every instance of a backend-defined type'
|
||||
)
|
||||
it.todo(
|
||||
'extension can add widgets to the handle inside setup() and they appear on all matching nodes'
|
||||
)
|
||||
it.todo(
|
||||
'schema-level augmentation (adding an input slot) declared via defineNodeExtension takes effect before the node is first rendered'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S8.P1 — serialization of virtual links', () => {
|
||||
it.todo(
|
||||
'links through a virtual node are transparently resolved in the serialized output so backend sees direct source→target connections'
|
||||
)
|
||||
it.todo(
|
||||
'removing the virtual node from the canvas also removes any dangling link stubs from the serialized payload'
|
||||
)
|
||||
})
|
||||
})
|
||||
34
src/extension-api-v2/__tests__/bc-21.migration.test.ts
Normal file
34
src/extension-api-v2/__tests__/bc-21.migration.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Category: BC.21 — Custom widget-type registration
|
||||
// DB cross-ref: S1.H2
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// blast_radius: 4.32
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 getCustomWidgets factory → v2 defineWidgetExtension
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.21 migration — Custom widget-type registration', () => {
|
||||
describe('factory invocation parity (S1.H2)', () => {
|
||||
it.todo(
|
||||
'v1 factory (node, inputData, app) and v2 create(handle, inputData) both receive equivalent inputData for the same node def'
|
||||
)
|
||||
it.todo(
|
||||
'widget produced by v1 factory and v2 create have identical serialized value in node.widgets after creation'
|
||||
)
|
||||
})
|
||||
|
||||
describe('registration timing', () => {
|
||||
it.todo(
|
||||
'v1 getCustomWidgets fires during extension setup; v2 defineWidgetExtension registers before setup completes — both resolve before nodeCreated'
|
||||
)
|
||||
})
|
||||
|
||||
describe('scope cleanup on dispose', () => {
|
||||
it.todo(
|
||||
'v1 custom widget type persists after extension unregisters; v2 type is unregistered and nodes fall back to default rendering'
|
||||
)
|
||||
it.todo(
|
||||
'v2 cleanup on dispose does not affect widget types registered by other extensions'
|
||||
)
|
||||
})
|
||||
})
|
||||
29
src/extension-api-v2/__tests__/bc-21.v1.test.ts
Normal file
29
src/extension-api-v2/__tests__/bc-21.v1.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Category: BC.21 — Custom widget-type registration
|
||||
// DB cross-ref: S1.H2
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// blast_radius: 4.32
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: app.registerExtension({ getCustomWidgets(app) { return { MYWIDGET: (node, inputData, app) => { ... } } } })
|
||||
// Notes: small family — 2 evidence rows + 1 minor variant (acceptance carve-out)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.21 v1 contract — Custom widget-type registration', () => {
|
||||
describe('S1.H2 — getCustomWidgets hook', () => {
|
||||
it.todo(
|
||||
'extension returning a widget factory from getCustomWidgets registers the type globally'
|
||||
)
|
||||
it.todo(
|
||||
'registered widget factory is invoked with (node, inputData, app) when a node with that input type is created'
|
||||
)
|
||||
it.todo(
|
||||
'widget returned by factory is attached to node.widgets array'
|
||||
)
|
||||
it.todo(
|
||||
'two extensions registering distinct widget types do not collide'
|
||||
)
|
||||
it.todo(
|
||||
'registering the same widget type key twice: second registration wins (last-write semantics)'
|
||||
)
|
||||
})
|
||||
})
|
||||
28
src/extension-api-v2/__tests__/bc-21.v2.test.ts
Normal file
28
src/extension-api-v2/__tests__/bc-21.v2.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Category: BC.21 — Custom widget-type registration
|
||||
// DB cross-ref: S1.H2
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// blast_radius: 4.32
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: defineWidgetExtension({ widgetType: 'MYWIDGET', create(handle, inputData) { ... } })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.21 v2 contract — Custom widget-type registration', () => {
|
||||
describe('defineWidgetExtension() — declarative widget registration', () => {
|
||||
it.todo(
|
||||
'defineWidgetExtension({ widgetType, create }) registers the type before any nodeCreated fires'
|
||||
)
|
||||
it.todo(
|
||||
'create(handle, inputData) is called with a typed WidgetHandle and the input spec tuple'
|
||||
)
|
||||
it.todo(
|
||||
'widget registered via defineWidgetExtension appears in NodeHandle.widgets after node creation'
|
||||
)
|
||||
it.todo(
|
||||
'widget is removed from all nodes when the extension scope is disposed'
|
||||
)
|
||||
it.todo(
|
||||
'defineWidgetExtension throws if widgetType is an empty string or conflicts with a built-in type'
|
||||
)
|
||||
})
|
||||
})
|
||||
44
src/extension-api-v2/__tests__/bc-22.migration.test.ts
Normal file
44
src/extension-api-v2/__tests__/bc-22.migration.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Category: BC.22 — Context menu contributions (node and canvas)
|
||||
// DB cross-ref: S2.N5, S1.H3, S1.H4
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// blast_radius: 5.10
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 getNodeMenuItems / prototype.getExtraMenuOptions / getCanvasMenuItems
|
||||
// → v2 NodeHandle.addContextMenuItem / app.addCanvasMenuItem
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.22 migration — Context menu contributions (node and canvas)', () => {
|
||||
describe('node menu item parity (S1.H3 → NodeHandle.addContextMenuItem)', () => {
|
||||
it.todo(
|
||||
'v1 getNodeMenuItems item and v2 addContextMenuItem item both appear in the node context menu with equal label text'
|
||||
)
|
||||
it.todo(
|
||||
'action/callback invoked by clicking the item receives equivalent node context in both v1 and v2'
|
||||
)
|
||||
})
|
||||
|
||||
describe('prototype patch migration (S2.N5 → NodeHandle.addContextMenuItem)', () => {
|
||||
it.todo(
|
||||
'v1 prototype.getExtraMenuOptions items and v2 addContextMenuItem items both render in the same menu section'
|
||||
)
|
||||
it.todo(
|
||||
'migrating from prototype patch removes the need to manually chain prior implementations'
|
||||
)
|
||||
})
|
||||
|
||||
describe('canvas menu parity (S1.H4 → app.addCanvasMenuItem)', () => {
|
||||
it.todo(
|
||||
'v1 getCanvasMenuItems item and v2 addCanvasMenuItem item both appear when right-clicking empty canvas'
|
||||
)
|
||||
})
|
||||
|
||||
describe('scope cleanup on dispose', () => {
|
||||
it.todo(
|
||||
'v1 menu items persist after extension unregisters; v2 items are removed on dispose'
|
||||
)
|
||||
it.todo(
|
||||
'v2 item removal on dispose does not affect items contributed by other extensions'
|
||||
)
|
||||
})
|
||||
})
|
||||
48
src/extension-api-v2/__tests__/bc-22.v1.test.ts
Normal file
48
src/extension-api-v2/__tests__/bc-22.v1.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Category: BC.22 — Context menu contributions (node and canvas)
|
||||
// DB cross-ref: S2.N5, S1.H3, S1.H4
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// blast_radius: 5.10
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 node: app.registerExtension({ getNodeMenuItems(value, options) { return [{ content: 'My Item', callback: fn }] } })
|
||||
// or node.prototype.getExtraMenuOptions = function(...) { return [...] }
|
||||
// v1 canvas: app.registerExtension({ getCanvasMenuItems() { return [{ content: 'Canvas Option', callback: fn }] } })
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.22 v1 contract — Context menu contributions (node and canvas)', () => {
|
||||
describe('S1.H3 — getNodeMenuItems hook', () => {
|
||||
it.todo(
|
||||
'extension returning items from getNodeMenuItems appends them to the node right-click menu'
|
||||
)
|
||||
it.todo(
|
||||
'getNodeMenuItems receives (value, options) where options.node is the right-clicked LGraph node'
|
||||
)
|
||||
it.todo(
|
||||
'returning null or undefined from getNodeMenuItems does not break the menu'
|
||||
)
|
||||
it.todo(
|
||||
'multiple extensions contributing node menu items all appear in the same context menu'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N5 — prototype patch getExtraMenuOptions', () => {
|
||||
it.todo(
|
||||
'assigning node.prototype.getExtraMenuOptions appends extra items to the node context menu'
|
||||
)
|
||||
it.todo(
|
||||
'prototype-patched getExtraMenuOptions receives (app, options) and its items are merged after built-ins'
|
||||
)
|
||||
it.todo(
|
||||
'multiple prototype patches chain correctly without overwriting each other'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S1.H4 — getCanvasMenuItems hook', () => {
|
||||
it.todo(
|
||||
'extension returning items from getCanvasMenuItems appends them to the canvas right-click menu'
|
||||
)
|
||||
it.todo(
|
||||
'getCanvasMenuItems items appear only when no node is right-clicked'
|
||||
)
|
||||
})
|
||||
})
|
||||
38
src/extension-api-v2/__tests__/bc-22.v2.test.ts
Normal file
38
src/extension-api-v2/__tests__/bc-22.v2.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Category: BC.22 — Context menu contributions (node and canvas)
|
||||
// DB cross-ref: S2.N5, S1.H3, S1.H4
|
||||
// Exemplar: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
// blast_radius: 5.10
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: NodeHandle.addContextMenuItem(opts), app.addCanvasMenuItem(opts)
|
||||
// registered items removed on extension dispose
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.22 v2 contract — Context menu contributions (node and canvas)', () => {
|
||||
describe('NodeHandle.addContextMenuItem() — node-scoped menu items', () => {
|
||||
it.todo(
|
||||
'NodeHandle.addContextMenuItem({ label, action }) appends the item to that node\'s right-click menu'
|
||||
)
|
||||
it.todo(
|
||||
'action callback receives a MenuItemContext with the target NodeHandle'
|
||||
)
|
||||
it.todo(
|
||||
'addContextMenuItem returns a disposable; calling it removes only that item'
|
||||
)
|
||||
it.todo(
|
||||
'item added via addContextMenuItem is removed automatically when the extension scope is disposed'
|
||||
)
|
||||
})
|
||||
|
||||
describe('app.addCanvasMenuItem() — canvas-scoped menu items', () => {
|
||||
it.todo(
|
||||
'app.addCanvasMenuItem({ label, action }) appends the item to the canvas right-click menu'
|
||||
)
|
||||
it.todo(
|
||||
'canvas menu item is visible only when right-clicking empty canvas (no node hit)'
|
||||
)
|
||||
it.todo(
|
||||
'canvas menu item is removed when the extension scope is disposed'
|
||||
)
|
||||
})
|
||||
})
|
||||
38
src/extension-api-v2/__tests__/bc-23.migration.test.ts
Normal file
38
src/extension-api-v2/__tests__/bc-23.migration.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Category: BC.23 — Node property bag mutations
|
||||
// DB cross-ref: S2.N18
|
||||
// Exemplar: https://github.com/rgthree/rgthree-comfy/blob/main/src_web/comfyui/seed.ts#L78
|
||||
// blast_radius: 5.82
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 onPropertyChanged prototype patch / node.properties direct write
|
||||
// → v2 NodeHandle.on('propertyChanged') / NodeHandle.setProperty
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.23 migration — Node property bag mutations', () => {
|
||||
describe('observer parity (S2.N18)', () => {
|
||||
it.todo(
|
||||
'v1 onPropertyChanged and v2 propertyChanged listener both receive identical (name, value, prevValue) for the same mutation'
|
||||
)
|
||||
it.todo(
|
||||
'v2 listener fires for writes made via NodeHandle.setProperty; v1 hook fires for the same via native property set path'
|
||||
)
|
||||
})
|
||||
|
||||
describe('persistence parity', () => {
|
||||
it.todo(
|
||||
'property written via v1 node.properties.myKey and v2 NodeHandle.setProperty both round-trip through JSON serialization identically'
|
||||
)
|
||||
it.todo(
|
||||
'property survives node.clone() in both v1 and v2 paths'
|
||||
)
|
||||
})
|
||||
|
||||
describe('scope cleanup on dispose', () => {
|
||||
it.todo(
|
||||
'v1 prototype.onPropertyChanged persists after extension unregisters; v2 listener is removed on dispose'
|
||||
)
|
||||
it.todo(
|
||||
'v2 listener removal on dispose does not silence listeners registered by other extensions on the same node'
|
||||
)
|
||||
})
|
||||
})
|
||||
38
src/extension-api-v2/__tests__/bc-23.v1.test.ts
Normal file
38
src/extension-api-v2/__tests__/bc-23.v1.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Category: BC.23 — Node property bag mutations
|
||||
// DB cross-ref: S2.N18
|
||||
// Exemplar: https://github.com/rgthree/rgthree-comfy/blob/main/src_web/comfyui/seed.ts#L78
|
||||
// blast_radius: 5.82
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1: node.prototype.onPropertyChanged = function(name, value, prevValue) { ... }
|
||||
// or node.properties.myKey = value
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.23 v1 contract — Node property bag mutations', () => {
|
||||
describe('S2.N18 — onPropertyChanged lifecycle hook', () => {
|
||||
it.todo(
|
||||
'assigning node.prototype.onPropertyChanged wires a callback invoked when any property value changes'
|
||||
)
|
||||
it.todo(
|
||||
'onPropertyChanged receives (name, value, prevValue) with correct types for each argument'
|
||||
)
|
||||
it.todo(
|
||||
'onPropertyChanged is NOT called for properties set before the node is created'
|
||||
)
|
||||
it.todo(
|
||||
'multiple prototype patches to onPropertyChanged: later patch overwrites earlier unless manually chained'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S2.N18 — direct node.properties mutation', () => {
|
||||
it.todo(
|
||||
'setting node.properties.myKey = value persists the value through graph serialization and deserialization'
|
||||
)
|
||||
it.todo(
|
||||
'direct property mutation does not automatically trigger onPropertyChanged'
|
||||
)
|
||||
it.todo(
|
||||
'properties bag survives node clone (node.clone() copies node.properties by value)'
|
||||
)
|
||||
})
|
||||
})
|
||||
38
src/extension-api-v2/__tests__/bc-23.v2.test.ts
Normal file
38
src/extension-api-v2/__tests__/bc-23.v2.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Category: BC.23 — Node property bag mutations
|
||||
// DB cross-ref: S2.N18
|
||||
// Exemplar: https://github.com/rgthree/rgthree-comfy/blob/main/src_web/comfyui/seed.ts#L78
|
||||
// blast_radius: 5.82
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: NodeHandle.on('propertyChanged', (name, value, prevValue) => { ... })
|
||||
// NodeHandle.setProperty(name, value)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.23 v2 contract — Node property bag mutations', () => {
|
||||
describe('NodeHandle.on(\'propertyChanged\') — reactive property observation', () => {
|
||||
it.todo(
|
||||
'NodeHandle.on(\'propertyChanged\', cb) fires cb with (name, value, prevValue) on every property write'
|
||||
)
|
||||
it.todo(
|
||||
'propertyChanged event fires for mutations made via both NodeHandle.setProperty and direct node.properties writes'
|
||||
)
|
||||
it.todo(
|
||||
'multiple listeners on the same node all receive the event independently'
|
||||
)
|
||||
it.todo(
|
||||
'listener registered via NodeHandle.on is removed when the extension scope is disposed'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NodeHandle.setProperty() — managed property mutation', () => {
|
||||
it.todo(
|
||||
'NodeHandle.setProperty(name, value) updates node.properties[name] and triggers propertyChanged listeners'
|
||||
)
|
||||
it.todo(
|
||||
'value set via setProperty survives graph serialization and deserialization'
|
||||
)
|
||||
it.todo(
|
||||
'setProperty with the same value as current does not fire propertyChanged (no-op guard)'
|
||||
)
|
||||
})
|
||||
})
|
||||
37
src/extension-api-v2/__tests__/bc-24.migration.test.ts
Normal file
37
src/extension-api-v2/__tests__/bc-24.migration.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Category: BC.24 — Node-def schema inspection
|
||||
// DB cross-ref: S13.SC1
|
||||
// Exemplar: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/index.js#L1
|
||||
// blast_radius: 5.00
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 raw nodeData property access → v2 NodeHandle.def / NodeHandle.inputDefs / NodeHandle.outputDefs
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.24 migration — Node-def schema inspection', () => {
|
||||
describe('input schema parity (S13.SC1)', () => {
|
||||
it.todo(
|
||||
'v1 nodeData.input.required and v2 NodeHandle.def.input.required contain identical keys for the same node type'
|
||||
)
|
||||
it.todo(
|
||||
'v1 InputSpec tuple first element and v2 InputDef.type are equal strings for every slot'
|
||||
)
|
||||
it.todo(
|
||||
'v1 nodeData.input.optional and v2 NodeHandle.def.input.optional both reflect server-provided optional inputs'
|
||||
)
|
||||
})
|
||||
|
||||
describe('output schema parity', () => {
|
||||
it.todo(
|
||||
'v1 nodeData.output array and v2 NodeHandle.def.output have the same length and type strings in slot order'
|
||||
)
|
||||
it.todo(
|
||||
'v1 nodeData.output_node and v2 NodeHandle.def.output_node are the same boolean value'
|
||||
)
|
||||
})
|
||||
|
||||
describe('category parity', () => {
|
||||
it.todo(
|
||||
'v1 nodeData.category and v2 NodeHandle.def.category are identical strings for the same node type'
|
||||
)
|
||||
})
|
||||
})
|
||||
41
src/extension-api-v2/__tests__/bc-24.v1.test.ts
Normal file
41
src/extension-api-v2/__tests__/bc-24.v1.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Category: BC.24 — Node-def schema inspection
|
||||
// DB cross-ref: S13.SC1
|
||||
// Exemplar: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/index.js#L1
|
||||
// blast_radius: 5.00
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1: direct inspection of nodeData.input.required, nodeData.input.optional, nodeData.output,
|
||||
// nodeData.output_node, nodeData.category, InputSpec sentinel tuples
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.24 v1 contract — Node-def schema inspection', () => {
|
||||
describe('S13.SC1 — input slot inspection', () => {
|
||||
it.todo(
|
||||
'nodeData.input.required is an object mapping slot names to InputSpec tuples [type, opts?]'
|
||||
)
|
||||
it.todo(
|
||||
'nodeData.input.optional is an object mapping slot names to InputSpec tuples and may be undefined'
|
||||
)
|
||||
it.todo(
|
||||
'nodeData.input.hidden is an object or undefined; hidden inputs do not appear in the node UI'
|
||||
)
|
||||
it.todo(
|
||||
'InputSpec tuple first element is a string type name or array of enum values'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S13.SC1 — output slot inspection', () => {
|
||||
it.todo(
|
||||
'nodeData.output is an array of output type name strings in slot order'
|
||||
)
|
||||
it.todo(
|
||||
'nodeData.output_node is a boolean indicating whether this node routes data to the server output'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S13.SC1 — category inspection', () => {
|
||||
it.todo(
|
||||
'nodeData.category is a slash-delimited string used to place the node in the Add Node menu hierarchy'
|
||||
)
|
||||
})
|
||||
})
|
||||
44
src/extension-api-v2/__tests__/bc-24.v2.test.ts
Normal file
44
src/extension-api-v2/__tests__/bc-24.v2.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Category: BC.24 — Node-def schema inspection
|
||||
// DB cross-ref: S13.SC1
|
||||
// Exemplar: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/index.js#L1
|
||||
// blast_radius: 5.00
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: NodeHandle.def — typed ComfyNodeDef shape with same fields but typed accessors
|
||||
// NodeHandle.inputDefs, NodeHandle.outputDefs
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.24 v2 contract — Node-def schema inspection', () => {
|
||||
describe('NodeHandle.def — typed ComfyNodeDef accessor', () => {
|
||||
it.todo(
|
||||
'NodeHandle.def.input.required is a typed Record<string, InputDef> mirroring the v1 shape'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.def.input.optional is a typed Record<string, InputDef> or undefined'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.def.output is a typed readonly array of OutputDef in slot order'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.def.output_node is a boolean identical to the server-provided value'
|
||||
)
|
||||
it.todo(
|
||||
'NodeHandle.def.category is the slash-delimited category string'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NodeHandle.inputDefs — convenience accessor', () => {
|
||||
it.todo(
|
||||
'NodeHandle.inputDefs returns a flat array merging required and optional inputs with a slot-order index'
|
||||
)
|
||||
it.todo(
|
||||
'each InputDef entry exposes .name, .type, .required, and .options fields'
|
||||
)
|
||||
})
|
||||
|
||||
describe('NodeHandle.outputDefs — convenience accessor', () => {
|
||||
it.todo(
|
||||
'NodeHandle.outputDefs returns an array of OutputDef with .name, .type, and .index fields'
|
||||
)
|
||||
})
|
||||
})
|
||||
44
src/extension-api-v2/__tests__/bc-25.migration.test.ts
Normal file
44
src/extension-api-v2/__tests__/bc-25.migration.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Category: BC.25 — Shell UI registration (commands, sidebars, toasts)
|
||||
// DB cross-ref: S12.UI1
|
||||
// Exemplar: https://github.com/robertvoy/ComfyUI-Distributed/blob/main/web/main.js#L269
|
||||
// blast_radius: 4.02
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 extensionManager / commandManager / toastManager imports
|
||||
// → v2 comfyApp.registerSidebarTab / registerCommand / showToast (stable import path)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.25 migration — Shell UI registration (commands, sidebars, toasts)', () => {
|
||||
describe('sidebar tab parity (S12.UI1)', () => {
|
||||
it.todo(
|
||||
'v1 extensionManager.registerSidebarTab and v2 comfyApp.registerSidebarTab both result in a visible tab with equivalent id and title'
|
||||
)
|
||||
it.todo(
|
||||
'v2 tab render context provides the same root element accessible in v1 raw render callback'
|
||||
)
|
||||
})
|
||||
|
||||
describe('command parity', () => {
|
||||
it.todo(
|
||||
'command registered via v1 commandManager.registerCommand and v2 comfyApp.registerCommand are both invocable by the same id'
|
||||
)
|
||||
it.todo(
|
||||
'execute/function callback receives equivalent context objects in v1 and v2'
|
||||
)
|
||||
})
|
||||
|
||||
describe('toast parity', () => {
|
||||
it.todo(
|
||||
'v1 toastManager.add and v2 comfyApp.showToast both display a notification with the same severity and summary text'
|
||||
)
|
||||
it.todo(
|
||||
'auto-dismiss timing is equivalent between v1 life and v2 life options'
|
||||
)
|
||||
})
|
||||
|
||||
describe('scope cleanup on dispose', () => {
|
||||
it.todo(
|
||||
'v1 sidebar tabs and commands persist after extension unregisters; v2 contributions are removed on dispose'
|
||||
)
|
||||
})
|
||||
})
|
||||
52
src/extension-api-v2/__tests__/bc-25.v1.test.ts
Normal file
52
src/extension-api-v2/__tests__/bc-25.v1.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Category: BC.25 — Shell UI registration (commands, sidebars, toasts)
|
||||
// DB cross-ref: S12.UI1
|
||||
// Exemplar: https://github.com/robertvoy/ComfyUI-Distributed/blob/main/web/main.js#L269
|
||||
// blast_radius: 4.02
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1: app.registerExtension({ settings: [...] })
|
||||
// extensionManager.registerSidebarTab(opts)
|
||||
// commandManager.registerCommand(opts)
|
||||
// toastManager.add(opts)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.25 v1 contract — Shell UI registration (commands, sidebars, toasts)', () => {
|
||||
describe('S12.UI1 — settings registration', () => {
|
||||
it.todo(
|
||||
'extension passing a settings array to registerExtension adds each setting to the ComfyUI settings panel'
|
||||
)
|
||||
it.todo(
|
||||
'registered setting value is readable via app.ui.settings.getSettingValue(id) after registration'
|
||||
)
|
||||
it.todo(
|
||||
'setting onChange callback fires when the user changes the value in the settings panel'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S12.UI1 — sidebar tab registration', () => {
|
||||
it.todo(
|
||||
'extensionManager.registerSidebarTab({ id, icon, title, render }) adds a tab to the sidebar'
|
||||
)
|
||||
it.todo(
|
||||
'render function is called with the tab container element when the tab is first activated'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S12.UI1 — command registration', () => {
|
||||
it.todo(
|
||||
'commandManager.registerCommand({ id, label, function }) makes the command invocable by id'
|
||||
)
|
||||
it.todo(
|
||||
'registered command appears in the command palette UI'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S12.UI1 — toast notifications', () => {
|
||||
it.todo(
|
||||
'toastManager.add({ severity, summary, detail }) displays a toast notification in the UI'
|
||||
)
|
||||
it.todo(
|
||||
'toast with a specified life value auto-dismisses after the given number of milliseconds'
|
||||
)
|
||||
})
|
||||
})
|
||||
48
src/extension-api-v2/__tests__/bc-25.v2.test.ts
Normal file
48
src/extension-api-v2/__tests__/bc-25.v2.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Category: BC.25 — Shell UI registration (commands, sidebars, toasts)
|
||||
// DB cross-ref: S12.UI1
|
||||
// Exemplar: https://github.com/robertvoy/ComfyUI-Distributed/blob/main/web/main.js#L269
|
||||
// blast_radius: 4.02
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: same APIs stabilized — comfyApp.registerSidebarTab(opts),
|
||||
// comfyApp.registerCommand(opts), comfyApp.showToast(opts)
|
||||
// consistent import path from @comfyorg/extension-api
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.25 v2 contract — Shell UI registration (commands, sidebars, toasts)', () => {
|
||||
describe('comfyApp.registerSidebarTab() — stabilized sidebar API', () => {
|
||||
it.todo(
|
||||
'comfyApp.registerSidebarTab({ id, icon, title, render }) adds a tab accessible in the sidebar'
|
||||
)
|
||||
it.todo(
|
||||
'sidebar tab registered via comfyApp is removed when the extension scope is disposed'
|
||||
)
|
||||
it.todo(
|
||||
'render receives a typed SidebarTabContext instead of a raw DOM element'
|
||||
)
|
||||
})
|
||||
|
||||
describe('comfyApp.registerCommand() — stabilized command API', () => {
|
||||
it.todo(
|
||||
'comfyApp.registerCommand({ id, label, execute }) makes the command invocable by id'
|
||||
)
|
||||
it.todo(
|
||||
'command appears in the command palette with the provided label'
|
||||
)
|
||||
it.todo(
|
||||
'command is unregistered when the extension scope is disposed'
|
||||
)
|
||||
})
|
||||
|
||||
describe('comfyApp.showToast() — stabilized toast API', () => {
|
||||
it.todo(
|
||||
'comfyApp.showToast({ severity, summary, detail }) displays a toast notification'
|
||||
)
|
||||
it.todo(
|
||||
'showToast with life option auto-dismisses after the specified duration'
|
||||
)
|
||||
it.todo(
|
||||
'showToast returns a handle with a dismiss() method for programmatic removal'
|
||||
)
|
||||
})
|
||||
})
|
||||
41
src/extension-api-v2/__tests__/bc-26.migration.test.ts
Normal file
41
src/extension-api-v2/__tests__/bc-26.migration.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Category: BC.26 — Globals as ABI (window.LiteGraph, window.comfyAPI)
|
||||
// DB cross-ref: S7.G1
|
||||
// Exemplar: https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside/blob/main/web/js/index.js#L1
|
||||
// blast_radius: 4.55
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// Migration: v1 window.LiteGraph / window.comfyAPI / window.app access
|
||||
// → v2 explicit named imports from @comfyorg/extension-api
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.26 migration — Globals as ABI (window.LiteGraph, window.comfyAPI)', () => {
|
||||
describe('LiteGraph reference parity (S7.G1)', () => {
|
||||
it.todo(
|
||||
'window.LiteGraph.LGraphNode and the named import LGraphNode from @comfyorg/extension-api are the same constructor reference'
|
||||
)
|
||||
it.todo(
|
||||
'a node registered via window.LiteGraph.registerNodeType is identical to one registered via the v2 import path'
|
||||
)
|
||||
it.todo(
|
||||
'LiteGraph enum values accessed via window and via import are strictly equal (===)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('comfyAPI / comfyApp reference parity', () => {
|
||||
it.todo(
|
||||
'window.app and the imported comfyApp share the same graph state — mutations via one are visible on the other'
|
||||
)
|
||||
it.todo(
|
||||
'window.comfyAPI.modules.extensionService and imported extensionManager refer to the same instance'
|
||||
)
|
||||
})
|
||||
|
||||
describe('deprecation signal migration', () => {
|
||||
it.todo(
|
||||
'replacing window.LiteGraph access with named imports removes all deprecation console warnings'
|
||||
)
|
||||
it.todo(
|
||||
'replacing window.comfyAPI access with named imports removes all deprecation console warnings'
|
||||
)
|
||||
})
|
||||
})
|
||||
43
src/extension-api-v2/__tests__/bc-26.v1.test.ts
Normal file
43
src/extension-api-v2/__tests__/bc-26.v1.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Category: BC.26 — Globals as ABI (window.LiteGraph, window.comfyAPI)
|
||||
// DB cross-ref: S7.G1
|
||||
// Exemplar: https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside/blob/main/web/js/index.js#L1
|
||||
// blast_radius: 4.55
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1: window.LiteGraph.registerNodeType(...), window.comfyAPI.modules.extensionService, window.app
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.26 v1 contract — Globals as ABI (window.LiteGraph, window.comfyAPI)', () => {
|
||||
describe('S7.G1 — window.LiteGraph global usage', () => {
|
||||
it.todo(
|
||||
'window.LiteGraph is defined and exposes registerNodeType, LGraph, LGraphNode, and LLink constructors'
|
||||
)
|
||||
it.todo(
|
||||
'window.LiteGraph.registerNodeType(type, ctor) registers a custom node type visible in the Add Node menu'
|
||||
)
|
||||
it.todo(
|
||||
'LiteGraph enum constants (e.g. LiteGraph.INPUT, LiteGraph.OUTPUT) are accessible via window.LiteGraph'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S7.G1 — window.comfyAPI global registry', () => {
|
||||
it.todo(
|
||||
'window.comfyAPI is defined after the app boots and exposes a modules sub-object'
|
||||
)
|
||||
it.todo(
|
||||
'window.comfyAPI.modules.extensionService references the active extensionManager instance'
|
||||
)
|
||||
it.todo(
|
||||
'services accessed via window.comfyAPI.modules are the same objects as those available via ES module import'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S7.G1 — window.app global', () => {
|
||||
it.todo(
|
||||
'window.app is defined and is the same object as the app instance passed to extension hooks'
|
||||
)
|
||||
it.todo(
|
||||
'mutations made to the graph via window.app are reflected in the live canvas immediately'
|
||||
)
|
||||
})
|
||||
})
|
||||
44
src/extension-api-v2/__tests__/bc-26.v2.test.ts
Normal file
44
src/extension-api-v2/__tests__/bc-26.v2.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Category: BC.26 — Globals as ABI (window.LiteGraph, window.comfyAPI)
|
||||
// DB cross-ref: S7.G1
|
||||
// Exemplar: https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside/blob/main/web/js/index.js#L1
|
||||
// blast_radius: 4.55
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 replacement: explicit imports from @comfyorg/extension-api
|
||||
// globals still exported for compat shim but deprecated
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.26 v2 contract — Globals as ABI (window.LiteGraph, window.comfyAPI)', () => {
|
||||
describe('explicit LiteGraph imports from @comfyorg/extension-api', () => {
|
||||
it.todo(
|
||||
'LGraph, LGraphNode, LLink are importable by name from @comfyorg/extension-api'
|
||||
)
|
||||
it.todo(
|
||||
'LiteGraph enum constants (INPUT, OUTPUT, etc.) are importable as named exports'
|
||||
)
|
||||
it.todo(
|
||||
'imported constructors are the same references as window.LiteGraph equivalents during the compat shim window'
|
||||
)
|
||||
})
|
||||
|
||||
describe('explicit comfyApp / service imports', () => {
|
||||
it.todo(
|
||||
'comfyApp is importable from @comfyorg/extension-api and is the same instance as window.app'
|
||||
)
|
||||
it.todo(
|
||||
'extensionManager is importable from @comfyorg/extension-api and is the same instance as window.comfyAPI.modules.extensionService'
|
||||
)
|
||||
})
|
||||
|
||||
describe('compat shim deprecation', () => {
|
||||
it.todo(
|
||||
'accessing window.LiteGraph in v2 mode emits a deprecation warning to the console'
|
||||
)
|
||||
it.todo(
|
||||
'accessing window.comfyAPI in v2 mode emits a deprecation warning to the console'
|
||||
)
|
||||
it.todo(
|
||||
'compat shim globals are still functional (not removed) so v1 extensions continue working during migration window'
|
||||
)
|
||||
})
|
||||
})
|
||||
46
src/extension-api-v2/__tests__/bc-27.migration.test.ts
Normal file
46
src/extension-api-v2/__tests__/bc-27.migration.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Category: BC.27 — LiteGraph entity direct manipulation (reroute, group, link, slot)
|
||||
// DB cross-ref: S9.R1, S9.G1, S9.L1, S9.S1
|
||||
// Exemplar: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
|
||||
// blast_radius: 5.62
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// migration: direct raw object mutations → read-only v2 accessors (mutations deferred to D9 Phase C)
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.27 migration — LiteGraph entity direct manipulation', () => {
|
||||
describe('reroute migration', () => {
|
||||
it.todo(
|
||||
'v1 graph.reroutes raw access is replaced by comfyApp.graph.reroutes iterable'
|
||||
)
|
||||
it.todo(
|
||||
'v1 direct position mutation (graph.reroutes[id].pos = [...]) has no v2 equivalent until D9 Phase C'
|
||||
)
|
||||
})
|
||||
|
||||
describe('group migration', () => {
|
||||
it.todo(
|
||||
'v1 graph.groups[i].title mutation is replaced by a future GroupHandle.setTitle() (D9 Phase C)'
|
||||
)
|
||||
it.todo(
|
||||
'v1 graph.groups iteration is replaced by comfyApp.graph.groups read-only iterable'
|
||||
)
|
||||
})
|
||||
|
||||
describe('link migration', () => {
|
||||
it.todo(
|
||||
'v1 link.color direct assignment is replaced by a future LinkHandle.setColor() (D9 Phase C)'
|
||||
)
|
||||
it.todo(
|
||||
'v2 compat shim logs a deprecation warning when graph.links is accessed directly'
|
||||
)
|
||||
})
|
||||
|
||||
describe('slot migration', () => {
|
||||
it.todo(
|
||||
'v1 node.inputs[i].shape mutation has no v2 equivalent until D9 Phase C'
|
||||
)
|
||||
it.todo(
|
||||
'v2 compat shim throws a TypeError when slot mutation is attempted via legacy path'
|
||||
)
|
||||
})
|
||||
})
|
||||
52
src/extension-api-v2/__tests__/bc-27.v1.test.ts
Normal file
52
src/extension-api-v2/__tests__/bc-27.v1.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// Category: BC.27 — LiteGraph entity direct manipulation (reroute, group, link, slot)
|
||||
// DB cross-ref: S9.R1, S9.G1, S9.L1, S9.S1
|
||||
// Exemplar: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
|
||||
// blast_radius: 5.62
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v1 contract: direct graph.reroutes, graph.groups, link.color, slot.shape mutations — no API, raw object access
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.27 v1 contract — LiteGraph entity direct manipulation', () => {
|
||||
describe('S9.R1 — reroute direct access', () => {
|
||||
it.todo(
|
||||
'extension can read graph.reroutes and iterate all reroute nodes in the graph'
|
||||
)
|
||||
it.todo(
|
||||
'extension can mutate reroute position directly via graph.reroutes[id].pos'
|
||||
)
|
||||
it.todo(
|
||||
'reroute additions via graph.reroutes[id] = { ... } are reflected in the rendered canvas'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S9.G1 — group direct access', () => {
|
||||
it.todo(
|
||||
'extension can read graph.groups and iterate all groups'
|
||||
)
|
||||
it.todo(
|
||||
'extension can mutate group title via graph.groups[i].title = string'
|
||||
)
|
||||
it.todo(
|
||||
'extension can mutate group bounding box via graph.groups[i].bounding'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S9.L1 — link direct access', () => {
|
||||
it.todo(
|
||||
'extension can read link.color and link.type directly from graph.links[id]'
|
||||
)
|
||||
it.todo(
|
||||
'setting link.color mutates the rendered link color without requiring graph refresh'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S9.S1 — slot direct access', () => {
|
||||
it.todo(
|
||||
'extension can read node.inputs[i].shape and node.outputs[i].shape directly'
|
||||
)
|
||||
it.todo(
|
||||
'extension can mutate slot.shape to change rendered connector shape'
|
||||
)
|
||||
})
|
||||
})
|
||||
50
src/extension-api-v2/__tests__/bc-27.v2.test.ts
Normal file
50
src/extension-api-v2/__tests__/bc-27.v2.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Category: BC.27 — LiteGraph entity direct manipulation (reroute, group, link, slot)
|
||||
// DB cross-ref: S9.R1, S9.G1, S9.L1, S9.S1
|
||||
// Exemplar: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
|
||||
// blast_radius: 5.62
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// v2 contract: partial — reroute/group/link read APIs planned; mutations deferred to D9 Phase C.
|
||||
// For now: read-only accessors
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.27 v2 contract — LiteGraph entity direct manipulation', () => {
|
||||
describe('S9.R1 — reroute read-only accessors', () => {
|
||||
it.todo(
|
||||
'comfyApp.graph.reroutes returns an iterable of read-only RerouteHandle objects'
|
||||
)
|
||||
it.todo(
|
||||
'RerouteHandle exposes id, pos, and linked link IDs as read-only properties'
|
||||
)
|
||||
it.todo(
|
||||
'attempting to mutate RerouteHandle.pos in v2 throws or is silently ignored (write-protect)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S9.G1 — group read-only accessors', () => {
|
||||
it.todo(
|
||||
'comfyApp.graph.groups returns an iterable of read-only GroupHandle objects'
|
||||
)
|
||||
it.todo(
|
||||
'GroupHandle exposes title and bounding as read-only (mutations deferred to D9 Phase C)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S9.L1 — link read-only accessors', () => {
|
||||
it.todo(
|
||||
'comfyApp.graph.links returns a Map<id, LinkHandle> with read-only color and type'
|
||||
)
|
||||
it.todo(
|
||||
'link mutation API is not available in v2 Phase A (deferred to D9 Phase C)'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S9.S1 — slot read-only accessors', () => {
|
||||
it.todo(
|
||||
'NodeHandle.inputs and NodeHandle.outputs expose read-only SlotHandle with shape'
|
||||
)
|
||||
it.todo(
|
||||
'slot shape mutation is not available in v2 Phase A (deferred to D9 Phase C)'
|
||||
)
|
||||
})
|
||||
})
|
||||
39
src/extension-api-v2/__tests__/bc-28.migration.test.ts
Normal file
39
src/extension-api-v2/__tests__/bc-28.migration.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Category: BC.28 — Subgraph fan-out via set/get virtual nodes
|
||||
// DB cross-ref: S9.SG1
|
||||
// Exemplar: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1406
|
||||
// blast_radius: 4.97
|
||||
// compat-floor: blast_radius ≥ 2.0
|
||||
// migration: isVirtualNode=true + graphToPrompt monkey-patch → defineNodeExtension({ virtual: true, resolveConnections })
|
||||
// Decision: I-UWF.5 (2026-05-08) — S8.P1 → virtual: true (mechanical rename); S9.SG1 → add resolveConnections.
|
||||
// Classified uwf-resolved per I-PG.B2 — UWF Phase 3 is the migration path.
|
||||
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('BC.28 migration — subgraph fan-out via set/get virtual nodes', () => {
|
||||
describe('S8.P1 — isVirtualNode flag migration', () => {
|
||||
it.todo(
|
||||
'v1 class-level isVirtualNode=true is replaced by defineNodeExtension({ virtual: true, resolveConnections })'
|
||||
)
|
||||
it.todo(
|
||||
'v2 compat shim recognizes isVirtualNode=true on a registered class and emits a migration warning'
|
||||
)
|
||||
it.todo(
|
||||
'migration is mechanical: rename isVirtualNode=true to virtual: true and add resolveConnections stub'
|
||||
)
|
||||
})
|
||||
|
||||
describe('S9.SG1 — graphToPrompt monkey-patch migration', () => {
|
||||
it.todo(
|
||||
'v1 graphToPrompt patch that rewrites link.target_id is replaced by resolveConnections returning ResolvedEdges'
|
||||
)
|
||||
it.todo(
|
||||
'v2 resolveConnections receives the same graph state that v1 graphToPrompt received, as a read-only view'
|
||||
)
|
||||
it.todo(
|
||||
'v2 compat shim logs a deprecation warning when graphToPrompt is monkey-patched for virtual node resolution'
|
||||
)
|
||||
it.todo(
|
||||
'for cg-use-everywhere topology inference (graph-wide, not per-type): ctx.on("beforePrompt") is the bridge until UWF Phase 3'
|
||||
)
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user