Compare commits
5 Commits
ext-api/i-
...
kishore/oa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6898665823 | ||
|
|
607bd8a309 | ||
|
|
101f054395 | ||
|
|
cb042bee24 | ||
|
|
e3d2ae3cef |
88
.github/workflows/ci-tests-extension-api.yaml
vendored
@@ -1,88 +0,0 @@
|
||||
# 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
@@ -1,97 +0,0 @@
|
||||
# Description: Publish @comfyorg/extension-api to npm with provenance attestation.
|
||||
#
|
||||
# Triggered by a tag push matching 'extension-api-v*' (e.g. extension-api-v0.1.0).
|
||||
# Also supports workflow_dispatch for a manual dry-run (set dry_run: true).
|
||||
#
|
||||
# Prerequisites (one-time human setup):
|
||||
# - NPM_TOKEN secret must be set in the repo/org settings with publish
|
||||
# access to the @comfyorg scope on npmjs.com.
|
||||
# - The @comfyorg npm scope already exists (used by @comfyorg/comfyui-frontend).
|
||||
#
|
||||
# PKG4.D4 (MIG1 / Phase A — surface-only shim)
|
||||
name: 'Extension API: Publish'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'extension-api-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run — build and verify without publishing'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write # needed to create GitHub Release
|
||||
id-token: write # needed for npm provenance via OIDC
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish @comfyorg/extension-api
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # full history for release notes
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Setup npm registry
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
|
||||
- name: Build package
|
||||
run: pnpm --filter @comfyorg/extension-api build
|
||||
|
||||
- name: Typecheck package
|
||||
run: pnpm --filter @comfyorg/extension-api typecheck
|
||||
|
||||
- name: Verify package version matches tag
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}" # e.g. extension-api-v0.1.0
|
||||
PKG_VERSION=$(node -p "require('./packages/extension-api/package.json').version")
|
||||
TAG_VERSION="${TAG#extension-api-v}" # strip prefix → 0.1.0
|
||||
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "::error::Tag '$TAG' implies version '$TAG_VERSION' but packages/extension-api/package.json has '$PKG_VERSION'. Update the package.json before tagging."
|
||||
exit 1
|
||||
fi
|
||||
echo "Version check passed: $PKG_VERSION"
|
||||
|
||||
- name: Publish to npm (with provenance)
|
||||
if: github.event_name == 'push' || !inputs.dry_run
|
||||
run: |
|
||||
cd packages/extension-api
|
||||
npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Dry-run report
|
||||
if: inputs.dry_run
|
||||
run: |
|
||||
echo "=== DRY RUN — would publish ==="
|
||||
cd packages/extension-api
|
||||
npm pack --dry-run
|
||||
echo "=== End dry run ==="
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.event_name == 'push'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const tag = context.ref.replace('refs/tags/', '')
|
||||
const { data: release } = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: tag,
|
||||
name: tag,
|
||||
generate_release_notes: true,
|
||||
draft: false,
|
||||
prerelease: context.ref.includes('-alpha') || context.ref.includes('-beta') || context.ref.includes('-rc')
|
||||
})
|
||||
console.log(`Release created: ${release.html_url}`)
|
||||
65
.github/workflows/extension-api-typecheck.yml
vendored
@@ -1,65 +0,0 @@
|
||||
# Description: Typecheck and build the @comfyorg/extension-api package.
|
||||
# Runs on PRs and pushes touching the public type surface, the core .v2.ts
|
||||
# implementations, or the package scaffold — so regressions in the published
|
||||
# contract are caught before merge.
|
||||
#
|
||||
# PKG4.D3 (MIG1 / Phase A — surface-only shim)
|
||||
name: 'Extension API: Typecheck'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, dev*, core/*, extension-v2*]
|
||||
paths:
|
||||
- 'src/extension-api/**'
|
||||
- 'src/extensions/core/*.v2.ts'
|
||||
- 'src/services/extension-api-service.ts'
|
||||
- 'packages/extension-api/**'
|
||||
- '.github/workflows/extension-api-*.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
paths:
|
||||
- 'src/extension-api/**'
|
||||
- 'src/extensions/core/*.v2.ts'
|
||||
- 'src/services/extension-api-service.ts'
|
||||
- 'packages/extension-api/**'
|
||||
- '.github/workflows/extension-api-*.yml'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Build + typecheck @comfyorg/extension-api
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Build package (emit declarations)
|
||||
run: pnpm --filter @comfyorg/extension-api build
|
||||
|
||||
- name: Typecheck package
|
||||
run: pnpm --filter @comfyorg/extension-api typecheck
|
||||
|
||||
- name: Smoke-test consumer (tsc --noEmit on minimal extension)
|
||||
# Verifies the published types are consumable from an external module
|
||||
# that imports from '@comfyorg/extension-api'. Uses a minimal fixture
|
||||
# checked in to packages/extension-api/test/smoke/.
|
||||
run: |
|
||||
cd packages/extension-api
|
||||
if [ -d test/smoke ]; then
|
||||
pnpm exec tsc --noEmit --project test/smoke/tsconfig.json
|
||||
else
|
||||
echo "No smoke test found — skipping (add packages/extension-api/test/smoke/ to enable)"
|
||||
fi
|
||||
@@ -6,7 +6,6 @@
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"ignorePatterns": [
|
||||
"packages/extension-api/build/**",
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"public/materialdesignicons.min.css",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
|
||||
@@ -32,34 +32,16 @@ test.describe('Careers page @smoke', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('clicking a department button scrolls to and activates that section', async ({
|
||||
test('ENGINEERING category filter narrows the role list', async ({
|
||||
page
|
||||
}) => {
|
||||
const rolesSection = page.getByTestId('careers-roles')
|
||||
await rolesSection.scrollIntoViewIfNeeded()
|
||||
await expect(rolesSection).toBeVisible()
|
||||
|
||||
const allCount = await page.getByTestId('careers-role-link').count()
|
||||
|
||||
const engineeringButton = page.getByRole('button', {
|
||||
name: 'ENGINEERING',
|
||||
exact: true
|
||||
})
|
||||
|
||||
// RolesSection is hydrated via `client:visible`. Once the button responds
|
||||
// to a click by flipping aria-pressed, Vue is hydrated and the rest of
|
||||
// the locator logic is in effect.
|
||||
await expect(async () => {
|
||||
await engineeringButton.click()
|
||||
await expect(engineeringButton).toHaveAttribute('aria-pressed', 'true', {
|
||||
timeout: 1_000
|
||||
})
|
||||
}).toPass({ timeout: 10_000 })
|
||||
|
||||
const engineeringSection = page.locator('#careers-dept-engineering')
|
||||
await expect(engineeringSection).toBeInViewport()
|
||||
|
||||
expect(await page.getByTestId('careers-role-link').count()).toBe(allCount)
|
||||
await page.getByRole('button', { name: 'ENGINEERING', exact: true }).click()
|
||||
const engineeringLocator = page.getByTestId('careers-role-link')
|
||||
await expect(engineeringLocator.first()).toBeVisible()
|
||||
const engineeringCount = await engineeringLocator.count()
|
||||
expect(engineeringCount).toBeLessThanOrEqual(allCount)
|
||||
expect(engineeringCount).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const M4_PRO_14_INCH_VIEWPORT = { width: 2016, height: 1310 }
|
||||
const LAST_SECTION_HASH = '#contact'
|
||||
|
||||
test.describe(
|
||||
'ContentSection scroll-spy @smoke',
|
||||
{
|
||||
annotation: [
|
||||
{
|
||||
type: 'issue',
|
||||
description:
|
||||
'https://linear.app/comfyorg/issue/FE-604/bug-bottom-badge-not-activating-on-scroll-at-high-resolution-3024x1964'
|
||||
},
|
||||
{
|
||||
type: 'environment',
|
||||
description:
|
||||
'14" MacBook M4 Pro logical viewport reported in FE-604; /privacy-policy reproduces because of its short trailing sections'
|
||||
}
|
||||
]
|
||||
},
|
||||
() => {
|
||||
test.use({ viewport: M4_PRO_14_INCH_VIEWPORT })
|
||||
|
||||
test('activates the last badge when user scrolls to the bottom', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/privacy-policy')
|
||||
|
||||
const sidebarNav = page.getByRole('navigation', {
|
||||
name: 'Category filter'
|
||||
})
|
||||
const badges = sidebarNav.getByRole('button')
|
||||
const lastBadge = badges.last()
|
||||
|
||||
await expect(badges.first()).toHaveAttribute('aria-pressed', 'true')
|
||||
await expect(lastBadge).toHaveAttribute('aria-pressed', 'false')
|
||||
|
||||
await page.evaluate(() =>
|
||||
window.scrollTo(0, document.documentElement.scrollHeight)
|
||||
)
|
||||
|
||||
await expect(lastBadge).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
test('activates the last badge when page mounts already at the bottom via trailing hash', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(`/privacy-policy${LAST_SECTION_HASH}`)
|
||||
|
||||
const sidebarNav = page.getByRole('navigation', {
|
||||
name: 'Category filter'
|
||||
})
|
||||
const lastBadge = sidebarNav.getByRole('button').last()
|
||||
|
||||
await expect(lastBadge).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -1,71 +1,27 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { demos, getNextDemo } from '../src/config/demos'
|
||||
import { t } from '../src/i18n/translations'
|
||||
|
||||
const escapeRegExp = (value: string): string =>
|
||||
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
test.describe('Demo pages @smoke', () => {
|
||||
for (const demo of demos) {
|
||||
const nextDemo = getNextDemo(demo.slug)
|
||||
test('demo detail page renders hero and embed', async ({ page }) => {
|
||||
await page.goto('/demos/image-to-video')
|
||||
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText(
|
||||
'Create a Video from an Image'
|
||||
)
|
||||
const iframe = page.locator('iframe[title*="Interactive demo"]')
|
||||
await expect(iframe).toBeAttached()
|
||||
})
|
||||
|
||||
test(`/demos/${demo.slug} renders hero, embed, transcript, and next-demo nav`, async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(`/demos/${demo.slug}`)
|
||||
test('demo detail page has transcript section', async ({ page }) => {
|
||||
await page.goto('/demos/image-to-video')
|
||||
await expect(
|
||||
page.getByRole('button', { name: /demo transcript/i })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
const heading = page.getByRole('heading', { level: 1 })
|
||||
await expect(heading).toBeVisible()
|
||||
await expect(heading).toContainText(t(demo.title, 'en'))
|
||||
|
||||
const ogImage = page.locator('head meta[property="og:image"]')
|
||||
await expect(ogImage).toHaveAttribute(
|
||||
'content',
|
||||
new RegExp(`${escapeRegExp(demo.slug)}-og\\.png`)
|
||||
)
|
||||
|
||||
const iframe = page.locator(
|
||||
`iframe[title*="${t('demos.embed.label', 'en')}"]`
|
||||
)
|
||||
await expect(iframe).toBeAttached()
|
||||
await expect(iframe).toHaveAttribute(
|
||||
'src',
|
||||
new RegExp(escapeRegExp(demo.arcadeId))
|
||||
)
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: /demo transcript/i })
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.getByText(t(nextDemo.title, 'en')).first()
|
||||
).toBeVisible()
|
||||
const nextThumb = page.locator(`img[src="${nextDemo.thumbnail}"]`).first()
|
||||
await expect(nextThumb).toBeAttached()
|
||||
await expect(nextThumb).toBeVisible()
|
||||
const naturalWidth = await nextThumb.evaluate(
|
||||
(img) => (img as HTMLImageElement).naturalWidth
|
||||
)
|
||||
expect(naturalWidth).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
test(`/zh-CN/demos/${demo.slug} renders localized content`, async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(`/zh-CN/demos/${demo.slug}`)
|
||||
|
||||
await expect(page).toHaveURL(/\/zh-CN\/demos\//)
|
||||
|
||||
const heading = page.getByRole('heading', { level: 1 })
|
||||
await expect(heading).toContainText(t(demo.title, 'zh-CN'))
|
||||
await expect(heading).toContainText(/[\u4E00-\u9FFF]/)
|
||||
|
||||
await expect(
|
||||
page.getByText(t(nextDemo.title, 'zh-CN')).first()
|
||||
).toBeVisible()
|
||||
})
|
||||
}
|
||||
test('demo detail page has next demo navigation', async ({ page }) => {
|
||||
await page.goto('/demos/image-to-video')
|
||||
await expect(page.getByText(/what's next/i)).toBeVisible()
|
||||
})
|
||||
|
||||
test('demo library page renders', async ({ page }) => {
|
||||
await page.goto('/demos')
|
||||
@@ -76,4 +32,13 @@ test.describe('Demo pages @smoke', () => {
|
||||
const response = await page.goto('/demos/nonexistent')
|
||||
expect(response?.status()).toBe(404)
|
||||
})
|
||||
|
||||
test('zh-CN demo page renders localized content', async ({ page }) => {
|
||||
await page.goto('/zh-CN/demos/image-to-video')
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText(
|
||||
'从图片创建视频'
|
||||
)
|
||||
const nextDemoLink = page.locator('a[href*="/zh-CN/demos/"]').first()
|
||||
await expect(nextDemoLink).toBeAttached()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
@@ -48,105 +47,4 @@ test.describe('Mobile layout @mobile', () => {
|
||||
const mobileContainer = page.getByTestId('social-proof-mobile')
|
||||
await expect(mobileContainer).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('SocialProofBar seamless marquee', () => {
|
||||
test.use({ contextOptions: { reducedMotion: 'no-preference' } })
|
||||
|
||||
test('mobile forward marquee loops seamlessly', async ({ page }) => {
|
||||
const geometry = await measureMarqueeLoopGeometry(
|
||||
page,
|
||||
'[data-testid="social-proof-mobile"] .animate-marquee'
|
||||
)
|
||||
expectSeamlessForwardLoop(geometry)
|
||||
})
|
||||
|
||||
test('mobile reverse marquee loops seamlessly', async ({ page }) => {
|
||||
const geometry = await measureMarqueeLoopGeometry(
|
||||
page,
|
||||
'[data-testid="social-proof-mobile"] .animate-marquee-reverse'
|
||||
)
|
||||
expectSeamlessReverseLoop(geometry)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Desktop SocialProofBar @smoke', () => {
|
||||
test.use({ contextOptions: { reducedMotion: 'no-preference' } })
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
})
|
||||
|
||||
test('desktop marquee loops seamlessly', async ({ page }) => {
|
||||
const geometry = await measureMarqueeLoopGeometry(
|
||||
page,
|
||||
'[data-testid="social-proof-desktop"] .animate-marquee'
|
||||
)
|
||||
expectSeamlessForwardLoop(geometry)
|
||||
})
|
||||
})
|
||||
|
||||
type MarqueeGeometry = {
|
||||
copyWidths: number[]
|
||||
startPositions: number[]
|
||||
endPositions: number[]
|
||||
}
|
||||
|
||||
async function measureMarqueeLoopGeometry(
|
||||
page: Page,
|
||||
selector: string
|
||||
): Promise<MarqueeGeometry> {
|
||||
await page.locator(selector).first().waitFor()
|
||||
return page.evaluate((sel) => {
|
||||
const tracks = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(sel)
|
||||
).slice(0, 2)
|
||||
const firstAnimation = tracks[0]?.getAnimations()[0]
|
||||
if (!firstAnimation) {
|
||||
throw new Error(`No CSS animation found on ${sel}`)
|
||||
}
|
||||
const duration = firstAnimation.effect?.getTiming().duration
|
||||
if (typeof duration !== 'number' || duration <= 1) {
|
||||
throw new Error(
|
||||
`Animation on ${sel} has unusable duration: ${String(duration)}`
|
||||
)
|
||||
}
|
||||
const setAllTimes = (time: number) => {
|
||||
for (const track of tracks) {
|
||||
for (const anim of track.getAnimations()) {
|
||||
anim.currentTime = time
|
||||
}
|
||||
}
|
||||
void document.body.offsetWidth
|
||||
}
|
||||
const readX = () => tracks.map((track) => track.getBoundingClientRect().x)
|
||||
setAllTimes(0)
|
||||
const startPositions = readX()
|
||||
const copyWidths = tracks.map(
|
||||
(track) => track.getBoundingClientRect().width
|
||||
)
|
||||
setAllTimes(duration - 0.1)
|
||||
const endPositions = readX()
|
||||
return { copyWidths, startPositions, endPositions }
|
||||
}, selector)
|
||||
}
|
||||
|
||||
function expectTwoMatchingCopies(geometry: MarqueeGeometry) {
|
||||
const { copyWidths } = geometry
|
||||
expect(copyWidths.length, 'expected two duplicate marquee tracks').toBe(2)
|
||||
expect(copyWidths[0]).toBeGreaterThan(0)
|
||||
expect(copyWidths[1]).toBeCloseTo(copyWidths[0], 0)
|
||||
}
|
||||
|
||||
function expectSeamlessForwardLoop(geometry: MarqueeGeometry) {
|
||||
expectTwoMatchingCopies(geometry)
|
||||
// Copy 2 ends the cycle exactly where copy 1 started, so the restart
|
||||
// (when copy 1 jumps back to its start position) is visually indistinguishable.
|
||||
expect(geometry.endPositions[1]).toBeCloseTo(geometry.startPositions[0], 0)
|
||||
}
|
||||
|
||||
function expectSeamlessReverseLoop(geometry: MarqueeGeometry) {
|
||||
expectTwoMatchingCopies(geometry)
|
||||
// Reverse marquee: copy 1 ends the cycle where copy 2 started.
|
||||
expect(geometry.endPositions[0]).toBeCloseTo(geometry.startPositions[1], 0)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 69 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 69 B |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 69 B |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 69 B |
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener, useTemplateRefsList } from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { Department } from '../../data/roles'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { prefersReducedMotion } from '../../composables/useReducedMotion'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { scrollTo } from '../../scripts/smoothScroll'
|
||||
import CategoryNav from '../common/CategoryNav.vue'
|
||||
import SectionLabel from '../common/SectionLabel.vue'
|
||||
|
||||
@@ -16,72 +13,24 @@ const { locale = 'en', departments = [] } = defineProps<{
|
||||
departments?: readonly Department[]
|
||||
}>()
|
||||
|
||||
const activeCategory = ref('all')
|
||||
|
||||
const visibleDepartments = computed(() =>
|
||||
departments.filter((d) => d.roles.length > 0)
|
||||
)
|
||||
|
||||
const categories = computed(() =>
|
||||
visibleDepartments.value.map((d) => ({ label: d.name, value: d.key }))
|
||||
const categories = computed(() => [
|
||||
{ label: 'ALL', value: 'all' },
|
||||
...visibleDepartments.value.map((d) => ({ label: d.name, value: d.key }))
|
||||
])
|
||||
|
||||
const filteredDepartments = computed(() =>
|
||||
activeCategory.value === 'all'
|
||||
? visibleDepartments.value
|
||||
: visibleDepartments.value.filter((d) => d.key === activeCategory.value)
|
||||
)
|
||||
|
||||
const hasRoles = computed(() => visibleDepartments.value.length > 0)
|
||||
|
||||
const activeCategory = ref('')
|
||||
|
||||
const sectionRefs = useTemplateRefsList<HTMLElement>()
|
||||
|
||||
let isScrolling = false
|
||||
let pendingFrame = 0
|
||||
|
||||
const HEADER_OFFSET = -144
|
||||
const ACTIVATION_OFFSET = 300
|
||||
|
||||
const deptElementId = (key: string) => `careers-dept-${key}`
|
||||
|
||||
function pickActiveSection() {
|
||||
pendingFrame = 0
|
||||
if (isScrolling) return
|
||||
const sections = sectionRefs.value as HTMLElement[]
|
||||
if (sections.length === 0) return
|
||||
|
||||
let active = sections[0]
|
||||
for (const el of sections) {
|
||||
if (el.getBoundingClientRect().top - ACTIVATION_OFFSET <= 0) {
|
||||
active = el
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
activeCategory.value = active.id.replace(/^careers-dept-/, '')
|
||||
}
|
||||
|
||||
function scheduleUpdate() {
|
||||
if (pendingFrame !== 0) return
|
||||
pendingFrame = requestAnimationFrame(pickActiveSection)
|
||||
}
|
||||
|
||||
onMounted(pickActiveSection)
|
||||
useEventListener('scroll', scheduleUpdate, { passive: true })
|
||||
useEventListener('resize', scheduleUpdate, { passive: true })
|
||||
|
||||
function scrollToDepartment(deptKey: string) {
|
||||
activeCategory.value = deptKey
|
||||
isScrolling = true
|
||||
const el = document.getElementById(deptElementId(deptKey))
|
||||
if (!el) {
|
||||
isScrolling = false
|
||||
return
|
||||
}
|
||||
scrollTo(el, {
|
||||
offset: HEADER_OFFSET,
|
||||
duration: 0.8,
|
||||
immediate: prefersReducedMotion(),
|
||||
onComplete: () => {
|
||||
isScrolling = false
|
||||
pickActiveSection()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -99,10 +48,9 @@ function scrollToDepartment(deptKey: string) {
|
||||
</h2>
|
||||
<CategoryNav
|
||||
v-if="hasRoles"
|
||||
v-model="activeCategory"
|
||||
:categories="categories"
|
||||
:model-value="activeCategory"
|
||||
class="mt-4"
|
||||
@update:model-value="scrollToDepartment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,11 +65,9 @@ function scrollToDepartment(deptKey: string) {
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-for="dept in visibleDepartments"
|
||||
:id="deptElementId(dept.key)"
|
||||
:ref="sectionRefs.set"
|
||||
v-for="dept in filteredDepartments"
|
||||
:key="dept.key"
|
||||
class="mb-12 scroll-mt-24 last:mb-0 md:scroll-mt-36"
|
||||
class="mb-12 last:mb-0"
|
||||
>
|
||||
<SectionLabel>
|
||||
{{ dept.name }}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import {
|
||||
useEventListener,
|
||||
useIntersectionObserver,
|
||||
useTemplateRefsList
|
||||
} from '@vueuse/core'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useIntersectionObserver, useTemplateRefsList } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { Locale, TranslationKey } from '../../i18n/translations'
|
||||
|
||||
@@ -44,25 +40,13 @@ const activeSection = ref(sections[0]?.id ?? '')
|
||||
|
||||
const sectionRefs = useTemplateRefsList<HTMLElement>()
|
||||
let isScrolling = false
|
||||
let scrollSafetyTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const HEADER_OFFSET = -144
|
||||
const BOTTOM_THRESHOLD_PX = 4
|
||||
const SCROLL_SAFETY_MS = 1500
|
||||
|
||||
function clearScrollLock() {
|
||||
isScrolling = false
|
||||
if (scrollSafetyTimer !== undefined) {
|
||||
clearTimeout(scrollSafetyTimer)
|
||||
scrollSafetyTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
useIntersectionObserver(
|
||||
sectionRefs,
|
||||
(entries) => {
|
||||
if (isScrolling) return
|
||||
if (isAtBottom()) return
|
||||
let best: IntersectionObserverEntry | null = null
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue
|
||||
@@ -74,39 +58,22 @@ useIntersectionObserver(
|
||||
{ rootMargin: '-20% 0px -60% 0px' }
|
||||
)
|
||||
|
||||
function isAtBottom(): boolean {
|
||||
const scrollBottom = window.scrollY + window.innerHeight
|
||||
return (
|
||||
scrollBottom >= document.documentElement.scrollHeight - BOTTOM_THRESHOLD_PX
|
||||
)
|
||||
}
|
||||
|
||||
function activateLastIfAtBottom() {
|
||||
if (isScrolling) return
|
||||
if (!isAtBottom()) return
|
||||
const lastId = sections[sections.length - 1]?.id
|
||||
if (lastId) activeSection.value = lastId
|
||||
}
|
||||
|
||||
onMounted(activateLastIfAtBottom)
|
||||
useEventListener('scroll', activateLastIfAtBottom, { passive: true })
|
||||
|
||||
function scrollToSection(id: string) {
|
||||
activeSection.value = id
|
||||
clearScrollLock()
|
||||
isScrolling = true
|
||||
scrollSafetyTimer = setTimeout(clearScrollLock, SCROLL_SAFETY_MS)
|
||||
const el = document.getElementById(id)
|
||||
if (el) {
|
||||
scrollTo(el, {
|
||||
offset: HEADER_OFFSET,
|
||||
duration: 0.8,
|
||||
immediate: prefersReducedMotion(),
|
||||
onComplete: clearScrollLock
|
||||
onComplete: () => {
|
||||
isScrolling = false
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
clearScrollLock()
|
||||
isScrolling = false
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,28 +14,23 @@ const logos = [
|
||||
'Ubisoft'
|
||||
]
|
||||
|
||||
const mobileRow1Logos = logos.slice(0, 6)
|
||||
const mobileRow2Logos = logos.slice(6)
|
||||
const desktopLogos = Array.from({ length: 4 }, () => logos).flat()
|
||||
const row1 = logos.slice(0, 6)
|
||||
const mobileRow1 = [...row1, ...row1]
|
||||
const row2 = logos.slice(6)
|
||||
const mobileRow2 = [...row2, ...row2]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="overflow-hidden py-12">
|
||||
<!-- Single row on desktop -->
|
||||
<div data-testid="social-proof-desktop" class="hidden w-max gap-2 md:flex">
|
||||
<div class="animate-marquee hidden items-center gap-2 md:flex">
|
||||
<div
|
||||
v-for="copy in 2"
|
||||
:key="copy"
|
||||
class="animate-marquee flex shrink-0 items-center gap-2"
|
||||
style="--marquee-gap: 0.5rem"
|
||||
:aria-hidden="copy === 2 ? 'true' : undefined"
|
||||
v-for="(logo, i) in desktopLogos"
|
||||
:key="`${logo}-${i}`"
|
||||
class="flex h-20 w-50 shrink-0 items-center justify-center"
|
||||
>
|
||||
<div
|
||||
v-for="logo in logos"
|
||||
:key="logo"
|
||||
class="flex h-20 w-50 shrink-0 items-center justify-center"
|
||||
>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
</div>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,38 +39,22 @@ const mobileRow2Logos = logos.slice(6)
|
||||
data-testid="social-proof-mobile"
|
||||
class="flex flex-col gap-8 md:hidden"
|
||||
>
|
||||
<div class="flex w-max gap-8">
|
||||
<div class="animate-marquee flex items-center gap-8">
|
||||
<div
|
||||
v-for="copy in 2"
|
||||
:key="copy"
|
||||
class="animate-marquee flex shrink-0 items-center gap-8"
|
||||
style="--marquee-gap: 2rem"
|
||||
:aria-hidden="copy === 2 ? 'true' : undefined"
|
||||
v-for="(logo, i) in mobileRow1"
|
||||
:key="`${logo}-${i}`"
|
||||
class="flex h-14 w-40 shrink-0 items-center justify-center"
|
||||
>
|
||||
<div
|
||||
v-for="logo in mobileRow1Logos"
|
||||
:key="logo"
|
||||
class="flex h-14 w-40 shrink-0 items-center justify-center"
|
||||
>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
</div>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-max gap-8">
|
||||
<div class="animate-marquee-reverse flex items-center gap-8">
|
||||
<div
|
||||
v-for="copy in 2"
|
||||
:key="copy"
|
||||
class="animate-marquee-reverse flex shrink-0 items-center gap-8"
|
||||
style="--marquee-gap: 2rem"
|
||||
:aria-hidden="copy === 2 ? 'true' : undefined"
|
||||
v-for="(logo, i) in mobileRow2"
|
||||
:key="`${logo}-${i}`"
|
||||
class="flex h-14 w-40 shrink-0 items-center justify-center"
|
||||
>
|
||||
<div
|
||||
v-for="logo in mobileRow2Logos"
|
||||
:key="logo"
|
||||
class="flex h-14 w-40 shrink-0 items-center justify-center"
|
||||
>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
</div>
|
||||
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,10 @@ import { t } from '../../i18n/translations'
|
||||
const {
|
||||
arcadeId,
|
||||
title,
|
||||
aspectRatio = 16 / 9,
|
||||
locale = 'en'
|
||||
} = defineProps<{
|
||||
arcadeId: string
|
||||
title: string
|
||||
aspectRatio?: number
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
@@ -26,8 +24,7 @@ const loaded = ref(false)
|
||||
:aria-label="t('demos.embed.label', locale)"
|
||||
>
|
||||
<div
|
||||
class="relative mx-auto max-w-6xl overflow-hidden rounded-4xl border border-white/10"
|
||||
:style="{ aspectRatio }"
|
||||
class="relative mx-auto aspect-video max-w-6xl overflow-hidden rounded-4xl border border-white/10"
|
||||
>
|
||||
<div
|
||||
v-if="!loaded"
|
||||
|
||||
@@ -276,6 +276,29 @@ onUnmounted(() => {
|
||||
fill="#211927"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<!-- Left-edge fade -->
|
||||
<rect
|
||||
x="300"
|
||||
y="150"
|
||||
width="250"
|
||||
height="900"
|
||||
fill="url(#localHeroFadeLeft)"
|
||||
/>
|
||||
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="localHeroFadeLeft"
|
||||
x1="550"
|
||||
y1="600"
|
||||
x2="300"
|
||||
y2="600"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#211927" stop-opacity="0" />
|
||||
<stop offset="1" stop-color="#211927" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,14 +15,6 @@ interface Demo {
|
||||
readonly transcript?: TranslationKey
|
||||
readonly publishedDate: string
|
||||
readonly modifiedDate: string
|
||||
/**
|
||||
* Width / height of the Arcade demo's source recording (e.g. 1.93 for a
|
||||
* landscape screencast). Sizes the embed container to match so rounded
|
||||
* corners hug the content instead of empty letterbox space. Source from
|
||||
* Arcade's `_serializablePublicFlow.aspectRatio` (which is height/width —
|
||||
* invert it). Defaults to 16/9 if omitted.
|
||||
*/
|
||||
readonly aspectRatio?: number
|
||||
}
|
||||
|
||||
export const demos: readonly Demo[] = [
|
||||
@@ -40,8 +32,7 @@ export const demos: readonly Demo[] = [
|
||||
difficulty: 'beginner',
|
||||
tags: ['templates', 'image', 'video'],
|
||||
publishedDate: '2026-04-19',
|
||||
modifiedDate: '2026-04-19',
|
||||
aspectRatio: 1.931
|
||||
modifiedDate: '2026-04-19'
|
||||
},
|
||||
{
|
||||
slug: 'workflow-templates',
|
||||
@@ -57,25 +48,7 @@ export const demos: readonly Demo[] = [
|
||||
difficulty: 'beginner',
|
||||
tags: ['getting-started', 'templates', 'workflow'],
|
||||
publishedDate: '2026-04-19',
|
||||
modifiedDate: '2026-04-19',
|
||||
aspectRatio: 1.931
|
||||
},
|
||||
{
|
||||
slug: 'community-workflows',
|
||||
arcadeId: 'mqZh17oWDuWIyhK0xwEV',
|
||||
category: 'demos.category.gettingStarted',
|
||||
title: 'demos.community-workflows.title',
|
||||
description: 'demos.community-workflows.description',
|
||||
transcript: 'demos.community-workflows.transcript',
|
||||
ogImage: '/images/demos/community-workflows-og.png',
|
||||
thumbnail: '/images/demos/community-workflows-thumb.webp',
|
||||
estimatedTime: 'demos.duration.2min',
|
||||
durationIso: 'PT2M',
|
||||
difficulty: 'beginner',
|
||||
tags: ['getting-started', 'community', 'workflow', 'hub'],
|
||||
publishedDate: '2026-05-04',
|
||||
modifiedDate: '2026-05-04',
|
||||
aspectRatio: 1.931
|
||||
modifiedDate: '2026-04-19'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -3570,20 +3570,6 @@ const translations = {
|
||||
'<ol><li><strong>打开模板浏览器</strong> — 点击 ComfyUI 侧栏中的模板图标。</li><li><strong>浏览分类</strong> — 模板按任务分类:图像生成、视频、放大等。</li><li><strong>预览模板</strong> — 将鼠标悬停在模板上查看预览。</li><li><strong>加载并自定义</strong> — 点击加载模板,然后修改参数。</li></ol>'
|
||||
},
|
||||
|
||||
'demos.community-workflows.title': {
|
||||
en: 'Explore and Use a Community Workflow from the Hub',
|
||||
'zh-CN': '探索并使用社区工作流'
|
||||
},
|
||||
'demos.community-workflows.description': {
|
||||
en: 'Discover how to find and get started with popular community workflows for generative AI projects.',
|
||||
'zh-CN': '了解如何查找并使用流行的社区工作流来构建生成式 AI 项目。'
|
||||
},
|
||||
'demos.community-workflows.transcript': {
|
||||
en: '<ol><li><strong>Open the Workflow Hub</strong> — From the ComfyUI sidebar, navigate to the community Workflow Hub to browse curated and trending workflows shared by the community.</li><li><strong>Browse popular workflows</strong> — Explore featured projects sorted by popularity, recency, and category to find one that matches your goal.</li><li><strong>Preview a workflow</strong> — Click a workflow card to see example outputs, required models, and a description of what it produces.</li><li><strong>Open in ComfyUI</strong> — Use the "Get Started" action to load the selected community workflow directly onto your canvas.</li><li><strong>Run and customize</strong> — Queue the workflow to generate your first result, then tweak prompts, models, and parameters to make it your own.</li></ol>',
|
||||
'zh-CN':
|
||||
'<ol><li><strong>打开工作流中心</strong> — 在 ComfyUI 侧栏中,进入社区工作流中心,浏览社区分享的精选和热门工作流。</li><li><strong>浏览热门工作流</strong> — 按热度、时间和分类浏览精选项目,找到符合需求的工作流。</li><li><strong>预览工作流</strong> — 点击工作流卡片,查看示例输出、所需模型和功能描述。</li><li><strong>在 ComfyUI 中打开</strong> — 使用"开始使用"按钮,将选中的社区工作流直接加载到画布。</li><li><strong>运行并自定义</strong> — 排队执行工作流以生成首个结果,然后调整提示词、模型和参数。</li></ol>'
|
||||
},
|
||||
|
||||
'demos.nav.nextDemo': { en: "What's Next", 'zh-CN': '下一个演示' },
|
||||
'demos.nav.viewDemo': { en: 'View Demo', 'zh-CN': '查看演示' },
|
||||
'demos.nav.allDemos': { en: 'All Demos', 'zh-CN': '所有演示' },
|
||||
|
||||
@@ -121,7 +121,6 @@ const breadcrumbJsonLd = {
|
||||
<ArcadeEmbed
|
||||
arcadeId={demo.arcadeId}
|
||||
title={title}
|
||||
aspectRatio={demo.aspectRatio}
|
||||
client:load
|
||||
/>
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ const breadcrumbJsonLd = {
|
||||
<ArcadeEmbed
|
||||
arcadeId={demo.arcadeId}
|
||||
title={title}
|
||||
aspectRatio={demo.aspectRatio}
|
||||
locale="zh-CN"
|
||||
client:load
|
||||
/>
|
||||
|
||||
@@ -101,13 +101,13 @@
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-100% - var(--marquee-gap, 0px)));
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-reverse {
|
||||
0% {
|
||||
transform: translateX(calc(-100% - var(--marquee-gap, 0px)));
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
@@ -115,15 +115,11 @@
|
||||
}
|
||||
|
||||
@utility animate-marquee {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: marquee 30s linear infinite;
|
||||
}
|
||||
animation: marquee 30s linear infinite;
|
||||
}
|
||||
|
||||
@utility animate-marquee-reverse {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: marquee-reverse 30s linear infinite;
|
||||
}
|
||||
animation: marquee-reverse 30s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes ripple-effect {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 10,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 50],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": ["this-image-does-not-exist-deadbeef.png", "image"]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export class Topbar {
|
||||
}
|
||||
|
||||
getSaveDialog(): Locator {
|
||||
return this.page.getByRole('dialog').getByRole('textbox')
|
||||
return this.page.locator('.p-dialog-content input')
|
||||
}
|
||||
|
||||
saveWorkflow(workflowName: string): Promise<void> {
|
||||
@@ -116,9 +116,9 @@ export class Topbar {
|
||||
|
||||
// Check if a confirmation dialog appeared (e.g., "Overwrite existing file?")
|
||||
// If so, return early to let the test handle the confirmation
|
||||
const confirmationDialog = this.page
|
||||
.getByRole('dialog')
|
||||
.filter({ hasText: 'Overwrite' })
|
||||
const confirmationDialog = this.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
if (await confirmationDialog.isVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -127,7 +127,9 @@ export class BuilderSelectHelper {
|
||||
await popoverTrigger.click()
|
||||
await this.page.getByText('Rename', { exact: true }).click()
|
||||
|
||||
const dialogInput = this.page.getByRole('dialog').getByRole('textbox')
|
||||
const dialogInput = this.page.locator(
|
||||
'.p-dialog-content input[type="text"]'
|
||||
)
|
||||
await dialogInput.fill(newName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
await dialogInput.waitFor({ state: 'hidden' })
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { WebSocketRoute } from '@playwright/test'
|
||||
|
||||
import type {
|
||||
NodeError,
|
||||
NodeProgressState,
|
||||
PromptResponse
|
||||
} from '@/schemas/apiSchema'
|
||||
import type { NodeError, PromptResponse } from '@/schemas/apiSchema'
|
||||
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { createMockJob } from '@e2e/fixtures/helpers/AssetsHelper'
|
||||
@@ -234,16 +230,6 @@ export class ExecutionHelper {
|
||||
)
|
||||
}
|
||||
|
||||
/** Send `progress_state` WS event with per-node execution state. */
|
||||
progressState(jobId: string, nodes: Record<string, NodeProgressState>): void {
|
||||
this.requireWs().send(
|
||||
JSON.stringify({
|
||||
type: 'progress_state',
|
||||
data: { prompt_id: jobId, nodes }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a job by adding it to mock history, sending execution_success,
|
||||
* and triggering a history refresh via a status event.
|
||||
|
||||
@@ -18,7 +18,9 @@ export class NodeOperationsHelper {
|
||||
public readonly promptDialogInput: Locator
|
||||
|
||||
constructor(private comfyPage: ComfyPage) {
|
||||
this.promptDialogInput = this.page.getByRole('dialog').getByRole('textbox')
|
||||
this.promptDialogInput = this.page.locator(
|
||||
'.p-dialog-content input[type="text"]'
|
||||
)
|
||||
}
|
||||
|
||||
private get page() {
|
||||
|
||||
@@ -14,7 +14,6 @@ export class VueNodeFixture {
|
||||
public readonly collapseIcon: Locator
|
||||
public readonly root: Locator
|
||||
public readonly widgets: Locator
|
||||
public readonly imagePreview: Locator
|
||||
|
||||
constructor(private readonly locator: Locator) {
|
||||
this.header = locator.locator('[data-testid^="node-header-"]')
|
||||
@@ -26,7 +25,6 @@ export class VueNodeFixture {
|
||||
this.collapseIcon = this.collapseButton.locator('i')
|
||||
this.root = locator
|
||||
this.widgets = this.locator.locator('.lg-node-widget')
|
||||
this.imagePreview = locator.locator('.image-preview')
|
||||
}
|
||||
|
||||
async getTitle(): Promise<string> {
|
||||
|
||||
@@ -229,9 +229,9 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
// The dialog appearing proves the keybinding was intercepted by the app.
|
||||
await comfyPage.keyboard.press('Control+s')
|
||||
|
||||
// The Save As dialog should appear
|
||||
const saveDialog = comfyPage.page.getByRole('dialog')
|
||||
await expect(saveDialog).toBeVisible()
|
||||
// The Save As dialog should appear (p-dialog overlay)
|
||||
const dialogOverlay = comfyPage.page.locator('.p-dialog-mask')
|
||||
await expect(dialogOverlay).toBeVisible()
|
||||
|
||||
// Dismiss the dialog
|
||||
await comfyPage.keyboard.press('Escape')
|
||||
|
||||
@@ -16,9 +16,9 @@ async function saveAndOpenPublishDialog(
|
||||
workflowName: string
|
||||
): Promise<void> {
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowName)
|
||||
const overwriteDialog = comfyPage.page
|
||||
.getByRole('dialog')
|
||||
.filter({ hasText: 'Overwrite' })
|
||||
const overwriteDialog = comfyPage.page.locator(
|
||||
'.p-dialog:has-text("Overwrite")'
|
||||
)
|
||||
// Bounded wait: point-in-time isVisible() can miss dialogs that open
|
||||
// slightly after saveWorkflow() resolves.
|
||||
try {
|
||||
|
||||
@@ -8,9 +8,6 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
const DEPRECATED_NODE_TYPE = 'ImageBatch'
|
||||
const API_NODE_TYPE = 'FluxProUltraImageNode'
|
||||
|
||||
test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
|
||||
test('Can add badge', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
@@ -144,73 +141,3 @@ test.describe(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
for (const vueEnabled of [false, true] as const) {
|
||||
const renderer = vueEnabled ? 'vue' : 'classic'
|
||||
const tag = vueEnabled
|
||||
? ['@vue-nodes', '@screenshot', '@node']
|
||||
: ['@screenshot', '@node']
|
||||
|
||||
test.describe(`Node lifecycle badge (${renderer})`, { tag }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasInfo', false)
|
||||
})
|
||||
|
||||
for (const mode of [NodeBadgeMode.ShowAll, NodeBadgeMode.None] as const) {
|
||||
test(`renders deprecated node with mode=${mode}`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
|
||||
mode
|
||||
)
|
||||
await comfyPage.nodeOps.clearGraph()
|
||||
await comfyPage.nodeOps.addNode(DEPRECATED_NODE_TYPE, undefined, {
|
||||
x: 100,
|
||||
y: 100
|
||||
})
|
||||
await comfyPage.canvasOps.resetView()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
`node-lifecycle-${mode}-${renderer}.png`
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test.describe(`API pricing badge (${renderer})`, { tag }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Graph.CanvasInfo', false)
|
||||
await comfyPage.page.evaluate((type) => {
|
||||
const registered = window.LiteGraph!.registered_node_types[type] as {
|
||||
nodeData?: { price_badge?: unknown }
|
||||
}
|
||||
if (!registered?.nodeData) throw new Error(`No nodeData for ${type}`)
|
||||
registered.nodeData.price_badge = {
|
||||
engine: 'jsonata',
|
||||
expr: "{'type': 'text', 'text': '99.9 credits/Run'}",
|
||||
depends_on: { widgets: [], inputs: [], input_groups: [] }
|
||||
}
|
||||
}, API_NODE_TYPE)
|
||||
})
|
||||
|
||||
for (const enabled of [true, false] as const) {
|
||||
test(`renders api node with showApiPricing=${enabled}`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeBadge.ShowApiPricing',
|
||||
enabled
|
||||
)
|
||||
await comfyPage.nodeOps.clearGraph()
|
||||
await comfyPage.nodeOps.addNode(API_NODE_TYPE, undefined, {
|
||||
x: 100,
|
||||
y: 100
|
||||
})
|
||||
await comfyPage.canvasOps.resetView()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
`api-pricing-${enabled ? 'on' : 'off'}-${renderer}.png`
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 43 KiB |
@@ -21,8 +21,9 @@ test.describe('Vue Nodes Image Preview', { tag: '@vue-nodes' }, () => {
|
||||
})
|
||||
|
||||
const nodeId = String(loadImageNode.id)
|
||||
const { imagePreview } =
|
||||
await comfyPage.vueNodes.getFixtureByTitle('Load Image')
|
||||
const imagePreview = comfyPage.vueNodes
|
||||
.getNodeLocator(nodeId)
|
||||
.locator('.image-preview')
|
||||
|
||||
await expect(imagePreview).toBeVisible()
|
||||
await expect(imagePreview.locator('img')).toBeVisible({ timeout: 30_000 })
|
||||
@@ -43,25 +44,6 @@ test.describe('Vue Nodes Image Preview', { tag: '@vue-nodes' }, () => {
|
||||
await expect(comfyPage.page.locator('.mask-editor-dialog')).toBeVisible()
|
||||
})
|
||||
|
||||
test('hides mask and download buttons when image is missing', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'widgets/load_image_widget_missing_file'
|
||||
)
|
||||
|
||||
const { imagePreview } =
|
||||
await comfyPage.vueNodes.getFixtureByTitle('Load Image')
|
||||
|
||||
await expect(imagePreview).toBeVisible()
|
||||
await expect(imagePreview.getByTestId('error-loading-image')).toBeVisible()
|
||||
|
||||
await imagePreview.getByRole('region').hover()
|
||||
|
||||
await expect(imagePreview.getByLabel('Edit or mask image')).toHaveCount(0)
|
||||
await expect(imagePreview.getByLabel('Download image')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('shows image context menu options', async ({ comfyPage }) => {
|
||||
const { nodeId } = await loadImageOnNode(comfyPage)
|
||||
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import type { WebSocketRoute } from '@playwright/test'
|
||||
import { mergeTests } from '@playwright/test'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper'
|
||||
import { webSocketFixture } from '@e2e/fixtures/ws'
|
||||
import type {
|
||||
RawJobListItem,
|
||||
zJobsListResponse
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
type JobsListResponse = z.infer<typeof zJobsListResponse>
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
const KSAMPLER_NODE = '3'
|
||||
const EXECUTING_CLASS = /outline-node-stroke-executing/
|
||||
|
||||
const QUEUE_ROUTE = /\/api\/jobs\?[^/]*status=in_progress,pending/
|
||||
const HISTORY_ROUTE = /\/api\/jobs\?[^/]*status=completed/
|
||||
|
||||
function jobsResponse(jobs: RawJobListItem[]): JobsListResponse {
|
||||
return {
|
||||
jobs,
|
||||
pagination: { offset: 0, limit: 200, total: jobs.length, has_more: false }
|
||||
}
|
||||
}
|
||||
|
||||
async function mockJobsRoute(
|
||||
comfyPage: ComfyPage,
|
||||
pattern: RegExp,
|
||||
body: string,
|
||||
status: number = 200
|
||||
): Promise<() => number> {
|
||||
let count = 0
|
||||
await comfyPage.page.route(pattern, async (route) => {
|
||||
count += 1
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: 'application/json',
|
||||
body
|
||||
})
|
||||
})
|
||||
return () => count
|
||||
}
|
||||
|
||||
const emptyJobsBody = JSON.stringify(jobsResponse([]))
|
||||
|
||||
type Scenario = {
|
||||
name: string
|
||||
/** Built per-test so it can incorporate the runtime-assigned jobId. */
|
||||
queueBody: (jobId: string) => string
|
||||
/** Whether the active job state should still be reflected after reconnect. */
|
||||
expectsActiveAfter: boolean
|
||||
}
|
||||
|
||||
const scenarios: Scenario[] = [
|
||||
{
|
||||
name: 'clears stale active job when queue is empty after reconnect',
|
||||
queueBody: () => emptyJobsBody,
|
||||
expectsActiveAfter: false
|
||||
},
|
||||
{
|
||||
name: 'preserves active job when the job is still in the queue',
|
||||
queueBody: (jobId) =>
|
||||
JSON.stringify(
|
||||
jobsResponse([
|
||||
{ id: jobId, status: 'in_progress', create_time: Date.now() }
|
||||
])
|
||||
),
|
||||
expectsActiveAfter: true
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Stub the queue/history endpoints per `scenario`, close the WS, and wait
|
||||
* for the auto-reconnect to issue a fresh queue fetch.
|
||||
*/
|
||||
async function triggerReconnect(
|
||||
comfyPage: ComfyPage,
|
||||
ws: WebSocketRoute,
|
||||
scenario: Scenario,
|
||||
jobId: string
|
||||
): Promise<void> {
|
||||
await mockJobsRoute(comfyPage, HISTORY_ROUTE, emptyJobsBody)
|
||||
const queueFetches = await mockJobsRoute(
|
||||
comfyPage,
|
||||
QUEUE_ROUTE,
|
||||
scenario.queueBody(jobId)
|
||||
)
|
||||
const fetchesBeforeClose = queueFetches()
|
||||
await ws.close()
|
||||
await expect.poll(queueFetches).toBeGreaterThan(fetchesBeforeClose)
|
||||
}
|
||||
|
||||
test.describe('WebSocket reconnect with stale job', { tag: '@ui' }, () => {
|
||||
test.describe('app mode skeleton', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([[KSAMPLER_NODE, 'seed']])
|
||||
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
|
||||
})
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
test(scenario.name, async ({ comfyPage, getWebSocket }) => {
|
||||
const ws = await getWebSocket()
|
||||
const exec = new ExecutionHelper(comfyPage, ws)
|
||||
|
||||
const jobId = await exec.run()
|
||||
exec.executionStart(jobId)
|
||||
|
||||
// Skeleton visibility is the deterministic sync point: it appears
|
||||
// once both `storeJob` (HTTP) and `executionStart` (WS) have been
|
||||
// processed, regardless of arrival order.
|
||||
const firstSkeleton = comfyPage.appMode.outputHistory.skeletons.first()
|
||||
await expect(firstSkeleton).toBeVisible()
|
||||
|
||||
await triggerReconnect(comfyPage, ws, scenario, jobId)
|
||||
|
||||
if (scenario.expectsActiveAfter) {
|
||||
await expect(firstSkeleton).toBeVisible()
|
||||
} else {
|
||||
await expect(comfyPage.appMode.outputHistory.skeletons).toHaveCount(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('preserves active job when the queue endpoint fails on reconnect', async ({
|
||||
comfyPage,
|
||||
getWebSocket
|
||||
}) => {
|
||||
const ws = await getWebSocket()
|
||||
const exec = new ExecutionHelper(comfyPage, ws)
|
||||
|
||||
const jobId = await exec.run()
|
||||
exec.executionStart(jobId)
|
||||
|
||||
const firstSkeleton = comfyPage.appMode.outputHistory.skeletons.first()
|
||||
await expect(firstSkeleton).toBeVisible()
|
||||
|
||||
await mockJobsRoute(comfyPage, HISTORY_ROUTE, emptyJobsBody)
|
||||
|
||||
// Prime queueStore.runningTasks with the active job — a WS status
|
||||
// event drives GraphView.onStatus -> queueStore.update().
|
||||
const primer = await mockJobsRoute(
|
||||
comfyPage,
|
||||
QUEUE_ROUTE,
|
||||
JSON.stringify(
|
||||
jobsResponse([
|
||||
{ id: jobId, status: 'in_progress', create_time: Date.now() }
|
||||
])
|
||||
)
|
||||
)
|
||||
exec.status(1)
|
||||
await expect.poll(primer).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Swap to a failing handler so the reconnect-driven fetch 500s.
|
||||
// The fix should preserve runningTasks from the priming call rather
|
||||
// than overwriting it with empty/error state.
|
||||
await comfyPage.page.unroute(QUEUE_ROUTE)
|
||||
const failed = await mockJobsRoute(comfyPage, QUEUE_ROUTE, '{}', 500)
|
||||
|
||||
const before = failed()
|
||||
await ws.close()
|
||||
await expect.poll(failed).toBeGreaterThan(before)
|
||||
|
||||
await expect(firstSkeleton).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('vue node executing class', { tag: '@vue-nodes' }, () => {
|
||||
for (const scenario of scenarios) {
|
||||
test(scenario.name, async ({ comfyPage, getWebSocket }) => {
|
||||
const ws = await getWebSocket()
|
||||
const exec = new ExecutionHelper(comfyPage, ws)
|
||||
|
||||
// The executing outline lives on the outer `[data-node-id]`
|
||||
// container, not the inner wrapper.
|
||||
const ksamplerNode = comfyPage.vueNodes.getNodeLocator(KSAMPLER_NODE)
|
||||
await expect(ksamplerNode).toBeVisible()
|
||||
|
||||
const jobId = await exec.run()
|
||||
exec.executionStart(jobId)
|
||||
exec.progressState(jobId, {
|
||||
[KSAMPLER_NODE]: {
|
||||
value: 0,
|
||||
max: 1,
|
||||
state: 'running',
|
||||
node_id: KSAMPLER_NODE,
|
||||
display_node_id: KSAMPLER_NODE,
|
||||
prompt_id: jobId
|
||||
}
|
||||
})
|
||||
|
||||
await expect(ksamplerNode).toHaveClass(EXECUTING_CLASS)
|
||||
|
||||
await triggerReconnect(comfyPage, ws, scenario, jobId)
|
||||
|
||||
if (scenario.expectsActiveAfter) {
|
||||
await expect(ksamplerNode).toHaveClass(EXECUTING_CLASS)
|
||||
} else {
|
||||
await expect(ksamplerNode).not.toHaveClass(EXECUTING_CLASS)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,108 +0,0 @@
|
||||
# 10. Deprecate Node-Level Serialization Control
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The v2 extension API initially included `node.on('beforeSerialize', handler)` as a migration path from v1 patterns like `node.onSerialize` and `nodeType.prototype.serialize` patching. This allowed extensions to:
|
||||
|
||||
1. **Append extra fields** to the serialized node object
|
||||
2. **Transform the entire serialized object** via a replace function
|
||||
|
||||
However, during design review (PR #12142), we questioned whether node-level serialization control is the right abstraction:
|
||||
|
||||
### The Problem
|
||||
|
||||
Node-level serialization control is fundamentally **wrong-layered**:
|
||||
|
||||
- **Extension state should live in widgets**, not as arbitrary fields on the node
|
||||
- Widget-level `beforeSerialize` already handles all legitimate use cases
|
||||
- Node-level hooks encourage storing extension state in ad-hoc `node.properties` or custom fields, which:
|
||||
- Breaks the clean separation between framework concerns and extension concerns
|
||||
- Creates hidden dependencies between serialization format and extension behavior
|
||||
- Makes migration and format evolution harder
|
||||
|
||||
### v1 Usage Analysis
|
||||
|
||||
Touch-point audit of `nodeType.prototype.serialize` and `node.onSerialize` patterns in the wild:
|
||||
|
||||
| Use Case | Proper v2 Alternative |
|
||||
| --------------------------- | --------------------------------------------------- |
|
||||
| Store extension state | Use widget values with `beforeSerialize` |
|
||||
| Persist per-instance config | Use `widget.setOption()` → `widget_options` sidecar |
|
||||
| Add metadata for export | Use a dedicated extension state widget |
|
||||
| Transform output format | Framework concern, not extension concern |
|
||||
|
||||
No use case requires node-level control that can't be better served by widget-level APIs.
|
||||
|
||||
## Decision
|
||||
|
||||
**Deprecate `node.on('beforeSerialize')`** — mark as `@deprecated` with clear guidance pointing to widget-level alternatives. Remove in v1.0.
|
||||
|
||||
Widget-level serialization control (`widget.on('beforeSerialize')`) remains fully supported as the correct abstraction.
|
||||
|
||||
### Migration Path
|
||||
|
||||
Extensions currently using `node.on('beforeSerialize')` should:
|
||||
|
||||
1. **Store state in widgets** instead of arbitrary node fields
|
||||
2. **Use `widget.on('beforeSerialize')`** to control serialization per-widget
|
||||
3. **Use `widget.setOption()`** for per-instance configuration
|
||||
|
||||
Example migration:
|
||||
|
||||
```ts
|
||||
// BEFORE (v1 / deprecated v2)
|
||||
node.on('beforeSerialize', (e) => {
|
||||
e.data['my_extension_state'] = computeState()
|
||||
})
|
||||
|
||||
// AFTER (recommended v2)
|
||||
const stateWidget = node.addWidget('STRING', '_my_state', '', {
|
||||
hidden: true,
|
||||
serialize: true
|
||||
})
|
||||
stateWidget.on('beforeSerialize', (e) => {
|
||||
e.setSerializedValue(JSON.stringify(computeState()))
|
||||
})
|
||||
```
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
1. Add `@deprecated` tag to `node.on('beforeSerialize')` with migration guidance
|
||||
2. Add console.warn when the deprecated event is used (dev mode only)
|
||||
3. Update documentation to recommend widget-level patterns
|
||||
4. Remove `NodeBeforeSerializeEvent` type and handler in v1.0
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Cleaner architecture**: Extension state flows through widgets, the designed data channel
|
||||
- **Better debuggability**: Widget values are visible in workflow JSON at predictable locations
|
||||
- **Easier migration**: Future format changes only need to consider widget serialization
|
||||
- **Reduced API surface**: One less event type to maintain and document
|
||||
|
||||
### Negative
|
||||
|
||||
- **Migration burden**: Extensions using node-level serialization must refactor
|
||||
- **Potential edge cases**: Some exotic use cases may require workarounds
|
||||
|
||||
### Risk Mitigation
|
||||
|
||||
- Deprecation warning gives extension authors runway to migrate
|
||||
- Widget-level APIs are already more capable than node-level alternatives
|
||||
- The `@deprecated` tag and docs provide clear migration path
|
||||
|
||||
## Notes
|
||||
|
||||
This decision was made during design review of PR #12142 (ext-api foundation). See `design-review-12142.md` Topic 11 for the full discussion thread.
|
||||
|
||||
Related decisions:
|
||||
|
||||
- Widget-level `beforeSerialize` remains the primary extension serialization hook
|
||||
- `setSerializeEnabled()` remains for simple static opt-out cases
|
||||
@@ -1,111 +0,0 @@
|
||||
# 11. Immutability Enforcement via Fresh Copies
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The extension API exposes collection-returning methods like `widgets()`, `inputs()`, `outputs()`, and object-returning methods like `getProperties()`. These methods need immutability guarantees to prevent extensions from accidentally or intentionally mutating internal state.
|
||||
|
||||
### The Problem
|
||||
|
||||
Without runtime immutability enforcement:
|
||||
|
||||
- Extensions could push items into `widgets()` array, corrupting internal state
|
||||
- Mutations to returned objects would silently affect internal data
|
||||
- Debugging would be difficult — state corruption could surface far from the mutation site
|
||||
- Internal framework code might inadvertently rely on returned arrays being stable
|
||||
|
||||
TypeScript's `readonly` modifier and JSDoc annotations provide compile-time protection, but:
|
||||
|
||||
- JavaScript consumers have no protection
|
||||
- Type assertions can bypass readonly
|
||||
- Agent-generated code may not respect type hints
|
||||
|
||||
### Options Considered
|
||||
|
||||
| Option | Pros | Cons |
|
||||
| ------------------------ | --------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| **1. `Object.freeze()`** | Runtime immutability, throws on mutation | Performance overhead, nested objects need deep freeze |
|
||||
| **2. Return fresh copy** | Simple, functional style, no mutation affects source | Slight memory overhead, multiple calls = multiple arrays |
|
||||
| **3. Proxy wrapper** | Helpful error messages, can intercept specific operations | Complexity, performance overhead, harder to debug |
|
||||
| **4. TypeScript only** | Zero runtime cost | No protection for JS consumers, can be bypassed |
|
||||
| **5. Private fields** | True encapsulation | Blocks read access too, not suitable for APIs |
|
||||
|
||||
## Decision
|
||||
|
||||
**Return fresh copies** (Option 2) for all collection-returning and object-returning methods in the extension API.
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```ts
|
||||
// CORRECT: Return fresh copy
|
||||
widgets(): readonly WidgetHandle[] {
|
||||
const container = world.getComponent(nodeId, WidgetComponentContainer)
|
||||
return (container?.widgetIds ?? []).map(createWidgetHandle)
|
||||
// Each call creates new array — mutations don't affect internal state
|
||||
}
|
||||
|
||||
getProperties(): Record<string, unknown> {
|
||||
return { ...world.getComponent(nodeId, NodeTypeKey)?.properties }
|
||||
// Shallow copy — mutations don't affect source
|
||||
}
|
||||
```
|
||||
|
||||
### Scope
|
||||
|
||||
Apply this pattern to:
|
||||
|
||||
- `NodeHandle.widgets()` — returns fresh `WidgetHandle[]`
|
||||
- `NodeHandle.inputs()` — returns fresh `SlotInfo[]`
|
||||
- `NodeHandle.outputs()` — returns fresh `SlotInfo[]`
|
||||
- `NodeHandle.getProperties()` — returns fresh `Record<string, unknown>`
|
||||
- `WidgetHandle` methods that return objects (if any)
|
||||
- Any future collection/object-returning methods
|
||||
|
||||
### Internal Callers
|
||||
|
||||
Framework-internal code must also use mutation APIs rather than mutating returned collections:
|
||||
|
||||
```ts
|
||||
// WRONG: Mutating returned array
|
||||
const widgets = node.widgets()
|
||||
widgets.push(newWidget) // No effect on node!
|
||||
|
||||
// CORRECT: Use mutation API
|
||||
node.addWidget(type, name, value, options)
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **True immutability**: Mutations to returned data never affect internal state
|
||||
- **Predictable behavior**: Each call returns fresh data reflecting current state
|
||||
- **Simple mental model**: "This is your copy, do what you want with it"
|
||||
- **JavaScript-safe**: Works regardless of TypeScript types
|
||||
|
||||
### Negative
|
||||
|
||||
- **Memory overhead**: Multiple calls create multiple arrays (usually negligible)
|
||||
- **No mutation detection**: Extensions silently get isolated copies, won't know their mutations are ignored
|
||||
- **Fresh reference each call**: Cannot use `===` to detect changes (use deep comparison or events)
|
||||
|
||||
### Mitigations
|
||||
|
||||
- Document that returned collections are snapshots
|
||||
- Use events (`valueChange`, `propertyChange`) to observe changes
|
||||
- The memory overhead is negligible for typical widget/slot counts
|
||||
|
||||
## Notes
|
||||
|
||||
This decision was made during design review of PR #12142 (ext-api foundation). See `design-review-12142.md` Topic 14 for the full discussion thread.
|
||||
|
||||
The alternative of `Object.freeze()` was rejected because:
|
||||
|
||||
- It requires deep freezing for nested objects
|
||||
- Performance overhead for each call
|
||||
- Fresh copies achieve the same goal more simply
|
||||
@@ -1,138 +0,0 @@
|
||||
# 12. Pure Function Loader Pattern for Extension Registration
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The v2 extension API needs a mechanism for extensions to register themselves with the runtime. Two broad approaches exist:
|
||||
|
||||
### Side-Effect Registration (Vue 2 Plugin Pattern)
|
||||
|
||||
```ts
|
||||
// Extension self-registers at import time
|
||||
import { app } from '@comfyorg/core'
|
||||
|
||||
app.use({
|
||||
install(app) {
|
||||
app.component('MyWidget', MyWidget)
|
||||
app.directive('my-directive', myDirective)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Problems:
|
||||
|
||||
- **Import order matters**: If extension A depends on extension B being registered first, import order must be carefully managed
|
||||
- **Hard to test**: Side effects at import time make mocking difficult; tests must manipulate module cache
|
||||
- **Hard to tree-shake**: Bundlers can't eliminate unused extensions — the import executes
|
||||
- **Timing coupling**: Registration and activation are conflated; can't collect extensions first, then activate later
|
||||
|
||||
### Pure Function + Loader Pattern
|
||||
|
||||
```ts
|
||||
// Extension declares intent — no side effects
|
||||
export default defineNode({
|
||||
name: 'my-extension',
|
||||
nodeTypes: ['MyNode'],
|
||||
nodeCreated(handle) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
|
||||
// App bootstrap activates all registered extensions
|
||||
startExtensionSystem()
|
||||
```
|
||||
|
||||
## Decision
|
||||
|
||||
**Adopt the pure function + loader pattern** for v2 extension registration.
|
||||
|
||||
### Implementation
|
||||
|
||||
```ts
|
||||
// Extension Registry (data collection only)
|
||||
const nodeExtensions: NodeExtensionOptions[] = []
|
||||
|
||||
export function defineNode(options: NodeExtensionOptions): void {
|
||||
nodeExtensions.push(options)
|
||||
}
|
||||
|
||||
// Loader (activation)
|
||||
export function startExtensionSystem(): void {
|
||||
const world = getWorld()
|
||||
watch(
|
||||
() => world.entitiesWith(NodeTypeKey),
|
||||
(nodeEntityIds) => {
|
||||
for (const id of nodeEntityIds) {
|
||||
mountExtensionsForNode(id)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
1. **Pure registration**: `defineNode()` has no side effects beyond pushing to an array. It doesn't touch the World, DOM, or any reactive state.
|
||||
|
||||
2. **Centralized activation**: `startExtensionSystem()` is called exactly once during app bootstrap. This single entry point controls when the extension system "goes live".
|
||||
|
||||
3. **Reactive mounting**: The loader watches the World for entity changes. Extensions are mounted/unmounted in response to ECS state, not imperative calls.
|
||||
|
||||
4. **Order independence**: Extensions can be defined in any order. The loader sorts by name (lexicographic, see D10b) for deterministic execution.
|
||||
|
||||
### Registration Flow
|
||||
|
||||
```
|
||||
Extension files App bootstrap World
|
||||
| | |
|
||||
| defineNode({...}) | |
|
||||
|--------------------->| |
|
||||
| (push to array) | |
|
||||
| | |
|
||||
| | startExtensionSystem()
|
||||
| |------------------>|
|
||||
| | (watch for NodeType entities)
|
||||
| | |
|
||||
| | NodeType added |
|
||||
| |<------------------|
|
||||
| | |
|
||||
| | mountExtensionsForNode(id)
|
||||
| | (runs setup) |
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Testability**: Extensions are plain objects; tests can construct them without side effects. `_clearExtensionsForTesting()` resets state between tests.
|
||||
- **Tree-shakeable**: Bundlers can eliminate unused extension files if their exports are never referenced.
|
||||
- **Order independent**: No import order bugs — the loader handles activation order.
|
||||
- **Lazy activation**: Registration is instant; activation only happens when `startExtensionSystem()` is called.
|
||||
- **SSR friendly**: Pure functions don't execute browser-only code at import time.
|
||||
|
||||
### Negative
|
||||
|
||||
- **Manual bootstrap**: App must call `startExtensionSystem()` — forgetting it silently disables extensions.
|
||||
- **Two-step mental model**: Developers must understand "register" vs "activate" phases.
|
||||
|
||||
### Mitigations
|
||||
|
||||
- App bootstrap is a well-defined location; the call is hard to miss.
|
||||
- Clear documentation and starter templates include the bootstrap call.
|
||||
- Dev-mode warnings if extensions are defined but the system never starts.
|
||||
|
||||
## Notes
|
||||
|
||||
This pattern aligns with modern framework conventions:
|
||||
|
||||
- **Vite plugins**: `vite.config.ts` collects plugins as an array; Vite activates them at build time.
|
||||
- **Vue 3 Composition API**: `setup()` returns reactive state; the framework activates it.
|
||||
- **React hooks**: Pure functions declare effects; React schedules them.
|
||||
|
||||
The key insight is separating **declaration** (what do I want?) from **execution** (make it happen). This separation enables testing, lazy loading, and predictable behavior.
|
||||
@@ -8,19 +8,16 @@ An Architecture Decision Record captures an important architectural decision mad
|
||||
|
||||
## ADR Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
| ---------------------------------------------------------- | ------------------------------------------ | -------- | ---------- |
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
|
||||
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
|
||||
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
|
||||
| [0010](0010-deprecate-node-level-serialization-control.md) | Deprecate Node-Level Serialization Control | Accepted | 2026-05-12 |
|
||||
| [0011](0011-immutability-via-fresh-copies.md) | Immutability Enforcement via Fresh Copies | Accepted | 2026-05-12 |
|
||||
| [0012](0012-pure-function-loader-pattern.md) | Pure Function Loader Pattern | Accepted | 2026-05-12 |
|
||||
| ADR | Title | Status | Date |
|
||||
| -------------------------------------------------------- | ---------------------------------------- | -------- | ---------- |
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
|
||||
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
|
||||
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
|
||||
|
||||
## Creating a New ADR
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
# Research: Canvas vs Client/Pixel Coordinate Usage
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
How should the extension API handle coordinate systems? Should it expose canvas coordinates, screen/client coordinates, or both?
|
||||
|
||||
## Coordinate Systems in ComfyUI
|
||||
|
||||
### 1. Canvas Space (Logical Units)
|
||||
|
||||
Node positions and sizes are in canvas logical units:
|
||||
|
||||
- Independent of zoom/pan
|
||||
- `[0, 0]` is the canvas origin
|
||||
- Moving a node to `[100, 200]` places it at canvas position (100, 200) regardless of viewport state
|
||||
|
||||
### 2. Screen/Client Space (Pixels)
|
||||
|
||||
DOM elements use pixel coordinates relative to the viewport:
|
||||
|
||||
- Affected by zoom/pan/scroll
|
||||
- `clientX`/`clientY` from mouse events
|
||||
- `getBoundingClientRect()` returns pixel values
|
||||
|
||||
### 3. Widget Height (Pixels)
|
||||
|
||||
DOM widgets reserve height in pixels:
|
||||
|
||||
```ts
|
||||
addDOMWidget({ name: 'preview', element: img, height: 200 }) // 200px
|
||||
```
|
||||
|
||||
## Current Extension API
|
||||
|
||||
| Method | Coordinate System | Notes |
|
||||
| -------------------------- | ----------------- | ----------------------------------------- |
|
||||
| `getPosition()` | Canvas | Returns `[x, y]` in canvas units |
|
||||
| `setPosition()` | Canvas | Accepts `[x, y]` in canvas units |
|
||||
| `getSize()` | Canvas | Returns `[width, height]` in canvas units |
|
||||
| `setSize()` | Canvas | Accepts `[width, height]` in canvas units |
|
||||
| `addDOMWidget({ height })` | Pixels | Reserved height in pixels |
|
||||
| `widget.setHeight(px)` | Pixels | Widget height in pixels |
|
||||
|
||||
## Analysis
|
||||
|
||||
### When Extensions Need Canvas Coordinates
|
||||
|
||||
1. **Node positioning**: Placing nodes relative to each other
|
||||
2. **Layout algorithms**: Auto-arranging nodes in a pattern
|
||||
3. **Collision detection**: Checking if nodes overlap
|
||||
|
||||
### When Extensions Need Screen Coordinates
|
||||
|
||||
1. **Custom overlays**: Drawing UI at a specific screen location
|
||||
2. **Drag-and-drop from external sources**: Converting mouse position to canvas position
|
||||
3. **Context menus**: Positioning menus near the cursor
|
||||
|
||||
### Current State
|
||||
|
||||
The extension API currently exposes:
|
||||
|
||||
- **Canvas coordinates** for node position/size — appropriate, as these are logical values
|
||||
- **Pixel values** for DOM widget height — appropriate, as these are DOM measurements
|
||||
|
||||
**Missing**: No conversion helpers between canvas and screen coordinates.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**The current approach is appropriate.** Extensions that manipulate node positions should work in canvas space. This is the natural abstraction — extensions shouldn't need to account for zoom/pan when laying out nodes.
|
||||
|
||||
### For Advanced Cases
|
||||
|
||||
Extensions needing coordinate conversion (e.g., custom overlays) should either:
|
||||
|
||||
1. **Use LiteGraph's existing transform utilities** (available on `app.canvas`)
|
||||
2. **Access the transform state** via a future canvas API (not part of node/widget handles)
|
||||
|
||||
### Why Not Expose Conversion Helpers on NodeHandle?
|
||||
|
||||
- **Wrong abstraction level**: Coordinate conversion is a canvas concern, not a node concern
|
||||
- **State dependency**: Conversion requires current zoom/pan state, which changes frequently
|
||||
- **Rare use case**: Most extensions work entirely in canvas space
|
||||
|
||||
## Future Considerations
|
||||
|
||||
If multiple extensions need coordinate conversion, consider:
|
||||
|
||||
1. **Canvas API**: `canvas.screenToCanvas(point)` / `canvas.canvasToScreen(point)`
|
||||
2. **Events with both coordinates**: `positionChanged` could include both canvas and screen positions
|
||||
|
||||
For now, no changes are needed — the current API serves the common cases well.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Research: DOM Widget Convergence with Base Widget
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
Should DOM widgets be unified with base widgets, or kept as a separate concept?
|
||||
|
||||
## Current State
|
||||
|
||||
### Creation APIs
|
||||
|
||||
- `node.addWidget(type, name, value, options)` — creates a standard widget
|
||||
- `node.addDOMWidget({ name, element, height })` — creates a DOM-backed widget
|
||||
|
||||
### Internal Implementation
|
||||
|
||||
Both use the same underlying `CreateWidget` command:
|
||||
|
||||
```ts
|
||||
addWidget(type, name, defaultValue, options) {
|
||||
return dispatch({ type: 'CreateWidget', widgetType: type, ... })
|
||||
}
|
||||
|
||||
addDOMWidget(opts) {
|
||||
return dispatch({ type: 'CreateWidget', widgetType: 'DOM', ... })
|
||||
}
|
||||
```
|
||||
|
||||
DOM widgets are just widgets with `widgetType: 'DOM'` and an element reference.
|
||||
|
||||
### Shared WidgetHandle Interface
|
||||
|
||||
Both widget types share the same `WidgetHandle` interface:
|
||||
|
||||
| Method | Standard Widget | DOM Widget |
|
||||
| -------------------------------- | --------------- | ----------------------- |
|
||||
| `entityId`, `name`, `widgetType` | ✓ | ✓ |
|
||||
| `getValue()` / `setValue()` | ✓ (scalar) | ✓ (often unused) |
|
||||
| `isHidden()` / `setHidden()` | ✓ | ✓ |
|
||||
| `isDisabled()` / `setDisabled()` | ✓ | ✓ |
|
||||
| `setHeight(px)` | no-op | ✓ (updates reservation) |
|
||||
| `on('valueChange')` | ✓ | ✓ |
|
||||
| `getOption()` / `setOption()` | ✓ | ✓ |
|
||||
|
||||
## Analysis
|
||||
|
||||
### Arguments FOR Full Convergence
|
||||
|
||||
1. **Single mental model**: Extensions learn one widget concept, not two.
|
||||
2. **Consistent behavior**: All widgets appear in `node.widgets()`, serialize the same way.
|
||||
3. **Simpler API surface**: Fewer methods to document and maintain.
|
||||
|
||||
### Arguments FOR Keeping Separate APIs
|
||||
|
||||
1. **Different ergonomics**: Standard widgets are data-driven (name, value, options); DOM widgets are element-driven (pass an HTMLElement).
|
||||
2. **Type safety**: `addDOMWidget` can require `element: HTMLElement` at compile time; merging would make it optional with runtime checks.
|
||||
3. **Clear intent**: Separate APIs signal different use cases.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Keep the current partial convergence.** The implementation is unified (`CreateWidget` command), but the creation APIs remain separate for ergonomic reasons.
|
||||
|
||||
### Rationale
|
||||
|
||||
1. **Creation differs, usage is unified.** Extensions create DOM widgets differently (need an element), but interact with them the same way (via `WidgetHandle`).
|
||||
|
||||
2. **Type safety is valuable.** `addDOMWidget({ element })` is clearer than `addWidget('DOM', name, null, { element })`.
|
||||
|
||||
3. **Already well-integrated.** DOM widgets appear in `node.widgets()`, get the same events, and use the same serialization infrastructure.
|
||||
|
||||
### What "Convergence" Means Here
|
||||
|
||||
The widgets are already converged at:
|
||||
|
||||
- **Entity level**: Same `WidgetEntityId` brand
|
||||
- **Interface level**: Same `WidgetHandle` type
|
||||
- **Command level**: Same `CreateWidget` command internally
|
||||
|
||||
The APIs are intentionally separate at:
|
||||
|
||||
- **Creation level**: `addWidget` vs `addDOMWidget`
|
||||
|
||||
This is the right split — unified where it matters (runtime behavior), separate where it improves DX (creation ergonomics).
|
||||
|
||||
## Future Considerations
|
||||
|
||||
If we add more widget creation patterns (e.g., `addCanvasWidget`, `addThreeJSWidget`), we might consider:
|
||||
|
||||
1. **Factory pattern**: `node.widgets.create('DOM', { element })` / `node.widgets.create('INT', { min, max })`
|
||||
2. **Builder pattern**: `node.addWidget('DOM').withElement(el).withHeight(200).build()`
|
||||
|
||||
For now, two explicit methods (`addWidget`, `addDOMWidget`) serve the common cases well.
|
||||
@@ -1,112 +0,0 @@
|
||||
# Research: Identity Encapsulation in the Extension API
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
When do extensions need access to raw entity IDs (`NodeEntityId`, `WidgetEntityId`, `SlotEntityId`)? Should these be exposed or hidden?
|
||||
|
||||
## Current State
|
||||
|
||||
The v2 extension API exposes entity IDs as read-only properties:
|
||||
|
||||
```ts
|
||||
interface NodeHandle {
|
||||
readonly entityId: NodeEntityId
|
||||
// ...
|
||||
}
|
||||
|
||||
interface WidgetHandle {
|
||||
readonly entityId: WidgetEntityId
|
||||
// ...
|
||||
}
|
||||
|
||||
interface SlotInfo {
|
||||
readonly entityId: SlotEntityId
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
All IDs are **branded types** to prevent accidental mixing at compile time.
|
||||
|
||||
## Use Cases for Raw Entity IDs
|
||||
|
||||
### 1. Per-Instance State Mapping
|
||||
|
||||
Extensions maintaining external state per node:
|
||||
|
||||
```ts
|
||||
const nodeCache = new Map<NodeEntityId, CachedData>()
|
||||
|
||||
defineNode({
|
||||
name: 'my-cache-extension',
|
||||
nodeCreated(handle) {
|
||||
nodeCache.set(handle.entityId, computeExpensiveData())
|
||||
onNodeRemoved(() => nodeCache.delete(handle.entityId))
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Logging and Debugging
|
||||
|
||||
```ts
|
||||
node.on('executed', (e) => {
|
||||
console.log(`[${node.entityId}] Output:`, e.output)
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Inter-Extension Communication
|
||||
|
||||
Extensions that need to coordinate across multiple nodes:
|
||||
|
||||
```ts
|
||||
// Extension A stores data
|
||||
globalState.set(nodeA.entityId, data)
|
||||
|
||||
// Extension B retrieves it
|
||||
const data = globalState.get(nodeB.entityId)
|
||||
```
|
||||
|
||||
### 4. External System Interop
|
||||
|
||||
Extensions integrating with analytics, debugging tools, or external services that need stable node identifiers.
|
||||
|
||||
## Analysis
|
||||
|
||||
### Arguments FOR Exposing Entity IDs
|
||||
|
||||
1. **Legitimate need exists** — The use cases above are real and common.
|
||||
2. **Branded types prevent misuse** — Can't accidentally use `NodeEntityId` where `WidgetEntityId` is expected.
|
||||
3. **Read-only access** — Extensions can't mutate the ID or corrupt internal state.
|
||||
4. **Opaque value** — The format (`node:<graphUuid>:<localId>`) is an implementation detail; extensions should treat it as an opaque string.
|
||||
|
||||
### Arguments AGAINST Exposing Entity IDs
|
||||
|
||||
1. **Format coupling** — Extensions might parse the ID string and break if format changes.
|
||||
2. **Internal detail leakage** — Knowing the ID scheme reveals ECS architecture.
|
||||
3. **Future migration friction** — Changing ID representation requires careful deprecation.
|
||||
|
||||
### Mitigations
|
||||
|
||||
- **Document as opaque**: JSDoc clearly states IDs are opaque, not to be parsed.
|
||||
- **Branded types**: TypeScript prevents misuse across entity categories.
|
||||
- **Phase A format**: Current format includes graph UUID + local ID; this can evolve via semver.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Keep exposing entity IDs.** The use cases are legitimate, the branded types provide safety, and the read-only nature limits risk. Document that IDs are opaque strings — extensions should never parse or construct them.
|
||||
|
||||
### Guidelines for Extension Authors
|
||||
|
||||
1. **Use IDs only for keying** — Maps, Sets, logging, external system references.
|
||||
2. **Never parse IDs** — The format is an implementation detail subject to change.
|
||||
3. **Prefer handles over IDs** — When passing references between functions, use the handle object, not the raw ID.
|
||||
4. **Clean up on removal** — Always use `onNodeRemoved()` to clean up Maps keyed by entityId.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
If the ID format needs to change significantly, the branded types allow us to:
|
||||
|
||||
1. Introduce a new branded type (e.g., `NodeEntityIdV2`)
|
||||
2. Deprecate the old ID with migration guidance
|
||||
3. Keep both supported during a transition period
|
||||
@@ -1,121 +0,0 @@
|
||||
# Research: Serialization Context Simplification
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Question
|
||||
|
||||
Can the serialization context be simplified from 4 values to fewer?
|
||||
|
||||
Current contexts:
|
||||
|
||||
- `'workflow'` — saving workflow to disk
|
||||
- `'prompt'` — queueing a run (API call)
|
||||
- `'clone'` — copy/paste operation
|
||||
- `'subgraph-promote'` — widget becoming subgraph IO
|
||||
|
||||
## Use Case Analysis
|
||||
|
||||
### Context: 'workflow'
|
||||
|
||||
**Purpose**: Full persistence of user's work.
|
||||
|
||||
**What extensions need**: Serialize everything the user configured.
|
||||
|
||||
**Example**: A widget storing user preferences needs to include all settings.
|
||||
|
||||
### Context: 'prompt'
|
||||
|
||||
**Purpose**: Sending data to the backend for execution.
|
||||
|
||||
**What extensions need**:
|
||||
|
||||
- Transform values (dynamic prompts → resolved text)
|
||||
- Skip preview-only widgets
|
||||
- Materialize async sources (webcam → frame data)
|
||||
|
||||
**Example**:
|
||||
|
||||
```ts
|
||||
widget.on('beforeSerialize', async (e) => {
|
||||
if (e.context === 'prompt') {
|
||||
e.setSerializedValue(await captureFrame())
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Context: 'clone'
|
||||
|
||||
**Purpose**: Copy/paste should yield independent copy.
|
||||
|
||||
**What extensions need**: Reset instance-specific state while keeping user settings.
|
||||
|
||||
**Example**: A random seed widget might want a new seed on paste.
|
||||
|
||||
### Context: 'subgraph-promote'
|
||||
|
||||
**Purpose**: Widget becomes an input/output on a subgraph.
|
||||
|
||||
**What extensions need**: Convert internal representation to subgraph IO format.
|
||||
|
||||
**Example**: Internal state becomes an exposed parameter.
|
||||
|
||||
## Simplification Options
|
||||
|
||||
### Option A: Keep All 4 (Current State)
|
||||
|
||||
| Pro | Con |
|
||||
| ---------------------------------------- | ----------------- |
|
||||
| Each context has distinct semantics | 4 cases to handle |
|
||||
| Type system enforces valid values | More complex API |
|
||||
| Clear intent for each serialization path | |
|
||||
|
||||
### Option B: Collapse to 2 ('persist' | 'execute')
|
||||
|
||||
```ts
|
||||
context: 'persist' | 'execute'
|
||||
// 'persist' = workflow, clone, subgraph-promote
|
||||
// 'execute' = prompt
|
||||
```
|
||||
|
||||
| Pro | Con |
|
||||
| ------------------------------------------ | ------------------------------- |
|
||||
| Simpler mental model | Loses clone/promote distinction |
|
||||
| Most extensions only care about this split | Can't reset seed on clone |
|
||||
|
||||
### Option C: Remove Context Entirely
|
||||
|
||||
Extensions always transform regardless of context. The framework handles differences.
|
||||
|
||||
| Pro | Con |
|
||||
| ---------------------------- | ---------------------------------------------- |
|
||||
| Simplest API | Loses control for edge cases |
|
||||
| Framework handles all nuance | Some extensions need context-specific behavior |
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Keep all 4 contexts.** The use cases are genuinely different:
|
||||
|
||||
1. **workflow vs prompt**: Very common distinction. Dynamic prompts only process on prompt; preview widgets skip prompt. This is the most important split.
|
||||
|
||||
2. **clone**: Less common, but needed for stateful widgets (random seeds, generated IDs, captured frames).
|
||||
|
||||
3. **subgraph-promote**: Specialized, but necessary for the subgraph feature to work correctly.
|
||||
|
||||
### Rationale
|
||||
|
||||
- Extensions that don't care can ignore the context.
|
||||
- Extensions that do care have the information they need.
|
||||
- The 4 values map to 4 distinct operations in the framework.
|
||||
- Collapsing contexts would remove functionality with no real simplification gain.
|
||||
|
||||
### Mitigation for Complexity
|
||||
|
||||
- Document common patterns clearly
|
||||
- Most extensions only need: `if (context === 'prompt')`
|
||||
- Provide examples in JSDoc
|
||||
|
||||
## Note on Deprecation
|
||||
|
||||
The `NodeBeforeSerializeEvent` is deprecated (ADR-0010). The `WidgetBeforeSerializeEvent` remains supported and uses the same 4 contexts.
|
||||
|
||||
Since node-level serialization is being removed, this research applies to widget-level serialization only.
|
||||
@@ -1,148 +0,0 @@
|
||||
# Widget State Categories
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
## Overview
|
||||
|
||||
Widget state in the v2 extension API is organized into distinct categories, each with different characteristics for mutability, persistence, and event handling.
|
||||
|
||||
## Categories
|
||||
|
||||
### 1. Identity (Read-Only Invariants)
|
||||
|
||||
Set at construction, never change.
|
||||
|
||||
| Property | Type | Notes |
|
||||
| ------------ | ---------------- | ------------------------------------ |
|
||||
| `entityId` | `WidgetEntityId` | Branded, stable for widget lifetime |
|
||||
| `name` | `string` | Widget name as registered |
|
||||
| `widgetType` | `string` | e.g., `'INT'`, `'STRING'`, `'COMBO'` |
|
||||
| `label` | `string` | Display label, defaults to `name` |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- No setters exist for these properties
|
||||
- Extensions cannot modify identity after creation
|
||||
- Attempting to change identity is a design error
|
||||
|
||||
### 2. Value (First-Class, Every Widget)
|
||||
|
||||
The primary user-edited data.
|
||||
|
||||
| Method | Notes |
|
||||
| ------------------- | ----------------------------------- |
|
||||
| `getValue()` | Returns current value |
|
||||
| `setValue(v)` | Dispatches `SetWidgetValue` command |
|
||||
| `on('valueChange')` | Fires on value mutation |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Type varies by widget type (`number` for INT, `string` for STRING, etc.)
|
||||
- Persisted to `widgets_values` in workflow JSON
|
||||
- Included in API prompt by default (unless `setSerializeEnabled(false)`)
|
||||
- Changes are undo-able via command dispatch
|
||||
|
||||
### 3. Properties (First-Class, Every Widget)
|
||||
|
||||
Common properties all widgets share.
|
||||
|
||||
| Property | Getter | Setter | Event |
|
||||
| ----------- | ---------------------- | ------------------------ | ---------------- |
|
||||
| `hidden` | `isHidden()` | `setHidden(b)` | `propertyChange` |
|
||||
| `disabled` | `isDisabled()` | `setDisabled(b)` | `propertyChange` |
|
||||
| `serialize` | `isSerializeEnabled()` | `setSerializeEnabled(b)` | `propertyChange` |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Boolean values only
|
||||
- `hidden` affects UI visibility, not serialization
|
||||
- `disabled` makes widget read-only in UI
|
||||
- `serialize` controls inclusion in workflow/prompt output
|
||||
- Changes fire `propertyChange`, not `valueChange`
|
||||
|
||||
### 4. Options Bag (Type-Specific)
|
||||
|
||||
Per-instance overrides for type-specific configuration.
|
||||
|
||||
| Method | Notes |
|
||||
| ----------------------- | ---------------------------------------------- |
|
||||
| `getOption(key)` | Returns per-instance override or class default |
|
||||
| `setOption(key, value)` | Persists to `widget_options` sidecar |
|
||||
| `on('optionChange')` | Fires on option mutation |
|
||||
|
||||
**Common options by widget type:**
|
||||
|
||||
| Widget Type | Options |
|
||||
| ----------- | ---------------------------------- |
|
||||
| INT, FLOAT | `min`, `max`, `step`, `precision` |
|
||||
| STRING | `multiline`, `placeholder`, `rows` |
|
||||
| COMBO | `values` |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Options are JSON-serializable values
|
||||
- Persisted separately from `widgets_values` (additive, backward-compatible)
|
||||
- Extensions can add custom options
|
||||
- Option keys should be documented per widget type
|
||||
|
||||
### 5. DOM-Specific
|
||||
|
||||
Properties unique to DOM widgets.
|
||||
|
||||
| Method | Notes |
|
||||
| --------------- | ------------------------------------------ |
|
||||
| `setHeight(px)` | Updates reserved height, triggers relayout |
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- Only meaningful for `addDOMWidget()` widgets
|
||||
- No-op for non-DOM widgets
|
||||
- Measured in pixels (screen space)
|
||||
- No event fired; relayout is automatic
|
||||
|
||||
## Category Interaction Rules
|
||||
|
||||
### Event Separation
|
||||
|
||||
Each category has its own event:
|
||||
|
||||
| Category | Event |
|
||||
| ---------- | ---------------- |
|
||||
| Value | `valueChange` |
|
||||
| Properties | `propertyChange` |
|
||||
| Options | `optionChange` |
|
||||
|
||||
**Rule**: Events do not cross categories. Changing `hidden` does not fire `valueChange`.
|
||||
|
||||
### Serialization Behavior
|
||||
|
||||
| Category | Serialization |
|
||||
| ---------- | ---------------------------------------------------------------- |
|
||||
| Identity | Not serialized (derived from node type) |
|
||||
| Value | `widgets_values` array |
|
||||
| Properties | `hidden`/`disabled` not persisted; `serialize` affects inclusion |
|
||||
| Options | `widget_options` sidecar object |
|
||||
|
||||
### Mutability Summary
|
||||
|
||||
| Category | Mutable | Undo-able | Fires Event |
|
||||
| ---------- | ------- | --------- | ---------------- |
|
||||
| Identity | ✗ | — | — |
|
||||
| Value | ✓ | ✓ | `valueChange` |
|
||||
| Properties | ✓ | ✓ | `propertyChange` |
|
||||
| Options | ✓ | ✓ | `optionChange` |
|
||||
| DOM Height | ✓ | ✗ | — |
|
||||
|
||||
## Agent Implementation Notes
|
||||
|
||||
Agents working with widget state should:
|
||||
|
||||
1. **Respect category boundaries**: Don't try to `setValue()` to change visibility; use `setHidden()`.
|
||||
|
||||
2. **Use appropriate events**: Listen to `propertyChange` for UI state, `valueChange` for data.
|
||||
|
||||
3. **Handle type-specific options carefully**: Check widget type before accessing type-specific options.
|
||||
|
||||
4. **Preserve identity invariants**: Never try to change `entityId`, `name`, `widgetType`, or `label`.
|
||||
|
||||
5. **Consider serialization context**: Options persist to a sidecar; values persist to the main array.
|
||||
@@ -103,9 +103,7 @@ export default defineConfig([
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'packages/extension-api/scripts/build-docs.ts',
|
||||
'packages/extension-api/vite.config.mts'
|
||||
'vite.types.config.mts'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
global.d.ts
vendored
@@ -5,6 +5,7 @@ declare const __SENTRY_DSN__: string
|
||||
declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
declare const __USE_PROD_CONFIG__: boolean
|
||||
declare const __DEV_SERVER_COMFYUI_URL__: string
|
||||
|
||||
interface ImpactQueueFunction {
|
||||
(...args: unknown[]): void
|
||||
|
||||
@@ -9,10 +9,6 @@ const config: KnipConfig = {
|
||||
'src/assets/css/style.css',
|
||||
'src/scripts/ui/menu/index.ts',
|
||||
'src/types/index.ts',
|
||||
// Public extension API surface — published package entry point.
|
||||
// Per AGENTS.md, this barrel is the explicit exception to the
|
||||
// no-barrel-files-in-src rule because it IS the package entry.
|
||||
'src/extension-api/index.ts',
|
||||
'src/storybook/mocks/**/*.ts'
|
||||
],
|
||||
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}', '!.claude/**']
|
||||
@@ -36,12 +32,6 @@ const config: KnipConfig = {
|
||||
'packages/ingest-types': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
},
|
||||
'packages/extension-api': {
|
||||
// Build output is committed for npm package visibility
|
||||
ignore: ['build/**'],
|
||||
// typedoc is invoked via execSync in scripts/build-docs.ts
|
||||
ignoreDependencies: ['typedoc']
|
||||
},
|
||||
'apps/website': {
|
||||
entry: ['src/scripts/**/*.ts']
|
||||
}
|
||||
@@ -70,30 +60,7 @@ const config: KnipConfig = {
|
||||
// Agent review check config, not part of the build
|
||||
'.agents/checks/eslint.strict.config.js',
|
||||
// Devtools extensions, included dynamically
|
||||
'tools/devtools/web/**',
|
||||
// Deprecated stub re-exporting from `@/extension-api`. Will be removed
|
||||
// once PKG2 (`@comfyorg/extension-api`) ships and downstream imports
|
||||
// migrate to the package path.
|
||||
'src/types/extensionV2.ts',
|
||||
// D18 Phase 1 scaffolding — empty registries the loader will populate
|
||||
// in Phase 2 once side-effect registration moves out of
|
||||
// extension-api-service. See decisions/D18-pure-functions-loader-registration.md.
|
||||
'src/services/registries/**',
|
||||
// D18 Phase 1 — brand symbol + isBrandedExtension guard. Currently
|
||||
// consumed only by the define* call sites inside extension-api-service;
|
||||
// the type-guard and getBrandKind are exported for the Phase 2 loader.
|
||||
'src/extension-api/brand.ts',
|
||||
// Strangler-pattern v2 conversions of core extensions. Not yet wired
|
||||
// into the bootstrap (registration lands in a follow-up PR alongside
|
||||
// the v1→v2 cut-over). Tracked by I-EXT (#12144).
|
||||
'src/extensions/core/noteNode.v2.ts',
|
||||
'src/extensions/core/rerouteNode.v2.ts',
|
||||
'src/extensions/core/slotDefaults.v2.ts',
|
||||
// W6.P3.D — defineWidget+mount showcase port (D-widget-converge / A12).
|
||||
'src/extensions/core/webcamCapture.v2.ts',
|
||||
// W6.P4.D — canvas-units canary + escape-hatch annotation example
|
||||
// (D-coord-space / A13).
|
||||
'src/extensions/core/coordSpaceDemo.v2.ts'
|
||||
'tools/devtools/web/**'
|
||||
],
|
||||
vite: {
|
||||
config: ['vite?(.*).config.mts']
|
||||
@@ -112,15 +79,7 @@ const config: KnipConfig = {
|
||||
tags: [
|
||||
'-knipIgnoreUnusedButUsedByCustomNodes',
|
||||
'-knipIgnoreUnusedButUsedByVueNodesBranch',
|
||||
'-knipIgnoreUsedByStackedPR',
|
||||
// Public API surface consumed externally by extension authors and the
|
||||
// TypeDoc docgen pipeline (PKG2). Mark exports with @publicAPI when they
|
||||
// are part of `@comfyorg/extension-api` but not internally referenced.
|
||||
'-publicAPI',
|
||||
// Per D20, the three *EntityId brand re-exports in src/extension-api/{node,widget}.ts
|
||||
// are demoted to @internal — they stay available for internal package modules
|
||||
// but are removed from the public barrel and from TypeDoc output.
|
||||
'-internal'
|
||||
'-knipIgnoreUsedByStackedPR'
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,7 @@ export default {
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames: string[]) {
|
||||
// Exclude package build directories from linting
|
||||
const filtered = fileNames.filter(
|
||||
(f) => !f.includes('/packages/') || !f.includes('/build/')
|
||||
)
|
||||
if (filtered.length === 0) return []
|
||||
const joinedPaths = toJoinedRelativePaths(filtered)
|
||||
const joinedPaths = toJoinedRelativePaths(fileNames)
|
||||
return [
|
||||
`pnpm exec oxfmt --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.45.4",
|
||||
"version": "1.45.1",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -48,7 +48,6 @@
|
||||
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:unit": "nx run test",
|
||||
"test:extension-api": "[ -f vitest.extension-api.config.mts ] && vitest run --config vitest.extension-api.config.mts || echo 'SKIP: vitest.extension-api.config.mts not found'",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json",
|
||||
"typecheck:desktop": "nx run @comfyorg/desktop-ui:typecheck",
|
||||
|
||||
2
packages/extension-api/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
docs-build/
|
||||
node_modules/
|
||||
@@ -1,9 +0,0 @@
|
||||
src/
|
||||
scripts/
|
||||
tsconfig*.json
|
||||
typedoc.json
|
||||
docs-build/
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
__tests__/
|
||||
node_modules/
|
||||
@@ -1,50 +0,0 @@
|
||||
# @comfyorg/extension-api
|
||||
|
||||
> **Status**: scaffolded. Package implementation pending PKG3 — see
|
||||
> `../../../plans/P2-extension-api-package.md` and
|
||||
> `../../../plans/prompts/PKG3-npm-package.md` in the workspace root.
|
||||
|
||||
The official TypeScript declaration package for ComfyUI extensions. This
|
||||
package replaces the practice of vendoring `comfy.d.ts` files in custom
|
||||
node repos.
|
||||
|
||||
## Install (post-publish)
|
||||
|
||||
```bash
|
||||
pnpm add -D @comfyorg/extension-api
|
||||
```
|
||||
|
||||
```ts
|
||||
import { defineExtension } from '@comfyorg/extension-api'
|
||||
|
||||
export default defineExtension({
|
||||
name: 'MyExtension',
|
||||
setup(ctx) {
|
||||
ctx.onNodeMounted((node) => {
|
||||
// ...
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Source
|
||||
|
||||
This package is built from the source-of-truth folder
|
||||
`../../src/extension-api/`. Do not edit the package's `build/` output
|
||||
directly.
|
||||
|
||||
## Versioning
|
||||
|
||||
- `0.x.y` — experimental during parallel-paths transition (D6 Phase A).
|
||||
- `1.0.0` — first stable release once D5/D6/D7/D8 are accepted and the
|
||||
surface has stabilized.
|
||||
- Breaking changes follow semver strictly from `1.0.0` onward.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- `decisions/D6-parallel-paths-migration.md` — versioning rationale
|
||||
- `plans/P2-extension-api-package.md` — package structure plan
|
||||
- `plans/prompts/PKG3-npm-package.md` — implementation prompt
|
||||
- `plans/prompts/PKG4-ci-workflows.md` — publish workflow
|
||||
- `plans/prompts/PKG5-docgen-mdx.md` — docgen pipeline
|
||||
- `plans/prompts/PKG6-docs-comfy-org.md` — docs.comfy.org integration
|
||||
2
packages/extension-api/build/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
index.js
|
||||
index.js.map
|
||||
1254
packages/extension-api/build/index.d.ts
vendored
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "@comfyorg/extension-api",
|
||||
"version": "0.1.0",
|
||||
"description": "Official TypeScript extension API for ComfyUI custom nodes",
|
||||
"files": [
|
||||
"build",
|
||||
"README.md"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./build/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/index.d.ts",
|
||||
"import": "./build/index.js",
|
||||
"default": "./build/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "vite build --logLevel warn",
|
||||
"build": "vite build",
|
||||
"docs:build": "tsx scripts/build-docs.ts",
|
||||
"docs:watch": "tsx scripts/build-docs.ts --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "catalog:",
|
||||
"typedoc": "0.28.19",
|
||||
"typedoc-plugin-markdown": "^4.6.3",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-plugin-dts": "catalog:"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:api"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console -- CLI build script; stdout progress is intentional */
|
||||
|
||||
/**
|
||||
* PKG5 docgen pipeline: TypeDoc → Mintlify MDX
|
||||
*
|
||||
* Steps:
|
||||
* 1. Run TypeDoc with typedoc-plugin-markdown to emit raw markdown into docs-build/raw/
|
||||
* 2. Post-process each markdown file:
|
||||
* - Add Mintlify frontmatter (title, description, sidebarTitle, icon)
|
||||
* - Convert ``` fences without lang tag → ```ts
|
||||
* - Replace raw [TypeName] cross-refs with MDX relative links
|
||||
* - Wrap @example blocks in proper code fences
|
||||
* 3. Write final .mdx files to docs-build/mintlify/
|
||||
* 4. Emit docs-build/mintlify/nav-snippet.json — merges into docs.comfy.org mint.json
|
||||
*
|
||||
* Run: pnpm --filter @comfyorg/extension-api docs:build
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pkgRoot = path.resolve(__dirname, '..')
|
||||
const rawDir = path.join(pkgRoot, 'docs-build', 'raw')
|
||||
const mintlifyDir = path.join(pkgRoot, 'docs-build', 'mintlify')
|
||||
const watchMode = process.argv.includes('--watch')
|
||||
|
||||
// ── Page metadata ────────────────────────────────────────────────────────────
|
||||
// Controls frontmatter for each generated page. Key = TypeDoc output filename
|
||||
// stem (lowercased). Unrecognised files get generic metadata.
|
||||
|
||||
interface PageMeta {
|
||||
title: string
|
||||
sidebarTitle?: string
|
||||
description: string
|
||||
icon?: string
|
||||
group: 'core' | 'handles' | 'events' | 'shell' | 'identity' | 'root'
|
||||
order: number
|
||||
}
|
||||
|
||||
const PAGE_META: Record<string, PageMeta> = {
|
||||
// Top-level overview
|
||||
index: {
|
||||
title: 'Extension API Overview',
|
||||
description: 'TypeScript API reference for ComfyUI custom node extensions.',
|
||||
icon: 'puzzle-piece',
|
||||
group: 'root',
|
||||
order: 0
|
||||
},
|
||||
// Lifecycle / registration
|
||||
defineextension: {
|
||||
title: 'defineExtension',
|
||||
description:
|
||||
'Register an app-scoped extension for init, setup, and shell UI contributions.',
|
||||
icon: 'code',
|
||||
group: 'core',
|
||||
order: 1
|
||||
},
|
||||
definenodeextension: {
|
||||
title: 'defineNodeExtension',
|
||||
description:
|
||||
'Register a node-scoped extension reacting to node lifecycle events.',
|
||||
icon: 'code',
|
||||
group: 'core',
|
||||
order: 2
|
||||
},
|
||||
definewidgetextension: {
|
||||
title: 'defineWidgetExtension',
|
||||
description: 'Register a custom widget type with its own DOM rendering.',
|
||||
icon: 'code',
|
||||
group: 'core',
|
||||
order: 3
|
||||
},
|
||||
extensionoptions: {
|
||||
title: 'ExtensionOptions',
|
||||
description:
|
||||
'Options object for defineExtension — app-wide lifecycle and shell UI.',
|
||||
group: 'core',
|
||||
order: 4
|
||||
},
|
||||
nodeextensionoptions: {
|
||||
title: 'NodeExtensionOptions',
|
||||
description:
|
||||
'Options object for defineNodeExtension — node lifecycle hooks.',
|
||||
group: 'core',
|
||||
order: 5
|
||||
},
|
||||
widgetextensionoptions: {
|
||||
title: 'WidgetExtensionOptions',
|
||||
description:
|
||||
'Options object for defineWidgetExtension — custom widget rendering.',
|
||||
group: 'core',
|
||||
order: 6
|
||||
},
|
||||
onnoderemoved: {
|
||||
title: 'onNodeRemoved',
|
||||
sidebarTitle: 'onNodeRemoved',
|
||||
description:
|
||||
'Implicit-context lifecycle hook: fires when a node is removed from the graph.',
|
||||
group: 'core',
|
||||
order: 7
|
||||
},
|
||||
onnodemounted: {
|
||||
title: 'onNodeMounted',
|
||||
sidebarTitle: 'onNodeMounted',
|
||||
description:
|
||||
'Implicit-context lifecycle hook: fires when a node is fully mounted.',
|
||||
group: 'core',
|
||||
order: 8
|
||||
},
|
||||
// Handles
|
||||
nodehandle: {
|
||||
title: 'NodeHandle',
|
||||
description:
|
||||
'Controlled access to node state, mutations, slots, and events.',
|
||||
icon: 'circle-nodes',
|
||||
group: 'handles',
|
||||
order: 10
|
||||
},
|
||||
widgethandle: {
|
||||
title: 'WidgetHandle',
|
||||
description: 'Controlled access to widget state, mutations, and events.',
|
||||
icon: 'sliders',
|
||||
group: 'handles',
|
||||
order: 11
|
||||
},
|
||||
slotinfo: {
|
||||
title: 'SlotInfo',
|
||||
description: 'Read-only snapshot of a node slot (input or output).',
|
||||
group: 'handles',
|
||||
order: 12
|
||||
},
|
||||
// Events
|
||||
nodeexecutedevent: {
|
||||
title: 'NodeExecutedEvent',
|
||||
description: 'Payload fired when a node finishes execution.',
|
||||
group: 'events',
|
||||
order: 20
|
||||
},
|
||||
nodeconnectedevent: {
|
||||
title: 'NodeConnectedEvent',
|
||||
description: 'Payload fired when a slot connection is made.',
|
||||
group: 'events',
|
||||
order: 21
|
||||
},
|
||||
nodedisconnectedevent: {
|
||||
title: 'NodeDisconnectedEvent',
|
||||
description: 'Payload fired when a slot connection is removed.',
|
||||
group: 'events',
|
||||
order: 22
|
||||
},
|
||||
nodepositionchangedevent: {
|
||||
title: 'NodePositionChangedEvent',
|
||||
description: 'Payload fired when a node is moved on the canvas.',
|
||||
group: 'events',
|
||||
order: 23
|
||||
},
|
||||
nodesizechangedevent: {
|
||||
title: 'NodeSizeChangedEvent',
|
||||
description: 'Payload fired when a node is resized.',
|
||||
group: 'events',
|
||||
order: 24
|
||||
},
|
||||
nodemodechangedevent: {
|
||||
title: 'NodeModeChangedEvent',
|
||||
description: 'Payload fired when a node execution mode changes.',
|
||||
group: 'events',
|
||||
order: 25
|
||||
},
|
||||
nodebeforeserializeevent: {
|
||||
title: 'NodeBeforeSerializeEvent',
|
||||
description: 'Pre-serialization hook payload — override or skip node data.',
|
||||
group: 'events',
|
||||
order: 26
|
||||
},
|
||||
widgetvaluechangeevent: {
|
||||
title: 'WidgetValueChangeEvent',
|
||||
description: 'Payload fired when a widget value changes.',
|
||||
group: 'events',
|
||||
order: 27
|
||||
},
|
||||
widgetbeforeserializeevent: {
|
||||
title: 'WidgetBeforeSerializeEvent',
|
||||
description:
|
||||
'Pre-serialization hook payload — override or skip widget value.',
|
||||
group: 'events',
|
||||
order: 28
|
||||
},
|
||||
widgetbeforequeueevent: {
|
||||
title: 'WidgetBeforeQueueEvent',
|
||||
description:
|
||||
'Pre-queue validation payload — call reject() to cancel queue.',
|
||||
group: 'events',
|
||||
order: 29
|
||||
},
|
||||
// Shell UI
|
||||
sidebartabextension: {
|
||||
title: 'SidebarTabExtension',
|
||||
description: 'Register a custom sidebar tab.',
|
||||
group: 'shell',
|
||||
order: 40
|
||||
},
|
||||
bottompanelextension: {
|
||||
title: 'BottomPanelExtension',
|
||||
description: 'Register a custom bottom panel tab.',
|
||||
group: 'shell',
|
||||
order: 41
|
||||
},
|
||||
toastmanager: {
|
||||
title: 'ToastManager',
|
||||
description: 'Show toast notifications to the user.',
|
||||
group: 'shell',
|
||||
order: 42
|
||||
},
|
||||
commandmanager: {
|
||||
title: 'CommandManager',
|
||||
description: 'Register keyboard shortcuts and command palette entries.',
|
||||
group: 'shell',
|
||||
order: 43
|
||||
},
|
||||
extensionmanager: {
|
||||
title: 'ExtensionManager',
|
||||
description: 'Access shell UI registration APIs.',
|
||||
group: 'shell',
|
||||
order: 44
|
||||
},
|
||||
// Identity
|
||||
nodelocatorid: {
|
||||
title: 'NodeLocatorId',
|
||||
description:
|
||||
'Branded string ID that uniquely locates a node across graph snapshots.',
|
||||
group: 'identity',
|
||||
order: 50
|
||||
},
|
||||
nodeexecutionid: {
|
||||
title: 'NodeExecutionId',
|
||||
description: 'Branded string ID for a specific node execution run.',
|
||||
group: 'identity',
|
||||
order: 51
|
||||
}
|
||||
}
|
||||
|
||||
const GROUP_LABELS: Record<PageMeta['group'], string> = {
|
||||
root: 'Extensions API',
|
||||
core: 'Registration',
|
||||
handles: 'Handles',
|
||||
events: 'Events',
|
||||
shell: 'Shell UI',
|
||||
identity: 'Identity'
|
||||
}
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────────
|
||||
|
||||
function slug(stem: string): string {
|
||||
return stem
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
|
||||
function metaFor(stem: string): PageMeta {
|
||||
const key = stem.toLowerCase().replace(/[^a-z]/g, '')
|
||||
return (
|
||||
PAGE_META[key] ?? {
|
||||
title: stem,
|
||||
description: `API reference for ${stem}.`,
|
||||
group: 'core',
|
||||
order: 99
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** Convert TypeDoc raw markdown to Mintlify-compatible MDX. */
|
||||
function toMintlifyMdx(raw: string, stem: string): string {
|
||||
const meta = metaFor(stem)
|
||||
|
||||
// Build frontmatter
|
||||
const fm: string[] = [
|
||||
`---`,
|
||||
`title: "${meta.title}"`,
|
||||
...(meta.sidebarTitle ? [`sidebarTitle: "${meta.sidebarTitle}"`] : []),
|
||||
`description: "${meta.description}"`,
|
||||
...(meta.icon ? [`icon: "${meta.icon}"`] : []),
|
||||
`---`
|
||||
]
|
||||
|
||||
let body = raw
|
||||
|
||||
// Strip TypeDoc breadcrumb header lines (e.g. "[**@comfyorg/...**](../index.md)\n\n***\n\n[@comfyorg...]...")
|
||||
body = body.replace(/^\[.*?\]\(\.\.\/index\.md\)\n+\*+\n+/gm, '')
|
||||
body = body.replace(/^\[.*?\]\(\.\.\/index\.md\).*\n+/gm, '')
|
||||
|
||||
// Remove the TypeDoc-generated H1 (we use frontmatter title instead)
|
||||
body = body.replace(/^# .+\n+/, '')
|
||||
|
||||
// Ensure opening code fences that have no lang tag get `ts`
|
||||
// Only match a ``` that is immediately followed by a newline (opening fence),
|
||||
// not a closing fence (which also has just ``` + newline but we can detect
|
||||
// by context: opening fences follow non-fence lines; closing fences follow content).
|
||||
// Simpler heuristic: replace ``` at start of line only when not already closing a block.
|
||||
// We track state via a flag pass instead of a single regex.
|
||||
let inBlock = false
|
||||
body = body
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (inBlock) {
|
||||
if (line.trim() === '```') {
|
||||
inBlock = false
|
||||
return line
|
||||
}
|
||||
return line
|
||||
}
|
||||
if (line.startsWith('```')) {
|
||||
if (line.trim() === '```') {
|
||||
// bare opening fence → add ts
|
||||
inBlock = true
|
||||
return '```ts'
|
||||
}
|
||||
// has a lang tag already
|
||||
inBlock = true
|
||||
return line
|
||||
}
|
||||
return line
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// TypeDoc emits `typescript` lang tag; normalize to `ts`
|
||||
body = body.replace(/^```typescript\b/gm, '```ts')
|
||||
|
||||
// Fix TypeDoc cross-ref links: [TypeName](../type-alias/TypeName.md) → relative MDX paths
|
||||
// Pattern: [Label](../category/FileName.md) → [Label](./filename)
|
||||
body = body.replace(
|
||||
/\[([^\]]+)\]\(\.\.\/([\w-]+)\/([\w-]+)\.md\)/g,
|
||||
(_match, label, _category, file) => `[${label}](./${slug(file)})`
|
||||
)
|
||||
// Same-dir links
|
||||
body = body.replace(
|
||||
/\[([^\]]+)\]\(([\w-]+)\.md\)/g,
|
||||
(_match, label, file) => `[${label}](./${slug(file)})`
|
||||
)
|
||||
|
||||
// TypeDoc wraps @example content in a "## Example" heading; Mintlify prefers
|
||||
// code examples to be directly under prose without a sub-heading.
|
||||
// Flatten "## Example\n\n```ts" → "```ts"
|
||||
body = body.replace(/^## Example\s*\n+/gm, '')
|
||||
|
||||
// Stability tags: render as a <Tip> callout
|
||||
body = body.replace(
|
||||
/\*\*Stability\*\*: `(stable|experimental|deprecated)`/g,
|
||||
(_match, level) => {
|
||||
const label =
|
||||
level === 'stable'
|
||||
? '<Tip>**Stability:** Stable — part of the public API contract.</Tip>'
|
||||
: level === 'experimental'
|
||||
? '<Warning>**Stability:** Experimental — may change before 1.0.</Warning>'
|
||||
: '<Warning>**Stability:** Deprecated — will be removed. See migration guide.</Warning>'
|
||||
return label
|
||||
}
|
||||
)
|
||||
|
||||
// @stability TSDoc tag (appears as plain text after TypeDoc strips tags)
|
||||
body = body.replace(
|
||||
/^Stability: (stable|experimental|deprecated)\s*$/gm,
|
||||
(_match, level) => {
|
||||
if (level === 'stable') return '<Tip>**Stability:** Stable</Tip>'
|
||||
if (level === 'experimental')
|
||||
return '<Warning>**Stability:** Experimental</Warning>'
|
||||
return '<Warning>**Stability:** Deprecated</Warning>'
|
||||
}
|
||||
)
|
||||
|
||||
return [...fm, '', body.trim(), ''].join('\n')
|
||||
}
|
||||
|
||||
// ── Nav snippet builder ───────────────────────────────────────────────────────
|
||||
|
||||
interface NavPage {
|
||||
group?: string
|
||||
pages: (string | NavPage)[]
|
||||
}
|
||||
|
||||
function buildNavSnippet(stems: string[]): NavPage {
|
||||
// Sort stems by order then group by category
|
||||
const sortedStems = stems
|
||||
.slice()
|
||||
.sort((a, b) => metaFor(a).order - metaFor(b).order)
|
||||
const sortedByGroup: Record<string, string[]> = {}
|
||||
for (const stem of sortedStems) {
|
||||
const group = metaFor(stem).group
|
||||
if (!sortedByGroup[group]) sortedByGroup[group] = []
|
||||
sortedByGroup[group].push(`extensions/api/${slug(stem)}`)
|
||||
}
|
||||
|
||||
const groupOrder: PageMeta['group'][] = [
|
||||
'root',
|
||||
'core',
|
||||
'handles',
|
||||
'events',
|
||||
'shell',
|
||||
'identity'
|
||||
]
|
||||
|
||||
const pages: (string | NavPage)[] = []
|
||||
|
||||
// Overview at top level
|
||||
if (sortedByGroup['root']) {
|
||||
for (const p of sortedByGroup['root']) pages.push(p)
|
||||
}
|
||||
|
||||
for (const grp of groupOrder) {
|
||||
if (grp === 'root') continue
|
||||
const grpPages = sortedByGroup[grp]
|
||||
if (!grpPages?.length) continue
|
||||
pages.push({ group: GROUP_LABELS[grp], pages: grpPages })
|
||||
}
|
||||
|
||||
return { group: 'Extensions API', pages }
|
||||
}
|
||||
|
||||
// ── Main pipeline ────────────────────────────────────────────────────────────
|
||||
|
||||
function runTypedoc(): void {
|
||||
console.log('▶ Running TypeDoc...')
|
||||
execSync(
|
||||
`pnpm exec typedoc --options ${path.join(pkgRoot, 'typedoc.json')} --out ${rawDir}`,
|
||||
{ cwd: pkgRoot, stdio: 'inherit' }
|
||||
)
|
||||
}
|
||||
|
||||
function processFiles(): void {
|
||||
if (!fs.existsSync(rawDir)) {
|
||||
throw new Error(`TypeDoc output directory not found: ${rawDir}`)
|
||||
}
|
||||
|
||||
fs.mkdirSync(mintlifyDir, { recursive: true })
|
||||
|
||||
const mdFiles = fs
|
||||
.readdirSync(rawDir, { recursive: true })
|
||||
.filter((f): f is string => typeof f === 'string' && f.endsWith('.md'))
|
||||
|
||||
const stems: string[] = []
|
||||
|
||||
for (const relPath of mdFiles) {
|
||||
const src = path.join(rawDir, relPath)
|
||||
const stem = path.basename(relPath, '.md')
|
||||
const raw = fs.readFileSync(src, 'utf8')
|
||||
const mdx = toMintlifyMdx(raw, stem)
|
||||
|
||||
const destName = slug(stem) + '.mdx'
|
||||
const dest = path.join(mintlifyDir, destName)
|
||||
fs.writeFileSync(dest, mdx)
|
||||
console.log(` ✔ ${relPath} → mintlify/${destName}`)
|
||||
stems.push(stem)
|
||||
}
|
||||
|
||||
// Write nav snippet
|
||||
const nav = buildNavSnippet(stems)
|
||||
const navDest = path.join(mintlifyDir, 'nav-snippet.json')
|
||||
fs.writeFileSync(navDest, JSON.stringify(nav, null, 2) + '\n')
|
||||
console.log(` ✔ nav-snippet.json`)
|
||||
|
||||
console.log(`\n✅ Mintlify MDX written to: ${mintlifyDir}`)
|
||||
console.log(` ${stems.length} pages + nav-snippet.json`)
|
||||
}
|
||||
|
||||
function run(): void {
|
||||
runTypedoc()
|
||||
processFiles()
|
||||
}
|
||||
|
||||
if (watchMode) {
|
||||
// Simple watch: re-run on change to source files
|
||||
console.log('👁 Watch mode — watching src/extension-api/**')
|
||||
const srcDir = path.resolve(pkgRoot, '../../src/extension-api')
|
||||
let debounce: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
run()
|
||||
|
||||
fs.watch(srcDir, { recursive: true }, () => {
|
||||
if (debounce) clearTimeout(debounce)
|
||||
debounce = setTimeout(() => {
|
||||
console.log('\n🔄 Source changed — rebuilding...')
|
||||
try {
|
||||
run()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
} else {
|
||||
run()
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* @comfyorg/extension-api — Public Extension API for ComfyUI
|
||||
*
|
||||
* This is the package entry point compiled to `build/index.js` + `build/index.d.ts`.
|
||||
* It is a single re-export of the canonical surface defined in
|
||||
* `src/extension-api/index.ts` in the main app — that file is the one source
|
||||
* of truth for what is part of the stable, semver-versioned public contract.
|
||||
*
|
||||
* Do NOT add exports here. Add them to `src/extension-api/index.ts` and they
|
||||
* will flow through this barrel automatically.
|
||||
*
|
||||
* The tsconfig.json `paths` alias `@/*` → `../../src/*` resolves the import
|
||||
* below at both typecheck and build time.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export * from '@/extension-api/index'
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023", "ES2023.Array", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"noEmit": false,
|
||||
"outDir": "./build",
|
||||
"paths": {
|
||||
"@/*": ["../../src/*"],
|
||||
"@/utils/formatUtil": [
|
||||
"../../packages/shared-frontend-utils/src/formatUtil.ts"
|
||||
],
|
||||
"@/utils/networkUtil": [
|
||||
"../../packages/shared-frontend-utils/src/networkUtil.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"../../src/**/*.ts",
|
||||
"../../src/types/litegraph-augmentation.d.ts",
|
||||
"../../global.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"../../src/**/*.test.ts",
|
||||
"../../src/**/*.spec.ts",
|
||||
"../../src/**/*.vue",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"scripts/**"
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import dts from 'vite-plugin-dts'
|
||||
|
||||
const here = fileURLToPath(new URL('.', import.meta.url))
|
||||
const repoRoot = resolve(here, '..', '..')
|
||||
const repoSrc = resolve(repoRoot, 'src')
|
||||
const surfaceRoot = resolve(repoSrc, 'extension-api')
|
||||
|
||||
/**
|
||||
* Library build for `@comfyorg/extension-api`.
|
||||
*
|
||||
* Per ADR D17 (PKG2 build strategy), the package is built from the canonical
|
||||
* surface defined in the main app at `src/extension-api/index.ts`. Vite
|
||||
* resolves the `@/*` aliases against the main app's `src/` directory and
|
||||
* emits a single bundled `index.js` plus a single bundled `index.d.ts`.
|
||||
*
|
||||
* The package barrel at `packages/extension-api/src/index.ts` is the
|
||||
* Vite entry point and re-exports `@/extension-api/index` — preserving
|
||||
* "the barrel is the source of truth in main app `src/extension-api/`"
|
||||
* intent in `packages/extension-api/AGENTS.md`.
|
||||
*
|
||||
* Vue is externalized as a peer dependency (per D6.1 Phase A — extension
|
||||
* authors share the host app's Vue runtime).
|
||||
*/
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@/utils/formatUtil': resolve(
|
||||
repoRoot,
|
||||
'packages/shared-frontend-utils/src/formatUtil.ts'
|
||||
),
|
||||
'@/utils/networkUtil': resolve(
|
||||
repoRoot,
|
||||
'packages/shared-frontend-utils/src/networkUtil.ts'
|
||||
),
|
||||
'@': repoSrc
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: resolve(here, 'build'),
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
target: 'es2022',
|
||||
minify: false,
|
||||
lib: {
|
||||
// Build directly from the canonical surface in the main app — the
|
||||
// package's own `src/index.ts` exists only as a documented entry
|
||||
// point that re-exports the same surface, but we point Vite at the
|
||||
// canonical file so dts paths line up cleanly with the JS bundle.
|
||||
entry: resolve(surfaceRoot, 'index.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'index.js'
|
||||
},
|
||||
rollupOptions: {
|
||||
// Vue is provided by the host app at runtime.
|
||||
external: ['vue', /^@vue\//]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
// Bundle all types into a single index.d.ts. This ensures the package
|
||||
// is self-contained and doesn't reference paths outside build/.
|
||||
rollupTypes: true,
|
||||
outDir: resolve(here, 'build'),
|
||||
tsconfigPath: resolve(here, 'tsconfig.build.json'),
|
||||
logLevel: 'warn',
|
||||
// Only include the extension-api surface, not the entire app
|
||||
include: [resolve(surfaceRoot, '**/*.ts')]
|
||||
})
|
||||
]
|
||||
})
|
||||
4
packages/ingest-types/src/types.gen.ts
generated
@@ -523,6 +523,10 @@ export type ImportPublishedAssetsRequest = {
|
||||
* IDs of published assets (inputs and models) to import.
|
||||
*/
|
||||
published_asset_ids: Array<string>
|
||||
/**
|
||||
* The share ID of the published workflow these assets belong to. Required for authorization.
|
||||
*/
|
||||
share_id: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
3
packages/ingest-types/src/zod.gen.ts
generated
@@ -310,7 +310,8 @@ export const zImportPublishedAssetsResponse = z.object({
|
||||
* Request body for importing assets from a published workflow.
|
||||
*/
|
||||
export const zImportPublishedAssetsRequest = z.object({
|
||||
published_asset_ids: z.array(z.string())
|
||||
published_asset_ids: z.array(z.string().min(1).max(64)).max(1000),
|
||||
share_id: z.string().min(1).max(64)
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -10057,8 +10057,6 @@ export interface components {
|
||||
};
|
||||
progress: number;
|
||||
create_time: number;
|
||||
/** @description Actual credits consumed by the task. Present once status is finalized; 0 for failed tasks. */
|
||||
consumed_credit?: number;
|
||||
};
|
||||
TripoSuccessTask: {
|
||||
/** @enum {integer} */
|
||||
|
||||
@@ -3,14 +3,12 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
appendWorkflowJsonExt,
|
||||
ensureWorkflowSuffix,
|
||||
getFilePathSeparatorVariants,
|
||||
getFilenameDetails,
|
||||
getMediaTypeFromFilename,
|
||||
getPathDetails,
|
||||
highlightQuery,
|
||||
isCivitaiModelUrl,
|
||||
isPreviewableMediaType,
|
||||
joinFilePath,
|
||||
truncateFilename
|
||||
} from './formatUtil'
|
||||
|
||||
@@ -85,11 +83,9 @@ describe('formatUtil', () => {
|
||||
describe('video files', () => {
|
||||
it('should identify video extensions correctly', () => {
|
||||
expect(getMediaTypeFromFilename('video.mp4')).toBe('video')
|
||||
expect(getMediaTypeFromFilename('apple.m4v')).toBe('video')
|
||||
expect(getMediaTypeFromFilename('clip.webm')).toBe('video')
|
||||
expect(getMediaTypeFromFilename('movie.mov')).toBe('video')
|
||||
expect(getMediaTypeFromFilename('film.avi')).toBe('video')
|
||||
expect(getMediaTypeFromFilename('episode.mkv')).toBe('video')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -303,42 +299,6 @@ describe('formatUtil', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('joinFilePath', () => {
|
||||
it('joins subfolder and filename with normalized slash separators', () => {
|
||||
expect(joinFilePath('nested\\folder', 'child\\file.png')).toBe(
|
||||
'nested/folder/child/file.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('trims boundary separators without changing the filename body', () => {
|
||||
expect(joinFilePath('/nested/folder/', '/file.png')).toBe(
|
||||
'nested/folder/file.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the normalized filename when no subfolder is provided', () => {
|
||||
expect(joinFilePath('', 'nested\\file.png')).toBe('nested/file.png')
|
||||
})
|
||||
|
||||
it('returns the normalized subfolder without a trailing slash when no filename is provided', () => {
|
||||
expect(joinFilePath('nested\\folder', '')).toBe('nested/folder')
|
||||
expect(joinFilePath('nested\\folder', null)).toBe('nested/folder')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFilePathSeparatorVariants', () => {
|
||||
it('returns slash and backslash variants for nested paths', () => {
|
||||
expect(getFilePathSeparatorVariants('nested\\folder/file.png')).toEqual([
|
||||
'nested/folder/file.png',
|
||||
'nested\\folder\\file.png'
|
||||
])
|
||||
})
|
||||
|
||||
it('returns a single value when no separator is present', () => {
|
||||
expect(getFilePathSeparatorVariants('file.png')).toEqual(['file.png'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('appendWorkflowJsonExt', () => {
|
||||
it('appends .app.json when isApp is true', () => {
|
||||
expect(appendWorkflowJsonExt('test', true)).toBe('test.app.json')
|
||||
|
||||
@@ -256,31 +256,6 @@ export function isValidUrl(url: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function joinFilePath(
|
||||
subfolder: string | null | undefined,
|
||||
filename: string | null | undefined
|
||||
): string {
|
||||
const normalizedSubfolder = normalizeFilePathSeparators(
|
||||
subfolder ?? ''
|
||||
).replace(/^\/+|\/+$/g, '')
|
||||
const normalizedFilename = normalizeFilePathSeparators(
|
||||
filename ?? ''
|
||||
).replace(/^\/+/g, '')
|
||||
if (!normalizedSubfolder) return normalizedFilename
|
||||
if (!normalizedFilename) return normalizedSubfolder
|
||||
return `${normalizedSubfolder}/${normalizedFilename}`
|
||||
}
|
||||
|
||||
export function getFilePathSeparatorVariants(filepath: string): string[] {
|
||||
const slashPath = normalizeFilePathSeparators(filepath)
|
||||
const backslashPath = slashPath.replace(/\//g, '\\')
|
||||
return slashPath === backslashPath ? [slashPath] : [slashPath, backslashPath]
|
||||
}
|
||||
|
||||
function normalizeFilePathSeparators(filepath: string): string {
|
||||
return filepath.replace(/[\\/]+/g, '/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a filepath into its filename and subfolder components.
|
||||
*
|
||||
@@ -299,7 +274,8 @@ export function parseFilePath(filepath: string): {
|
||||
} {
|
||||
if (!filepath?.trim()) return { filename: '', subfolder: '' }
|
||||
|
||||
const normalizedPath = normalizeFilePathSeparators(filepath)
|
||||
const normalizedPath = filepath
|
||||
.replace(/[\\/]+/g, '/') // Normalize path separators
|
||||
.replace(/^\//, '') // Remove leading slash
|
||||
.replace(/\/$/, '') // Remove trailing slash
|
||||
|
||||
@@ -581,7 +557,7 @@ const IMAGE_EXTENSIONS = [
|
||||
'tiff',
|
||||
'svg'
|
||||
] as const
|
||||
const VIDEO_EXTENSIONS = ['mp4', 'm4v', 'webm', 'mov', 'avi', 'mkv'] as const
|
||||
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi'] as const
|
||||
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac'] as const
|
||||
const THREE_D_EXTENSIONS = ['obj', 'fbx', 'gltf', 'glb', 'usdz'] as const
|
||||
const TEXT_EXTENSIONS = [
|
||||
|
||||
420
pnpm-lock.yaml
generated
@@ -650,7 +650,7 @@ importers:
|
||||
version: 22.6.1(@babel/traverse@7.29.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.6.1)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
|
||||
'@nx/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(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.2))(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.9.0))
|
||||
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))
|
||||
'@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.9.0))(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.2))(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.9.0))
|
||||
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))
|
||||
'@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.9.0))(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.2))(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.9.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)
|
||||
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.9.0))
|
||||
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))
|
||||
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.9.0))
|
||||
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))
|
||||
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.9.0))(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.2))(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.9.0)
|
||||
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)
|
||||
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.9.0))
|
||||
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))
|
||||
'@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.9.0))(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.2))(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.9.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)
|
||||
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.9.0))
|
||||
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))
|
||||
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.9.0))(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.2))(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.9.0))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.9.0)
|
||||
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)
|
||||
'@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.9.0))
|
||||
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))
|
||||
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.9.0)
|
||||
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)
|
||||
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.9.0)
|
||||
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)
|
||||
|
||||
packages/design-system:
|
||||
dependencies:
|
||||
@@ -1030,31 +1030,6 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
|
||||
packages/extension-api:
|
||||
dependencies:
|
||||
vue:
|
||||
specifier: 'catalog:'
|
||||
version: 3.5.13(typescript@5.9.3)
|
||||
devDependencies:
|
||||
tsx:
|
||||
specifier: 'catalog:'
|
||||
version: 4.19.4
|
||||
typedoc:
|
||||
specifier: 0.28.19
|
||||
version: 0.28.19(typescript@5.9.3)
|
||||
typedoc-plugin-markdown:
|
||||
specifier: ^4.6.3
|
||||
version: 4.11.0(typedoc@0.28.19(typescript@5.9.3))
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-dts:
|
||||
specifier: 'catalog:'
|
||||
version: 4.5.4(@types/node@25.0.3)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
|
||||
packages/ingest-types:
|
||||
dependencies:
|
||||
zod:
|
||||
@@ -2456,9 +2431,6 @@ 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}
|
||||
@@ -5481,10 +5453,6 @@ 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'}
|
||||
@@ -7656,9 +7624,6 @@ 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
|
||||
@@ -7899,10 +7864,6 @@ 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==}
|
||||
|
||||
@@ -9382,19 +9343,6 @@ 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'}
|
||||
@@ -10238,11 +10186,6 @@ packages:
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.9.0:
|
||||
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10524,14 +10467,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.9.0))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.9.0)':
|
||||
'@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)':
|
||||
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.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx': 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
'@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))
|
||||
'@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.9.0)
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vite-plugin-vue-devtools: 7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
|
||||
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))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
@@ -11920,14 +11863,6 @@ 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
|
||||
@@ -12316,14 +12251,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/api-extractor-model@7.33.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
'@microsoft/tsdoc-config': 0.18.0
|
||||
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/api-extractor@7.57.2(@types/node@24.10.4)':
|
||||
dependencies:
|
||||
'@microsoft/api-extractor-model': 7.33.1(@types/node@24.10.4)
|
||||
@@ -12343,25 +12270,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/api-extractor@7.57.2(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@microsoft/api-extractor-model': 7.33.1(@types/node@25.0.3)
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
'@microsoft/tsdoc-config': 0.18.0
|
||||
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
|
||||
'@rushstack/rig-package': 0.7.1
|
||||
'@rushstack/terminal': 0.22.1(@types/node@25.0.3)
|
||||
'@rushstack/ts-command-line': 5.3.1(@types/node@25.0.3)
|
||||
diff: 8.0.3
|
||||
lodash: 4.17.23
|
||||
minimatch: 10.2.1
|
||||
resolve: 1.22.11
|
||||
semver: 7.5.4
|
||||
source-map: 0.6.1
|
||||
typescript: 5.8.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@microsoft/tsdoc-config@0.18.0':
|
||||
dependencies:
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
@@ -12587,11 +12495,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.9.0))(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.2))(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.9.0))(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.2))(vitest@4.0.16)
|
||||
'@phenomnomnominal/tsquery': 6.1.4(typescript@5.9.3)
|
||||
ajv: 8.18.0
|
||||
enquirer: 2.3.6
|
||||
@@ -12599,8 +12507,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.9.0)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -12611,7 +12519,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.9.0))(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.2))(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)
|
||||
@@ -12619,8 +12527,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.9.0)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -13234,27 +13142,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@rushstack/node-core-library@5.20.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
ajv: 8.13.0
|
||||
ajv-draft-04: 1.0.0(ajv@8.13.0)
|
||||
ajv-formats: 3.0.1(ajv@8.13.0)
|
||||
fs-extra: 11.3.2
|
||||
import-lazy: 4.0.0
|
||||
jju: 1.4.0
|
||||
resolve: 1.22.11
|
||||
semver: 7.5.4
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@rushstack/problem-matcher@0.2.1(@types/node@24.10.4)':
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@rushstack/problem-matcher@0.2.1(@types/node@25.0.3)':
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@rushstack/rig-package@0.7.1':
|
||||
dependencies:
|
||||
resolve: 1.22.11
|
||||
@@ -13268,14 +13159,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.4
|
||||
|
||||
'@rushstack/terminal@0.22.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
|
||||
'@rushstack/problem-matcher': 0.2.1(@types/node@25.0.3)
|
||||
supports-color: 8.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@rushstack/ts-command-line@5.3.1(@types/node@24.10.4)':
|
||||
dependencies:
|
||||
'@rushstack/terminal': 0.22.1(@types/node@24.10.4)
|
||||
@@ -13285,15 +13168,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@rushstack/ts-command-line@5.3.1(@types/node@25.0.3)':
|
||||
dependencies:
|
||||
'@rushstack/terminal': 0.22.1(@types/node@25.0.3)
|
||||
'@types/argparse': 1.0.38
|
||||
argparse: 1.0.10
|
||||
string-argv: 0.3.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
'@sentry-internal/browser-utils@10.32.1':
|
||||
@@ -13443,10 +13317,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.9.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))':
|
||||
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.9.0))
|
||||
'@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/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
|
||||
@@ -13472,25 +13346,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.9.0))':
|
||||
'@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))':
|
||||
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.9.0))
|
||||
'@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: 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.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)
|
||||
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.9.0))':
|
||||
'@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))':
|
||||
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.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)
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
|
||||
@@ -13515,14 +13389,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.9.0))(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.2))(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.9.0))
|
||||
'@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/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.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)
|
||||
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:
|
||||
@@ -13604,19 +13478,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.9.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))':
|
||||
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.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)
|
||||
|
||||
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
'@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))':
|
||||
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.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)
|
||||
|
||||
'@tanstack/virtual-core@3.13.12': {}
|
||||
|
||||
@@ -14209,32 +14083,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.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue-jsx@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(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.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)
|
||||
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.9.0))(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.2))(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.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)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@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))':
|
||||
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.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)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
|
||||
'@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))':
|
||||
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.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)
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
|
||||
'@vitest/coverage-v8@4.0.16(vitest@4.0.16)':
|
||||
@@ -14250,7 +14124,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.9.0)
|
||||
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -14271,21 +14145,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.9.0))':
|
||||
'@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))':
|
||||
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.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)
|
||||
|
||||
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
|
||||
'@vitest/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))':
|
||||
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.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)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -14321,7 +14195,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@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
vitest: 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/utils@3.2.4':
|
||||
dependencies:
|
||||
@@ -14496,38 +14370,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.9.0))(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.2))(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.9.0))
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
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.9.0))(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.2))(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.9.0))
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
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.9.0))(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.2))(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.9.0))
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
@@ -14935,7 +14809,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.9.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):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 2.13.1
|
||||
'@astrojs/internal-helpers': 0.7.6
|
||||
@@ -14992,8 +14866,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.9.0)
|
||||
vitefu: 1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
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))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 21.1.1
|
||||
yocto-spinner: 0.2.3
|
||||
@@ -15186,10 +15060,6 @@ 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
|
||||
@@ -17593,8 +17463,6 @@ snapshots:
|
||||
|
||||
lru-cache@8.0.5: {}
|
||||
|
||||
lunr@2.3.9: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
lz-utils@2.1.0: {}
|
||||
@@ -18030,10 +17898,6 @@ 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
|
||||
@@ -19963,19 +19827,6 @@ snapshots:
|
||||
|
||||
typed-binary@4.3.2: {}
|
||||
|
||||
typedoc-plugin-markdown@4.11.0(typedoc@0.28.19(typescript@5.9.3)):
|
||||
dependencies:
|
||||
typedoc: 0.28.19(typescript@5.9.3)
|
||||
|
||||
typedoc@0.28.19(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@gerrit0/mini-shiki': 3.23.0
|
||||
lunr: 2.3.9
|
||||
markdown-it: 14.1.1
|
||||
minimatch: 10.2.5
|
||||
typescript: 5.9.3
|
||||
yaml: 2.9.0
|
||||
|
||||
typegpu@0.8.2:
|
||||
dependencies:
|
||||
tinyest: 0.1.2
|
||||
@@ -20260,27 +20111,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.9.0)):
|
||||
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
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.9.0)
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite: 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-dev-rpc@1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
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)):
|
||||
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.9.0)
|
||||
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite: 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-hot-client@2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-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)):
|
||||
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.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@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-hot-client@2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
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.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-plugin-dts@4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
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)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.57.2(@types/node@24.10.4)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
@@ -20293,32 +20144,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.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)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-dts@4.5.4(@types/node@25.0.3)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.57.2(@types/node@25.0.3)
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
'@volar/typescript': 2.4.28
|
||||
'@vue/language-core': 2.2.0(typescript@5.9.3)
|
||||
compare-versions: 6.1.1
|
||||
debug: 4.4.3
|
||||
kolorist: 1.8.0
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.21
|
||||
typescript: 5.9.3
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
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)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -20332,9 +20164,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.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-plugin-html@3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
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)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 4.2.1
|
||||
colorette: 2.0.20
|
||||
@@ -20348,9 +20180,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.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-plugin-inspect@0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
vite-plugin-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)):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
@@ -20361,12 +20193,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.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)
|
||||
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.9.0)):
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
@@ -20376,12 +20208,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.9.0)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
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))
|
||||
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.9.0)):
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
@@ -20391,56 +20223,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.9.0)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
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))
|
||||
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.9.0))(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.2))(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.9.0))(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.2))(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.9.0)
|
||||
vite-plugin-inspect: 0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
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))
|
||||
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.9.0))(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.2))(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.9.0))(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.2))(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.9.0)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
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))
|
||||
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.9.0))(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.2))(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.9.0))(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.2))(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.9.0)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
|
||||
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))
|
||||
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.9.0)):
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||
@@ -20451,11 +20283,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.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)
|
||||
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.9.0)):
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
|
||||
@@ -20466,11 +20298,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.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)
|
||||
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.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):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.115.0
|
||||
lightningcss: 1.32.0
|
||||
@@ -20485,9 +20317,9 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
terser: 5.39.2
|
||||
tsx: 4.19.4
|
||||
yaml: 2.9.0
|
||||
yaml: 2.8.2
|
||||
|
||||
vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
vite@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):
|
||||
dependencies:
|
||||
'@oxc-project/runtime': 0.115.0
|
||||
lightningcss: 1.32.0
|
||||
@@ -20502,16 +20334,16 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
terser: 5.39.2
|
||||
tsx: 4.19.4
|
||||
yaml: 2.9.0
|
||||
yaml: 2.8.2
|
||||
|
||||
vitefu@1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
|
||||
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)):
|
||||
optionalDependencies:
|
||||
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
|
||||
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@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
|
||||
vitest@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):
|
||||
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.9.0))
|
||||
'@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/pretty-format': 4.0.16
|
||||
'@vitest/runner': 4.0.16
|
||||
'@vitest/snapshot': 4.0.16
|
||||
@@ -20528,7 +20360,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.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)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -20550,10 +20382,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.9.0):
|
||||
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):
|
||||
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.9.0))
|
||||
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 4.0.16
|
||||
'@vitest/runner': 4.0.16
|
||||
'@vitest/snapshot': 4.0.16
|
||||
@@ -20570,7 +20402,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.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)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
@@ -20980,7 +20812,7 @@ snapshots:
|
||||
yaml-eslint-parser@1.3.0:
|
||||
dependencies:
|
||||
eslint-visitor-keys: 3.4.3
|
||||
yaml: 2.9.0
|
||||
yaml: 2.8.2
|
||||
|
||||
yaml-language-server@1.20.0:
|
||||
dependencies:
|
||||
@@ -21002,8 +20834,6 @@ snapshots:
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
yaml@2.9.0: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
|
||||
@@ -1,915 +0,0 @@
|
||||
meta:
|
||||
schema_version: 1
|
||||
generated_from:
|
||||
- database.yaml
|
||||
- rollup.yaml
|
||||
- star-cache.yaml
|
||||
generated_by: scripts/build-behavior-categories.py (I-TF.1)
|
||||
source_pattern_count: 62
|
||||
category_count: 37
|
||||
usage_weight_formula: sum_over_members(blast_radius * occurrences)
|
||||
exemplar_ranking: repo_stars desc, then pattern blast_radius desc; distinct (repo, pattern_id)
|
||||
notes:
|
||||
- Categories cluster by intent, not by surface_family. S2 is split into creation / teardown / hydration / interaction /
|
||||
drawing / connection / serialization / properties.
|
||||
- S1 hooks are merged with their prototype-patching equivalents where intent matches (BC.20 node-type reg, BC.22 menus,
|
||||
BC.03 hydration).
|
||||
- S8.P1 isVirtualNode is a registration-time flag, so it lives in BC.20 alongside the node-type registration hooks.
|
||||
- S10.D3 setSize and S15.OS1 dynamic outputs join S10.D1 in BC.09 dynamic-slot-mutation since they all describe runtime
|
||||
topology mutation.
|
||||
- S14.ID1 NodeLocatorId joins S11.G2 graph enumeration in BC.29 because both are about cross-scope node identity/resolution.
|
||||
- S11.G1/G3/G4 (version, batching, setDirtyCanvas) collapse into BC.30 graph change-tracking — the v2 reactivity story replaces
|
||||
all three.
|
||||
- BC.21 (S1.H2 getCustomWidgets) has only 2 evidence rows in database.yaml; this is the 'small family — 2 + 1 minor variant'
|
||||
acceptance carve-out. The two exemplars are kept as-is, no synthetic third row.
|
||||
- BC.31 and BC.32 added 2026-05-08 from Notion API usage research (notion-api-research-evidence.yaml staging).
|
||||
S16 is a new surface family (DOM injection) not previously tracked. S16.VUE1 grouped with BC.32 (embedded runtimes).
|
||||
S3.C2 (ContextMenu replacement) added to BC.06 member list.
|
||||
- Notion source also upgrades occurrence signal on BC.01/BC.02/BC.04/BC.06/BC.07/BC.09/BC.26/BC.29/BC.30 — reflected
|
||||
in staging file; usage_weight values below are NOT yet updated (need re-run of rollup-blast-radius.py after merge).
|
||||
- BC.33 (cross-ext DOM widget obs), BC.34 (settings dialog), BC.35 (pre-queue validation) added 2026-05-08 from Notion COM-3668.
|
||||
- BC.36 (PrimeVue widget API surface) added 2026-05-08 from Notion Widget Component APIs page; was erroneously numbered BC.33 — corrected.
|
||||
- BC.37 (VueNode bridge timing) added 2026-05-08 from Notion Frontend Architecture page (3536d73d). Captures the
|
||||
nodeCreated→VueNode-not-yet-mounted hazard and the waitForLoad3d deferral pattern as a concrete test fixture.
|
||||
categories:
|
||||
- category_id: BC.01
|
||||
name: 'Node lifecycle: creation'
|
||||
intent: Hooks fired when a node is constructed or attached to the graph (per-instance setup).
|
||||
notes: >-
|
||||
nodeCreated fires BEFORE the VueNode Vue component mounts. Extensions that need to access
|
||||
VueNode-backed state (DOM widgets, Three.js renderers, etc.) must defer to onNodeMounted
|
||||
(v2) or waitForLoad3d-style callbacks (v1). See BC.37 for the deferred-mount bridge pattern.
|
||||
Source: Notion Frontend Architecture page (2026-05-08).
|
||||
member_pattern_ids:
|
||||
- S2.N1
|
||||
- S2.N8
|
||||
usage_weight: 37.56
|
||||
exemplars:
|
||||
- pattern_id: S2.N1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/saveImageExtraOutput.ts#L31
|
||||
stars: 1787
|
||||
- pattern_id: S2.N8
|
||||
repo: Azornes/Comfyui-LayerForge
|
||||
url: https://github.com/Azornes/Comfyui-LayerForge/blob/main/src/CanvasView.ts#L1401
|
||||
stars: 313
|
||||
- pattern_id: S2.N1
|
||||
repo: SKBv0/ComfyUI_SpideyReroute
|
||||
url: https://github.com/SKBv0/ComfyUI_SpideyReroute/blob/main/js/SpideyReroute.js#L41
|
||||
stars: 13
|
||||
- category_id: BC.02
|
||||
name: 'Node lifecycle: teardown'
|
||||
intent: Single de-facto teardown surface for cleaning up DOM widgets, intervals, and observers when a node is removed.
|
||||
member_pattern_ids:
|
||||
- S2.N4
|
||||
usage_weight: 29.35
|
||||
exemplars:
|
||||
- pattern_id: S2.N4
|
||||
repo: Lightricks/ComfyUI-LTXVideo
|
||||
url: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L137
|
||||
stars: 3581
|
||||
- pattern_id: S2.N4
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/help_popup.js#L348
|
||||
stars: 2568
|
||||
- pattern_id: S2.N4
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/docs/architecture/ecs-migration-plan.md#L587
|
||||
stars: 1787
|
||||
- category_id: BC.03
|
||||
name: 'Node lifecycle: hydration from saved workflows'
|
||||
intent: React when a node is rehydrated from a stored workflow; the working replacement for the unused loadedGraphNode hook.
|
||||
member_pattern_ids:
|
||||
- S1.H1
|
||||
- S2.N7
|
||||
usage_weight: 15.42
|
||||
exemplars:
|
||||
- pattern_id: S1.H1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H1
|
||||
repo: sofakid/dandy
|
||||
url: https://github.com/sofakid/dandy/blob/main/web/main.js#L114
|
||||
stars: 54
|
||||
- pattern_id: S2.N7
|
||||
repo: akawana/ComfyUI-Folded-Prompts
|
||||
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L1265
|
||||
stars: 4
|
||||
- category_id: BC.04
|
||||
name: 'Node interaction: pointer, selection, resize'
|
||||
intent: 'User-driven per-node events: mouse down for custom click regions, selection focus, and resize feedback for relayout.'
|
||||
member_pattern_ids:
|
||||
- S2.N10
|
||||
- S2.N17
|
||||
- S2.N19
|
||||
usage_weight: 38.07
|
||||
exemplars:
|
||||
- pattern_id: S2.N10
|
||||
repo: diodiogod/TTS-Audio-Suite
|
||||
url: https://github.com/diodiogod/TTS-Audio-Suite/blob/main/web/chatterbox_voice_capture.js#L202
|
||||
stars: 906
|
||||
- pattern_id: S2.N10
|
||||
repo: melMass/comfy_mtb
|
||||
url: https://github.com/melMass/comfy_mtb/blob/main/web/comfy_shared.js#L1047
|
||||
stars: 702
|
||||
- pattern_id: S2.N10
|
||||
repo: pixaroma/ComfyUI-Pixaroma
|
||||
url: https://github.com/pixaroma/ComfyUI-Pixaroma/blob/main/js/compare/index.js#L360
|
||||
stars: 137
|
||||
- category_id: BC.05
|
||||
name: Custom DOM widgets and node sizing
|
||||
intent: Contribute DOM-backed widgets and override computeSize so the node reserves the right area for them.
|
||||
member_pattern_ids:
|
||||
- S4.W2
|
||||
- S2.N11
|
||||
usage_weight: 33.35
|
||||
exemplars:
|
||||
- pattern_id: S4.W2
|
||||
repo: Lightricks/ComfyUI-LTXVideo
|
||||
url: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218
|
||||
stars: 3581
|
||||
- pattern_id: S4.W2
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/editors/editor_base.js#L511
|
||||
stars: 2568
|
||||
- pattern_id: S2.N11
|
||||
repo: o-l-l-i/ComfyUI-Olm-ImageAdjust
|
||||
url: https://github.com/o-l-l-i/ComfyUI-Olm-ImageAdjust/blob/main/web/olm_imageadjust.js#L319
|
||||
stars: 45
|
||||
- category_id: BC.06
|
||||
name: Custom canvas drawing (per-node and canvas-level)
|
||||
intent:
|
||||
Per-node onDrawForeground and full LGraphCanvas.prototype overrides for badges, indicators, keyboard, and custom
|
||||
render passes. Includes global ContextMenu replacement (S3.C2) as the most destructive canvas-level override.
|
||||
v1_scope_note: >-
|
||||
Simon Tranter (COM-3668, 2025-05-12) explicitly vetoed canvas drawing overrides as "too hacky/specific
|
||||
to implement APIs for". Confirmed out of v2 v1 scope. S3.C* patterns remain in DB for blast-radius
|
||||
tracking and strangler-fig planning but v2 need not replace them 1:1. Supports D9 Phase C deferral.
|
||||
member_pattern_ids:
|
||||
- S2.N9
|
||||
- S3.C1
|
||||
- S3.C2
|
||||
usage_weight: 58.97
|
||||
exemplars:
|
||||
- pattern_id: S3.C1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1256
|
||||
stars: 2568
|
||||
- pattern_id: S3.C1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/simpleTouchSupport.ts#L174
|
||||
stars: 1787
|
||||
- pattern_id: S3.C1
|
||||
repo: melMass/comfy_mtb
|
||||
url: https://github.com/melMass/comfy_mtb/blob/main/web/note_plus.js#L1
|
||||
stars: 702
|
||||
- category_id: BC.07
|
||||
name: Connection observation, intercept, and veto
|
||||
intent:
|
||||
Subscribe to link connect/disconnect events on a node and intercept incoming/outgoing connections before they are
|
||||
wired to refuse them, mutate slots, or coerce types.
|
||||
member_pattern_ids:
|
||||
- S2.N3
|
||||
- S2.N12
|
||||
- S2.N13
|
||||
usage_weight: 51.08
|
||||
exemplars:
|
||||
- pattern_id: S2.N13
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/node_mode_relay.js#L90
|
||||
stars: 3049
|
||||
- pattern_id: S2.N12
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/jsnodes.js#L152
|
||||
stars: 2568
|
||||
- pattern_id: S2.N12
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/core/graph/widgets/dynamicWidgets.ts#L539
|
||||
stars: 1787
|
||||
- category_id: BC.08
|
||||
name: Programmatic linking
|
||||
intent: Extensions wire connections from code (workflow templates, auto-routing).
|
||||
member_pattern_ids:
|
||||
- S10.D2
|
||||
usage_weight: 11.81
|
||||
exemplars:
|
||||
- pattern_id: S10.D2
|
||||
repo: MockbaTheBorg/ComfyUI-Mockba
|
||||
url: https://github.com/MockbaTheBorg/ComfyUI-Mockba/blob/main/js/slider.js#L1
|
||||
stars: 1
|
||||
- pattern_id: S10.D2
|
||||
repo: vjumpkung/comfyui-infinitetalk-native-sampler
|
||||
url: https://github.com/vjumpkung/comfyui-infinitetalk-native-sampler/blob/main/README.md#L1
|
||||
stars: 1
|
||||
- pattern_id: S10.D2
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts/blob/main/web/js/quickNodes.js#L138
|
||||
stars: 0
|
||||
- category_id: BC.09
|
||||
name: Dynamic slot and output mutation
|
||||
intent: Grow/shrink inputs and outputs at runtime, with the obligatory computeSize+setSize reflow that follows.
|
||||
member_pattern_ids:
|
||||
- S10.D1
|
||||
- S10.D3
|
||||
- S15.OS1
|
||||
usage_weight: 38.63
|
||||
exemplars:
|
||||
- pattern_id: S10.D1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts#L121
|
||||
stars: 1787
|
||||
- pattern_id: S10.D1
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-mode-nodes.js#L42
|
||||
stars: 19
|
||||
- pattern_id: S15.OS1
|
||||
repo: yorkane/ComfyUI-KYNode
|
||||
url: https://github.com/yorkane/ComfyUI-KYNode/blob/main/web/python-editor.js#L243
|
||||
stars: 10
|
||||
- category_id: BC.10
|
||||
name: Widget value subscription
|
||||
intent: Subscribe to widget value changes either at the widget (callback chain) or node (onWidgetChanged) level.
|
||||
member_pattern_ids:
|
||||
- S4.W1
|
||||
- S2.N14
|
||||
usage_weight: 32.22
|
||||
exemplars:
|
||||
- pattern_id: S2.N14
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/widgetInputs.ts#L317
|
||||
stars: 1787
|
||||
- pattern_id: S4.W1
|
||||
repo: crom8505/ComfyUI-Dynamic-Sigmas
|
||||
url: https://github.com/crom8505/ComfyUI-Dynamic-Sigmas/blob/main/web/js/graph_sigmas.js#L79
|
||||
stars: 8
|
||||
- pattern_id: S4.W1
|
||||
repo: 834t/ComfyUI_834t_scene_composer
|
||||
url: https://github.com/834t/ComfyUI_834t_scene_composer/blob/main/js/b34t_scene_composer.js#L148
|
||||
stars: 5
|
||||
- category_id: BC.11
|
||||
name: Widget imperative state writes
|
||||
intent: Imperatively mutate widget value, COMBO option lists, or the node.widgets array (insert/remove/reorder).
|
||||
member_pattern_ids:
|
||||
- S4.W4
|
||||
- S4.W5
|
||||
- S2.N16
|
||||
usage_weight: 28.42
|
||||
exemplars:
|
||||
- pattern_id: S2.N16
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9
|
||||
stars: 19
|
||||
- pattern_id: S4.W4
|
||||
repo: EnragedAntelope/EA_LMStudio
|
||||
url: https://github.com/EnragedAntelope/EA_LMStudio/blob/main/web/ea_lmstudio.js#L11
|
||||
stars: 7
|
||||
- pattern_id: S4.W4
|
||||
repo: zzggi2024/shaobkj
|
||||
url: https://github.com/zzggi2024/shaobkj/blob/main/js/dynamic_inputs.js#L374
|
||||
stars: 1
|
||||
- category_id: BC.12
|
||||
name: Per-widget serialization transform
|
||||
intent: Transform a widget's value at workflow-serialization time (dynamic prompts, hidden state, expand-on-save).
|
||||
notes: >-
|
||||
widget.options.serialize===false widgets (e.g. control_after_generate) still occupy a widgets_values
|
||||
slot and still fire serializeValue — excluded only from the backend prompt by graphToPrompt(). Test
|
||||
triple must cover this case explicitly. PR #10392 widgets_values_named is the v2 migration path;
|
||||
WidgetHandle identity must be by name not position. See research/architecture/widget-serialization-historical-analysis.md.
|
||||
member_pattern_ids:
|
||||
- S4.W3
|
||||
usage_weight: 27.94
|
||||
exemplars:
|
||||
- pattern_id: S4.W3
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/helpers/painter.ts#L70
|
||||
stars: 1787
|
||||
- pattern_id: S4.W3
|
||||
repo: Raykosan/ComfyUI_RaykoStudio
|
||||
url: https://github.com/Raykosan/ComfyUI_RaykoStudio/blob/main/web/rayko_lora_widget.js#L31
|
||||
stars: 45
|
||||
- pattern_id: S4.W3
|
||||
repo: 834t/ComfyUI_834t_scene_composer
|
||||
url: https://github.com/834t/ComfyUI_834t_scene_composer/blob/main/js/b34t_scene_composer.js#L135
|
||||
stars: 5
|
||||
- category_id: BC.13
|
||||
name: Per-node serialization interception
|
||||
intent: Intercept node-level serialize/onSerialize to inject custom workflow JSON fields.
|
||||
notes: >-
|
||||
Root cause: widgets_values is positional — prototype.serialize patchers consume/produce this array
|
||||
directly. Three index-drift sources: control_after_generate slot occupancy, extension-injected
|
||||
widgets, V3 IO.MultiType topology-dependent widget count. NaN→null pipeline produces silent
|
||||
corruption (backend crash is first visible symptom). Test triple must cover: (a) positional v1
|
||||
compat, (b) named-map v2 round-trip parity, (c) null-in-numeric-widget logs warning + substitutes
|
||||
default. PR #11884 guard, PR #10392 named map. See research/architecture/widget-serialization-historical-analysis.md.
|
||||
member_pattern_ids:
|
||||
- S2.N6
|
||||
- S2.N15
|
||||
usage_weight: 47.07
|
||||
exemplars:
|
||||
- pattern_id: S2.N15
|
||||
repo: Azornes/Comfyui-LayerForge
|
||||
url: https://github.com/Azornes/Comfyui-LayerForge/blob/main/js/CanvasView.js#L1438
|
||||
stars: 313
|
||||
- pattern_id: S2.N15
|
||||
repo: IAMCCS/IAMCCS-nodes
|
||||
url: https://github.com/IAMCCS/IAMCCS-nodes/blob/main/web/iamccs_wan_motion_presets.js#L598
|
||||
stars: 92
|
||||
- pattern_id: S2.N15
|
||||
repo: DazzleNodes/ComfyUI-Smart-Resolution-Calc
|
||||
url: https://github.com/DazzleNodes/ComfyUI-Smart-Resolution-Calc/blob/main/web/utils/serialization.js#L32
|
||||
stars: 7
|
||||
- category_id: BC.14
|
||||
name: Workflow → API serialization interception (graphToPrompt)
|
||||
intent: Patch app.graphToPrompt to resolve virtual nodes, inject custom metadata, or rewrite the API payload before submit.
|
||||
member_pattern_ids:
|
||||
- S6.A1
|
||||
usage_weight: 46.66
|
||||
exemplars:
|
||||
- pattern_id: S6.A1
|
||||
repo: Comfy-Org/ComfyUI-Manager
|
||||
url: https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/js/components-manager.js#L781
|
||||
stars: 14554
|
||||
- pattern_id: S6.A1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1406
|
||||
stars: 2568
|
||||
- pattern_id: S6.A1
|
||||
repo: m3rr/h4_Live
|
||||
url: https://github.com/m3rr/h4_Live/blob/main/js/h4_datastream.js#L23
|
||||
stars: 2
|
||||
- category_id: BC.15
|
||||
name: Workflow loading into the editor
|
||||
intent: External/embed scenario where a workflow JSON is pushed into the running editor via app.loadGraphData.
|
||||
member_pattern_ids:
|
||||
- S6.A2
|
||||
usage_weight: 20.31
|
||||
exemplars:
|
||||
- pattern_id: S6.A2
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/fixtures/helpers/WorkflowHelper.ts#L215
|
||||
stars: 1787
|
||||
- pattern_id: S6.A2
|
||||
repo: BennyKok/comfyui-deploy
|
||||
url: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/workflow-list.js#L456
|
||||
stars: 1507
|
||||
- pattern_id: S6.A2
|
||||
repo: ketle-man/ComfyUI-Workflow-Studio
|
||||
url: https://github.com/ketle-man/ComfyUI-Workflow-Studio/blob/main/static/js/workflow-tab.js#L67
|
||||
stars: 2
|
||||
- category_id: BC.16
|
||||
name: Execution output consumption (per-node)
|
||||
intent: Consume backend execution output on a specific node (text, JSON, image) to drive display.
|
||||
member_pattern_ids:
|
||||
- S2.N2
|
||||
usage_weight: 5.74
|
||||
exemplars:
|
||||
- pattern_id: S2.N2
|
||||
repo: andreszs/ComfyUI-Ultralytics-Studio
|
||||
url: https://github.com/andreszs/ComfyUI-Ultralytics-Studio/blob/main/js/show_string.js#L9
|
||||
stars: 3
|
||||
- pattern_id: S2.N2
|
||||
repo: AlexZ1967/ComfyUI_ALEXZ_tools
|
||||
url: https://github.com/AlexZ1967/ComfyUI_ALEXZ_tools/blob/main/web/show_json.js#L49
|
||||
stars: 0
|
||||
- pattern_id: S2.N2
|
||||
repo: becky3/comfyui-workspace
|
||||
url: https://github.com/becky3/comfyui-workspace/blob/main/custom_nodes/ComfyUI-Becky3-Common/js/show_text.js#L33
|
||||
stars: 0
|
||||
- category_id: BC.17
|
||||
name: Backend execution lifecycle and progress events
|
||||
intent: Subscribe to api.addEventListener for execution_*, progress, status, and reconnecting events.
|
||||
member_pattern_ids:
|
||||
- S5.A1
|
||||
- S5.A2
|
||||
- S5.A3
|
||||
usage_weight: 51.25
|
||||
exemplars:
|
||||
- pattern_id: S5.A2
|
||||
repo: AIGODLIKE/AIGODLIKE-ComfyUI-Studio
|
||||
url: https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Studio/blob/main/loader/components/public/iconRenderer.js#L39
|
||||
stars: 405
|
||||
- pattern_id: S5.A3
|
||||
repo: kyuz0/amd-strix-halo-comfyui-toolboxes
|
||||
url: https://github.com/kyuz0/amd-strix-halo-comfyui-toolboxes/blob/main/scripts/benchmark_workflows.py#L52
|
||||
stars: 109
|
||||
- pattern_id: S5.A1
|
||||
repo: ShakerSmith/ShakerNodesSuite
|
||||
url: https://github.com/ShakerSmith/ShakerNodesSuite/blob/main/js/shaker_preview_ui.js#L58
|
||||
stars: 8
|
||||
- category_id: BC.18
|
||||
name: Backend HTTP calls
|
||||
intent: Call ComfyAPI.fetchApi as the canonical authenticated path to backend HTTP endpoints.
|
||||
member_pattern_ids:
|
||||
- S6.A3
|
||||
usage_weight: 22.74
|
||||
exemplars:
|
||||
- pattern_id: S6.A3
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/components/common/BackgroundImageUpload.vue#L61
|
||||
stars: 1787
|
||||
- pattern_id: S6.A3
|
||||
repo: akawana/ComfyUI-Folded-Prompts
|
||||
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L1227
|
||||
stars: 4
|
||||
- pattern_id: S6.A3
|
||||
repo: zhupeter010903/ComfyUI-XYZ-prompt-library
|
||||
url: https://github.com/zhupeter010903/ComfyUI-XYZ-prompt-library/blob/main/js/prompt_library_window.js#L1379
|
||||
stars: 1
|
||||
- category_id: BC.19
|
||||
name: Workflow execution trigger
|
||||
intent: Trigger or intercept queuePrompt for sidebar Run buttons, auth tokens, or payload mutation.
|
||||
member_pattern_ids:
|
||||
- S6.A4
|
||||
usage_weight: 12.65
|
||||
exemplars:
|
||||
- pattern_id: S6.A4
|
||||
repo: MajoorWaldi/ComfyUI-Majoor-AssetsManager
|
||||
url: https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/blob/main/js/features/viewer/workflowSidebar/sidebarRunButton.js#L317
|
||||
stars: 97
|
||||
- pattern_id: S6.A4
|
||||
repo: gigici/ComfyUI_BlendPack
|
||||
url: https://github.com/gigici/ComfyUI_BlendPack/blob/main/js/ui/NodeUI.js#L99
|
||||
stars: 1
|
||||
- pattern_id: S6.A4
|
||||
repo: rohapa/comfyui-replay
|
||||
url: https://github.com/rohapa/comfyui-replay/blob/main/README.md#L497
|
||||
stars: 0
|
||||
- category_id: BC.20
|
||||
name: Custom node-type registration (frontend-only / virtual)
|
||||
intent: Register pure-frontend or fully virtual node types and mark them with isVirtualNode so the backend ignores them.
|
||||
member_pattern_ids:
|
||||
- S1.H5
|
||||
- S1.H6
|
||||
- S8.P1
|
||||
usage_weight: 27.49
|
||||
exemplars:
|
||||
- pattern_id: S1.H6
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/rerouteNode.ts
|
||||
stars: 1787
|
||||
- pattern_id: S1.H5
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H6
|
||||
repo: sofakid/dandy
|
||||
url: https://github.com/sofakid/dandy/blob/main/web/main.js#L111
|
||||
stars: 54
|
||||
- category_id: BC.21
|
||||
name: Custom widget-type registration
|
||||
intent: Register new widget types (color picker, file uploader, custom inputs) via getCustomWidgets.
|
||||
member_pattern_ids:
|
||||
- S1.H2
|
||||
usage_weight: 7.17
|
||||
exemplars:
|
||||
- pattern_id: S1.H2
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H2
|
||||
repo: haohaocreates/PR-rk-comfy-nodes-36d8f0a5
|
||||
url: https://github.com/haohaocreates/PR-rk-comfy-nodes-36d8f0a5/blob/main/web/rk_nodes.ts#L22
|
||||
stars: 0
|
||||
- category_id: BC.22
|
||||
name: Context menu contributions (node and canvas)
|
||||
intent:
|
||||
Contribute right-click menu items at both the node and canvas scope, including legacy prototype patches and the
|
||||
supported v1 hooks.
|
||||
member_pattern_ids:
|
||||
- S2.N5
|
||||
- S1.H3
|
||||
- S1.H4
|
||||
usage_weight: 19.53
|
||||
exemplars:
|
||||
- pattern_id: S1.H3
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H4
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
|
||||
stars: 1787
|
||||
- pattern_id: S1.H3
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-canvas-utils.js#L2
|
||||
stars: 19
|
||||
- category_id: BC.23
|
||||
name: Node property bag mutations
|
||||
intent: React to mutations of node.properties — the persistent property bag that survives serialization.
|
||||
member_pattern_ids:
|
||||
- S2.N18
|
||||
usage_weight: 14.42
|
||||
exemplars:
|
||||
- pattern_id: S2.N18
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/src_web/comfyui/seed.ts#L78
|
||||
stars: 3049
|
||||
- pattern_id: S2.N18
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/seed.js#L26
|
||||
stars: 3049
|
||||
- pattern_id: S2.N18
|
||||
repo: rgthree/rgthree-comfy
|
||||
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/power_primitive.js#L142
|
||||
stars: 3049
|
||||
- category_id: BC.24
|
||||
name: Node-def schema inspection
|
||||
intent: Branch on ComfyNodeDef shape (input.required/optional/hidden, output, output_node, category) to drive UI.
|
||||
member_pattern_ids:
|
||||
- S13.SC1
|
||||
usage_weight: 22.43
|
||||
exemplars:
|
||||
- pattern_id: S13.SC1
|
||||
repo: BennyKok/comfyui-deploy
|
||||
url: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/index.js#L1
|
||||
stars: 1507
|
||||
- pattern_id: S13.SC1
|
||||
repo: StableLlama/ComfyUI-basic_data_handling
|
||||
url: https://github.com/StableLlama/ComfyUI-basic_data_handling/blob/main/web/js/dynamicnode.js#L1
|
||||
stars: 43
|
||||
- pattern_id: S13.SC1
|
||||
repo: xeinherjer-dev/ComfyUI-XENodes
|
||||
url: https://github.com/xeinherjer-dev/ComfyUI-XENodes/blob/main/web/js/combo_selector.js#L1
|
||||
stars: 1
|
||||
- category_id: BC.25
|
||||
name: Shell UI registration (commands, sidebars, toasts)
|
||||
intent: Declarative shell-UI contributions through extensionManager / commandManager / sidebarTab / bottomPanel.
|
||||
member_pattern_ids:
|
||||
- S12.UI1
|
||||
usage_weight: 10.98
|
||||
exemplars:
|
||||
- pattern_id: S12.UI1
|
||||
repo: robertvoy/ComfyUI-Distributed
|
||||
url: https://github.com/robertvoy/ComfyUI-Distributed/blob/main/web/main.js#L269
|
||||
stars: 544
|
||||
- pattern_id: S12.UI1
|
||||
repo: maxi45274/ComfyUI_LinkFX
|
||||
url: https://github.com/maxi45274/ComfyUI_LinkFX/blob/main/js/LinkFX.js#L707
|
||||
stars: 3
|
||||
- pattern_id: S12.UI1
|
||||
repo: criskb/Comfypencil
|
||||
url: https://github.com/criskb/Comfypencil/blob/main/web/comfy_pencil_extension.js#L955
|
||||
stars: 0
|
||||
- category_id: BC.26
|
||||
name: Globals as ABI (window.LiteGraph, window.comfyAPI)
|
||||
intent: Reach into the global namespace for LiteGraph constructors/enums or the module-as-global comfyAPI registry.
|
||||
member_pattern_ids:
|
||||
- S7.G1
|
||||
usage_weight: 27.0
|
||||
exemplars:
|
||||
- pattern_id: S7.G1
|
||||
repo: ryanontheinside/ComfyUI_RyanOnTheInside
|
||||
url: https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside/blob/main/web/js/index.js#L1
|
||||
stars: 801
|
||||
- pattern_id: S7.G1
|
||||
repo: ArtHommage/HommageTools
|
||||
url: https://github.com/ArtHommage/HommageTools/blob/main/web/js/index.js#L1
|
||||
stars: 4
|
||||
- pattern_id: S7.G1
|
||||
repo: PROJECTMAD/PROJECT-MAD-NODES
|
||||
url: https://github.com/PROJECTMAD/PROJECT-MAD-NODES/blob/main/web/js/index.js#L1
|
||||
stars: 4
|
||||
- category_id: BC.27
|
||||
name: LiteGraph entity direct manipulation (reroute, group, link, slot)
|
||||
intent: Direct read/mutation of reroutes, groups, links, and slots — no public extension API exists today.
|
||||
member_pattern_ids:
|
||||
- S9.R1
|
||||
- S9.G1
|
||||
- S9.L1
|
||||
- S9.S1
|
||||
usage_weight: 39.37
|
||||
exemplars:
|
||||
- pattern_id: S9.R1
|
||||
repo: nodetool-ai/nodetool
|
||||
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
|
||||
stars: 330
|
||||
- pattern_id: S9.S1
|
||||
repo: nodetool-ai/nodetool
|
||||
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L267
|
||||
stars: 330
|
||||
- pattern_id: S9.S1
|
||||
repo: Stibo/comfyui-nifty-nodes
|
||||
url: https://github.com/Stibo/comfyui-nifty-nodes/blob/main/js/nifty_nodes.js#L112
|
||||
stars: 3
|
||||
- category_id: BC.28
|
||||
name: Subgraph fan-out via set/get virtual nodes
|
||||
intent: Fan out a single named value across the graph without explicit links (KJNodes-style Set/Get nodes).
|
||||
member_pattern_ids:
|
||||
- S9.SG1
|
||||
usage_weight: 16.89
|
||||
exemplars:
|
||||
- pattern_id: S9.SG1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1406
|
||||
stars: 2568
|
||||
- pattern_id: S9.SG1
|
||||
repo: krismasdev/ComfyUI-Flux-Continuum
|
||||
url: https://github.com/krismasdev/ComfyUI-Flux-Continuum/blob/main/web/hint.js#L1
|
||||
stars: 0
|
||||
- pattern_id: S9.SG1
|
||||
repo: SpaceWarpStudio/ComfyUI-SetInputGetOutput
|
||||
url: https://github.com/SpaceWarpStudio/ComfyUI-SetInputGetOutput/blob/main/web/js/setinputgetoutput.js#L1
|
||||
stars: 0
|
||||
- category_id: BC.29
|
||||
name: Graph enumeration, mutation, and cross-scope identity
|
||||
intent:
|
||||
Enumerate or mutate the node set (graph.add/remove/findNodesByType/serialize/configure) and resolve cross-subgraph
|
||||
references via NodeLocatorId / NodeExecutionId.
|
||||
member_pattern_ids:
|
||||
- S11.G2
|
||||
- S14.ID1
|
||||
usage_weight: 23.56
|
||||
exemplars:
|
||||
- pattern_id: S11.G2
|
||||
repo: yolain/ComfyUI-Easy-Use
|
||||
url: https://github.com/yolain/ComfyUI-Easy-Use/blob/main/web_version/v1/js/easy/easyExtraMenu.js#L439
|
||||
stars: 2503
|
||||
- pattern_id: S11.G2
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/tests/workflowPersistence.spec.ts#L351
|
||||
stars: 1787
|
||||
- pattern_id: S11.G2
|
||||
repo: r-vage/ComfyUI_Eclipse
|
||||
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-ui-enhancements.js#L29
|
||||
stars: 19
|
||||
- category_id: BC.30
|
||||
name: Graph change tracking, batching, and reactivity flush
|
||||
intent:
|
||||
'Coordinate graph-level change: graph._version monotonic counter, beforeChange/afterChange batching, and the imperative
|
||||
setDirtyCanvas redraw flush.'
|
||||
member_pattern_ids:
|
||||
- S11.G1
|
||||
- S11.G3
|
||||
- S11.G4
|
||||
usage_weight: 34.38
|
||||
exemplars:
|
||||
- pattern_id: S11.G3
|
||||
repo: nodetool-ai/nodetool
|
||||
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
|
||||
stars: 330
|
||||
- pattern_id: S11.G4
|
||||
repo: akawana/ComfyUI-Folded-Prompts
|
||||
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L776
|
||||
stars: 4
|
||||
- pattern_id: S11.G3
|
||||
repo: linjm8780860/ljm_comfyui
|
||||
url: https://github.com/linjm8780860/ljm_comfyui/blob/main/src/utils/vintageClipboard.ts#L1
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.31
|
||||
name: DOM injection and style management
|
||||
intent:
|
||||
Extensions add UI chrome, toolbars, and style overrides directly into the document outside any provided API —
|
||||
style tags into head, arbitrary elements into body, innerHTML rendering, and external script loading.
|
||||
member_pattern_ids:
|
||||
- S16.DOM1
|
||||
- S16.DOM2
|
||||
- S16.DOM3
|
||||
- S16.DOM4
|
||||
usage_weight: 0.0
|
||||
notes:
|
||||
'usage_weight pending rollup-blast-radius.py re-run after database.yaml merge (I-N4.1). Notion counts: DOM1=354
|
||||
occ, DOM2=364 occ, DOM3=443 occ, DOM4=232 occ across ~81 packages — among the highest raw occurrence counts in
|
||||
the entire dataset. v2 replacements: injectStyles(), addPanel(), addToolbarItem(), safe HTML rendering API.'
|
||||
exemplars:
|
||||
- pattern_id: S16.DOM1
|
||||
repo: kijai/ComfyUI-KJNodes
|
||||
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/help_popup.js
|
||||
stars: 2568
|
||||
- pattern_id: S16.DOM2
|
||||
repo: yolain/ComfyUI-Easy-Use
|
||||
url: https://github.com/yolain/ComfyUI-Easy-Use/blob/main/web_version/v1/js/easy/easy.js
|
||||
stars: 2503
|
||||
- pattern_id: S16.DOM3
|
||||
repo: '(aggregate — Notion §2.3)'
|
||||
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.32
|
||||
name: Embedded framework runtimes and Vue widget bundling
|
||||
intent:
|
||||
Extensions bundle their own copy of Vue (or another framework) inside a DOM widget, bypassing the host app
|
||||
instance and losing access to shared stores, i18n, and theme.
|
||||
member_pattern_ids:
|
||||
- S16.VUE1
|
||||
usage_weight: 0.0
|
||||
notes:
|
||||
'usage_weight pending rollup-blast-radius.py re-run. 9 packages confirmed (Notion §2.9). v2 replacement:
|
||||
registerVueWidget(nodeType, name, Component) sharing host Vue instance — already in plans/P1 §5 Custom widget type.
|
||||
This BC provides the evidence base for that P1 design decision.'
|
||||
exemplars:
|
||||
- pattern_id: S16.VUE1
|
||||
repo: ComfyUI-NKD-Sigmas-Curve
|
||||
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
|
||||
stars: 0
|
||||
- pattern_id: S16.VUE1
|
||||
repo: '(aggregate — 9 packages, Notion §2.9)'
|
||||
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
|
||||
stars: 0
|
||||
|
||||
# ── Categories added 2026-05-08 from Notion COM-3668 (Simon Tranter, Custom Scripts API requirements) ──
|
||||
|
||||
- category_id: BC.33
|
||||
name: Cross-extension DOM widget creation observation
|
||||
intent:
|
||||
An extension observes when *any* DOM widget is created (by any other extension) so it can attach its own
|
||||
listeners — the mechanism the Autocomplete extension needs to wire its input handler to every text widget.
|
||||
member_pattern_ids:
|
||||
- S4.W6
|
||||
usage_weight: 0.0
|
||||
notes: >-
|
||||
Identified from COM-3668. Distinct from BC.05 (creating DOM widgets) and BC.10 (subscribing to value changes).
|
||||
Gap: no v1 hook fires for cross-extension widget creation observation. v2 shape: onDOMWidgetCreated(handler)
|
||||
in defineExtension setup context. usage_weight pending blast-radius re-run.
|
||||
source: notion-COM-3668
|
||||
exemplars:
|
||||
- pattern_id: S4.W6
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.34
|
||||
name: Settings-panel custom dialog integration
|
||||
intent: Extensions open custom modal dialogs triggered from the settings panel, rather than injecting raw DOM.
|
||||
member_pattern_ids:
|
||||
- S12.UI3
|
||||
usage_weight: 0.0
|
||||
notes: >-
|
||||
Identified from COM-3668. Currently worked around via S16.DOM3 innerHTML injection. Distinct from S12.UI1
|
||||
(sidebar/command registration) — this is about dialog lifecycle tied to settings entries. v2 shape:
|
||||
app.ui.openDialog(Component) or settings entry type 'dialog-trigger'. usage_weight pending blast-radius re-run.
|
||||
source: notion-COM-3668
|
||||
exemplars:
|
||||
- pattern_id: S12.UI3
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.35
|
||||
name: Pre-queue widget validation
|
||||
intent:
|
||||
Validate widget values before a workflow is submitted and surface typed errors to the user — rejecting
|
||||
the queue rather than silently mutating or failing.
|
||||
member_pattern_ids:
|
||||
- S6.A5
|
||||
usage_weight: 0.0
|
||||
notes: >-
|
||||
Identified from COM-3668. Currently worked around via S6.A4 queuePrompt monkey-patching (silent_breakage=true
|
||||
when multiple extensions patch). Distinct from D5 beforeSerialize (transforms values) and BC.19 (triggers
|
||||
execution). v2 needs explicit beforeQueue event with event.reject(message). usage_weight pending re-run.
|
||||
source: notion-COM-3668
|
||||
exemplars:
|
||||
- pattern_id: S6.A5
|
||||
repo: goodtab/ComfyUI-Custom-Scripts
|
||||
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.36
|
||||
name: PrimeVue widget component API surface
|
||||
intent: >-
|
||||
Custom node authors configuring widget behavior via per-component prop subsets — the v2 replacement
|
||||
for direct widget.options mutation (S4.W4, S4.W1) and DOM widget construction (S4.W5, S4.W6).
|
||||
15 PrimeVue components are the authoritative widget-kind enumeration for v2.
|
||||
member_pattern_ids:
|
||||
- S4.W1
|
||||
- S4.W4
|
||||
- S4.W5
|
||||
notes: >-
|
||||
Source: Notion page "Widget Component APIs" (2026-05-08). 15 components: Button, InputText, Select,
|
||||
ColorPicker, MultiSelect, SelectButton, Slider, Textarea, ToggleSwitch, Chart, Image, ImageCompare,
|
||||
Galleria, FileUpload, TreeSelect. Exclusion rule (Pablo): strip style/class/dt/pt/*Class/*Style.
|
||||
ToggleSwitch is the only component with completed Pick<> types so far (WIP).
|
||||
Informs: D7 typed options bags (future pivot), I-TF.2 widget-kind test triples,
|
||||
PKG2 WidgetHandle.getOption key surface. disabled/readonly map to D7 first-class fields,
|
||||
not options bag.
|
||||
usage_weight: 0.0
|
||||
exemplars:
|
||||
- pattern_id: S4.W4
|
||||
repo: '(see database.yaml S4.W4 exemplars — widget.options.values mutation)'
|
||||
url: https://www.notion.so/comfy-org/Widget-Component-APIs-2126d73d365080b0bf30f241c09dd756
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.37
|
||||
name: VueNode bridge timing — deferred mount access
|
||||
intent: >-
|
||||
Extensions that register in nodeCreated but need to access Vue-component-backed state
|
||||
(Three.js renderer, DOM widget, ComponentWidgetImpl value) must defer until the Vue
|
||||
component's onMounted fires. The v1 pattern is waitForLoad3d(node, cb); the v2 pattern
|
||||
is onNodeMounted(() => { ... }) inside defineNodeExtension.
|
||||
member_pattern_ids:
|
||||
- S4.W5
|
||||
notes: >-
|
||||
Source: Notion Frontend Architecture page 3536d73d (2026-05-08). nodeCreated gives the
|
||||
LiteGraph node; the VueNode Vue component has NOT mounted yet. waitForLoad3d in
|
||||
src/extensions/core/Load3D is the canonical v1 fixture. ComponentWidgetImpl dual-identity:
|
||||
LiteGraph side (value/callback/name) vs Vue side (props/emits/lifecycle).
|
||||
v2 contract: onNodeMounted() hook fires after Vue component mount — this is the correct
|
||||
timing for accessing VueNode-backed resources.
|
||||
Informs: I-SR.2.B2 (NodeInstanceScope must not sync-access VueNode at setup time),
|
||||
I-TF.3.C1 (harness must simulate two-phase mount), I-TF.2 test triple for BC.37.
|
||||
D8 relevance: app.rootGraph is not reactive (confirmed by this doc) — the exact gap D8 solves.
|
||||
usage_weight: 0.0
|
||||
source: notion-frontend-architecture-3536d73d
|
||||
exemplars:
|
||||
- pattern_id: S4.W5
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/load3d.ts
|
||||
stars: 1787
|
||||
|
||||
- category_id: BC.38
|
||||
name: Canvas mode observation
|
||||
intent: >-
|
||||
Detect and react to ComfyUI canvas mode transitions (graph / app / builder:inputs /
|
||||
builder:outputs / builder:arrange). Custom nodes that adapt rendering, widget resize
|
||||
behavior, or read-only state across modes need a stable event — not polling or heuristics
|
||||
against internal Pinia store state.
|
||||
member_pattern_ids:
|
||||
- S17.AM1
|
||||
mechanism: absent-api
|
||||
notes: >-
|
||||
appModeStore is a Pinia composable; JS extensions cannot use Vue composables. v2 gap:
|
||||
no node.on('canvasModeChanged') exists yet in node.ts — distinct from NodeModeChangedEvent
|
||||
(execution mode only). v2 contract: app-level or node-level canvasModeChanged event.
|
||||
Flagged: Terry DX walkthrough A.1. Informs: node.ts overloads (add canvasModeChanged
|
||||
or document as known gap), I-TF.2 test triple for BC.38.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.AM1
|
||||
repo: (first-principles assessment — Terry Jia)
|
||||
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.39
|
||||
name: Subgraph boundary event propagation
|
||||
intent: >-
|
||||
Custom node callbacks (onExecuted, MatchType, autogrow onConnectionsChange, promoted widget
|
||||
callbacks) that must propagate across subgraph boundaries. Four distinct silent-failure modes
|
||||
when custom nodes are placed inside subgraphs.
|
||||
member_pattern_ids:
|
||||
- S17.SB1
|
||||
mechanism: absent-api
|
||||
notes: >-
|
||||
Requires D9 Phase B (post-Alex rebase on #11939). ECS substrate must forward SubgraphNode
|
||||
execution events from internal nodes. MatchType and autogrow propagation require subgraph
|
||||
boundary awareness in World dispatcher. Blocked: I-PG.B1. Short-term: @experimental on
|
||||
affected NodeHandle events; subgraphCompatible flag in NodeExtensionOptions.
|
||||
Intersects: ADR 0006 (I-NEW.1), Austin's fix-linked-widget-promotion.
|
||||
Flagged: Terry DX walkthrough A.2.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.SB1
|
||||
repo: (first-principles assessment — Terry Jia)
|
||||
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.40
|
||||
name: File upload and asset URL construction
|
||||
intent: >-
|
||||
Upload files to ComfyUI backend and construct retrieval URLs. 32+ packages duplicate this
|
||||
pattern from scratch — FormData construction, fetchApi('/upload/image'), /view?filename URL
|
||||
assembly. A helper API would collapse this to comfyAPI.uploadFile() + comfyAPI.getFileUrl().
|
||||
member_pattern_ids:
|
||||
- S17.FA1
|
||||
mechanism: absent-api
|
||||
notes: >-
|
||||
Out of scope for @comfyorg/extension-api (node extension surface). Belongs in future
|
||||
@comfyorg/comfy-api package. 32+ packages affected; 9 implement video upload variants.
|
||||
Upload timeout hardcoded 120s; large 3D/video fail silently. No temp file lifecycle.
|
||||
Document as known gap in src/extension-api/README.md.
|
||||
Flagged: Terry DX walkthrough A.3.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.FA1
|
||||
repo: (first-principles assessment — Terry Jia)
|
||||
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
|
||||
stars: 0
|
||||
|
||||
- category_id: BC.41
|
||||
name: Widget values positional serialization fragility
|
||||
intent: >-
|
||||
Widget values serialized as positional array [v1, v2, v3] instead of named dict.
|
||||
Any input definition change (add, reorder, rename, remove, required→optional) silently
|
||||
misaligns values when loading existing workflows. Root cause of #1 user complaint:
|
||||
"my workflow broke after I updated the custom node."
|
||||
member_pattern_ids:
|
||||
- S17.WV1
|
||||
mechanism: positional-array
|
||||
notes: >-
|
||||
Blocked on workflow-schema-migration (out of v2 surface scope). D7 Part 4 (4→2
|
||||
serialization collapse) + beforeSerialize as partial mitigation. Long-term fix: named dict
|
||||
format { widgetName: value } — breaking JSON schema change requiring versioning +
|
||||
migrateWidgetValues() callback. PR #10392 added widgets_values_named opt-in; PR #11884
|
||||
null guard. v2 contract: name-keyed identity (WidgetHandle by name not position).
|
||||
Intersects: ADR 0006, widget-serialization-historical-analysis.md, Austin's work.
|
||||
Flagged: Terry DX walkthrough A.4.
|
||||
usage_weight: 0.0
|
||||
source: notion-pain-point-assessment
|
||||
exemplars:
|
||||
- pattern_id: S17.WV1
|
||||
repo: Comfy-Org/ComfyUI_frontend
|
||||
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/utils/nodeDefOrderingUtil.ts
|
||||
stars: 1787
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Compat-floor gate: Verify all high-impact behavior categories have test triples.
|
||||
|
||||
Per PLAN.md §Compat-floor: "Every blast_radius ≥ 2.0 pattern MUST pass v1 + v2 +
|
||||
migration tests before v2 ships."
|
||||
|
||||
This script:
|
||||
1. Reads research/touch-points/behavior-categories.yaml
|
||||
2. Finds all categories with usage_weight >= 2.0 (blast_radius threshold)
|
||||
3. Checks that each has all three test files: bc-XX.v1.test.ts, bc-XX.v2.test.ts, bc-XX.migration.test.ts
|
||||
4. Exits 0 if all present, exits 1 if any missing (fails CI)
|
||||
|
||||
Usage: python3 scripts/check-compat-floor.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("ERROR: PyYAML not installed. Run: pip install pyyaml", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
COMPAT_FLOOR_THRESHOLD = 2.0
|
||||
BEHAVIOR_CATEGORIES_PATH = Path("research/touch-points/behavior-categories.yaml")
|
||||
TESTS_DIR = Path("src/extension-api-v2/__tests__")
|
||||
|
||||
def main():
|
||||
# Check that behavior-categories.yaml exists
|
||||
if not BEHAVIOR_CATEGORIES_PATH.exists():
|
||||
print(f"ERROR: {BEHAVIOR_CATEGORIES_PATH} not found", file=sys.stderr)
|
||||
print(" Run scripts/build-behavior-categories.py first or copy from workspace", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Skip check if tests directory doesn't exist (tests only in tf branch)
|
||||
if not TESTS_DIR.exists():
|
||||
print(f"SKIP: {TESTS_DIR} not found — compat-floor tests not yet added to this branch")
|
||||
print(" The compat-floor gate only enforces on branches with extension-api-v2 tests.")
|
||||
sys.exit(0)
|
||||
|
||||
# Load categories
|
||||
with open(BEHAVIOR_CATEGORIES_PATH, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
categories = data.get("categories", [])
|
||||
|
||||
# Find categories above compat floor
|
||||
above_floor = []
|
||||
for cat in categories:
|
||||
cat_id = cat.get("category_id", "")
|
||||
usage_weight = cat.get("usage_weight", 0)
|
||||
if usage_weight >= COMPAT_FLOOR_THRESHOLD:
|
||||
above_floor.append({
|
||||
"id": cat_id,
|
||||
"name": cat.get("name", ""),
|
||||
"usage_weight": usage_weight
|
||||
})
|
||||
|
||||
print(f"Compat-floor check: {len(above_floor)} categories with usage_weight >= {COMPAT_FLOOR_THRESHOLD}")
|
||||
print()
|
||||
|
||||
# Check each category for test triples
|
||||
missing = []
|
||||
for cat in above_floor:
|
||||
cat_id = cat["id"]
|
||||
# Extract number from BC.XX
|
||||
num_str = cat_id.replace("BC.", "").zfill(2)
|
||||
|
||||
required_files = [
|
||||
f"bc-{num_str}.v1.test.ts",
|
||||
f"bc-{num_str}.v2.test.ts",
|
||||
f"bc-{num_str}.migration.test.ts"
|
||||
]
|
||||
|
||||
cat_missing = []
|
||||
for fname in required_files:
|
||||
fpath = TESTS_DIR / fname
|
||||
if not fpath.exists():
|
||||
cat_missing.append(fname)
|
||||
|
||||
if cat_missing:
|
||||
missing.append({
|
||||
"category": cat_id,
|
||||
"name": cat["name"],
|
||||
"usage_weight": cat["usage_weight"],
|
||||
"missing": cat_missing
|
||||
})
|
||||
status = "❌ MISSING"
|
||||
else:
|
||||
status = "✅"
|
||||
|
||||
print(f" {cat_id} ({cat['usage_weight']:.2f}) {cat['name'][:40]:<40} {status}")
|
||||
if cat_missing:
|
||||
for m in cat_missing:
|
||||
print(f" └─ {m}")
|
||||
|
||||
print()
|
||||
|
||||
if missing:
|
||||
print(f"FAIL: {len(missing)} categories missing test files", file=sys.stderr)
|
||||
print()
|
||||
print("Per PLAN.md §Compat-floor, all blast_radius >= 2.0 categories", file=sys.stderr)
|
||||
print("must have complete test triples (v1, v2, migration) before v2 ships.", file=sys.stderr)
|
||||
print()
|
||||
print("Missing files:", file=sys.stderr)
|
||||
for m in missing:
|
||||
for f in m["missing"]:
|
||||
print(f" - {TESTS_DIR / f}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"PASS: All {len(above_floor)} compat-floor categories have test triples")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/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
|
||||
@@ -11,6 +11,7 @@ declare global {
|
||||
const __ALGOLIA_APP_ID__: string
|
||||
const __ALGOLIA_API_KEY__: string
|
||||
const __USE_PROD_CONFIG__: boolean
|
||||
const __DEV_SERVER_COMFYUI_URL__: string
|
||||
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
|
||||
const __IS_NIGHTLY__: boolean
|
||||
}
|
||||
@@ -22,6 +23,7 @@ type GlobalWithDefines = typeof globalThis & {
|
||||
__ALGOLIA_APP_ID__: string
|
||||
__ALGOLIA_API_KEY__: string
|
||||
__USE_PROD_CONFIG__: boolean
|
||||
__DEV_SERVER_COMFYUI_URL__: string
|
||||
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
|
||||
__IS_NIGHTLY__: boolean
|
||||
window?: Record<string, unknown>
|
||||
@@ -37,6 +39,7 @@ globalWithDefines.__SENTRY_DSN__ = ''
|
||||
globalWithDefines.__ALGOLIA_APP_ID__ = ''
|
||||
globalWithDefines.__ALGOLIA_API_KEY__ = ''
|
||||
globalWithDefines.__USE_PROD_CONFIG__ = false
|
||||
globalWithDefines.__DEV_SERVER_COMFYUI_URL__ = ''
|
||||
globalWithDefines.__DISTRIBUTION__ = 'localhost'
|
||||
globalWithDefines.__IS_NIGHTLY__ = false
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<DialogOverlay />
|
||||
<DialogContent
|
||||
:size="item.dialogComponentProps.size ?? 'md'"
|
||||
:class="item.dialogComponentProps.contentClass"
|
||||
:aria-labelledby="item.key"
|
||||
@escape-key-down="
|
||||
(e) =>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import ConfirmationDialogContent from './ConfirmationDialogContent.vue'
|
||||
|
||||
type Props = ComponentProps<typeof ConfirmationDialogContent>
|
||||
@@ -13,23 +13,7 @@ type Props = ComponentProps<typeof ConfirmationDialogContent>
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: {
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
delete: 'Delete',
|
||||
overwrite: 'Overwrite',
|
||||
save: 'Save',
|
||||
no: 'No',
|
||||
ok: 'OK',
|
||||
close: 'Close'
|
||||
},
|
||||
desktopMenu: {
|
||||
reinstall: 'Reinstall'
|
||||
}
|
||||
}
|
||||
},
|
||||
messages: { en: {} },
|
||||
missingWarn: false,
|
||||
fallbackWarn: false
|
||||
})
|
||||
@@ -40,9 +24,10 @@ describe('ConfirmationDialogContent', () => {
|
||||
})
|
||||
|
||||
function renderComponent(props: Partial<Props> = {}) {
|
||||
const user = userEvent.setup()
|
||||
render(ConfirmationDialogContent, {
|
||||
global: { plugins: [i18n] },
|
||||
return render(ConfirmationDialogContent, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n]
|
||||
},
|
||||
props: {
|
||||
message: 'Test message',
|
||||
type: 'default',
|
||||
@@ -50,7 +35,6 @@ describe('ConfirmationDialogContent', () => {
|
||||
...props
|
||||
} as Props
|
||||
})
|
||||
return { user }
|
||||
}
|
||||
|
||||
it('renders long messages without breaking layout', () => {
|
||||
@@ -60,103 +44,42 @@ describe('ConfirmationDialogContent', () => {
|
||||
expect(screen.getByText(longFilename)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the hint as a status alert when provided', () => {
|
||||
renderComponent({ hint: 'This action cannot be undone.' })
|
||||
const status = screen.getByRole('status')
|
||||
expect(status).toHaveTextContent('This action cannot be undone.')
|
||||
it('omits the Cancel button when type is dirtyClose', () => {
|
||||
renderComponent({ type: 'dirtyClose' })
|
||||
expect(screen.queryByText('g.cancel')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('g.save')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render a status alert when hint is omitted', () => {
|
||||
renderComponent()
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
it('uses the provided denyLabel for the deny button on dirtyClose', () => {
|
||||
renderComponent({ type: 'dirtyClose', denyLabel: 'Sign out anyway' })
|
||||
expect(screen.getByText('Sign out anyway')).toBeInTheDocument()
|
||||
expect(screen.queryByText('g.no')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('button surface per type', () => {
|
||||
it("type='default' renders Cancel and Confirm", () => {
|
||||
renderComponent({ type: 'default' })
|
||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Confirm' })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("type='delete' renders Cancel and Delete", () => {
|
||||
renderComponent({ type: 'delete' })
|
||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("type='overwrite' renders Cancel and Overwrite", () => {
|
||||
renderComponent({ type: 'overwrite' })
|
||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Overwrite' })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("type='dirtyClose' renders No and Save (no Cancel)", () => {
|
||||
renderComponent({ type: 'dirtyClose' })
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Cancel' })
|
||||
).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("type='info' renders only OK (no Cancel)", () => {
|
||||
renderComponent({ type: 'info' })
|
||||
expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Cancel' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('confirm callback receives true and closes the dialog', async () => {
|
||||
it('calls onConfirm(false) when deny is clicked on dirtyClose', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
const { user } = renderComponent({ type: 'default', onConfirm })
|
||||
const closeSpy = vi.spyOn(useDialogStore(), 'closeDialog')
|
||||
renderComponent({
|
||||
type: 'dirtyClose',
|
||||
denyLabel: 'Close anyway',
|
||||
onConfirm
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Confirm' }))
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Close anyway' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('calls onConfirm(true) when save is clicked on dirtyClose', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
renderComponent({ type: 'dirtyClose', onConfirm })
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'g.save' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true)
|
||||
expect(closeSpy).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
describe('dirtyClose deny label', () => {
|
||||
it('uses the provided denyLabel for the deny button', () => {
|
||||
renderComponent({ type: 'dirtyClose', denyLabel: 'Sign out anyway' })
|
||||
expect(screen.getByText('Sign out anyway')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'No' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('falls back to "No" when denyLabel is not provided', () => {
|
||||
renderComponent({ type: 'dirtyClose' })
|
||||
expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onConfirm(false) when deny is clicked', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
const { user } = renderComponent({
|
||||
type: 'dirtyClose',
|
||||
denyLabel: 'Close anyway',
|
||||
onConfirm
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Close anyway' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('calls onConfirm(true) when save is clicked', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
const { user } = renderComponent({ type: 'dirtyClose', onConfirm })
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }))
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true)
|
||||
})
|
||||
it('falls back to "no" label when denyLabel is not provided', () => {
|
||||
renderComponent({ type: 'dirtyClose' })
|
||||
expect(screen.getByText('g.no')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,14 +9,16 @@
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
<Message
|
||||
v-if="hint"
|
||||
role="status"
|
||||
class="mt-2 flex items-start gap-2 text-sm text-muted-foreground"
|
||||
class="mt-2"
|
||||
icon="pi pi-info-circle"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
<i class="pi pi-info-circle mt-0.5" aria-hidden="true" />
|
||||
<span>{{ hint }}</span>
|
||||
</div>
|
||||
{{ hint }}
|
||||
</Message>
|
||||
</div>
|
||||
<div class="flex shrink-0 flex-wrap justify-end gap-4">
|
||||
<div
|
||||
@@ -113,6 +115,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from 'primevue/message'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
@@ -44,24 +44,24 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
function isSectionCollapsed(nodeId: NodeId): boolean {
|
||||
function isSectionCollapsed(nodeId: string): boolean {
|
||||
// Defaults to collapsed when not explicitly set by the user
|
||||
return collapseMap[nodeId] ?? true
|
||||
}
|
||||
|
||||
function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
|
||||
function setSectionCollapsed(nodeId: string, collapsed: boolean) {
|
||||
collapseMap[nodeId] = collapsed
|
||||
}
|
||||
|
||||
const isAllCollapsed = computed({
|
||||
get() {
|
||||
return searchedWidgetsSectionDataList.value.every(({ node }) =>
|
||||
isSectionCollapsed(node.id)
|
||||
isSectionCollapsed(String(node.id))
|
||||
)
|
||||
},
|
||||
set(collapse: boolean) {
|
||||
for (const { node } of widgetsSectionDataList.value) {
|
||||
setSectionCollapsed(node.id, collapse)
|
||||
setSectionCollapsed(String(node.id), collapse)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -101,7 +101,7 @@ async function searcher(query: string) {
|
||||
:key="node.id"
|
||||
:node
|
||||
:widgets
|
||||
:collapse="isSectionCollapsed(node.id) && !isSearching"
|
||||
:collapse="isSectionCollapsed(String(node.id)) && !isSearching"
|
||||
:tooltip="
|
||||
isSearching || widgets.length
|
||||
? ''
|
||||
@@ -109,7 +109,7 @@ async function searcher(query: string) {
|
||||
"
|
||||
show-locate-button
|
||||
class="border-b border-interface-stroke"
|
||||
@update:collapse="setSectionCollapsed(node.id, $event)"
|
||||
@update:collapse="setSectionCollapsed(String(node.id), $event)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -68,19 +68,19 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
function isSectionCollapsed(nodeId: NodeId): boolean {
|
||||
function isSectionCollapsed(nodeId: string): boolean {
|
||||
// When not explicitly set, sections are collapsed if multiple nodes are selected
|
||||
return collapseMap[nodeId] ?? isMultipleNodesSelected.value
|
||||
}
|
||||
|
||||
function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
|
||||
function setSectionCollapsed(nodeId: string, collapsed: boolean) {
|
||||
collapseMap[nodeId] = collapsed
|
||||
}
|
||||
|
||||
const isAllCollapsed = computed({
|
||||
get() {
|
||||
const normalAllCollapsed = searchedWidgetsSectionDataList.value.every(
|
||||
({ node }) => isSectionCollapsed(node.id)
|
||||
({ node }) => isSectionCollapsed(String(node.id))
|
||||
)
|
||||
const hasAdvanced = advancedWidgetsSectionDataList.value.length > 0
|
||||
return hasAdvanced
|
||||
@@ -89,7 +89,7 @@ const isAllCollapsed = computed({
|
||||
},
|
||||
set(collapse: boolean) {
|
||||
for (const { node } of widgetsSectionDataList.value) {
|
||||
setSectionCollapsed(node.id, collapse)
|
||||
setSectionCollapsed(String(node.id), collapse)
|
||||
}
|
||||
advancedCollapsed.value = collapse
|
||||
}
|
||||
@@ -154,7 +154,7 @@ const advancedLabel = computed(() => {
|
||||
:node
|
||||
:label
|
||||
:widgets
|
||||
:collapse="isSectionCollapsed(node.id) && !isSearching"
|
||||
:collapse="isSectionCollapsed(String(node.id)) && !isSearching"
|
||||
:show-locate-button="isMultipleNodesSelected"
|
||||
:tooltip="
|
||||
isSearching || widgets.length
|
||||
@@ -162,7 +162,7 @@ const advancedLabel = computed(() => {
|
||||
: t('rightSidePanel.inputsNoneTooltip')
|
||||
"
|
||||
class="border-b border-interface-stroke"
|
||||
@update:collapse="setSectionCollapsed(node.id, $event)"
|
||||
@update:collapse="setSectionCollapsed(String(node.id), $event)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||
|
||||
@@ -252,20 +252,6 @@ describe('CurrentUserPopoverLegacy', () => {
|
||||
expect(screen.getByText('Log Out')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('credits help icon (FE-617)', () => {
|
||||
it('renders the credits help icon as an interactive button with the unified-credits tooltip as its accessible name', () => {
|
||||
renderComponent()
|
||||
|
||||
const helpButton = screen.getByTestId('credits-info-button')
|
||||
expect(helpButton).toBeInTheDocument()
|
||||
expect(helpButton.tagName).toBe('BUTTON')
|
||||
expect(helpButton).toHaveAttribute(
|
||||
'aria-label',
|
||||
enMessages.credits.unified.tooltip
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('opens user settings and emits close event when settings item is clicked', async () => {
|
||||
const { user, onClose } = renderComponent()
|
||||
|
||||
|
||||
@@ -41,16 +41,10 @@
|
||||
<span v-else class="text-base font-semibold text-base-foreground">{{
|
||||
formattedBalance
|
||||
}}</span>
|
||||
<Button
|
||||
<i
|
||||
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
||||
variant="muted-textonly"
|
||||
size="icon-sm"
|
||||
class="mr-auto"
|
||||
:aria-label="$t('credits.unified.tooltip')"
|
||||
data-testid="credits-info-button"
|
||||
>
|
||||
<i class="icon-[lucide--circle-help]" />
|
||||
</Button>
|
||||
class="mr-auto icon-[lucide--circle-help] cursor-help text-base text-muted-foreground"
|
||||
/>
|
||||
<Button
|
||||
v-if="isCloud && isFreeTier"
|
||||
variant="gradient"
|
||||
|
||||
@@ -543,7 +543,7 @@ describe('realtime scan verifies pending cloud candidates', () => {
|
||||
}
|
||||
])
|
||||
const verifySpy = vi
|
||||
.spyOn(missingMediaScan, 'verifyMediaCandidates')
|
||||
.spyOn(missingMediaScan, 'verifyCloudMediaCandidates')
|
||||
.mockImplementation(async (candidates) => {
|
||||
for (const c of candidates) c.isMissing = true
|
||||
})
|
||||
@@ -686,7 +686,7 @@ describe('realtime verification staleness guards', () => {
|
||||
let resolveVerify: (() => void) | undefined
|
||||
const verifyPromise = new Promise<void>((r) => (resolveVerify = r))
|
||||
const verifySpy = vi
|
||||
.spyOn(missingMediaScan, 'verifyMediaCandidates')
|
||||
.spyOn(missingMediaScan, 'verifyCloudMediaCandidates')
|
||||
.mockImplementation(async (candidates) => {
|
||||
await verifyPromise
|
||||
for (const c of candidates) c.isMissing = true
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import {
|
||||
scanNodeMediaCandidates,
|
||||
verifyMediaCandidates
|
||||
verifyCloudMediaCandidates
|
||||
} from '@/platform/missingMedia/missingMediaScan'
|
||||
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
|
||||
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
|
||||
@@ -209,8 +209,8 @@ function scanSingleNodeErrors(node: LGraphNode): void {
|
||||
if (confirmedMedia.length) {
|
||||
useMissingMediaStore().addMissingMedia(confirmedMedia)
|
||||
}
|
||||
// Cloud media scans return pending for asset verification. OSS scans only
|
||||
// return pending for generated output/temp media.
|
||||
// Cloud media scans always return isMissing: undefined pending
|
||||
// verification against the input-assets list.
|
||||
const pendingMedia = mediaCandidates.filter((c) => c.isMissing === undefined)
|
||||
if (pendingMedia.length) {
|
||||
void verifyAndAddPendingMedia(pendingMedia)
|
||||
@@ -282,7 +282,7 @@ async function verifyAndAddPendingMedia(
|
||||
): Promise<void> {
|
||||
const rootGraphAtScan = app.rootGraph
|
||||
try {
|
||||
await verifyMediaCandidates(pending, { isCloud })
|
||||
await verifyCloudMediaCandidates(pending)
|
||||
if (app.rootGraph !== rootGraphAtScan) return
|
||||
const verified = pending.filter(
|
||||
(c) => c.isMissing === true && isCandidateStillActive(c.nodeId)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useReconnectQueueRefresh } from '@/composables/useReconnectQueueRefresh'
|
||||
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
|
||||
function makeJob(id: string, status: JobListItem['status']): JobListItem {
|
||||
return {
|
||||
id,
|
||||
status,
|
||||
create_time: 0,
|
||||
update_time: 0,
|
||||
last_state_update: 0,
|
||||
priority: 0
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getQueue: vi.fn(),
|
||||
getHistory: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
apiURL: vi.fn((p: string) => `/api${p}`)
|
||||
}
|
||||
}))
|
||||
|
||||
describe('useReconnectQueueRefresh', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.restoreAllMocks()
|
||||
vi.mocked(api.getQueue).mockResolvedValue({ Running: [], Pending: [] })
|
||||
vi.mocked(api.getHistory).mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('forwards running+pending job ids to clearActiveJobIfStale', async () => {
|
||||
vi.mocked(api.getQueue).mockResolvedValue({
|
||||
Running: [makeJob('run-1', 'in_progress')],
|
||||
Pending: [makeJob('pend-1', 'pending'), makeJob('pend-2', 'pending')]
|
||||
})
|
||||
const executionStore = useExecutionStore()
|
||||
const clearSpy = vi
|
||||
.spyOn(executionStore, 'clearActiveJobIfStale')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const refresh = useReconnectQueueRefresh()
|
||||
await refresh()
|
||||
|
||||
expect(clearSpy).toHaveBeenCalledTimes(1)
|
||||
expect(clearSpy).toHaveBeenCalledWith(
|
||||
new Set(['run-1', 'pend-1', 'pend-2'])
|
||||
)
|
||||
})
|
||||
|
||||
it('passes an empty set when the queue is genuinely empty', async () => {
|
||||
const executionStore = useExecutionStore()
|
||||
const clearSpy = vi
|
||||
.spyOn(executionStore, 'clearActiveJobIfStale')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const refresh = useReconnectQueueRefresh()
|
||||
await refresh()
|
||||
|
||||
expect(clearSpy).toHaveBeenCalledWith(new Set())
|
||||
})
|
||||
|
||||
it('reuses the prior queue snapshot when the fetch fails, so a still-running job is not falsely cleared', async () => {
|
||||
vi.mocked(api.getQueue)
|
||||
.mockResolvedValueOnce({
|
||||
Running: [makeJob('run-1', 'in_progress')],
|
||||
Pending: []
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('network down'))
|
||||
const executionStore = useExecutionStore()
|
||||
const clearSpy = vi
|
||||
.spyOn(executionStore, 'clearActiveJobIfStale')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const refresh = useReconnectQueueRefresh()
|
||||
await refresh() // primes the store with run-1
|
||||
await refresh() // network failure here — store must not go empty
|
||||
|
||||
expect(clearSpy).toHaveBeenLastCalledWith(new Set(['run-1']))
|
||||
})
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
|
||||
/**
|
||||
* After a WebSocket reconnect, refresh the queue from the server and clear
|
||||
* any active job that finished during the disconnect window. Returns the
|
||||
* handler so the caller can wire it to the `reconnected` api event.
|
||||
*
|
||||
* `update()` preserves the previous queue snapshot when the fetch fails, so
|
||||
* if the network is still flaky we reconcile against the last known good
|
||||
* state rather than an empty (and falsely "stale") set.
|
||||
*/
|
||||
export function useReconnectQueueRefresh() {
|
||||
const queueStore = useQueueStore()
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
return async function refreshOnReconnect() {
|
||||
await queueStore.update()
|
||||
const activeJobIds = new Set([
|
||||
...queueStore.runningTasks.map((t) => t.jobId),
|
||||
...queueStore.pendingTasks.map((t) => t.jobId)
|
||||
])
|
||||
executionStore.clearActiveJobIfStale(activeJobIds)
|
||||
}
|
||||
}
|
||||
26
src/config/comfyApi.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getComfyApiBaseUrlForEnvironment } from '@/config/comfyApi'
|
||||
|
||||
describe('comfy api config', () => {
|
||||
it('uses same-origin API calls for cloud local development', () => {
|
||||
expect(
|
||||
getComfyApiBaseUrlForEnvironment({
|
||||
isCloudDistribution: true,
|
||||
isDev: true,
|
||||
devServerComfyUIUrl: 'http://127.0.0.1:8188',
|
||||
useProdConfig: false
|
||||
})
|
||||
).toBe('')
|
||||
})
|
||||
|
||||
it('keeps staging API for non-local staging builds', () => {
|
||||
expect(
|
||||
getComfyApiBaseUrlForEnvironment({
|
||||
isCloudDistribution: true,
|
||||
isDev: false,
|
||||
useProdConfig: false
|
||||
})
|
||||
).toBe('https://stagingapi.comfy.org')
|
||||
})
|
||||
})
|
||||
@@ -10,34 +10,68 @@ const STAGING_API_BASE_URL = 'https://stagingapi.comfy.org'
|
||||
const PROD_PLATFORM_BASE_URL = 'https://platform.comfy.org'
|
||||
const STAGING_PLATFORM_BASE_URL = 'https://stagingplatform.comfy.org'
|
||||
|
||||
const BUILD_TIME_API_BASE_URL = __USE_PROD_CONFIG__
|
||||
? PROD_API_BASE_URL
|
||||
: STAGING_API_BASE_URL
|
||||
type ComfyApiEnvironment = {
|
||||
isCloudDistribution: boolean
|
||||
isDev: boolean
|
||||
devServerComfyUIUrl?: string
|
||||
useProdConfig: boolean
|
||||
}
|
||||
|
||||
const BUILD_TIME_PLATFORM_BASE_URL = __USE_PROD_CONFIG__
|
||||
? PROD_PLATFORM_BASE_URL
|
||||
: STAGING_PLATFORM_BASE_URL
|
||||
const localOriginPattern =
|
||||
/^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/
|
||||
|
||||
export function getComfyApiBaseUrl(): string {
|
||||
if (!isCloud) {
|
||||
return BUILD_TIME_API_BASE_URL
|
||||
function buildTimeApiBaseUrl(useProdConfig: boolean): string {
|
||||
return useProdConfig ? PROD_API_BASE_URL : STAGING_API_BASE_URL
|
||||
}
|
||||
|
||||
function buildTimePlatformBaseUrl(useProdConfig: boolean): string {
|
||||
return useProdConfig ? PROD_PLATFORM_BASE_URL : STAGING_PLATFORM_BASE_URL
|
||||
}
|
||||
|
||||
function isLocalDevServer(url?: string): boolean {
|
||||
return url ? localOriginPattern.test(url) : false
|
||||
}
|
||||
|
||||
export function getComfyApiBaseUrlForEnvironment({
|
||||
isCloudDistribution,
|
||||
isDev,
|
||||
devServerComfyUIUrl,
|
||||
useProdConfig
|
||||
}: ComfyApiEnvironment): string {
|
||||
const buildTimeApiBaseUrlValue = buildTimeApiBaseUrl(useProdConfig)
|
||||
if (!isCloudDistribution) {
|
||||
return buildTimeApiBaseUrlValue
|
||||
}
|
||||
if (isDev && isLocalDevServer(devServerComfyUIUrl)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return configValueOrDefault(
|
||||
remoteConfig.value,
|
||||
'comfy_api_base_url',
|
||||
BUILD_TIME_API_BASE_URL
|
||||
buildTimeApiBaseUrlValue
|
||||
)
|
||||
}
|
||||
|
||||
export function getComfyApiBaseUrl(): string {
|
||||
return getComfyApiBaseUrlForEnvironment({
|
||||
isCloudDistribution: isCloud,
|
||||
isDev: import.meta.env.DEV,
|
||||
devServerComfyUIUrl: __DEV_SERVER_COMFYUI_URL__,
|
||||
useProdConfig: __USE_PROD_CONFIG__
|
||||
})
|
||||
}
|
||||
|
||||
export function getComfyPlatformBaseUrl(): string {
|
||||
const buildTimePlatformBaseUrlValue =
|
||||
buildTimePlatformBaseUrl(__USE_PROD_CONFIG__)
|
||||
if (!isCloud) {
|
||||
return BUILD_TIME_PLATFORM_BASE_URL
|
||||
return buildTimePlatformBaseUrlValue
|
||||
}
|
||||
|
||||
return configValueOrDefault(
|
||||
remoteConfig.value,
|
||||
'comfy_platform_base_url',
|
||||
BUILD_TIME_PLATFORM_BASE_URL
|
||||
buildTimePlatformBaseUrlValue
|
||||
)
|
||||
}
|
||||
|
||||
37
src/config/firebase.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
getFirebaseConfigForEnvironment,
|
||||
getFirebaseAuthEmulatorUrl
|
||||
} from '@/config/firebase'
|
||||
|
||||
describe('firebase config', () => {
|
||||
it('uses the explicit local project id when the auth emulator is enabled', () => {
|
||||
const config = getFirebaseConfigForEnvironment({
|
||||
isCloudBuild: false,
|
||||
useProdConfig: false,
|
||||
authEmulatorHost: '127.0.0.1:9099',
|
||||
localProjectId: 'demo-cloud'
|
||||
})
|
||||
|
||||
expect(config.projectId).toBe('demo-cloud')
|
||||
expect(config.authDomain).toBe('demo-cloud.firebaseapp.com')
|
||||
})
|
||||
|
||||
it('fails fast when the auth emulator is enabled without a local project id', () => {
|
||||
expect(() =>
|
||||
getFirebaseConfigForEnvironment({
|
||||
isCloudBuild: false,
|
||||
useProdConfig: false,
|
||||
authEmulatorHost: '127.0.0.1:9099'
|
||||
})
|
||||
).toThrow('VITE_FIREBASE_PROJECT_ID is required')
|
||||
})
|
||||
|
||||
it('does not connect to the emulator without the explicit host flag', () => {
|
||||
expect(getFirebaseAuthEmulatorUrl(undefined)).toBeNull()
|
||||
expect(getFirebaseAuthEmulatorUrl('127.0.0.1:9099')).toBe(
|
||||
'http://127.0.0.1:9099'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -25,7 +25,54 @@ const PROD_CONFIG: FirebaseOptions = {
|
||||
measurementId: 'G-3ZBD3MBTG4'
|
||||
}
|
||||
|
||||
const BUILD_TIME_CONFIG = __USE_PROD_CONFIG__ ? PROD_CONFIG : DEV_CONFIG
|
||||
type FirebaseEnvironment = {
|
||||
isCloudBuild: boolean
|
||||
useProdConfig: boolean
|
||||
authEmulatorHost?: string
|
||||
localProjectId?: string
|
||||
}
|
||||
|
||||
function buildLocalEmulatorConfig(
|
||||
buildTimeConfig: FirebaseOptions,
|
||||
localProjectId: string | undefined
|
||||
): FirebaseOptions {
|
||||
if (!localProjectId) {
|
||||
throw new Error(
|
||||
'VITE_FIREBASE_PROJECT_ID is required when VITE_FIREBASE_AUTH_EMULATOR_HOST is set'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...buildTimeConfig,
|
||||
projectId: localProjectId,
|
||||
authDomain: `${localProjectId}.firebaseapp.com`
|
||||
}
|
||||
}
|
||||
|
||||
export function getFirebaseAuthEmulatorUrl(
|
||||
host: string | undefined
|
||||
): string | null {
|
||||
return host ? `http://${host}` : null
|
||||
}
|
||||
|
||||
export function getFirebaseConfigForEnvironment({
|
||||
isCloudBuild,
|
||||
useProdConfig,
|
||||
authEmulatorHost,
|
||||
localProjectId
|
||||
}: FirebaseEnvironment): FirebaseOptions {
|
||||
const buildTimeConfig = useProdConfig ? PROD_CONFIG : DEV_CONFIG
|
||||
if (authEmulatorHost) {
|
||||
return buildLocalEmulatorConfig(buildTimeConfig, localProjectId)
|
||||
}
|
||||
|
||||
if (!isCloudBuild) {
|
||||
return buildTimeConfig
|
||||
}
|
||||
|
||||
const runtimeConfig = remoteConfig.value.firebase_config
|
||||
return runtimeConfig ?? buildTimeConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Firebase configuration for the current environment.
|
||||
@@ -33,10 +80,10 @@ const BUILD_TIME_CONFIG = __USE_PROD_CONFIG__ ? PROD_CONFIG : DEV_CONFIG
|
||||
* - OSS / localhost builds fall back to the build-time config determined by __USE_PROD_CONFIG__
|
||||
*/
|
||||
export function getFirebaseConfig(): FirebaseOptions {
|
||||
if (!isCloud) {
|
||||
return BUILD_TIME_CONFIG
|
||||
}
|
||||
|
||||
const runtimeConfig = remoteConfig.value.firebase_config
|
||||
return runtimeConfig ?? BUILD_TIME_CONFIG
|
||||
return getFirebaseConfigForEnvironment({
|
||||
isCloudBuild: isCloud,
|
||||
useProdConfig: __USE_PROD_CONFIG__,
|
||||
authEmulatorHost: import.meta.env.VITE_FIREBASE_AUTH_EMULATOR_HOST,
|
||||
localProjectId: import.meta.env.VITE_FIREBASE_PROJECT_ID
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# Extension API — Public Source of Truth
|
||||
|
||||
> **Status**: Implemented (Phase A). Runtime backed by stub ECS components;
|
||||
> full ECS integration lands with #11939.
|
||||
|
||||
This folder is the single source of truth for the public ComfyUI extension
|
||||
API. Every file here is part of the published `@comfyorg/extension-api`
|
||||
npm package. Do not re-export from `/src` — this barrel is the **published
|
||||
package entry point**, which is the explicit exception to the project's
|
||||
"no barrel files in /src" rule (root AGENTS.md rule #19).
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
extension-api/
|
||||
├── index.ts ← barrel — package entry point
|
||||
├── node.ts ← NodeHandle interface + node event payload types
|
||||
├── widget.ts ← WidgetHandle interface + widget event payload types
|
||||
├── types.ts ← ExtensionOptions, NodeExtensionOptions, WidgetExtensionOptions
|
||||
├── events.ts ← Handler<E>, AsyncHandler<E>, Unsubscribe
|
||||
├── lifecycle.ts ← onNodeMounted, onNodeRemoved hooks + rationale docs
|
||||
├── shell.ts ← SidebarTabExtension, BottomPanelExtension, CommandManager, etc.
|
||||
├── identifiers.ts ← NodeLocatorId, NodeExecutionId + parsers/type guards
|
||||
└── README.md ← this file
|
||||
```
|
||||
|
||||
## What about v1?
|
||||
|
||||
v1 (`ComfyExtension` interface in `../types/comfy.ts`, `app.registerExtension(...)`
|
||||
runtime entry point in `../scripts/app.ts`) **stays in its current locations**.
|
||||
Custom extensions in the wild consume the runtime entry point, not the type
|
||||
file — moving the type file would churn ~30 internal imports for zero runtime
|
||||
benefit. The v1↔v2 distinction is at the entry point, not the folder.
|
||||
|
||||
## Authoring rules
|
||||
|
||||
1. **Hand-authored**, not generated. This is a public API; we own the shape.
|
||||
2. **No `any`, no `as any`, no `@ts-expect-error`.** If you need an escape
|
||||
hatch, the type is wrong.
|
||||
3. Every public type has a TSDoc block with at minimum:
|
||||
- 1-line summary
|
||||
- `@stability` tag (`stable` | `experimental` | `deprecated`)
|
||||
- `@example` block (where applicable)
|
||||
4. Naming follows conventions:
|
||||
- Read-only invariants (set at construction): `readonly` property
|
||||
- Read-only state (changes over time): method (`getValue()`)
|
||||
- Mutating actions: method (`setValue(v)`)
|
||||
- Boolean predicates: method (`isHidden()`)
|
||||
5. Events: typed payloads, no `Function`, split-channel events
|
||||
(`valueChange` / `optionChange` / `propertyChange`).
|
||||
6. No internal types (`World`, `Component<T>`, branded `EntityId` internals)
|
||||
leak through this barrel.
|
||||
|
||||
## Key design decisions
|
||||
|
||||
| ADR | Decision |
|
||||
| ----------------------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| [ADR-0008](../../docs/adr/0008-entity-component-system.md) | Entity Component System architecture |
|
||||
| [ADR-0010](../../docs/adr/0010-deprecate-node-level-serialization-control.md) | Deprecate `node.on('beforeSerialize')` — use widget-level |
|
||||
| [ADR-0011](../../docs/adr/0011-immutability-via-fresh-copies.md) | Return fresh copies from collection methods |
|
||||
| [ADR-0012](../../docs/adr/0012-pure-function-loader-pattern.md) | Pure function registration + loader activation |
|
||||
|
||||
## Related research
|
||||
|
||||
- [Identity encapsulation](../../docs/research/identity-encapsulation.md) — when extensions need raw entity IDs
|
||||
- [Coordinate systems](../../docs/research/coordinate-systems.md) — canvas vs screen coordinates
|
||||
- [Widget state categories](../../docs/research/widget-state-categories.md) — value/properties/options/DOM
|
||||
- [Serialization context](../../docs/research/serialization-context.md) — workflow/prompt/clone/subgraph-promote
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* D18 Phase 1 — brand symbols for `define*` outputs.
|
||||
*
|
||||
* Per D18, `defineNode` / `defineWidget` / `defineExtension` will become
|
||||
* pure functions whose return values are recognized at registration time
|
||||
* by a loader that walks module exports and dispatches based on the brand.
|
||||
*
|
||||
* Phase 1 (this file) introduces the brand symbol and the `isBrandedExtension`
|
||||
* type-guard. The `define*` functions stamp the brand on their returned
|
||||
* options so a future loader can identify them. Side-effect registration
|
||||
* remains unchanged in Phase 1; Phase 2 removes it.
|
||||
*
|
||||
* The brand is a `Symbol.for(...)` so HMR + duplicate-package scenarios still
|
||||
* resolve to the same identity (per realm / per JS context).
|
||||
*
|
||||
* @internal — not re-exported from `@comfyorg/extension-api/index.ts`. The
|
||||
* loader lives inside the runtime, not in the published package.
|
||||
*/
|
||||
|
||||
export const EXTENSION_BRAND = Symbol.for('@comfyorg/extension-api:brand')
|
||||
|
||||
export type ExtensionKind = 'node' | 'widget' | 'app'
|
||||
|
||||
export interface Branded {
|
||||
readonly [EXTENSION_BRAND]: ExtensionKind
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp a brand on an options object and freeze it. Returned reference is
|
||||
* the same object — branding is non-enumerable so JSON serialization,
|
||||
* spread operations, and shallow-equal comparisons are unaffected.
|
||||
*/
|
||||
export function stampBrand<T extends object>(
|
||||
options: T,
|
||||
kind: ExtensionKind
|
||||
): T & Branded {
|
||||
Object.defineProperty(options, EXTENSION_BRAND, {
|
||||
value: kind,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: false
|
||||
})
|
||||
return Object.freeze(options) as T & Branded
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-guard for branded extension options. The loader uses this to
|
||||
* decide whether a module export is a `defineX(...)` result.
|
||||
*
|
||||
* Unbranded values (utility exports, constants, helper functions) return
|
||||
* `false` and are silently ignored by the loader.
|
||||
*/
|
||||
export function isBrandedExtension(value: unknown): value is Branded {
|
||||
if (value === null || typeof value !== 'object') return false
|
||||
const kind = (value as Record<symbol, unknown>)[EXTENSION_BRAND]
|
||||
return kind === 'node' || kind === 'widget' || kind === 'app'
|
||||
}
|
||||
|
||||
export function getBrandKind(value: Branded): ExtensionKind {
|
||||
return value[EXTENSION_BRAND]
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
/**
|
||||
* Shared event infrastructure for the ComfyUI extension API.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
/**
|
||||
* A typed event handler function.
|
||||
*
|
||||
* @typeParam E - The event payload type.
|
||||
* @example
|
||||
* ```ts
|
||||
* const handler: Handler<WidgetValueChangeEvent<number>> = (e) => {
|
||||
* console.log(e.oldValue, '->', e.newValue)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type Handler<E> = (event: E) => void
|
||||
|
||||
/**
|
||||
* A typed async-capable event handler. Only valid for events that explicitly
|
||||
* support async handling (currently only `beforeSerialize`).
|
||||
*
|
||||
* @typeParam E - The event payload type.
|
||||
* @example
|
||||
* ```ts
|
||||
* import type { AsyncHandler, WidgetBeforeSerializeEvent } from '@comfyorg/extension-api'
|
||||
*
|
||||
* const handler: AsyncHandler<WidgetBeforeSerializeEvent> = async (e) => {
|
||||
* const frame = await captureFrame()
|
||||
* e.setSerializedValue(frame)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type AsyncHandler<E> = (event: E) => void | Promise<void>
|
||||
|
||||
/**
|
||||
* Cleanup function returned by `on()` — call to remove the listener.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const off = node.on('executed', handler)
|
||||
* // later:
|
||||
* off()
|
||||
* ```
|
||||
*/
|
||||
export type Unsubscribe = () => void
|
||||
|
||||
// Event-namespace facades
|
||||
//
|
||||
// Four typed event-namespace handles (`graph` / `execution` / `server` /
|
||||
// `workbench`) replace the ad-hoc `api.addEventListener('execution_start', ...)`
|
||||
// pattern documented in 360+ ecosystem call sites. Each namespace is a
|
||||
// module-level singleton (SD-4 (a), handoff-11) — call from any setup() body
|
||||
// or hook closure. Subscriptions registered inside a setup context auto-dispose
|
||||
// when the surrounding instance is unmounted (Vue-style; subscription is added
|
||||
// to the context's unmountHooks). Outside a setup context, the returned
|
||||
// `Unsubscribe` is the caller's responsibility.
|
||||
//
|
||||
// Payload typing (SD-5 (b)): each `on()` accepts a string event name and
|
||||
// returns `Handler<EventPayloadMap[ns][evt]>`. The maps default to `unknown`
|
||||
// today and are tightened by D5 module augmentation in a follow-on PR. Authors
|
||||
// get autocomplete on canonical event names; payload narrowing arrives when
|
||||
// D5 lands.
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
import { getCurrentExtensionInstance } from '@/services/extension-api-service'
|
||||
|
||||
/**
|
||||
* Per-namespace event payload map. **Augment via TS module augmentation** to
|
||||
* narrow payloads for canonical events. Until D5 ships, all payloads default
|
||||
* to `unknown`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* declare module '@comfyorg/extension-api' {
|
||||
* interface ExecutionEventPayloads {
|
||||
* start: { promptId: string }
|
||||
* progress: { value: number; max: number }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface GraphEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
/**
|
||||
* See {@link GraphEventPayloads | the augmentation example} —
|
||||
* augment this interface the same way to narrow `execution.*` payloads.
|
||||
*/
|
||||
export interface ExecutionEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
/**
|
||||
* See {@link GraphEventPayloads | the augmentation example} —
|
||||
* augment this interface the same way to narrow `server.*` payloads.
|
||||
*/
|
||||
export interface ServerEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
/**
|
||||
* See {@link GraphEventPayloads | the augmentation example} —
|
||||
* augment this interface the same way to narrow `workbench.*` payloads.
|
||||
*/
|
||||
export interface WorkbenchEventPayloads {
|
||||
[event: string]: unknown
|
||||
}
|
||||
|
||||
interface EventNamespace<M> {
|
||||
/**
|
||||
* Subscribe to an event. Returns an {@link Unsubscribe} function.
|
||||
*
|
||||
* Inside a `setup()` body the subscription is also added to the
|
||||
* surrounding instance's `onUnmounted` queue and auto-disposes when the
|
||||
* extension/tab/panel is unmounted.
|
||||
*/
|
||||
on<K extends keyof M & string>(event: K, handler: Handler<M[K]>): Unsubscribe
|
||||
|
||||
/**
|
||||
* Remove a previously registered handler. Same as the {@link Unsubscribe}
|
||||
* returned by `on()`. Exposed for symmetry with `addEventListener`/`removeEventListener`.
|
||||
*/
|
||||
off<K extends keyof M & string>(event: K, handler: Handler<M[K]>): void
|
||||
}
|
||||
|
||||
function makeNamespace<M>(rename: (evt: string) => string): EventNamespace<M> {
|
||||
// ComfyApi extends EventTarget but its addEventListener is strictly typed
|
||||
// against the validated ApiCalls union. The bootstrap-hooks facade
|
||||
// accepts any string (custom-node events ride server.on with arbitrary
|
||||
// names per ADR), so we widen via EventTarget to get the generic overload.
|
||||
const target = api as unknown as EventTarget
|
||||
return {
|
||||
on<K extends keyof M & string>(
|
||||
event: K,
|
||||
handler: Handler<M[K]>
|
||||
): Unsubscribe {
|
||||
const wireName = rename(event)
|
||||
// payload arrives as CustomEvent.detail.
|
||||
const adapter = (e: Event): void => {
|
||||
const detail = (e as CustomEvent).detail as M[K]
|
||||
handler(detail)
|
||||
}
|
||||
target.addEventListener(wireName, adapter)
|
||||
const unsubscribe: Unsubscribe = () => {
|
||||
target.removeEventListener(wireName, adapter)
|
||||
}
|
||||
// Auto-dispose inside a setup() context (mirrors Vue's onScopeDispose).
|
||||
const ctx = getCurrentExtensionInstance()
|
||||
if (ctx) {
|
||||
ctx.unmountHooks.push(unsubscribe)
|
||||
}
|
||||
return unsubscribe
|
||||
},
|
||||
off<K extends keyof M & string>(event: K, handler: Handler<M[K]>): void {
|
||||
// Note: off() with a raw handler only matches if the caller saved the
|
||||
// exact adapter reference returned from on(). The recommended path is
|
||||
// to call the Unsubscribe returned by on(). This off() is retained for
|
||||
// API symmetry but does NOT round-trip with on() handlers — they wrap
|
||||
// the user fn in an adapter for CustomEvent unwrap. Authors that need
|
||||
// explicit off() should use the Unsubscribe handle.
|
||||
target.removeEventListener(
|
||||
rename(event),
|
||||
handler as unknown as EventListener
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph-mutation events (frontend-dispatched).
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { onMounted, graph } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* graph.on('changed', (e) => console.log('graph changed', e))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const graph: EventNamespace<GraphEventPayloads> = makeNamespace(
|
||||
(evt) => `graph:${evt}`
|
||||
)
|
||||
|
||||
/**
|
||||
* Prompt-run lifecycle events (backend-dispatched).
|
||||
*
|
||||
* Canonical events: `'start'`, `'end'`, `'error'`, `'interrupted'`, `'cached'`,
|
||||
* `'executing'`, `'progress'`, `'preview'`. The wire-name mapping rewrites
|
||||
* `'start'` → `'execution_start'`, etc., matching the legacy
|
||||
* `api.addEventListener('execution_start', ...)` shape.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension, onMounted, execution } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* execution.on('start', (e) => console.log('run started', e))
|
||||
* execution.on('progress', (e) => console.log('progress', e))
|
||||
* execution.on('end', () => console.log('run done'))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const execution: EventNamespace<ExecutionEventPayloads> = makeNamespace(
|
||||
(evt) => `execution_${evt}`
|
||||
)
|
||||
|
||||
/**
|
||||
* Non-execution backend events + custom-node events.
|
||||
*
|
||||
* Canonical events: `'status'`, `'logs'`, `'reconnected'`, `'feature_flags'`,
|
||||
* `'assets'`. Custom-node events ride this channel with arbitrary string
|
||||
* (e.g. `server.on('rayko.inspline.show', ...)`). Module-augment
|
||||
* `ServerEventPayloads` to type custom events.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension, onMounted, server } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* server.on('reconnected', () => console.log('server back online'))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const server: EventNamespace<ServerEventPayloads> = makeNamespace(
|
||||
(evt) => evt
|
||||
)
|
||||
|
||||
/**
|
||||
* UI shell events.
|
||||
*
|
||||
* Canonical events today: `'notification'`. Future: `'themeChanged'`,
|
||||
* `'panelToggled'`, `'commandInvoked'`. NOT a DI container — see
|
||||
* the bootstrap-hooks design for the "thin event-namespace handle only"
|
||||
* scope-back.
|
||||
*
|
||||
* @publicAPI
|
||||
* @stability experimental
|
||||
* @example
|
||||
* ```ts
|
||||
* import { defineExtension, onMounted, workbench } from '@comfyorg/extension-api'
|
||||
*
|
||||
* defineExtension({
|
||||
* name: 'my-ext',
|
||||
* setup() {
|
||||
* onMounted(() => {
|
||||
* workbench.on('notification', (e) => console.log('workbench notif', e))
|
||||
* })
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const workbench: EventNamespace<WorkbenchEventPayloads> = makeNamespace(
|
||||
(evt) => `workbench:${evt}`
|
||||
)
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Node identity helpers — re-exported from internal `nodeIdentification.ts`.
|
||||
*
|
||||
* `NodeLocatorId` and `NodeExecutionId` are the two stable node identity
|
||||
* primitives in the public API. All extension-facing code that needs to
|
||||
* reference a node across subgraph boundaries or execution runs should use
|
||||
* these rather than raw LiteGraph integer node IDs.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export type { NodeLocatorId, NodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
/**
|
||||
* Node identity round-trip helpers. Create/parse branded `NodeLocatorId` and
|
||||
* `NodeExecutionId` values, or narrow an `unknown` to one with the type
|
||||
* guards. Use these instead of raw string manipulation so future changes to
|
||||
* the identity scheme stay transparent.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* createNodeLocatorId,
|
||||
* parseNodeLocatorId,
|
||||
* isNodeLocatorId,
|
||||
* createNodeExecutionId,
|
||||
* parseNodeExecutionId,
|
||||
* isNodeExecutionId
|
||||
* } from '@comfyorg/extension-api'
|
||||
*
|
||||
* // Construct
|
||||
* const locator = createNodeLocatorId(graphUuid, localId)
|
||||
* const execId = createNodeExecutionId(locator, runTag)
|
||||
*
|
||||
* // Narrow
|
||||
* if (isNodeLocatorId(maybe)) {
|
||||
* const parts = parseNodeLocatorId(maybe)
|
||||
* console.log(parts.graphUuid, parts.localId)
|
||||
* }
|
||||
*
|
||||
* if (isNodeExecutionId(maybe)) {
|
||||
* const parts = parseNodeExecutionId(maybe)
|
||||
* console.log(parts.locator, parts.runTag)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export {
|
||||
isNodeLocatorId,
|
||||
isNodeExecutionId,
|
||||
parseNodeLocatorId,
|
||||
createNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
createNodeExecutionId
|
||||
} from '@/types/nodeIdentification'
|
||||