Compare commits

..

20 Commits

Author SHA1 Message Date
snomiao
842515c2be [docs] Update API changelog for v1.32.4 2025-11-10 09:30:05 +00:00
sno
e36e25ebd6 Merge branch 'main' into sno-api-changelog 2025-11-10 18:26:42 +09:00
snomiao
f844d3e95b [feat] Use source file links instead of dist .d.ts in API changelog
Update changelog link generation to prefer source file locations over
generated declaration file locations. Links now point to the actual
source code where APIs are defined (e.g., src/stores/commandStore.ts)
instead of the bundled dist/index.d.ts file.

This makes the changelog much more useful as developers can click
through to see the real implementation and context.

Falls back to .d.ts location if source mapping is unavailable.

Fixes the issue where links like:
https://github.com/.../dist/index.d.ts#L1247

Are now:
https://github.com/.../src/stores/commandStore.ts#L10

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 17:22:34 +00:00
snomiao
98ed124b02 [feat] Add source file location discovery to API snapshot tool
Enhance snapshot-api.js to discover source file locations for exported
declarations using grep search. This enables mapping from generated
.d.ts declarations back to their original source files.

For each declaration, the tool now searches the src directory for:
- export interface <Name>
- export class <Name>
- export type <Name>
- export enum <Name>
- export function <Name>
- export const <Name>

The source file path and line number are stored in the snapshot for use
in generating accurate GitHub permalinks in the changelog.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 17:22:11 +00:00
snomiao
e933b5c357 [feat] Enable declarationMap for source file mapping in API changelog
Generate declaration maps to enable mapping from generated .d.ts files
back to source files. This will allow the API changelog to link to actual
source code locations instead of generated declaration files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 17:21:30 +00:00
sno
cb32562f04 Merge branch 'main' into sno-api-changelog 2025-11-06 00:54:16 +08:00
snomiao
9c94a4818f [feat] Remove Additions section and add hyperlinks to API changelog
- Remove Additions section from changelog output (no longer needed)
- Add line number tracking in API snapshots
- Generate GitHub permalinks to referenced code in changelog
- Update workflows to pass git reference for link generation
- Breaking changes and modifications now link to source code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 10:15:37 +00:00
sno
779f539b0e Merge branch 'main' into sno-api-changelog 2025-11-05 07:51:42 +08:00
snomiao
95c815f17c [docs] Add comprehensive documentation for manual API changelog workflow
- Usage examples via UI and CLI
- Detailed explanation of workflow steps
- Common use cases and troubleshooting
- Security and monitoring information

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 23:27:33 +00:00
snomiao
30dcf8c133 [feat] Add manual dispatch workflow for API changelog generation
- Allows comparing any two versions via workflow_dispatch
- Accepts version inputs (e.g., 1.29.0 and 1.30.2)
- Validates version format and tag existence
- Generates changelog and uploads as artifact (90-day retention)
- Optional PR creation for updating docs/API-CHANGELOG.md
- Provides detailed summary in workflow output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 23:21:56 +00:00
snomiao
f1193a2f86 [fix] Exclude demo-snapshots from knip checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 12:23:14 +00:00
snomiao
75daf2e4d2 [docs] Add API changelog generation demo
Add comprehensive demo showing API changelog generation across two versions (v1.29.0 → v1.30.2):

- Mock TypeScript definitions for both versions
- Generated JSON snapshots showing structured API surface
- Generated changelog with breaking changes, additions, and modifications
- Detailed README explaining the system and benefits

The demo showcases:
 Detection of breaking changes (queuePrompt → queuePromptAsync)
 Tracking of new additions (ExtensionMetadata interface, WorkflowId type)
 Identification of modifications (NodeDef enhancements, NodeStatus enum extensions)

This demonstrates the automated system that will run on every release to document API changes for extension developers.

Also exclude demo-snapshots directory from ESLint checking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 12:21:37 +00:00
sno
11922709a9 Merge branch 'main' into sno-api-changelog 2025-10-30 16:57:22 +09:00
snomiao
3742a76cfb [fix] Use current branch as PR base when triggered by push event
- Set PR base to current branch (github.ref_name) when triggered by push
- Keeps main as base for workflow_run triggers
- Allows testing on feature branches without conflicts with main

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 11:05:37 +00:00
snomiao
c84144581d [fix] Remove copied scripts before git checkout to avoid conflicts
- Clean up scripts/snapshot-api.js and scripts/compare-api-snapshots.js before checkout
- Fixes untracked working tree files conflict when returning to current version

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 11:00:37 +00:00
snomiao
8b88f8ccae [fix] Preserve API changelog scripts when checking out previous version
- Copy snapshot and comparison scripts to /tmp before checking out previous version
- Restore scripts after checkout to ensure they're available in older versions
- Fixes MODULE_NOT_FOUND error when running snapshot on previous versions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 10:55:52 +00:00
snomiao
0aab7cba4b [feat] Configure API changelog workflow to run on push to sno-api-changelog branch
- Add push trigger for sno-api-changelog branch to allow testing
- Update job condition to handle both workflow_run and push events
- Limit version comparison to two most recent versions for efficiency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 10:50:12 +00:00
sno
608874a312 Merge branch 'main' into sno-api-changelog 2025-10-29 19:44:28 +09:00
GitHub Action
5cf6ac07ac [automated] Apply ESLint and Prettier fixes 2025-10-23 03:06:37 +00:00
snomiao
ff60bdf1bc [feat] Add automated API changelog generation workflow
Implements a GitHub Actions workflow that automatically generates API
changelogs by comparing TypeScript type definitions between versions.

Changes:
- Add release-api-changelogs.yaml workflow triggered after npm types release
- Create snapshot-api.js script to extract API surface from TypeScript defs
- Create compare-api-snapshots.js to generate human-readable changelogs
- Initialize docs/API-CHANGELOG.md to track public API changes

The workflow:
1. Triggers after Release NPM Types workflow completes
2. Builds and snapshots current and previous API surfaces
3. Compares snapshots to detect additions, removals, and modifications
4. Generates formatted changelog with breaking changes highlighted
5. Creates draft PR for review before merging

This automates documentation of breaking changes for extension developers
without manual effort, supporting the large extension ecosystem.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 03:03:11 +00:00
448 changed files with 13497 additions and 64804 deletions

View File

@@ -36,9 +36,9 @@ body:
3. Click Queue Prompt
4. See error
value: |
1.
2.
3.
1.
2.
3.
validations:
required: true

View File

@@ -28,7 +28,7 @@ runs:
REPO="${{ github.repository }}"
if [[ -z "$VERSION_FILE" ]]; then
echo "::error::version_file input is required" >&2
echo '::error::version_file input is required' >&2
exit 1
fi
@@ -55,13 +55,11 @@ runs:
case "$VERSION_FILE" in
package.json)
LINKS_VALUE=$(printf '%s\n%s' \
'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/' \
'npm types|https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/{{version}}')
LINKS_VALUE=$'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/\n''npm types|https://npm.im/@comfyorg/comfyui-frontend-types@{{version}}'
;;
apps/desktop-ui/package.json)
MARKER='desktop-release-summary'
LINKS_VALUE='npm desktop UI|https://www.npmjs.com/package/@comfyorg/desktop-ui/v/{{version}}'
LINKS_VALUE='npm desktop UI|https://npm.im/@comfyorg/desktop-ui@{{version}}'
;;
esac
@@ -71,13 +69,12 @@ runs:
COMMENT_FILE=$(mktemp)
{
echo "<!--$MARKER:$DIFF_PREFIX$NEW_VERSION-->"
echo "$MESSAGE"
echo ""
echo "- $DIFF_LABEL: [\`$DIFF_PREFIX$PREV_VERSION...$DIFF_PREFIX$NEW_VERSION\`]($DIFF_URL)"
printf '<!--%s:%s%s-->\n' "$MARKER" "$DIFF_PREFIX" "$NEW_VERSION"
printf '%s\n\n' "$MESSAGE"
printf -- '- %s: [%s%s...%s%s](%s)\n' "$DIFF_LABEL" "$DIFF_PREFIX" "$PREV_VERSION" "$DIFF_PREFIX" "$NEW_VERSION" "$DIFF_URL"
while IFS= read -r RAW_LINE; do
LINE=$(echo "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
LINE=$(printf '%s' "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$LINE" ]] && continue
if [[ "$LINE" != *"|"* ]]; then
echo "::warning::Skipping malformed link entry: $LINE" >&2
@@ -87,16 +84,16 @@ runs:
URL_TEMPLATE=${LINE#*|}
URL=${URL_TEMPLATE//\{\{version\}\}/$NEW_VERSION}
URL=${URL//\{\{prev_version\}\}/$PREV_VERSION}
echo "- $LABEL: [\`$NEW_VERSION\`]($URL)"
printf -- '- %s: %s\n' "$LABEL" "$URL"
done <<< "$LINKS_VALUE"
echo ""
printf '\n'
} > "$COMMENT_FILE"
{
echo "body<<COMMENT_BODY_END_MARKER"
echo "body<<'EOF'"
cat "$COMMENT_FILE"
echo "COMMENT_BODY_END_MARKER"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
echo "prev_version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
echo "marker_search=<!--$MARKER:" >> "$GITHUB_OUTPUT"

View File

@@ -0,0 +1,234 @@
# Manual API Changelog Generation
This workflow allows you to generate API changelogs by comparing any two versions of the ComfyUI Frontend package.
## Usage
### Via GitHub Actions UI
1. Go to **Actions** tab in the repository
2. Select **Manual API Changelog Generation** from the workflows list
3. Click **Run workflow** button
4. Fill in the inputs:
- **Previous version**: The earlier version (e.g., `1.29.0` or `v1.29.0`)
- **Current version**: The later version (e.g., `1.30.2` or `v1.30.2`)
- **Create PR**: Check this to automatically create a pull request with the changelog
### Via GitHub CLI
```bash
# Basic usage - just generate changelog
gh workflow run manual-api-changelog.yaml \
-f from_version=1.29.0 \
-f to_version=1.30.2 \
-f create_pr=false
# Generate changelog and create PR
gh workflow run manual-api-changelog.yaml \
-f from_version=1.29.0 \
-f to_version=1.30.2 \
-f create_pr=true
```
## What It Does
1. **Validates Inputs**: Checks that version formats are valid (X.Y.Z) and tags exist
2. **Builds Both Versions**: Checks out each version tag, installs dependencies, and builds TypeScript types
3. **Generates Snapshots**: Creates structured JSON snapshots of the public API surface for each version
4. **Compares APIs**: Analyzes differences and categorizes as:
- ⚠️ **Breaking changes** (removals, signature changes)
-**Additions** (new interfaces, methods, properties)
- 🔄 **Modifications** (non-breaking changes)
5. **Uploads Artifact**: Saves the changelog and snapshots as a workflow artifact (90-day retention)
6. **Creates PR** (optional): Generates a draft PR to update `docs/API-CHANGELOG.md`
## Output
### Workflow Artifacts
Every run produces an artifact containing:
- `CHANGELOG-{from}-to-{to}.md` - Human-readable changelog
- `from.json` - API snapshot of the earlier version
- `to.json` - API snapshot of the later version
**Retention**: 90 days
### Pull Request (Optional)
If `create_pr` is enabled and changes are detected:
- Creates a draft PR with title: `[docs] API Changelog: v{from} → v{to}`
- Updates `docs/API-CHANGELOG.md` with the new changelog entry
- Includes detailed metadata and review instructions
- Labeled with `documentation`
## Example Changelog Output
```markdown
## v1.30.2 (2025-11-04)
Comparing v1.29.0 → v1.30.2. This changelog documents changes to the public API surface.
### ✨ Additions
**Type Aliases**
- `WorkflowId`
**Interfaces**
- `ExtensionMetadata`
- Members: `id`, `name`, `version`, `description`
### 🔄 Modifications
> **Note**: Some modifications may be breaking changes.
**Interfaces**
- `ComfyApi`
- ✨ Added member: `queuePromptAsync`
- ✨ Added member: `cancelPrompt`
- ⚠️ **Breaking**: Removed member: `queuePrompt`
**Enums**
- `NodeStatus`
- ✨ Added enum value: `ERROR`
- ✨ Added enum value: `COMPLETED`
```
## Use Cases
### 1. Generate Changelog for Missed Releases
If the automatic workflow failed or was skipped for a release:
```bash
gh workflow run manual-api-changelog.yaml \
-f from_version=1.28.0 \
-f to_version=1.29.0 \
-f create_pr=true
```
### 2. Compare Non-Adjacent Versions
To see cumulative changes across multiple releases:
```bash
gh workflow run manual-api-changelog.yaml \
-f from_version=1.25.0 \
-f to_version=1.30.2 \
-f create_pr=false
```
### 3. Test Upcoming Changes
Compare current `main` branch against the latest release (requires creating a temporary tag):
```bash
# Create temporary tag for current main
git tag v1.31.0-preview
git push origin v1.31.0-preview
# Run comparison
gh workflow run manual-api-changelog.yaml \
-f from_version=1.30.2 \
-f to_version=1.31.0-preview \
-f create_pr=false
# Clean up temporary tag
git tag -d v1.31.0-preview
git push origin :refs/tags/v1.31.0-preview
```
### 4. Audit Historical Changes
Generate changelogs for documentation purposes:
```bash
# Compare multiple version pairs
for from in 1.26.0 1.27.0 1.28.0 1.29.0; do
to=$(echo "$from" | awk -F. '{print $1"."$2+1".0"}')
gh workflow run manual-api-changelog.yaml \
-f from_version=$from \
-f to_version=$to \
-f create_pr=false
done
```
## Validation
The workflow validates:
- ✅ Version format matches semantic versioning (X.Y.Z)
- ✅ Both version tags exist in the repository
- ✅ Tags reference valid commits with buildable code
If validation fails, the workflow exits early with a clear error message.
## Limitations
- **Tag requirement**: Both versions must have corresponding `vX.Y.Z` git tags
- **Build requirement**: Both versions must have functional build processes
- **Type files**: Requires `dist/index.d.ts` to exist after building
- **Scripts**: Requires `scripts/snapshot-api.js` and `scripts/compare-api-snapshots.js` to be present
## Related Workflows
- **[Release API Changelogs](.github/workflows/release-api-changelogs.yaml)**: Automatic changelog generation triggered by NPM releases
- **[Release NPM Types](.github/workflows/release-npm-types.yaml)**: Publishes type definitions and triggers automatic changelog
## Troubleshooting
### "Tag does not exist" error
Ensure the version exists as a git tag:
```bash
git tag -l 'v*' | grep 1.29.0
```
If missing, the version may not have been released yet.
### "Build failed" error
Check that the version can be built successfully:
```bash
git checkout v1.29.0
pnpm install
pnpm build:types
```
### No changes detected
If the workflow reports no changes but you expect some:
1. Check the artifact snapshots to verify they're different
2. Ensure you're comparing the correct versions
3. Review the comparison script logic in `scripts/compare-api-snapshots.js`
### PR not created
PR creation requires:
- `create_pr` input set to `true`
- Significant changes detected (more than just headers)
- `PR_GH_TOKEN` secret configured with appropriate permissions
## Security
- **Permissions**: Workflow requires `contents: write` and `pull-requests: write`
- **Token**: Uses `secrets.PR_GH_TOKEN` for PR creation
- **Isolation**: Each workflow run uses a unique concurrency group
- **Artifacts**: Retained for 90 days, accessible to repository collaborators
## Monitoring
View workflow runs:
```bash
gh run list --workflow=manual-api-changelog.yaml
```
View specific run details:
```bash
gh run view <run-id>
```
Download artifacts:
```bash
gh run download <run-id>
```

View File

@@ -105,4 +105,4 @@ jobs:
labels: Manager
delete-branch: true
add-paths: |
src/types/generatedManagerTypes.ts
src/types/generatedManagerTypes.ts

View File

@@ -6,8 +6,6 @@ on:
branches:
- main
pull_request:
paths:
- '**/*.json'
jobs:
json-lint:

View File

@@ -51,7 +51,7 @@ jobs:
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes

View File

@@ -6,7 +6,7 @@ on:
paths:
- 'tools/devtools/**'
push:
branches: [main]
branches: [ main ]
paths:
- 'tools/devtools/**'

View File

@@ -13,7 +13,7 @@ jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
if: |
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.repository != null &&
@@ -43,14 +43,14 @@ jobs:
repo: context.repo.repo,
state: 'open',
});
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
if (!pr) {
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
return null;
}
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
return pr.number;
@@ -74,7 +74,7 @@ jobs:
run-id: ${{ github.event.workflow_run.id }}
pattern: playwright-report-*
path: reports
- name: Handle Test Completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
env:
@@ -85,9 +85,9 @@ jobs:
# Rename merged report if exists
[ -d "reports/playwright-report-chromium-merged" ] && \
mv reports/playwright-report-chromium-merged reports/playwright-report-chromium
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"completed"
"completed"

View File

@@ -29,7 +29,7 @@ jobs:
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
# Save the entire workspace as cache for later test jobs to restore
- name: Generate cache key

View File

@@ -13,7 +13,7 @@ jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
if: |
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.head_repository != null &&
github.event.workflow_run.repository != null &&
@@ -43,14 +43,14 @@ jobs:
repo: context.repo.repo,
state: 'open',
});
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
if (!pr) {
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
return null;
}
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
return pr.number;
@@ -74,7 +74,7 @@ jobs:
run-id: ${{ github.event.workflow_run.id }}
name: storybook-static
path: storybook-static
- name: Handle Storybook Completion
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
env:
@@ -88,4 +88,4 @@ jobs:
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"completed"
"completed"

View File

@@ -2,7 +2,7 @@ name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
on:
workflow_dispatch: # Allow manual triggering
workflow_dispatch: # Allow manual triggering
pull_request:
branches: [main]
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Post starting comment
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -89,7 +89,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0 # Required for Chromatic baseline
fetch-depth: 0 # Required for Chromatic baseline
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -111,9 +111,9 @@ jobs:
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
onlyChanged: true # Only capture changed stories
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
onlyChanged: true # Only capture changed stories
- name: Set job status
id: job-status
@@ -138,17 +138,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
with:
name: storybook-static
path: storybook-static
- name: Make deployment script executable
run: chmod +x scripts/cicd/pr-storybook-deploy-and-comment.sh
- name: Deploy Storybook and comment on PR
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -176,25 +176,25 @@ jobs:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';
const storybookUrl = '${{ needs.chromatic-deployment.outputs.chromatic-storybook-url }}';
// Find the existing Storybook comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.pull_request.number }}
});
const storybookComment = comments.find(comment =>
const storybookComment = comments.find(comment =>
comment.body.includes('<!-- STORYBOOK_BUILD_STATUS -->')
);
if (storybookComment && buildUrl && storybookUrl) {
// Append Chromatic info to existing comment
const updatedBody = storybookComment.body.replace(
/---\n(.*)$/s,
`---\n### 🎨 Chromatic Visual Tests\n- 📊 [View Chromatic Build](${buildUrl})\n- 📚 [View Chromatic Storybook](${storybookUrl})\n\n$1`
);
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,

View File

@@ -1,33 +0,0 @@
name: "CI: YAML Validation"
description: "Validates YAML syntax and style using yamllint with relaxed rules"
on:
push:
branches:
- main
paths:
- '**/*.yml'
- '**/*.yaml'
pull_request:
paths:
- '**/*.yml'
- '**/*.yaml'
jobs:
yaml-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install yamllint
run: |
python -m pip install --upgrade pip
python -m pip install yamllint
- name: Validate YAML syntax and style
run: ./scripts/cicd/check-yaml.sh

View File

@@ -2,11 +2,11 @@ name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
on:
# Manual dispatch for urgent translation updates
# Manual dispatch for urgent translation updates
workflow_dispatch:
# Only trigger on PRs to main/master - additional branch filtering in job condition
pull_request:
branches: [main]
branches: [ main ]
types: [opened, synchronize, reopened]
jobs:
@@ -15,45 +15,45 @@ jobs:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup ComfyUI Server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Setup playwright environment
- name: Setup ComfyUI Frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup ComfyUI Server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
# Update locales, collect new strings and update translations using OpenAI, then commit changes
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
# Stash any local changes before checkout
git stash -u
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales"
git push origin HEAD:${{ github.head_ref }}
# Update locales, collect new strings and update translations using OpenAI, then commit changes
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
# Stash any local changes before checkout
git stash -u
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales"
git push origin HEAD:${{ github.head_ref }}

View File

@@ -21,116 +21,116 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
uses: ./.github/actions/setup-comfyui-server
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: 'true'
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
uses: ./.github/actions/setup-comfyui-server
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: 'true'
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Install the custom node repository
- name: Checkout custom node repository
uses: actions/checkout@v5
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- name: Install custom node Python requirements
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
# Install the custom node repository
- name: Checkout custom node repository
uses: actions/checkout@v5
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- name: Install custom node Python requirements
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
fi
# Start ComfyUI Server
- name: Start ComfyUI Server
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
wait-for-it --service
# Start ComfyUI Server
- name: Start ComfyUI Server
shell: bash
working-directory: ComfyUI
run: |
python main.py --cpu --multi-user --front-end-root ../dist --custom-node-path ../ComfyUI/custom_nodes/${{ inputs.repository }} &
wait-for-it --service
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Capture base i18n
run: pnpm exec tsx scripts/diff-i18n capture
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Diff base vs updated i18n
run: pnpm exec tsx scripts/diff-i18n diff
- name: Update i18n in custom node repository
run: |
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
install -d "$LOCALE_DIR"
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
# Git ops for pushing changes and creating PR
- name: Check and create fork of custom node repository
run: |
# Try to fork the repository
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
echo "Fork failed - repository might already be forked"
# Exit 0 to prevent the workflow from failing
exit 0
}
- name: Capture base i18n
run: pnpm exec tsx scripts/diff-i18n capture
- name: Update en.json
run: pnpm collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Diff base vs updated i18n
run: pnpm exec tsx scripts/diff-i18n diff
- name: Update i18n in custom node repository
run: |
LOCALE_DIR=ComfyUI/custom_nodes/${{ inputs.repository }}/locales/
install -d "$LOCALE_DIR"
cp -rf ComfyUI_frontend/temp/diff/* "$LOCALE_DIR"
# Enable workflows on the forked repository
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
-F can_approve_pull_request_reviews=true \
-F default_workflow_permissions="write" \
-F enabled=true
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
# Git ops for pushing changes and creating PR
- name: Check and create fork of custom node repository
run: |
# Try to fork the repository
gh repo fork ${{ inputs.owner }}/${{ inputs.repository }} --clone=false || {
echo "Fork failed - repository might already be forked"
# Exit 0 to prevent the workflow from failing
exit 0
}
- name: Commit changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
# Enable workflows on the forked repository
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
"/repos/${{ inputs.fork_owner }}/${{ inputs.repository }}/actions/permissions/workflow" \
-F can_approve_pull_request_reviews=true \
-F default_workflow_permissions="write" \
-F enabled=true
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
# Create and switch to new branch
git checkout -b update-locales
- name: Commit changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
# Stage and commit changes
git add -A
git commit -m "Update locales"
# Create and switch to new branch
git checkout -b update-locales
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
# github public key to confirm it's github server
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
# Stage and commit changes
git add -A
git commit -m "Update locales"
- name: Push changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Force push to create the branch
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}
# github public key to confirm it's github server
known_hosts: github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
- name: Push changes
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Force push to create the branch
echo "Pushing changes to ${{ inputs.fork_owner }}/${{ inputs.repository }}"
git push -f git@github.com:${{ inputs.fork_owner }}/${{ inputs.repository }}.git update-locales
- name: Create PR
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Create PR using gh cli
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
- name: Create PR
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
run: |
# Create PR using gh cli
gh pr create --title "Update locales for ${{ inputs.repository }}" --repo ${{ inputs.owner }}/${{ inputs.repository }} --head ${{ inputs.fork_owner }}:update-locales --body "Update locales for ${{ inputs.repository }}"
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}

View File

@@ -13,42 +13,42 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Checkout repository
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: "Update locales for node definitions"
title: "Update locales for node definitions"
body: |
Automated PR to update locales for node definitions
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
- name: Update translations
run: pnpm locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: "Update locales for node definitions"
title: "Update locales for node definitions"
body: |
Automated PR to update locales for node definitions
This PR was created automatically by the frontend update workflow.
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
base: main
labels: dependencies
This PR was created automatically by the frontend update workflow.
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
base: main
labels: dependencies

View File

@@ -0,0 +1,255 @@
name: Manual API Changelog Generation
on:
workflow_dispatch:
inputs:
from_version:
description: 'Previous version (e.g., 1.29.0 or v1.29.0)'
required: true
type: string
to_version:
description: 'Current version (e.g., 1.30.2 or v1.30.2)'
required: true
type: string
create_pr:
description: 'Create a pull request with the changelog'
required: false
type: boolean
default: false
concurrency:
group: manual-api-changelog-${{ github.run_id }}
cancel-in-progress: false
jobs:
generate_changelog:
name: Generate API Changelog
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Fetch all history for comparing versions
- name: Validate version inputs
id: validate_versions
run: |
# Normalize version strings (remove 'v' prefix if present)
FROM_VERSION="${{ github.event.inputs.from_version }}"
TO_VERSION="${{ github.event.inputs.to_version }}"
FROM_VERSION=${FROM_VERSION#v}
TO_VERSION=${TO_VERSION#v}
# Validate version format (semantic versioning)
if ! [[ "$FROM_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid from_version format: $FROM_VERSION"
echo "Expected format: X.Y.Z (e.g., 1.29.0)"
exit 1
fi
if ! [[ "$TO_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid to_version format: $TO_VERSION"
echo "Expected format: X.Y.Z (e.g., 1.30.2)"
exit 1
fi
# Check if tags exist
if ! git rev-parse "v$FROM_VERSION" >/dev/null 2>&1; then
echo "Error: Tag v$FROM_VERSION does not exist"
exit 1
fi
if ! git rev-parse "v$TO_VERSION" >/dev/null 2>&1; then
echo "Error: Tag v$TO_VERSION does not exist"
exit 1
fi
echo "from_version=$FROM_VERSION" >> $GITHUB_OUTPUT
echo "to_version=$TO_VERSION" >> $GITHUB_OUTPUT
echo "from_tag=v$FROM_VERSION" >> $GITHUB_OUTPUT
echo "to_tag=v$TO_VERSION" >> $GITHUB_OUTPUT
echo "✅ Validated versions: v$FROM_VERSION → v$TO_VERSION"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Create snapshots directory
run: mkdir -p .api-snapshots
- name: Preserve scripts
run: |
# Copy scripts to temporary location
mkdir -p /tmp/api-changelog-scripts
cp scripts/snapshot-api.js scripts/compare-api-snapshots.js /tmp/api-changelog-scripts/
- name: Build and snapshot TO version
run: |
echo "Building types for v${{ steps.validate_versions.outputs.to_version }}"
git checkout ${{ steps.validate_versions.outputs.to_tag }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
pnpm install --frozen-lockfile
pnpm build:types
# Generate snapshot
node scripts/snapshot-api.js dist/index.d.ts > /tmp/api-snapshots-to.json
echo "✅ Created snapshot for v${{ steps.validate_versions.outputs.to_version }}"
- name: Build and snapshot FROM version
run: |
echo "Building types for v${{ steps.validate_versions.outputs.from_version }}"
git checkout ${{ steps.validate_versions.outputs.from_tag }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
pnpm install --frozen-lockfile
pnpm build:types
# Generate snapshot
node scripts/snapshot-api.js dist/index.d.ts > /tmp/api-snapshots-from.json
echo "✅ Created snapshot for v${{ steps.validate_versions.outputs.from_version }}"
- name: Return to original branch
run: |
git checkout ${{ github.ref_name }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
# Copy snapshots to working directory
cp /tmp/api-snapshots-from.json .api-snapshots/from.json
cp /tmp/api-snapshots-to.json .api-snapshots/to.json
- name: Compare API snapshots and generate changelog
id: generate_changelog
run: |
# Get git ref for TO version
GIT_REF=$(git rev-parse ${{ steps.validate_versions.outputs.to_tag }})
# Run the comparison script
CHANGELOG_OUTPUT=$(node scripts/compare-api-snapshots.js \
.api-snapshots/from.json \
.api-snapshots/to.json \
${{ steps.validate_versions.outputs.from_version }} \
${{ steps.validate_versions.outputs.to_version }} \
Comfy-Org \
ComfyUI_frontend \
"$GIT_REF")
# Save changelog to file for artifact
echo "$CHANGELOG_OUTPUT" > .api-snapshots/CHANGELOG-${{ steps.validate_versions.outputs.from_version }}-to-${{ steps.validate_versions.outputs.to_version }}.md
# Also output to step summary
echo "## 📊 Generated API Changelog" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "$CHANGELOG_OUTPUT" >> $GITHUB_STEP_SUMMARY
# Check if changelog is empty or just header
if [ $(echo "$CHANGELOG_OUTPUT" | wc -l) -lt 5 ]; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "⚠️ No significant API changes detected" >> $GITHUB_STEP_SUMMARY
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "✅ API changes detected and documented" >> $GITHUB_STEP_SUMMARY
fi
echo "✅ Changelog generated successfully"
- name: Upload changelog artifact
uses: actions/upload-artifact@v4
with:
name: api-changelog-v${{ steps.validate_versions.outputs.from_version }}-to-v${{ steps.validate_versions.outputs.to_version }}
path: |
.api-snapshots/CHANGELOG-*.md
.api-snapshots/from.json
.api-snapshots/to.json
retention-days: 90
- name: Create Pull Request
if: github.event.inputs.create_pr == 'true' && steps.generate_changelog.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[docs] Update API changelog for v${{ steps.validate_versions.outputs.from_version }} → v${{ steps.validate_versions.outputs.to_version }}'
title: '[docs] API Changelog: v${{ steps.validate_versions.outputs.from_version }} → v${{ steps.validate_versions.outputs.to_version }}'
body: |
## API Changelog Update (Manual)
This PR documents public API changes between v${{ steps.validate_versions.outputs.from_version }} and v${{ steps.validate_versions.outputs.to_version }}.
The changelog has been manually generated by comparing TypeScript type definitions between versions.
### Version Comparison
- **From:** v${{ steps.validate_versions.outputs.from_version }}
- **To:** v${{ steps.validate_versions.outputs.to_version }}
- **Requested by:** @${{ github.actor }}
- **Workflow run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
### Review Instructions
- Review the changes in `docs/API-CHANGELOG.md`
- Verify accuracy of breaking changes
- Add any additional context or migration notes if needed
- Merge when ready to publish changelog
### Artifacts
The full changelog and snapshots are available as workflow artifacts for 90 days.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
branch: api-changelog-manual-${{ steps.validate_versions.outputs.from_version }}-to-${{ steps.validate_versions.outputs.to_version }}
base: ${{ github.ref_name }}
labels: documentation
delete-branch: true
draft: true
add-paths: |
docs/API-CHANGELOG.md
- name: Summary
run: |
echo "## 🎉 Workflow Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **From version:** v${{ steps.validate_versions.outputs.from_version }}" >> $GITHUB_STEP_SUMMARY
echo "- **To version:** v${{ steps.validate_versions.outputs.to_version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Changes detected:** ${{ steps.generate_changelog.outputs.has_changes }}" >> $GITHUB_STEP_SUMMARY
echo "- **Create PR:** ${{ github.event.inputs.create_pr }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Artifact" >> $GITHUB_STEP_SUMMARY
echo "The generated changelog and API snapshots have been uploaded as artifacts." >> $GITHUB_STEP_SUMMARY
echo "Retention: 90 days" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event.inputs.create_pr }}" == "true" ] && [ "${{ steps.generate_changelog.outputs.has_changes }}" == "true" ]; then
echo "### 🔀 Pull Request" >> $GITHUB_STEP_SUMMARY
echo "A draft pull request has been created with the changelog updates." >> $GITHUB_STEP_SUMMARY
elif [ "${{ github.event.inputs.create_pr }}" == "true" ]; then
echo "### No PR Created" >> $GITHUB_STEP_SUMMARY
echo "No significant changes were detected, so no PR was created." >> $GITHUB_STEP_SUMMARY
else
echo "### PR Creation Skipped" >> $GITHUB_STEP_SUMMARY
echo "Pull request creation was not requested. Enable 'Create PR' option to automatically create a PR." >> $GITHUB_STEP_SUMMARY
fi

View File

@@ -19,8 +19,8 @@ on:
jobs:
backport:
if: >
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
@@ -38,19 +38,19 @@ jobs:
echo "::error::Invalid PR number format. Must be a positive integer."
exit 1
fi
# Validate PR exists and is merged
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
exit 1
fi
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
if [ "$MERGED" != "true" ]; then
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
exit 1
fi
# Validate PR has needs-backport label
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
@@ -164,7 +164,6 @@ jobs:
PENDING=()
SKIPPED=()
REUSED=()
for target in $REQUESTED_TARGETS; do
SAFE_TARGET=$(echo "$target" | tr '/' '-')
@@ -177,22 +176,10 @@ jobs:
if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" |
grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then
OPEN_PR=$(
gh pr list \
--state open \
--head "${BACKPORT_BRANCH}" \
--json number \
--jq 'if length > 0 then .[0].number else "" end'
)
if [ -n "$OPEN_PR" ]; then
SKIPPED+=("${target} (PR #${OPEN_PR})")
continue
fi
REUSED+=("$BACKPORT_BRANCH")
SKIPPED+=("$target")
else
PENDING+=("$target")
fi
PENDING+=("$target")
done
SKIPPED_JOINED="${SKIPPED[*]:-}"
@@ -200,20 +187,16 @@ jobs:
echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT
echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT
echo "reused-branches=${REUSED[*]:-}" >> $GITHUB_OUTPUT
if [ -z "$PENDING_JOINED" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
if [ -n "$SKIPPED_JOINED" ]; then
echo "::warning::Backport branches exist: ${SKIPPED_JOINED}"
echo "::warning::Backport branches already exist for: ${SKIPPED_JOINED}"
fi
else
echo "skip=false" >> $GITHUB_OUTPUT
if [ -n "$SKIPPED_JOINED" ]; then
echo "::notice::Skipping backport targets: ${SKIPPED_JOINED}"
fi
if [ "${#REUSED[@]}" -gt 0 ]; then
echo "::notice::Reusing backport branches: ${REUSED[*]}"
echo "::notice::Skipping already backported targets: ${SKIPPED_JOINED}"
fi
fi
@@ -225,12 +208,7 @@ jobs:
run: |
FAILED=""
SUCCESS=""
CREATED_BRANCHES_FILE="$(
mktemp "$RUNNER_TEMP/backport-branches-XXXXXX"
)"
echo "CREATED_BRANCHES_FILE=$CREATED_BRANCHES_FILE" >> "$GITHUB_ENV"
# Get PR data for manual triggers
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
@@ -245,12 +223,6 @@ jobs:
TARGET_BRANCH="${target}"
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
REMOTE_BACKPORT_EXISTS=false
if git ls-remote --exit-code origin "${BACKPORT_BRANCH}" >/dev/null 2>&1; then
REMOTE_BACKPORT_EXISTS=true
echo "::notice::Updating existing branch ${BACKPORT_BRANCH}"
fi
echo "::group::Backporting to ${TARGET_BRANCH}"
@@ -275,12 +247,7 @@ jobs:
# Try cherry-pick
if git cherry-pick "${MERGE_COMMIT}"; then
if [ "$REMOTE_BACKPORT_EXISTS" = true ]; then
git push --force-with-lease origin "${BACKPORT_BRANCH}"
else
git push origin "${BACKPORT_BRANCH}"
fi
echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE"
git push origin "${BACKPORT_BRANCH}"
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
# Return to main (keep the branch, we need it for PR)
@@ -304,13 +271,6 @@ jobs:
echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
echo "failed=${FAILED}" >> $GITHUB_OUTPUT
if [ -s "$CREATED_BRANCHES_FILE" ]; then
CREATED_LIST=$(paste -sd' ' "$CREATED_BRANCHES_FILE")
echo "created-branches=${CREATED_LIST}" >> $GITHUB_OUTPUT
else
echo "created-branches=" >> $GITHUB_OUTPUT
fi
if [ -n "${FAILED}" ]; then
exit 1
fi
@@ -330,7 +290,7 @@ jobs:
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r target branch <<< "${backport}"
@@ -388,25 +348,6 @@ jobs:
fi
done
- name: Cleanup stranded backport branches
if: steps.filter-targets.outputs.skip != 'true' && failure()
run: |
FILE="${CREATED_BRANCHES_FILE:-}"
if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then
echo "No backport branches recorded for cleanup"
exit 0
fi
while IFS= read -r branch; do
[ -z "$branch" ] && continue
printf 'Deleting branch %s\n' "${branch}"
if ! git push origin --delete "$branch"; then
echo "::warning::Failed to delete ${branch}"
fi
done < "$FILE"
- name: Remove needs-backport label
if: steps.filter-targets.outputs.skip != 'true' && success()
run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"

View File

@@ -127,26 +127,26 @@ jobs:
echo "=========================================="
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
echo "=========================================="
# Get list of changed snapshot files
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
if [ -z "$changed_files" ]; then
echo "No snapshot changes in this shard"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "✓ Found changed files:"
echo "$changed_files"
file_count=$(echo "$changed_files" | wc -l)
echo "Count: $file_count"
echo "has-changes=true" >> $GITHUB_OUTPUT
echo ""
# Create staging directory
mkdir -p /tmp/changed_snapshots_shard
# Copy only changed files, preserving directory structure
# Strip 'browser_tests/' prefix to avoid double nesting
echo "Copying changed files to staging directory..."
@@ -159,7 +159,7 @@ jobs:
cp "$file" "/tmp/changed_snapshots_shard/$file_without_prefix"
echo " → $file_without_prefix"
done <<< "$changed_files"
echo ""
echo "Staged files for upload:"
find /tmp/changed_snapshots_shard -type f
@@ -233,18 +233,18 @@ jobs:
shard_name=$(basename "$shard_dir")
file_count=$(find "$shard_dir" -type f | wc -l)
if [ "$file_count" -eq 0 ]; then
echo " $shard_name: no files"
continue
fi
echo "Processing $shard_name ($file_count file(s))..."
# Copy files directly, preserving directory structure
# Since files are already in correct structure (no browser_tests/ prefix), just copy them all
cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /'
merged_count=$((merged_count + 1))
echo " ✓ Merged"
echo ""
@@ -272,25 +272,25 @@ jobs:
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
if git diff --quiet browser_tests/; then
echo "No changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "=========================================="
echo "COMMITTING CHANGES"
echo "=========================================="
echo "has-changes=true" >> $GITHUB_OUTPUT
git add browser_tests/
git commit -m "[automated] Update test expectations"
echo "Pushing to ${{ needs.setup.outputs.branch }}..."
git push origin ${{ needs.setup.outputs.branch }}
echo "✓ Commit and push successful"
- name: Add Done Reaction
@@ -306,4 +306,4 @@ jobs:
if: always() && github.event_name == 'pull_request'
run: gh pr edit ${{ needs.setup.outputs.pr-number }} --remove-label "New Browser Test Expectations"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,203 @@
name: Release API Changelogs
on:
workflow_run:
workflows: ['Release NPM Types']
types:
- completed
push:
branches:
- sno-api-changelog
concurrency:
group: release-api-changelogs-${{ github.workflow }}
cancel-in-progress: false
jobs:
generate_changelog:
name: Generate API Changelog
runs-on: ubuntu-latest
# Only run on successful completion of the Release NPM Types workflow or on push to sno-api-changelog
if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Fetch all history for comparing versions
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Get current version
id: current_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Current version: $VERSION"
- name: Get previous version
id: previous_version
run: |
# Get the two most recent version tags sorted
CURRENT_VERSION="${{ steps.current_version.outputs.version }}"
TAGS=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -2)
# Find the previous version tag (skip current if it exists)
PREVIOUS_TAG=""
for tag in $TAGS; do
TAG_VERSION=${tag#v}
if [ "$TAG_VERSION" != "$CURRENT_VERSION" ]; then
PREVIOUS_TAG=$tag
break
fi
done
if [ -z "$PREVIOUS_TAG" ]; then
echo "No previous version found, this may be the first release"
echo "version=" >> $GITHUB_OUTPUT
echo "tag=" >> $GITHUB_OUTPUT
else
echo "version=${PREVIOUS_TAG#v}" >> $GITHUB_OUTPUT
echo "tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
echo "Previous version: ${PREVIOUS_TAG#v}"
fi
- name: Build current types
run: pnpm build:types
- name: Snapshot current API
id: current_snapshot
run: |
# Create snapshots directory
mkdir -p .api-snapshots
# Generate snapshot of current types
node scripts/snapshot-api.js dist/index.d.ts > .api-snapshots/current.json
echo "Current API snapshot created"
- name: Preserve scripts for previous version
if: steps.previous_version.outputs.tag != ''
run: |
# Copy scripts to temporary location to use with previous version
mkdir -p /tmp/api-changelog-scripts
cp scripts/snapshot-api.js scripts/compare-api-snapshots.js /tmp/api-changelog-scripts/
- name: Checkout previous version
if: steps.previous_version.outputs.tag != ''
run: |
# Stash current changes
git stash
# Checkout previous version
git checkout ${{ steps.previous_version.outputs.tag }}
# Restore scripts
mkdir -p scripts
cp /tmp/api-changelog-scripts/*.js scripts/
- name: Build previous types
if: steps.previous_version.outputs.tag != ''
run: |
pnpm install --frozen-lockfile
pnpm build:types
- name: Snapshot previous API
if: steps.previous_version.outputs.tag != ''
run: |
# Generate snapshot of previous types
node scripts/snapshot-api.js dist/index.d.ts > .api-snapshots/previous.json
echo "Previous API snapshot created"
- name: Return to current version
if: steps.previous_version.outputs.tag != ''
run: |
# Remove copied scripts to avoid conflicts
rm -f scripts/snapshot-api.js scripts/compare-api-snapshots.js
git checkout -
git stash pop || true
- name: Compare API snapshots and generate changelog
id: generate_changelog
run: |
# Create docs directory if it doesn't exist
mkdir -p docs
# Get current git ref (commit SHA)
GIT_REF=$(git rev-parse HEAD)
# Run the comparison script
if [ -f .api-snapshots/previous.json ]; then
node scripts/compare-api-snapshots.js \
.api-snapshots/previous.json \
.api-snapshots/current.json \
${{ steps.previous_version.outputs.version }} \
${{ steps.current_version.outputs.version }} \
Comfy-Org \
ComfyUI_frontend \
"$GIT_REF" \
>> docs/API-CHANGELOG.md
else
# First release - just document the initial API surface
echo "## v${{ steps.current_version.outputs.version }} ($(date +%Y-%m-%d))" >> docs/API-CHANGELOG.md
echo "" >> docs/API-CHANGELOG.md
echo "Initial API release." >> docs/API-CHANGELOG.md
echo "" >> docs/API-CHANGELOG.md
fi
# Check if there are any changes
if git diff --quiet docs/API-CHANGELOG.md; then
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "No API changes detected"
else
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "API changes detected"
fi
- name: Create Pull Request
if: steps.generate_changelog.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[docs] Update API changelog for v${{ steps.current_version.outputs.version }}'
title: '[docs] API Changelog for v${{ steps.current_version.outputs.version }}'
body: |
## API Changelog Update
This PR documents public API changes between v${{ steps.previous_version.outputs.version }} and v${{ steps.current_version.outputs.version }}.
The changelog has been automatically generated by comparing TypeScript type definitions between versions.
### Review Instructions
- Review the changes in `docs/API-CHANGELOG.md`
- Verify accuracy of breaking changes
- Add any additional context or migration notes if needed
- Merge when ready to publish changelog
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
branch: api-changelog-v${{ steps.current_version.outputs.version }}
base: ${{ github.event_name == 'push' && github.ref_name || 'main' }}
labels: documentation
delete-branch: true
draft: true
add-paths: |
docs/API-CHANGELOG.md

View File

@@ -153,78 +153,8 @@ jobs:
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Ensure release labels
if: steps.check_version.outputs.is_minor_bump == 'true'
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
BRANCH_BASE="${{ steps.check_version.outputs.branch_base }}"
if [[ -z "$BRANCH_BASE" ]]; then
echo "::error::Branch base not set; unable to manage labels"
exit 1
fi
declare -A COLORS=(
[core]="4361ee"
[cloud]="4f6ef5"
)
for PREFIX in core cloud; do
LABEL="${PREFIX}/${BRANCH_BASE}"
COLOR="${COLORS[$PREFIX]}"
DESCRIPTION="Backport PRs for ${PREFIX} ${BRANCH_BASE}"
if gh label view "$LABEL" >/dev/null 2>&1; then
gh label edit "$LABEL" \
--color "$COLOR" \
--description "$DESCRIPTION"
echo "🔄 Updated label $LABEL"
else
gh label create "$LABEL" \
--color "$COLOR" \
--description "$DESCRIPTION"
echo "✨ Created label $LABEL"
fi
done
MIN_LABELS_TO_KEEP=3
MAX_LABELS_TO_FETCH=200
for PREFIX in core cloud; do
mapfile -t LABELS < <(
gh label list \
--json name \
--limit "$MAX_LABELS_TO_FETCH" \
--jq '.[].name' |
grep -E "^${PREFIX}/[0-9]+\.[0-9]+$" |
sort -t/ -k2,2V
)
TOTAL=${#LABELS[@]}
if (( TOTAL <= MIN_LABELS_TO_KEEP )); then
echo " Nothing to prune for $PREFIX labels"
continue
fi
REMOVE_COUNT=$((TOTAL - MIN_LABELS_TO_KEEP))
if (( REMOVE_COUNT > 1 )); then
REMOVE_COUNT=1
fi
for ((i=0; i<REMOVE_COUNT; i++)); do
OLD_LABEL="${LABELS[$i]}"
gh label delete "$OLD_LABEL" --yes
echo "🗑️ Removed old label $OLD_LABEL"
done
done
- name: Post summary
if: always() && steps.check_version.outputs.is_minor_bump == 'true'
if: steps.check_version.outputs.is_minor_bump == 'true'
run: |
CURRENT_VERSION="${{ steps.check_version.outputs.current_version }}"
RESULTS="${{ steps.create_branches.outputs.results }}"

View File

@@ -59,7 +59,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Bump version
id: bump-version

View File

@@ -92,3 +92,4 @@ jobs:
base: ${{ github.event.inputs.branch }}
labels: |
Release

View File

@@ -1,43 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
".i18nrc.cjs",
"components.d.ts",
"lint-staged.config.js",
"vitest.setup.ts",
"**/vite.config.*.timestamp*",
"**/vitest.config.*.timestamp*",
"packages/registry-types/src/comfyRegistryTypes.ts",
"src/extensions/core/*",
"src/scripts/*",
"src/types/generatedManagerTypes.ts",
"src/types/vue-shim.d.ts"
],
"rules": {
"no-async-promise-executor": "off",
"no-control-regex": "off",
"no-eval": "off",
"no-self-assign": "allow",
"no-unused-expressions": "off",
"no-unused-private-class-members": "off",
"no-useless-rename": "off",
"typescript/no-this-alias": "off",
"typescript/no-unnecessary-parameter-property-assignment": "off",
"typescript/no-unsafe-declaration-merging": "off",
"typescript/no-unused-vars": "off",
"unicorn/no-empty-file": "off",
"unicorn/no-new-array": "off",
"unicorn/no-single-promise-in-promise-methods": "off",
"unicorn/no-useless-fallback-in-spread": "off",
"unicorn/no-useless-spread": "off",
"typescript/await-thenable": "off",
"typescript/no-base-to-string": "off",
"typescript/no-duplicate-type-constituents": "off",
"typescript/no-for-in-array": "off",
"typescript/no-meaningless-void-operator": "off",
"typescript/no-redundant-type-constituents": "off",
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error"
}
}

View File

@@ -1,10 +0,0 @@
extends: default
ignore: |
node_modules/
dist/
rules:
line-length: disable
document-start: disable
truthy: disable

View File

@@ -17,7 +17,6 @@ This bootstraps the monorepo with dependencies, builds, tests, and dev server ve
- `pnpm typecheck`: Type checking
- `pnpm build`: Build for production (via nx)
- `pnpm lint`: Linting (via nx)
- `pnpm oxlint`: Fast Rust-based linting with Oxc
- `pnpm format`: Prettier formatting
- `pnpm test:unit`: Run all unit tests
- `pnpm test:browser`: Run E2E tests via Playwright

View File

@@ -243,7 +243,7 @@ pnpm format
### Styling
- Use Tailwind CSS classes instead of custom CSS
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface`
- Follow the existing dark theme pattern: `dark-theme:` prefix (not `dark:`)
### Internationalization
- All user-facing strings must use vue-i18n

View File

@@ -71,8 +71,8 @@ const updateConsent = async () => {
} catch (error) {
toast.add({
severity: 'error',
summary: t('install.settings.errorUpdatingConsent'),
detail: t('install.settings.errorUpdatingConsentDetail'),
summary: t('install.errorUpdatingConsent'),
detail: t('install.errorUpdatingConsentDetail'),
life: 3000
})
} finally {

View File

@@ -3,8 +3,9 @@
"compilerOptions": {
"noEmit": true,
"allowImportingTsExtensions": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/*": ["src/*"],
"@frontend-locales/*": ["../../src/locales/*"]
}
},

View File

@@ -1,20 +1,17 @@
import type { FullConfig } from '@playwright/test'
import { config as loadEnv } from 'dotenv'
import dotenv from 'dotenv'
import { backupPath } from './utils/backupUtils'
import { syncDevtools } from './utils/devtoolsSync'
loadEnv()
dotenv.config()
export default function globalSetup(_: FullConfig) {
export default function globalSetup(config: FullConfig) {
if (!process.env.CI) {
if (process.env.TEST_COMFYUI_DIR) {
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
renameAndReplaceWithScaffolding: true
})
syncDevtools(process.env.TEST_COMFYUI_DIR)
} else {
console.warn(
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'

View File

@@ -0,0 +1,144 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
interface ChatHistoryEntry {
prompt: string
response: string
response_id: string
}
async function renderChatHistory(page: Page, history: ChatHistoryEntry[]) {
const nodeId = await page.evaluate(() => window['app'].graph.nodes[0]?.id)
// Simulate API sending display_component message
await page.evaluate(
({ nodeId, history }) => {
const event = new CustomEvent('display_component', {
detail: {
node_id: nodeId,
component: 'ChatHistoryWidget',
props: {
history: JSON.stringify(history)
}
}
})
window['app'].api.dispatchEvent(event)
return true
},
{ nodeId, history }
)
return nodeId
}
test.describe('Chat History Widget', () => {
let nodeId: string
test.beforeEach(async ({ comfyPage }) => {
nodeId = await renderChatHistory(comfyPage.page, [
{ prompt: 'Hello', response: 'World', response_id: '123' }
])
// Wait for chat history to be rendered
await comfyPage.page.waitForSelector('.pi-pencil')
})
test('displays chat history when receiving display_component message', async ({
comfyPage
}) => {
// Verify the chat history is displayed correctly
await expect(comfyPage.page.getByText('Hello')).toBeVisible()
await expect(comfyPage.page.getByText('World')).toBeVisible()
})
test('handles message editing interaction', async ({ comfyPage }) => {
// Get first node's ID
nodeId = await comfyPage.page.evaluate(() => {
const node = window['app'].graph.nodes[0]
// Make sure the node has a prompt widget (for editing functionality)
if (!node.widgets) {
node.widgets = []
}
// Add a prompt widget if it doesn't exist
if (!node.widgets.find((w) => w.name === 'prompt')) {
node.widgets.push({
name: 'prompt',
type: 'text',
value: 'Original prompt'
})
}
return node.id
})
await renderChatHistory(comfyPage.page, [
{
prompt: 'Message 1',
response: 'Response 1',
response_id: '123'
},
{
prompt: 'Message 2',
response: 'Response 2',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
const originalTextAreaInput = await comfyPage.page
.getByPlaceholder('text')
.nth(1)
.inputValue()
// Click edit button on first message
await comfyPage.page.getByLabel('Edit').first().click()
await comfyPage.nextFrame()
// Verify cancel button appears
await expect(comfyPage.page.getByLabel('Cancel')).toBeVisible()
// Click cancel edit
await comfyPage.page.getByLabel('Cancel').click()
// Verify prompt input is restored
await expect(comfyPage.page.getByPlaceholder('text').nth(1)).toHaveValue(
originalTextAreaInput
)
})
test('handles real-time updates to chat history', async ({ comfyPage }) => {
// Send initial history
await renderChatHistory(comfyPage.page, [
{
prompt: 'Initial message',
response: 'Initial response',
response_id: '123'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
// Update history with additional messages
await renderChatHistory(comfyPage.page, [
{
prompt: 'Follow-up',
response: 'New response',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
// Move mouse over the canvas to force update
await comfyPage.page.mouse.move(100, 100)
await comfyPage.nextFrame()
// Verify new messages appear
await expect(comfyPage.page.getByText('Follow-up')).toBeVisible()
await expect(comfyPage.page.getByText('New response')).toBeVisible()
})
})

View File

@@ -88,10 +88,6 @@ test.describe('Missing models warning', () => {
const downloadButton = missingModelsWarning.getByLabel('Download')
await expect(downloadButton).toBeVisible()
// Check that the copy URL button is also visible for Desktop environment
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
await expect(copyUrlButton).toBeVisible()
})
test('Should display a warning when missing models are found in node properties', async ({
@@ -105,10 +101,6 @@ test.describe('Missing models warning', () => {
const downloadButton = missingModelsWarning.getByLabel('Download')
await expect(downloadButton).toBeVisible()
// Check that the copy URL button is also visible for Desktop environment
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
await expect(copyUrlButton).toBeVisible()
})
test('Should not display a warning when no missing models are found', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -1,52 +0,0 @@
import fs from 'fs-extra'
import path from 'path'
import { fileURLToPath } from 'url'
export function syncDevtools(targetComfyDir: string): boolean {
if (!targetComfyDir) {
console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set')
return false
}
// Validate and sanitize the target directory path
const resolvedTargetDir = path.resolve(targetComfyDir)
// Basic path validation to prevent directory traversal
if (resolvedTargetDir.includes('..') || !path.isAbsolute(resolvedTargetDir)) {
console.error('syncDevtools failed: Invalid target directory path')
return false
}
const moduleDir =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url))
const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools')
if (!fs.pathExistsSync(devtoolsSrc)) {
console.warn(
`syncDevtools skipped: source directory not found at ${devtoolsSrc}`
)
return false
}
const devtoolsDest = path.resolve(
resolvedTargetDir,
'custom_nodes',
'ComfyUI_devtools'
)
console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`)
try {
fs.removeSync(devtoolsDest)
fs.ensureDirSync(devtoolsDest)
fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true })
console.warn('syncDevtools: copy complete')
return true
} catch (error) {
console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error)
return false
}
}

View File

@@ -0,0 +1,49 @@
## v1.30.2 (2025-11-01)
Comparing v1.29.0 → v1.30.2. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.
### ✨ Additions
**Type Aliases**
- `WorkflowId`
**Interfaces**
- `ExtensionMetadata`
- Members: `id`, `name`, `version`, `description`
### 🔄 Modifications
> **Note**: Some modifications may be breaking changes.
**Interfaces**
- `ComfyApi`
- ✨ Added member: `queuePromptAsync`
- ✨ Added member: `cancelPrompt`
- ✨ Added member: `getQueueStatus`
- ⚠️ **Breaking**: Removed member: `queuePrompt`
- `NodeDef`
- ✨ Added member: `input`
- ✨ Added member: `output`
- ✨ Added member: `output_name`
- `WorkflowMetadata`
- ✨ Added member: `tags`
- ✨ Added member: `thumbnail`
**Enums**
- `NodeStatus`
- ✨ Added enum value: `ERROR`
- ✨ Added enum value: `COMPLETED`
**Classes**
- `WorkflowManager`
- ✨ Added member: `cache`
- ✨ Added method: `deleteWorkflow()`
- ✨ Added method: `searchWorkflows()`
---

188
demo-snapshots/README.md Normal file
View File

@@ -0,0 +1,188 @@
# API Changelog Generation Demo
This demo showcases the automated API changelog generation system comparing two versions of the ComfyUI Frontend public API.
## Overview
The demo compares **v1.29.0****v1.30.2** to demonstrate:
- Breaking change detection
- API additions tracking
- Non-breaking modifications
- Human-readable changelog generation
## Demo Files
### Input Files
- **`v1.29.0.d.ts`** - TypeScript definitions representing the v1.29.0 API surface
- **`v1.30.2.d.ts`** - TypeScript definitions representing the v1.30.2 API surface
### Generated Files
- **`v1.29.0.json`** - Structured API snapshot from v1.29.0
- **`v1.30.2.json`** - Structured API snapshot from v1.30.2
- **`CHANGELOG-DEMO.md`** - Generated changelog comparing the two versions
## Running the Demo
```bash
# Generate API snapshots
node scripts/snapshot-api.js demo-snapshots/v1.29.0.d.ts > demo-snapshots/v1.29.0.json
node scripts/snapshot-api.js demo-snapshots/v1.30.2.d.ts > demo-snapshots/v1.30.2.json
# Compare snapshots and generate changelog
node scripts/compare-api-snapshots.js \
demo-snapshots/v1.29.0.json \
demo-snapshots/v1.30.2.json \
1.29.0 \
1.30.2 \
> demo-snapshots/CHANGELOG-DEMO.md
```
## Key Changes Detected
### ⚠️ Breaking Changes
1. **`ComfyApi.queuePrompt()` removed**
- Replaced with `queuePromptAsync()` which includes additional options
- Extension developers need to update their code to use the new async method
### ✨ New Additions
1. **New Interface: `ExtensionMetadata`**
- Provides metadata for extensions
- Fields: `id`, `name`, `version`, `description`
2. **New Type: `WorkflowId`**
- Type alias for workflow identifiers
3. **Enhanced `ComfyApi` Interface**
- `queuePromptAsync()` - Async queue with priority support
- `cancelPrompt()` - Cancel queued prompts
- `getQueueStatus()` - Query queue state
4. **Extended `NodeDef` Interface**
- `input` - Input specification
- `output` - Output types
- `output_name` - Output names
5. **Enhanced `NodeStatus` Enum**
- Added `ERROR` state
- Added `COMPLETED` state
6. **Extended `WorkflowManager` Class**
- `cache` property for workflow caching
- `deleteWorkflow()` method
- `searchWorkflows()` method
### 🔄 Non-Breaking Modifications
1. **`WorkflowMetadata` enhancements**
- Added optional `tags` field
- Added optional `thumbnail` field
## Real-World Usage
In production, this system will:
1. **Automatic Triggering**: Run after each NPM types release
2. **Version Detection**: Automatically detect current and previous versions from git tags
3. **Build Integration**: Build actual TypeScript types from the repository
4. **PR Creation**: Generate draft pull requests with the changelog
5. **Human Review**: Allow maintainers to review and enhance before merging
## Benefits for Extension Developers
### Clear Breaking Change Visibility
Extension developers can immediately see:
- What APIs were removed
- What signatures changed
- How to migrate their code
### Migration Planning
With clear documentation of additions and changes, developers can:
- Plan updates around breaking changes
- Adopt new features when ready
- Understand version compatibility
### Historical Reference
The cumulative `docs/API-CHANGELOG.md` provides:
- Complete API evolution history
- Context for design decisions
- Migration guides for major versions
## Example Extension Migration
### Before (v1.29.0)
```typescript
// Old code using queuePrompt
const result = await api.queuePrompt(workflow);
console.log('Queued:', result.prompt_id);
```
### After (v1.30.2)
```typescript
// New code using queuePromptAsync with priority
const result = await api.queuePromptAsync(workflow, { priority: 1 });
console.log('Queued:', result.prompt_id, 'Position:', result.number);
```
## Snapshot Structure
The JSON snapshots contain structured representations of:
```json
{
"types": { /* Type aliases */ },
"interfaces": { /* Interface definitions with members */ },
"enums": { /* Enum values */ },
"functions": { /* Exported functions */ },
"classes": { /* Class definitions with methods */ },
"constants": { /* Exported constants */ }
}
```
Each entry includes:
- **Name**: Identifier
- **Kind**: Type of declaration
- **Members/Methods**: Properties and functions
- **Types**: Parameter and return types
- **Visibility**: Public/private/protected modifiers
- **Optional**: Whether parameters/properties are optional
## Comparison Algorithm
The comparison script:
1. **Categorizes changes** into breaking, additions, and modifications
2. **Detects breaking changes**:
- Removed interfaces, classes, functions
- Removed methods or properties
- Changed method signatures
- Changed return types
- Removed enum values
3. **Tracks additions**:
- New interfaces, classes, types
- New methods and properties
- New enum values
4. **Identifies modifications**:
- Type changes
- Optionality changes
- Signature changes
## Future Enhancements
Planned improvements include:
- **LLM Enhancement**: Use AI to generate better descriptions and migration guides
- **Email Notifications**: Alert developers on mailing list for major changes
- **Release Notes Integration**: Auto-include in GitHub releases
- **Deprecation Tracking**: Mark APIs as deprecated before removal
- **Example Code**: Generate migration code snippets automatically
## Conclusion
This automated system ensures:
- ✅ Zero manual effort for changelog generation
- ✅ Consistent documentation format
- ✅ Clear breaking change visibility
- ✅ Historical API evolution tracking
- ✅ Better extension developer experience

58
demo-snapshots/v1.29.0.d.ts vendored Normal file
View File

@@ -0,0 +1,58 @@
/**
* Mock TypeScript definitions representing v1.29.0 API surface
* This represents the public API as it existed in version 1.29.0
*/
export interface ComfyApi {
/**
* Get API URL for backend calls
*/
apiURL(path: string): string
/**
* Get file URL for static resources
*/
fileURL(path: string): string
/**
* Queue a prompt for execution
*/
queuePrompt(prompt: object): Promise<{ prompt_id: string }>
/**
* Interrupt current execution
*/
interrupt(): Promise<void>
}
export interface NodeDef {
name: string
category: string
display_name?: string
description?: string
python_module: string
}
export enum NodeStatus {
IDLE = 'idle',
QUEUED = 'queued',
RUNNING = 'running'
}
export interface WorkflowMetadata {
title?: string
description?: string
author?: string
version?: string
}
export class WorkflowManager {
workflows: Map<string, object>
constructor()
loadWorkflow(id: string): Promise<object>
saveWorkflow(id: string, data: object): Promise<void>
}
export type NodeId = string

192
demo-snapshots/v1.29.0.json Normal file
View File

@@ -0,0 +1,192 @@
{
"types": {
"NodeId": {
"kind": "type",
"name": "NodeId",
"text": "export type NodeId = string;",
"exported": true
}
},
"interfaces": {
"ComfyApi": {
"kind": "interface",
"name": "ComfyApi",
"members": [
{
"name": "apiURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "fileURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "queuePrompt",
"kind": "method",
"parameters": [
{
"name": "prompt",
"type": "object",
"optional": false
}
],
"returnType": "Promise<{ prompt_id: string }>"
},
{
"name": "interrupt",
"kind": "method",
"parameters": [],
"returnType": "Promise<void>"
}
],
"exported": true,
"heritage": []
},
"NodeDef": {
"kind": "interface",
"name": "NodeDef",
"members": [
{
"name": "name",
"type": "string",
"optional": false
},
{
"name": "category",
"type": "string",
"optional": false
},
{
"name": "display_name",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "python_module",
"type": "string",
"optional": false
}
],
"exported": true,
"heritage": []
},
"WorkflowMetadata": {
"kind": "interface",
"name": "WorkflowMetadata",
"members": [
{
"name": "title",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "author",
"type": "string",
"optional": true
},
{
"name": "version",
"type": "string",
"optional": true
}
],
"exported": true,
"heritage": []
}
},
"enums": {
"NodeStatus": {
"kind": "enum",
"name": "NodeStatus",
"members": [
{
"name": "IDLE",
"value": "\"idle\""
},
{
"name": "QUEUED",
"value": "\"queued\""
},
{
"name": "RUNNING",
"value": "\"running\""
}
],
"exported": true
}
},
"functions": {},
"classes": {
"WorkflowManager": {
"kind": "class",
"name": "WorkflowManager",
"members": [
{
"name": "workflows",
"type": "Map<string, object>",
"visibility": "public"
}
],
"methods": [
{
"name": "loadWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<object>",
"visibility": "public"
},
{
"name": "saveWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
},
{
"name": "data",
"type": "object",
"optional": false
}
],
"returnType": "Promise<void>",
"visibility": "public"
}
],
"exported": true,
"heritage": []
}
},
"constants": {}
}

92
demo-snapshots/v1.30.2.d.ts vendored Normal file
View File

@@ -0,0 +1,92 @@
/**
* Mock TypeScript definitions representing v1.30.2 API surface
* This represents the public API with several breaking changes and additions
*/
export interface ComfyApi {
/**
* Get API URL for backend calls
*/
apiURL(path: string): string
/**
* Get file URL for static resources
*/
fileURL(path: string): string
/**
* Queue a prompt for execution (async version)
*/
queuePromptAsync(
prompt: object,
options?: { priority?: number }
): Promise<{ prompt_id: string; number: number }>
/**
* Cancel a queued prompt
*/
cancelPrompt(prompt_id: string): Promise<void>
/**
* Interrupt current execution
*/
interrupt(): Promise<void>
/**
* Get queue status
*/
getQueueStatus(): Promise<{ queue_running: any[]; queue_pending: any[] }>
}
export interface NodeDef {
name: string
category: string
display_name?: string
description?: string
python_module: string
input: {
required?: Record<string, any>
optional?: Record<string, any>
}
output: string[]
output_name: string[]
}
export enum NodeStatus {
IDLE = 'idle',
QUEUED = 'queued',
RUNNING = 'running',
ERROR = 'error',
COMPLETED = 'completed'
}
export interface WorkflowMetadata {
title?: string
description?: string
author?: string
version?: string
tags?: string[]
thumbnail?: string
}
export interface ExtensionMetadata {
id: string
name: string
version: string
description?: string
}
export class WorkflowManager {
workflows: Map<string, object>
cache: Map<string, object>
constructor()
loadWorkflow(id: string): Promise<object>
saveWorkflow(id: string, data: object): Promise<void>
deleteWorkflow(id: string): Promise<void>
searchWorkflows(query: string): Promise<object[]>
}
export type NodeId = string
export type WorkflowId = string

311
demo-snapshots/v1.30.2.json Normal file
View File

@@ -0,0 +1,311 @@
{
"types": {
"NodeId": {
"kind": "type",
"name": "NodeId",
"text": "export type NodeId = string;",
"exported": true
},
"WorkflowId": {
"kind": "type",
"name": "WorkflowId",
"text": "export type WorkflowId = string;",
"exported": true
}
},
"interfaces": {
"ComfyApi": {
"kind": "interface",
"name": "ComfyApi",
"members": [
{
"name": "apiURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "fileURL",
"kind": "method",
"parameters": [
{
"name": "path",
"type": "string",
"optional": false
}
],
"returnType": "string"
},
{
"name": "queuePromptAsync",
"kind": "method",
"parameters": [
{
"name": "prompt",
"type": "object",
"optional": false
},
{
"name": "options",
"type": "{ priority?: number }",
"optional": true
}
],
"returnType": "Promise<{ prompt_id: string; number: number }>"
},
{
"name": "cancelPrompt",
"kind": "method",
"parameters": [
{
"name": "prompt_id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<void>"
},
{
"name": "interrupt",
"kind": "method",
"parameters": [],
"returnType": "Promise<void>"
},
{
"name": "getQueueStatus",
"kind": "method",
"parameters": [],
"returnType": "Promise<{ queue_running: any[]; queue_pending: any[] }>"
}
],
"exported": true,
"heritage": []
},
"NodeDef": {
"kind": "interface",
"name": "NodeDef",
"members": [
{
"name": "name",
"type": "string",
"optional": false
},
{
"name": "category",
"type": "string",
"optional": false
},
{
"name": "display_name",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "python_module",
"type": "string",
"optional": false
},
{
"name": "input",
"type": "{\n required?: Record<string, any>;\n optional?: Record<string, any>;\n }",
"optional": false
},
{
"name": "output",
"type": "string[]",
"optional": false
},
{
"name": "output_name",
"type": "string[]",
"optional": false
}
],
"exported": true,
"heritage": []
},
"WorkflowMetadata": {
"kind": "interface",
"name": "WorkflowMetadata",
"members": [
{
"name": "title",
"type": "string",
"optional": true
},
{
"name": "description",
"type": "string",
"optional": true
},
{
"name": "author",
"type": "string",
"optional": true
},
{
"name": "version",
"type": "string",
"optional": true
},
{
"name": "tags",
"type": "string[]",
"optional": true
},
{
"name": "thumbnail",
"type": "string",
"optional": true
}
],
"exported": true,
"heritage": []
},
"ExtensionMetadata": {
"kind": "interface",
"name": "ExtensionMetadata",
"members": [
{
"name": "id",
"type": "string",
"optional": false
},
{
"name": "name",
"type": "string",
"optional": false
},
{
"name": "version",
"type": "string",
"optional": false
},
{
"name": "description",
"type": "string",
"optional": true
}
],
"exported": true,
"heritage": []
}
},
"enums": {
"NodeStatus": {
"kind": "enum",
"name": "NodeStatus",
"members": [
{
"name": "IDLE",
"value": "\"idle\""
},
{
"name": "QUEUED",
"value": "\"queued\""
},
{
"name": "RUNNING",
"value": "\"running\""
},
{
"name": "ERROR",
"value": "\"error\""
},
{
"name": "COMPLETED",
"value": "\"completed\""
}
],
"exported": true
}
},
"functions": {},
"classes": {
"WorkflowManager": {
"kind": "class",
"name": "WorkflowManager",
"members": [
{
"name": "workflows",
"type": "Map<string, object>",
"visibility": "public"
},
{
"name": "cache",
"type": "Map<string, object>",
"visibility": "public"
}
],
"methods": [
{
"name": "loadWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<object>",
"visibility": "public"
},
{
"name": "saveWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
},
{
"name": "data",
"type": "object",
"optional": false
}
],
"returnType": "Promise<void>",
"visibility": "public"
},
{
"name": "deleteWorkflow",
"parameters": [
{
"name": "id",
"type": "string",
"optional": false
}
],
"returnType": "Promise<void>",
"visibility": "public"
},
{
"name": "searchWorkflows",
"parameters": [
{
"name": "query",
"type": "string",
"optional": false
}
],
"returnType": "Promise<object[]>",
"visibility": "public"
}
],
"exported": true,
"heritage": []
}
},
"constants": {}
}

40
docs/API-CHANGELOG.md Normal file
View File

@@ -0,0 +1,40 @@
# Public API Changelog
This changelog documents changes to the ComfyUI Frontend public API surface across versions. The public API surface includes types, interfaces, and objects used by third-party extensions and custom nodes.
**Important**: This is an automatically generated changelog based on TypeScript type definitions. Breaking changes are marked with ⚠️.
## What is tracked
This changelog tracks changes to the following public API components exported from `@comfyorg/comfyui-frontend-types`:
- **Type Aliases**: Type definitions used by extensions
- **Interfaces**: Object shapes and contracts
- **Enums**: Enumerated values
- **Functions**: Public utility functions
- **Classes**: Exported classes and their public members
- **Constants**: Public constant values
## Migration Guide
When breaking changes occur, refer to the specific version section below for:
- What changed
- Why it changed (if applicable)
- How to migrate your code
---
<!-- Automated changelog entries will be added below -->
## v1.32.4 (2025-11-10)
Comparing v1.32.3 → v1.32.4. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.
### 🔄 Modifications
**Interfaces**
- [`ComfyExtension`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/e36e25ebd614c1c996e66b5c382b6b1b1bd4587a/src/types/comfy.ts#L98)
- ✨ Added member: `actionBarButtons`
---

View File

@@ -3,7 +3,6 @@ import pluginJs from '@eslint/js'
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
import { importX } from 'eslint-plugin-import-x'
import oxlint from 'eslint-plugin-oxlint'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports'
@@ -34,18 +33,7 @@ const settings = {
],
noWarnOnMultipleProjects: true
})
],
'vue-i18n': {
localeDir: [
{
pattern: './src/locales/**/*.json',
localeKey: 'path',
localePattern:
/^\.?\/?src\/locales\/(?<locale>[A-Za-z0-9-]+)\/.+\.json$/
}
],
messageSyntaxVersion: '^9.0.0'
}
]
} as const
const commonParserOptions = {
@@ -70,7 +58,8 @@ export default defineConfig([
'src/extensions/core/*',
'src/scripts/*',
'src/types/generatedManagerTypes.ts',
'src/types/vue-shim.d.ts'
'src/types/vue-shim.d.ts',
'demo-snapshots/*'
]
},
{
@@ -106,23 +95,19 @@ export default defineConfig([
// @ts-ignore Bad types in the plugin
pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
storybook.configs['flat/recommended'],
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
// @ts-expect-error Bad types in the plugin
importX.flatConfigs.recommended,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility between import-x plugin and ESLint config types
// @ts-expect-error Bad types in the plugin
importX.flatConfigs.typescript,
{
plugins: {
'unused-imports': unusedImports,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Type incompatibility in i18n plugin
// @ts-expect-error Bad types in the plugin
'@intlify/vue-i18n': pluginI18n
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/prefer-as-const': 'off',
@@ -140,8 +125,8 @@ export default defineConfig([
'unused-imports/no-unused-imports': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
'vue/no-v-html': 'off',
// Prohibit dark-theme: and dark: prefixes
'vue/no-restricted-class': ['error', '/^dark(-theme)?:/'],
// Enforce dark-theme: instead of dark: prefix
'vue/no-restricted-class': ['error', '/^dark:/'],
'vue/multi-word-component-names': 'off', // TODO: fix
'vue/no-template-shadow': 'off', // TODO: fix
'vue/match-component-import-name': 'error',
@@ -275,7 +260,5 @@ export default defineConfig([
'@typescript-eslint/no-floating-promises': 'off',
'no-console': 'off'
}
},
// Turn off ESLint rules that are already handled by oxlint
...oxlint.buildFromOxlintConfigFile('./.oxlintrc.json')
}
])

View File

@@ -41,7 +41,9 @@ const config: KnipConfig = {
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts'
'src/scripts/ui/components/splitButton.ts',
// Demo snapshots for API changelog system
'demo-snapshots/**'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.32.5",
"version": "1.32.4",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -28,14 +28,13 @@
"json-schema": "tsx scripts/generate-json-schema.ts",
"knip:no-cache": "knip",
"knip": "knip --cache",
"lint:fix:no-cache": "oxlint src --type-aware --fix && eslint src --fix",
"lint:fix": "oxlint src --type-aware --fix && eslint src --cache --fix",
"lint:no-cache": "oxlint src --type-aware && eslint src",
"lint:fix:no-cache": "eslint src --fix",
"lint:fix": "eslint src --cache --fix",
"lint:no-cache": "eslint src",
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
"lint": "oxlint src --type-aware && eslint src --cache",
"lint": "eslint src --cache",
"locale": "lobe-i18n locale",
"oxlint": "oxlint src --type-aware",
"preinstall": "pnpm dlx only-allow pnpm",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
@@ -43,7 +42,6 @@
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
"test:browser": "pnpm exec nx e2e",
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 pnpm test:browser",
"test:unit": "nx run test",
"typecheck": "vue-tsc --noEmit",
"zipdist": "node scripts/zipdist.js",
@@ -80,7 +78,6 @@
"eslint-config-prettier": "catalog:",
"eslint-import-resolver-typescript": "catalog:",
"eslint-plugin-import-x": "catalog:",
"eslint-plugin-oxlint": "catalog:",
"eslint-plugin-prettier": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-unused-imports": "catalog:",
@@ -96,8 +93,6 @@
"markdown-table": "catalog:",
"mixpanel-browser": "catalog:",
"nx": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"picocolors": "catalog:",
"postcss-html": "catalog:",
"prettier": "catalog:",
@@ -131,7 +126,6 @@
"@comfyorg/comfyui-electron-types": "0.4.73-0",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/shared-frontend-utils": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@iconify/json": "catalog:",
"@primeuix/forms": "catalog:",

View File

@@ -160,6 +160,7 @@
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-smoke-300);
/* Code styling colors for help menu*/
--code-text-color: rgb(0 122 255 / 1);
@@ -264,13 +265,6 @@
--palette-interface-panel-selected-surface: color-mix(in srgb, var(--interface-panel-surface) 87.5%, var(--contrast-mix-color));
--palette-interface-button-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 82%, var(--contrast-mix-color));
--modal-card-background: var(--secondary-background);
--modal-card-button-surface: var(--color-smoke-300);
--modal-card-placeholder-background: var(--color-smoke-600);
--modal-card-tag-background: var(--color-smoke-200);
--modal-card-tag-foreground: var(--base-foreground);
--modal-panel-background: var(--color-white);
}
.dark-theme {
@@ -292,6 +286,8 @@
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-charcoal-300);
--dialog-surface: var(--color-neutral-700);
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
@@ -366,14 +362,6 @@
--component-node-widget-background-selected: var(--color-charcoal-100);
--component-node-widget-background-disabled: var(--color-alpha-charcoal-600-30);
--component-node-widget-background-highlighted: var(--color-graphite-400);
--modal-card-background: var(--secondary-background);
--modal-card-button-surface: var(--color-charcoal-300);
--modal-card-placeholder-background: var(--secondary-background);
--modal-card-tag-background: var(--color-charcoal-200);
--modal-card-tag-foreground: var(--base-foreground);
--modal-panel-background: var(--color-charcoal-600);
}
@theme inline {
@@ -384,14 +372,7 @@
--color-button-surface: var(--button-surface);
--color-button-surface-contrast: var(--button-surface-contrast);
--color-subscription-button-gradient: var(--subscription-button-gradient);
--color-modal-card-background: var(--modal-card-background);
--color-modal-card-button-surface: var(--modal-card-button-surface);
--color-modal-card-placeholder-background: var(--modal-card-placeholder-background);
--color-modal-card-tag-background: var(--modal-card-tag-background);
--color-modal-card-tag-foreground: var(--modal-card-tag-foreground);
--color-modal-panel-background: var(--modal-panel-background);
--color-dialog-surface: var(--dialog-surface);
--color-interface-menu-component-surface-hovered: var(
--interface-menu-component-surface-hovered
@@ -1365,466 +1346,3 @@ audio.comfy-audio.empty-audio-widget {
border-radius: 0;
}
/* END LOD specific styles */
/* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */
#maskEditor_brush {
position: absolute;
backgroundColor: transparent;
z-index: 8889;
pointer-events: none;
border-radius: 50%;
overflow: visible;
outline: 1px dashed black;
box-shadow: 0 0 0 1px white;
}
#maskEditor_brushPreviewGradient {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
display: none;
}
.maskEditor_sidePanelTitle {
text-align: center;
font-size: 15px;
font-family: sans-serif;
color: var(--descrip-text);
margin-top: 10px;
}
.maskEditor_sidePanelBrushShapeCircle {
width: 35px;
height: 35px;
border-radius: 50%;
border: 1px solid var(--border-color);
pointer-events: auto;
transition: background 0.1s;
margin-left: 7.5px;
}
.maskEditor_sidePanelBrushRange {
width: 180px;
appearance: none;
background: transparent;
cursor: pointer;
}
.maskEditor_sidePanelBrushRange::-webkit-slider-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
cursor: grab;
margin-top: -8px;
background: var(--p-surface-700);
border: 1px solid var(--border-color);
}
.maskEditor_sidePanelBrushRange::-moz-range-thumb {
height: 20px;
width: 20px;
border-radius: 50%;
cursor: grab;
background: var(--p-surface-800);
border: 1px solid var(--border-color);
}
.maskEditor_sidePanelBrushRange::-webkit-slider-runnable-track {
background: var(--p-surface-700);
height: 3px;
}
.maskEditor_sidePanelBrushRange::-moz-range-track {
background: var(--p-surface-700);
height: 3px;
}
.maskEditor_sidePanelBrushShapeSquare {
width: 35px;
height: 35px;
margin: 5px;
border: 1px solid var(--border-color);
pointer-events: auto;
transition: background 0.1s;
}
.maskEditor_brushShape_dark {
background: transparent;
}
.maskEditor_brushShape_dark:hover {
background: var(--p-surface-900);
}
.maskEditor_brushShape_light {
background: transparent;
}
.maskEditor_brushShape_light:hover {
background: var(--comfy-menu-bg);
}
.maskEditor_sidePanelLayer {
display: flex;
width: 100%;
height: 50px;
}
.maskEditor_sidePanelLayerVisibilityContainer {
width: 50px;
height: 50px;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
}
.maskEditor_sidePanelVisibilityToggle {
width: 12px;
height: 12px;
border-radius: 50%;
pointer-events: auto;
}
.maskEditor_sidePanelLayerIconContainer {
width: 60px;
height: 50px;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
fill: var(--input-text);
}
.maskEditor_sidePanelLayerIconContainer svg {
width: 30px;
height: 30px;
}
.maskEditor_sidePanelBigButton {
width: 85px;
height: 30px;
background: rgb(0 0 0 / 0.2);
border: 1px solid var(--border-color);
color: var(--input-text);
font-family: sans-serif;
font-size: 15px;
pointer-events: auto;
transition: background-color 0.1s;
}
.maskEditor_sidePanelBigButton:hover {
background-color: var(--p-overlaybadge-outline-color);
border: none;
}
.maskEditor_toolPanelContainer {
width: 4rem;
height: 4rem;
display: flex;
justify-content: center;
align-items: center;
position: relative;
transition: background-color 0.2s;
}
.maskEditor_toolPanelContainerSelected svg {
fill: var(--p-button-text-primary-color) !important;
}
.maskEditor_toolPanelContainerSelected .maskEditor_toolPanelIndicator {
display: block;
}
.maskEditor_toolPanelContainer svg {
width: 75%;
aspect-ratio: 1/1;
fill: var(--p-button-text-secondary-color);
}
.maskEditor_toolPanelContainerDark:hover {
background-color: var(--p-surface-800);
}
.maskEditor_toolPanelContainerLight:hover {
background-color: var(--p-surface-300);
}
.maskEditor_toolPanelIndicator {
display: none;
height: 100%;
width: 4px;
position: absolute;
left: 0;
background: var(--p-button-text-primary-color);
}
.maskEditor_sidePanelSeparator {
width: 100%;
height: 2px;
background: var(--border-color);
margin-top: 1.5em;
margin-bottom: 5px;
}
#maskEditorCanvasContainer {
position: absolute;
width: 1000px;
height: 667px;
left: 359px;
top: 280px;
}
.maskEditor_topPanelIconButton_dark {
width: 50px;
height: 30px;
pointer-events: auto;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.1s;
background: var(--p-surface-800);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
}
.maskEditor_topPanelIconButton_dark:hover {
background-color: var(--p-surface-900);
}
.maskEditor_topPanelIconButton_dark svg {
width: 25px;
height: 25px;
pointer-events: none;
fill: var(--input-text);
}
.maskEditor_topPanelIconButton_light {
width: 50px;
height: 30px;
pointer-events: auto;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.1s;
background: var(--comfy-menu-bg);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
}
.maskEditor_topPanelIconButton_light:hover {
background-color: var(--p-surface-300);
}
.maskEditor_topPanelIconButton_light svg {
width: 25px;
height: 25px;
pointer-events: none;
fill: var(--input-text);
}
.maskEditor_topPanelButton_dark {
height: 30px;
background: var(--p-surface-800);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
color: var(--input-text);
font-family: sans-serif;
pointer-events: auto;
transition: 0.1s;
width: 60px;
}
.maskEditor_topPanelButton_dark:hover {
background-color: var(--p-surface-900);
}
.maskEditor_topPanelButton_light {
height: 30px;
background: var(--comfy-menu-bg);
border: 1px solid var(--p-form-field-border-color);
border-radius: 10px;
color: var(--input-text);
font-family: sans-serif;
pointer-events: auto;
transition: 0.1s;
width: 60px;
}
.maskEditor_topPanelButton_light:hover {
background-color: var(--p-surface-300);
}
.maskEditor_sidePanel_paintBucket_Container {
width: 180px;
display: flex;
flex-direction: column;
position: relative;
}
.maskEditor_sidePanel_colorSelect_Container {
display: flex;
width: 180px;
align-items: center;
gap: 5px;
height: 30px;
}
.maskEditor_sidePanel_colorSelect_tolerance_container {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 10px;
}
.maskEditor_sidePanelContainerColumn {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 12px;
}
.maskEditor_sidePanelContainerRow {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
min-height: 24px;
position: relative;
}
.maskEditor_accent_bg_dark {
background: var(--p-surface-800);
}
.maskEditor_accent_bg_very_dark {
background: var(--p-surface-900);
}
.maskEditor_accent_bg_light {
background: var(--p-surface-300);
}
.maskEditor_accent_bg_very_light {
background: var(--comfy-menu-bg);
}
.maskEditor_sidePanelToggleContainer {
cursor: pointer;
display: inline-block;
position: absolute;
right: 0;
}
.maskEditor_sidePanelToggleSwitch {
display: inline-block;
border-radius: 16px;
width: 40px;
height: 24px;
position: relative;
vertical-align: middle;
transition: background 0.25s;
background: var(--p-surface-300);
}
.dark-theme .maskEditor_sidePanelToggleSwitch {
background: var(--p-surface-700);
}
.maskEditor_sidePanelToggleSwitch::before, .maskEditor_sidePanelToggleSwitch::after {
content: "";
}
.maskEditor_sidePanelToggleSwitch::before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
border-radius: 50%;
width: 16px;
height: 16px;
position: absolute;
top: 4px;
left: 4px;
transition: ease 0.2s;
}
.maskEditor_sidePanelToggleContainer:hover .maskEditor_sidePanelToggleSwitch::before {
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
}
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch {
background: var(--p-button-text-primary-color);
}
.maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
background: var(--comfy-menu-bg);
left: 20px;
}
.dark-theme .maskEditor_sidePanelToggleCheckbox:checked + .maskEditor_sidePanelToggleSwitch::before {
background: var(--p-surface-900);
}
.maskEditor_sidePanelToggleCheckbox {
position: absolute;
visibility: hidden;
}
.maskEditor_sidePanelDropdown {
border: 1px solid var(--p-form-field-border-color);
background: var(--comfy-menu-bg);
height: 24px;
padding-left: 5px;
padding-right: 5px;
border-radius: 6px;
transition: background 0.1s;
}
.maskEditor_sidePanelDropdown option {
background: var(--comfy-menu-bg);
}
.maskEditor_sidePanelDropdown:focus {
outline: 1px solid var(--p-surface-300);
}
.maskEditor_sidePanelDropdown option:hover {
background: white;
}
.maskEditor_sidePanelDropdown option:active {
background: var(--p-surface-300);
}
.dark-theme .maskEditor_sidePanelDropdown {
background: var(--p-surface-900);
}
.dark-theme .maskEditor_sidePanelDropdown option {
background: var(--p-surface-900);
}
.dark-theme .maskEditor_sidePanelDropdown:focus {
outline: 1px solid var(--p-button-text-primary-color);
}
.dark-theme .maskEditor_sidePanelDropdown option:active {
background: var(--p-highlight-background);
}
.maskEditor_layerRow {
height: 50px;
width: 100%;
border-radius: 10px;
}
.maskEditor_sidePanelLayerPreviewContainer {
width: 40px;
height: 30px;
}
.maskEditor_sidePanelLayerPreviewContainer > svg{
width: 100%;
height: 100%;
object-fit: contain;
fill: var(--p-surface-100);
}
.maskEditor_sidePanelImageLayerImage {
width: 100%;
height: 100%;
object-fit: contain;
}
.maskEditor_sidePanelSubTitle {
text-align: left;
font-size: 12px;
font-family: sans-serif;
color: var(--descrip-text);
}
.maskEditor_containerDropdown {
position: absolute;
right: 0;
}
.maskEditor_sidePanelLayerCheckbox {
margin-left: 15px;
}
/* ===================== End of Mask Editor Styles ===================== */

View File

@@ -1,32 +1,27 @@
import { defineConfig, devices } from '@playwright/test'
import type { PlaywrightTestConfig } from '@playwright/test'
const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL
? {
// VERY HELPFUL: Skip screenshot tests locally
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
timeout: 30_000, // Longer timeout for breakpoints
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
workers: 1, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
use: {
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
video: 'on' // Always record video (CI uses 'retain-on-failure')
}
}
: {
retries: process.env.CI ? 3 : 0,
use: {
trace: 'on-first-retry'
}
}
export default defineConfig({
testDir: './browser_tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
reporter: 'html',
...maybeLocalOptions,
// /* // Toggle for [LOCAL] testing.
retries: process.env.CI ? 3 : 0,
use: {
trace: 'on-first-retry'
},
/*/ // [LOCAL]
// VERY HELPFUL: Skip screenshot tests locally
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
timeout: 30_000, // Longer timeout for breakpoints
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
workers: 4, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
use: {
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
video: 'on' // Always record video (CI uses 'retain-on-failure')
},
//*/
globalSetup: './browser_tests/globalSetup.ts',
globalTeardown: './browser_tests/globalTeardown.ts',

194
pnpm-lock.yaml generated
View File

@@ -12,15 +12,9 @@ catalogs:
'@eslint/js':
specifier: ^9.35.0
version: 9.35.0
'@iconify-json/lucide':
specifier: ^1.1.178
version: 1.2.66
'@iconify/json':
specifier: ^2.2.380
version: 2.2.380
'@iconify/tailwind':
specifier: ^1.1.3
version: 1.2.0
'@intlify/eslint-plugin-vue-i18n':
specifier: ^4.1.0
version: 4.1.0
@@ -147,9 +141,6 @@ catalogs:
eslint-plugin-import-x:
specifier: ^4.16.1
version: 4.16.1
eslint-plugin-oxlint:
specifier: 1.25.0
version: 1.25.0
eslint-plugin-prettier:
specifier: ^5.5.4
version: 5.5.4
@@ -195,12 +186,6 @@ catalogs:
nx:
specifier: 21.4.1
version: 21.4.1
oxlint:
specifier: ^1.25.0
version: 1.28.0
oxlint-tsgolint:
specifier: ^0.4.0
version: 0.4.0
picocolors:
specifier: ^1.1.1
version: 1.1.1
@@ -326,9 +311,6 @@ importers:
'@comfyorg/registry-types':
specifier: workspace:*
version: link:packages/registry-types
'@comfyorg/shared-frontend-utils':
specifier: workspace:*
version: link:packages/shared-frontend-utils
'@comfyorg/tailwind-utils':
specifier: workspace:*
version: link:packages/tailwind-utils
@@ -573,9 +555,6 @@ importers:
eslint-plugin-import-x:
specifier: 'catalog:'
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
eslint-plugin-oxlint:
specifier: 'catalog:'
version: 1.25.0
eslint-plugin-prettier:
specifier: 'catalog:'
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
@@ -621,12 +600,6 @@ importers:
nx:
specifier: 'catalog:'
version: 21.4.1
oxlint:
specifier: 'catalog:'
version: 1.28.0(oxlint-tsgolint@0.4.0)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.4.0
picocolors:
specifier: 'catalog:'
version: 1.1.1
@@ -2556,76 +2529,6 @@ packages:
cpu: [x64]
os: [win32]
'@oxlint-tsgolint/darwin-arm64@0.4.0':
resolution: {integrity: sha512-2jNvhxs6JJy93Z4SQ/VErODBzZtFKxQ+sybcKYcw5/K41tXOiBbJSwBMZ2PPvivCVkVcyOkJvfs5UXWW7o79uw==}
cpu: [arm64]
os: [darwin]
'@oxlint-tsgolint/darwin-x64@0.4.0':
resolution: {integrity: sha512-6A+YBecdZhk2NJ8Dh3kRkR6htNekDmAopFkdyrtNvsHJs5qNNuwUv5RZlVMYiaQTh/Y/tZ0YWE4+cVdqPIEyxQ==}
cpu: [x64]
os: [darwin]
'@oxlint-tsgolint/linux-arm64@0.4.0':
resolution: {integrity: sha512-JaX8JfQnY3UwX7l6BXIjhEaJAVeKVASELLFCdoo5+DOHgPuiiSKcxCVgTl92WPAuS0TYFXOgqOg31WXkvdi8bQ==}
cpu: [arm64]
os: [linux]
'@oxlint-tsgolint/linux-x64@0.4.0':
resolution: {integrity: sha512-iu106lxV1O64O4vK2eRoIuY2iHuil/hyDNKLRNVaTg1un+yoxN6/C5uxrJix/EJ+1O27P9c+sXmMplcmbXujtg==}
cpu: [x64]
os: [linux]
'@oxlint-tsgolint/win32-arm64@0.4.0':
resolution: {integrity: sha512-KTp9EzkTCGAh4/sL3l5a9otX63TvTs5riBcrcqu0jYS3P762rZSezzMMDc0Ld51x+I37125p9+bue2vmlH/KbQ==}
cpu: [arm64]
os: [win32]
'@oxlint-tsgolint/win32-x64@0.4.0':
resolution: {integrity: sha512-ioyBLHx0HA+hn5of8mhnA8W8DWQyJEHc7SBvwku0EW9bWt7zvBtWRJfx1YilvM+KVBdLVX731qeofdJT1fbJiQ==}
cpu: [x64]
os: [win32]
'@oxlint/darwin-arm64@1.28.0':
resolution: {integrity: sha512-H7J41/iKbgm7tTpdSnA/AtjEAhxyzNzCMKWtKU5wDuP2v39jrc3fasQEJruk6hj1YXPbJY4N+1nK/jE27GMGDQ==}
cpu: [arm64]
os: [darwin]
'@oxlint/darwin-x64@1.28.0':
resolution: {integrity: sha512-bGsSDEwpyYzNc6FIwhTmbhSK7piREUjMlmWBt7eoR3ract0+RfhZYYG4se1Ngs+4WOFC0B3gbv23fyF+cnbGGQ==}
cpu: [x64]
os: [darwin]
'@oxlint/linux-arm64-gnu@1.28.0':
resolution: {integrity: sha512-eNH/evMpV3xAA4jIS8dMLcGkM/LK0WEHM0RO9bxrHPAwfS72jhyPJtd0R7nZhvhG6U1bhn5jhoXbk1dn27XIAQ==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-arm64-musl@1.28.0':
resolution: {integrity: sha512-ickvpcekNeRLND3llndiZOtJBb6LDZqNnZICIDkovURkOIWPGJGmAxsHUOI6yW6iny9gLmIEIGl/c1b5nFk6Ag==}
cpu: [arm64]
os: [linux]
'@oxlint/linux-x64-gnu@1.28.0':
resolution: {integrity: sha512-DkgAh4LQ8NR3DwTT7/LGMhaMau0RtZkih91Ez5Usk7H7SOxo1GDi84beE7it2Q+22cAzgY4hbw3c6svonQTjxg==}
cpu: [x64]
os: [linux]
'@oxlint/linux-x64-musl@1.28.0':
resolution: {integrity: sha512-VBnMi3AJ2w5p/kgeyrjcGOKNY8RzZWWvlGHjCJwzqPgob4MXu6T+5Yrdi7EVJyIlouL8E3LYPYjmzB9NBi9gZw==}
cpu: [x64]
os: [linux]
'@oxlint/win32-arm64@1.28.0':
resolution: {integrity: sha512-tomhIks+4dKs8axB+s4GXHy+ZWXhUgptf1XnG5cZg8CzRfX4JFX9k8l2fPUgFwytWnyyvZaaXLRPWGzoZ6yoHQ==}
cpu: [arm64]
os: [win32]
'@oxlint/win32-x64@1.28.0':
resolution: {integrity: sha512-4+VO5P/UJ2nq9sj6kQToJxFy5cKs7dGIN2DiUSQ7cqyUi7EKYNQKe+98HFcDOjtm33jQOQnc4kw8Igya5KPozg==}
cpu: [x64]
os: [win32]
'@phenomnomnominal/tsquery@5.0.1':
resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==}
peerDependencies:
@@ -4829,9 +4732,6 @@ packages:
'@typescript-eslint/parser':
optional: true
eslint-plugin-oxlint@1.25.0:
resolution: {integrity: sha512-grS4KdR9FAxoQC+wMkepeQHL4osMhoYfUI11Pot6Gitqr4wWi+JZrX0Shr8Bs9fjdWhEjtaZIV6cr4mbfytmyw==}
eslint-plugin-prettier@5.5.4:
resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -5792,9 +5692,6 @@ packages:
jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsondiffpatch@0.6.0:
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -6489,20 +6386,6 @@ packages:
oxc-resolver@11.6.1:
resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==}
oxlint-tsgolint@0.4.0:
resolution: {integrity: sha512-RpvLxPvSt0Xzr3frTiw5rP6HUW0djZ2uNdzHc8Pv556sbTnFWXmLdK8FRqqy7vVXZTkoVSdY3PsvOsVAqGjc+Q==}
hasBin: true
oxlint@1.28.0:
resolution: {integrity: sha512-gE97d0BcIlTTSJrim395B49mIbQ9VO8ZVoHdWai7Svl+lEeUAyCLTN4d7piw1kcB8VfgTp1JFVlAvMPD9GewMA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
oxlint-tsgolint: '>=0.4.0'
peerDependenciesMeta:
oxlint-tsgolint:
optional: true
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -7815,8 +7698,8 @@ packages:
vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
vue-component-type-helpers@3.1.3:
resolution: {integrity: sha512-V1dOD8XYfstOKCnXbWyEJIrhTBMwSyNjv271L1Jlx9ExpNlCSuqOs3OdWrGJ0V544zXufKbcYabi/o+gK8lyfQ==}
vue-component-type-helpers@3.1.2:
resolution: {integrity: sha512-ch3/SKBtxdZq18vsEntiGCdSszCRNfhX5QaTxjSacCAXLlNQRXfXo+ANjoQEYJMsJOJy1/vHF6Tkc4s85MS+zw==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -10209,48 +10092,6 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.6.1':
optional: true
'@oxlint-tsgolint/darwin-arm64@0.4.0':
optional: true
'@oxlint-tsgolint/darwin-x64@0.4.0':
optional: true
'@oxlint-tsgolint/linux-arm64@0.4.0':
optional: true
'@oxlint-tsgolint/linux-x64@0.4.0':
optional: true
'@oxlint-tsgolint/win32-arm64@0.4.0':
optional: true
'@oxlint-tsgolint/win32-x64@0.4.0':
optional: true
'@oxlint/darwin-arm64@1.28.0':
optional: true
'@oxlint/darwin-x64@1.28.0':
optional: true
'@oxlint/linux-arm64-gnu@1.28.0':
optional: true
'@oxlint/linux-arm64-musl@1.28.0':
optional: true
'@oxlint/linux-x64-gnu@1.28.0':
optional: true
'@oxlint/linux-x64-musl@1.28.0':
optional: true
'@oxlint/win32-arm64@1.28.0':
optional: true
'@oxlint/win32-x64@1.28.0':
optional: true
'@phenomnomnominal/tsquery@5.0.1(typescript@5.9.2)':
dependencies:
esquery: 1.6.0
@@ -10617,7 +10458,7 @@ snapshots:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.3
vue-component-type-helpers: 3.1.2
'@swc/helpers@0.5.17':
dependencies:
@@ -12675,10 +12516,6 @@ snapshots:
- supports-color
optional: true
eslint-plugin-oxlint@1.25.0:
dependencies:
jsonc-parser: 3.3.1
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
dependencies:
eslint: 9.35.0(jiti@2.4.2)
@@ -13744,8 +13581,6 @@ snapshots:
jsonc-parser@3.2.0: {}
jsonc-parser@3.3.1: {}
jsondiffpatch@0.6.0:
dependencies:
'@types/diff-match-patch': 1.0.36
@@ -14713,27 +14548,6 @@ snapshots:
'@oxc-resolver/binding-win32-ia32-msvc': 11.6.1
'@oxc-resolver/binding-win32-x64-msvc': 11.6.1
oxlint-tsgolint@0.4.0:
optionalDependencies:
'@oxlint-tsgolint/darwin-arm64': 0.4.0
'@oxlint-tsgolint/darwin-x64': 0.4.0
'@oxlint-tsgolint/linux-arm64': 0.4.0
'@oxlint-tsgolint/linux-x64': 0.4.0
'@oxlint-tsgolint/win32-arm64': 0.4.0
'@oxlint-tsgolint/win32-x64': 0.4.0
oxlint@1.28.0(oxlint-tsgolint@0.4.0):
optionalDependencies:
'@oxlint/darwin-arm64': 1.28.0
'@oxlint/darwin-x64': 1.28.0
'@oxlint/linux-arm64-gnu': 1.28.0
'@oxlint/linux-arm64-musl': 1.28.0
'@oxlint/linux-x64-gnu': 1.28.0
'@oxlint/linux-x64-musl': 1.28.0
'@oxlint/win32-arm64': 1.28.0
'@oxlint/win32-x64': 1.28.0
oxlint-tsgolint: 0.4.0
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -16343,7 +16157,7 @@ snapshots:
vue-component-type-helpers@3.1.1: {}
vue-component-type-helpers@3.1.3: {}
vue-component-type-helpers@3.1.2: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:

View File

@@ -50,7 +50,6 @@ catalog:
eslint-config-prettier: ^10.1.8
eslint-import-resolver-typescript: ^4.4.4
eslint-plugin-import-x: ^4.16.1
eslint-plugin-oxlint: 1.25.0
eslint-plugin-prettier: ^5.5.4
eslint-plugin-storybook: ^9.1.6
eslint-plugin-unused-imports: ^4.2.0
@@ -66,8 +65,6 @@ catalog:
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 21.4.1
oxlint: ^1.25.0
oxlint-tsgolint: ^0.4.0
picocolors: ^1.1.1
pinia: ^2.1.7
postcss-html: ^1.8.0

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
mapfile -t yaml_files < <(git ls-files '*.yml' '*.yaml')
if [[ ${#yaml_files[@]} -eq 0 ]]; then
echo "No YAML files found to lint"
exit 0
fi
yamllint --config-file .yamllint "${yaml_files[@]}"

View File

@@ -0,0 +1,444 @@
#!/usr/bin/env node
/**
* Compares two API snapshots and generates a human-readable changelog
* documenting additions, removals, and modifications to the public API.
*/
import * as fs from 'fs'
const args = process.argv.slice(2)
if (args.length < 4) {
console.error(
'Usage: compare-api-snapshots.js <previous.json> <current.json> <previous-version> <current-version> [repo-owner] [repo-name] [git-ref]'
)
process.exit(1)
}
const [
previousPath,
currentPath,
previousVersion,
currentVersion,
repoOwner = 'Comfy-Org',
repoName = 'ComfyUI_frontend',
gitRef = 'main'
] = args
if (!fs.existsSync(previousPath)) {
console.error(`Previous snapshot not found: ${previousPath}`)
process.exit(1)
}
if (!fs.existsSync(currentPath)) {
console.error(`Current snapshot not found: ${currentPath}`)
process.exit(1)
}
const previousApi = JSON.parse(fs.readFileSync(previousPath, 'utf-8'))
const currentApi = JSON.parse(fs.readFileSync(currentPath, 'utf-8'))
/**
* Generate GitHub permalink to source code
* Prefers source file location over dist .d.ts location
*/
function generateGitHubLink(name, item) {
// If we have source file information, use that
if (item?.sourceFile && item?.sourceLine) {
return `[\`${name}\`](https://github.com/${repoOwner}/${repoName}/blob/${gitRef}/${item.sourceFile}#L${item.sourceLine})`
}
// Fallback to .d.ts location if available
if (item?.line) {
return `[\`${name}\`](https://github.com/${repoOwner}/${repoName}/blob/${gitRef}/dist/index.d.ts#L${item.line})`
}
// No location info available
return `\`${name}\``
}
/**
* Compare two API snapshots and generate changelog
*/
function compareApis(previous, current) {
const changes = {
breaking: [],
additions: [],
modifications: [],
deprecations: []
}
const categories = [
'types',
'interfaces',
'enums',
'functions',
'classes',
'constants'
]
for (const category of categories) {
const prevItems = previous[category] || {}
const currItems = current[category] || {}
// Find additions
for (const name in currItems) {
if (!prevItems[name]) {
changes.additions.push({
category,
name,
item: currItems[name]
})
}
}
// Find removals and modifications
for (const name in prevItems) {
if (!currItems[name]) {
changes.breaking.push({
category,
name,
type: 'removed',
item: prevItems[name]
})
} else {
// Check for modifications
const diff = compareItems(prevItems[name], currItems[name], category)
if (diff.length > 0) {
changes.modifications.push({
category,
name,
changes: diff
})
}
}
}
}
return changes
}
/**
* Compare two items and return differences
*/
function compareItems(prev, curr, category) {
const differences = []
if (category === 'interfaces' || category === 'classes') {
// Compare members
const prevMembers = new Map(prev.members?.map((m) => [m.name, m]) || [])
const currMembers = new Map(curr.members?.map((m) => [m.name, m]) || [])
// Find added members
for (const [name, member] of currMembers) {
if (!prevMembers.has(name)) {
differences.push({
type: 'member_added',
name,
member
})
}
}
// Find removed members
for (const [name, member] of prevMembers) {
if (!currMembers.has(name)) {
differences.push({
type: 'member_removed',
name,
member
})
} else {
// Check if member type changed
const prevMember = prevMembers.get(name)
const currMember = currMembers.get(name)
if (prevMember.type !== currMember.type) {
differences.push({
type: 'member_type_changed',
name,
from: prevMember.type,
to: currMember.type
})
}
if (prevMember.optional !== currMember.optional) {
differences.push({
type: 'member_optionality_changed',
name,
from: prevMember.optional ? 'optional' : 'required',
to: currMember.optional ? 'optional' : 'required'
})
}
}
}
// Compare methods (for classes and interfaces)
if (category === 'classes') {
const prevMethods = new Map(prev.methods?.map((m) => [m.name, m]) || [])
const currMethods = new Map(curr.methods?.map((m) => [m.name, m]) || [])
for (const [name, method] of currMethods) {
if (!prevMethods.has(name)) {
differences.push({
type: 'method_added',
name,
method
})
}
}
for (const [name, method] of prevMethods) {
if (!currMethods.has(name)) {
differences.push({
type: 'method_removed',
name,
method
})
} else {
const prevMethod = prevMethods.get(name)
const currMethod = currMethods.get(name)
if (prevMethod.returnType !== currMethod.returnType) {
differences.push({
type: 'method_return_type_changed',
name,
from: prevMethod.returnType,
to: currMethod.returnType
})
}
// Compare parameters
if (
JSON.stringify(prevMethod.parameters) !==
JSON.stringify(currMethod.parameters)
) {
differences.push({
type: 'method_signature_changed',
name,
from: prevMethod.parameters,
to: currMethod.parameters
})
}
}
}
}
} else if (category === 'functions') {
// Compare function signatures
if (prev.returnType !== curr.returnType) {
differences.push({
type: 'return_type_changed',
from: prev.returnType,
to: curr.returnType
})
}
if (JSON.stringify(prev.parameters) !== JSON.stringify(curr.parameters)) {
differences.push({
type: 'parameters_changed',
from: prev.parameters,
to: curr.parameters
})
}
} else if (category === 'enums') {
// Compare enum members
const prevMembers = new Set(prev.members?.map((m) => m.name) || [])
const currMembers = new Set(curr.members?.map((m) => m.name) || [])
for (const member of currMembers) {
if (!prevMembers.has(member)) {
differences.push({
type: 'enum_member_added',
name: member
})
}
}
for (const member of prevMembers) {
if (!currMembers.has(member)) {
differences.push({
type: 'enum_member_removed',
name: member
})
}
}
}
return differences
}
/**
* Format changelog as markdown
*/
function formatChangelog(changes, prevVersion, currVersion) {
const lines = []
lines.push(`## v${currVersion} (${new Date().toISOString().split('T')[0]})`)
lines.push('')
lines.push(
`Comparing v${prevVersion} → v${currVersion}. This changelog documents changes to the public API surface that third-party extensions and custom nodes depend on.`
)
lines.push('')
// Breaking changes
if (changes.breaking.length > 0) {
lines.push('### ⚠️ Breaking Changes')
lines.push('')
const grouped = groupByCategory(changes.breaking)
for (const [category, items] of Object.entries(grouped)) {
lines.push(`**${categoryToTitle(category)}**`)
lines.push('')
for (const item of items) {
const displayName = generateGitHubLink(item.name, item.item)
lines.push(`- **Removed**: ${displayName}`)
}
lines.push('')
}
}
// Additions - commented out as per feedback
// if (changes.additions.length > 0) {
// lines.push('### ✨ Additions')
// lines.push('')
//
// const grouped = groupByCategory(changes.additions)
// for (const [category, items] of Object.entries(grouped)) {
// lines.push(`**${categoryToTitle(category)}**`)
// lines.push('')
// for (const item of items) {
// lines.push(`- \`${item.name}\``)
// if (item.item.members && item.item.members.length > 0) {
// const publicMembers = item.item.members.filter(
// (m) => !m.visibility || m.visibility === 'public'
// )
// if (publicMembers.length > 0 && publicMembers.length <= 5) {
// lines.push(
// ` - Members: ${publicMembers.map((m) => `\`${m.name}\``).join(', ')}`
// )
// }
// }
// }
// lines.push('')
// }
// }
// Modifications
if (changes.modifications.length > 0) {
lines.push('### 🔄 Modifications')
lines.push('')
const hasBreakingMods = changes.modifications.some((mod) =>
mod.changes.some((c) => isBreakingChange(c))
)
if (hasBreakingMods) {
lines.push('> **Note**: Some modifications may be breaking changes.')
lines.push('')
}
const grouped = groupByCategory(changes.modifications)
for (const [category, items] of Object.entries(grouped)) {
lines.push(`**${categoryToTitle(category)}**`)
lines.push('')
for (const item of items) {
// Get the current item to access source location
const currItem =
currentApi[item.category] && currentApi[item.category][item.name]
const displayName = generateGitHubLink(item.name, currItem)
lines.push(`- ${displayName}`)
for (const change of item.changes) {
const formatted = formatChange(change)
if (formatted) {
lines.push(` ${formatted}`)
}
}
}
lines.push('')
}
}
if (changes.breaking.length === 0 && changes.modifications.length === 0) {
lines.push('_No API changes detected._')
lines.push('')
}
lines.push('---')
lines.push('')
return lines.join('\n')
}
function groupByCategory(items) {
const grouped = {}
for (const item of items) {
if (!grouped[item.category]) {
grouped[item.category] = []
}
grouped[item.category].push(item)
}
return grouped
}
function categoryToTitle(category) {
const titles = {
types: 'Type Aliases',
interfaces: 'Interfaces',
enums: 'Enums',
functions: 'Functions',
classes: 'Classes',
constants: 'Constants'
}
return titles[category] || category
}
function isBreakingChange(change) {
const breakingTypes = [
'member_removed',
'method_removed',
'member_type_changed',
'method_return_type_changed',
'method_signature_changed',
'return_type_changed',
'parameters_changed',
'enum_member_removed'
]
return breakingTypes.includes(change.type)
}
function formatChange(change) {
switch (change.type) {
case 'member_added':
return `- ✨ Added member: \`${change.name}\``
case 'member_removed':
return `- ⚠️ **Breaking**: Removed member: \`${change.name}\``
case 'member_type_changed':
return `- ⚠️ **Breaking**: Member \`${change.name}\` type changed: \`${change.from}\`\`${change.to}\``
case 'member_optionality_changed':
return `- ${change.to === 'required' ? '⚠️ **Breaking**' : '✨'}: Member \`${change.name}\` is now ${change.to}`
case 'method_added':
return `- ✨ Added method: \`${change.name}()\``
case 'method_removed':
return `- ⚠️ **Breaking**: Removed method: \`${change.name}()\``
case 'method_return_type_changed':
return `- ⚠️ **Breaking**: Method \`${change.name}()\` return type changed: \`${change.from}\`\`${change.to}\``
case 'method_signature_changed':
return `- ⚠️ **Breaking**: Method \`${change.name}()\` signature changed`
case 'return_type_changed':
return `- ⚠️ **Breaking**: Return type changed: \`${change.from}\`\`${change.to}\``
case 'parameters_changed':
return `- ⚠️ **Breaking**: Function parameters changed`
case 'enum_member_added':
return `- ✨ Added enum value: \`${change.name}\``
case 'enum_member_removed':
return `- ⚠️ **Breaking**: Removed enum value: \`${change.name}\``
default:
return null
}
}
// Main execution
const changes = compareApis(previousApi, currentApi)
const changelog = formatChangelog(changes, previousVersion, currentVersion)
console.log(changelog)

313
scripts/snapshot-api.js Normal file
View File

@@ -0,0 +1,313 @@
#!/usr/bin/env node
/**
* Generates a JSON snapshot of the public API surface from TypeScript definitions.
* This snapshot is used to track API changes between versions.
*/
import { execSync } from 'child_process'
import * as fs from 'fs'
import * as path from 'path'
import * as ts from 'typescript'
const args = process.argv.slice(2)
if (args.length === 0) {
console.error('Usage: snapshot-api.js <path-to-index.d.ts>')
process.exit(1)
}
const filePath = args[0]
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`)
process.exit(1)
}
/**
* Search for the declaration in source files
* Returns {file, line} or null if not found
*/
function findInSourceFiles(declarationName, kind, sourceRoot = 'src') {
const searchPattern = getSearchPattern(declarationName, kind)
if (!searchPattern) return null
try {
// Search for the declaration pattern in source files
const result = execSync(
`grep -rn "${searchPattern}" ${sourceRoot} --include="*.ts" --include="*.tsx" | head -1`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
).trim()
if (result) {
// Parse grep output: filepath:line:content
const match = result.match(/^([^:]+):(\d+):/)
if (match) {
return {
file: match[1],
line: parseInt(match[2], 10)
}
}
}
} catch (error) {
// grep returns non-zero exit code if no match found
}
return null
}
/**
* Generate search pattern for finding declaration in source
*/
function getSearchPattern(name, kind) {
switch (kind) {
case 'interface':
return `export interface ${name}`
case 'class':
return `export class ${name}`
case 'type':
return `export type ${name}`
case 'enum':
return `export enum ${name}`
case 'function':
return `export function ${name}`
case 'constant':
return `export const ${name}`
default:
return null
}
}
/**
* Extract API surface from TypeScript definitions
*/
function extractApiSurface(sourceFile) {
const api = {
types: {},
interfaces: {},
enums: {},
functions: {},
classes: {},
constants: {}
}
function visit(node) {
// Extract type aliases
if (ts.isTypeAliasDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const sourceLocation = findInSourceFiles(name, 'type')
api.types[name] = {
kind: 'type',
name,
text: node.getText(sourceFile),
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract interfaces
if (ts.isInterfaceDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = []
node.members.forEach((member) => {
if (ts.isPropertySignature(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
type: member.type ? member.type.getText(sourceFile) : 'any',
optional: !!member.questionToken
})
} else if (ts.isMethodSignature(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
kind: 'method',
parameters: member.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: member.type ? member.type.getText(sourceFile) : 'void'
})
}
})
const sourceLocation = findInSourceFiles(name, 'interface')
api.interfaces[name] = {
kind: 'interface',
name,
members,
exported: hasExportModifier(node),
heritage: node.heritageClauses
? node.heritageClauses
.map((clause) =>
clause.types.map((type) => type.getText(sourceFile))
)
.flat()
: [],
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract enums
if (ts.isEnumDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = node.members.map((member) => ({
name: member.name.getText(sourceFile),
value: member.initializer
? member.initializer.getText(sourceFile)
: undefined
}))
const sourceLocation = findInSourceFiles(name, 'enum')
api.enums[name] = {
kind: 'enum',
name,
members,
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract functions
if (ts.isFunctionDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const sourceLocation = findInSourceFiles(name, 'function')
api.functions[name] = {
kind: 'function',
name,
parameters: node.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: node.type ? node.type.getText(sourceFile) : 'any',
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract classes
if (ts.isClassDeclaration(node) && node.name) {
const name = node.name.text
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
const members = []
const methods = []
node.members.forEach((member) => {
if (ts.isPropertyDeclaration(member) && member.name) {
members.push({
name: member.name.getText(sourceFile),
type: member.type ? member.type.getText(sourceFile) : 'any',
static: hasStaticModifier(member),
visibility: getVisibility(member)
})
} else if (ts.isMethodDeclaration(member) && member.name) {
methods.push({
name: member.name.getText(sourceFile),
parameters: member.parameters.map((p) => ({
name: p.name.getText(sourceFile),
type: p.type ? p.type.getText(sourceFile) : 'any',
optional: !!p.questionToken
})),
returnType: member.type ? member.type.getText(sourceFile) : 'any',
static: hasStaticModifier(member),
visibility: getVisibility(member)
})
}
})
const sourceLocation = findInSourceFiles(name, 'class')
api.classes[name] = {
kind: 'class',
name,
members,
methods,
exported: hasExportModifier(node),
heritage: node.heritageClauses
? node.heritageClauses
.map((clause) =>
clause.types.map((type) => type.getText(sourceFile))
)
.flat()
: [],
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
// Extract variable declarations (constants)
if (ts.isVariableStatement(node)) {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart())
node.declarationList.declarations.forEach((decl) => {
if (decl.name && ts.isIdentifier(decl.name)) {
const name = decl.name.text
const sourceLocation = findInSourceFiles(name, 'constant')
api.constants[name] = {
kind: 'constant',
name,
type: decl.type ? decl.type.getText(sourceFile) : 'unknown',
exported: hasExportModifier(node),
line: line + 1, // Convert to 1-indexed
sourceFile: sourceLocation?.file,
sourceLine: sourceLocation?.line
}
}
})
}
ts.forEachChild(node, visit)
}
function hasExportModifier(node) {
return (
node.modifiers &&
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)
)
}
function hasStaticModifier(node) {
return (
node.modifiers &&
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword)
)
}
function getVisibility(node) {
if (!node.modifiers) return 'public'
if (node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword))
return 'private'
if (
node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ProtectedKeyword)
)
return 'protected'
return 'public'
}
visit(sourceFile)
return api
}
// Read and parse the file
const sourceCode = fs.readFileSync(filePath, 'utf-8')
const sourceFile = ts.createSourceFile(
path.basename(filePath),
sourceCode,
ts.ScriptTarget.Latest,
true
)
const apiSurface = extractApiSurface(sourceFile)
// Output as JSON
console.log(JSON.stringify(apiSurface, null, 2))

View File

@@ -30,7 +30,7 @@
## Styling
- Use Tailwind CSS only (no custom CSS)
- Use the correct tokens from style.css in the design system package
- Dark theme: use "dark-theme:" prefix
- For common operations, try to use existing VueUse composables that automatically handle effect scope
- Example: Use `useElementHover` instead of manually managing mouseover/mouseout event listeners
- Example: Use `useIntersectionObserver` for visibility detection instead of custom scroll handlers

View File

@@ -7,7 +7,7 @@
class="flex flex-col"
>
<h3
class="subcategory-title mb-4 text-xs font-bold tracking-wide text-text-secondary uppercase"
class="subcategory-title mb-4 text-xs font-bold tracking-wide text-surface-600 uppercase dark-theme:text-surface-400"
>
{{ getSubcategoryTitle(subcategory) }}
</h3>
@@ -16,7 +16,7 @@
<div
v-for="command in subcategoryCommands"
:key="command.id"
class="shortcut-item flex items-center justify-between rounded py-2 transition-colors duration-200"
class="shortcut-item flex items-center justify-between rounded py-2 transition-colors duration-200 hover:bg-surface-100 dark-theme:hover:bg-surface-700"
>
<div class="shortcut-info grow pr-4">
<div class="shortcut-name text-sm font-medium">
@@ -32,7 +32,7 @@
<span
v-for="key in command.keybinding!.combo.getKeySequences()"
:key="key"
class="key-badge min-w-6 rounded bg-muted-background px-2 py-1 text-center font-mono text-xs"
class="key-badge min-w-6 rounded border bg-surface-200 px-2 py-1 text-center font-mono text-xs dark-theme:bg-surface-600"
>
{{ formatKey(key) }}
</span>
@@ -46,11 +46,11 @@
</template>
<script setup lang="ts">
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ComfyCommandImpl } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
const { t } = useI18n()
@@ -100,3 +100,21 @@ const formatKey = (key: string): string => {
return keyMap[key] || key
}
</script>
<style scoped>
.subcategory-title {
color: var(--p-text-muted-color);
}
.key-badge {
background-color: var(--p-surface-200);
border: 1px solid var(--p-surface-300);
min-width: 1.5rem;
text-align: center;
}
.dark-theme .key-badge {
background-color: var(--p-surface-600);
border-color: var(--p-surface-500);
}
</style>

View File

@@ -47,7 +47,6 @@
</template>
<script setup lang="ts">
import { appendJsonExt } from '@comfyorg/shared-frontend-utils/formatUtil'
import InputText from 'primevue/inputtext'
import type { MenuState } from 'primevue/menu'
import Menu from 'primevue/menu'
@@ -64,6 +63,7 @@ import {
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { appendJsonExt } from '@/utils/formatUtil'
interface Props {
item: MenuItem

View File

@@ -10,7 +10,8 @@ import { cn } from '@/utils/tailwindUtil'
const iconGroupClasses = cn(
'flex justify-center items-center shrink-0',
'outline-hidden border-none p-0 rounded-lg',
'bg-secondary-background shadow-sm',
'bg-white dark-theme:bg-zinc-700',
'text-neutral-950 dark-theme:text-white',
'transition-all duration-200',
'cursor-pointer'
)

View File

@@ -32,7 +32,7 @@ defineOptions({
interface IconTextButtonProps extends BaseButtonProps {
iconPosition?: 'left' | 'right'
label: string
onClick?: () => void
onClick: () => void
}
const {

View File

@@ -7,24 +7,13 @@
<Popover
ref="popover"
append-to="body"
auto-z-index
dismissable
close-on-escape
unstyled
:append-to="'body'"
:auto-z-index="true"
:base-z-index="1000"
:pt="{
root: {
class: cn('absolute z-50')
},
content: {
class: cn(
'mt-1 rounded-lg',
'bg-secondary-background text-base-foreground',
'shadow-lg'
)
}
}"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="pt"
@show="$emit('menuOpened')"
@hide="$emit('menuClosed')"
>
@@ -37,7 +26,7 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
@@ -68,4 +57,19 @@ const toggle = (event: Event) => {
const hide = () => {
popover.value?.hide()
}
const pt = computed(() => ({
root: {
class: cn('absolute z-50')
},
content: {
class: cn(
'mt-1 rounded-lg',
'bg-white dark-theme:bg-zinc-800',
'text-neutral dark-theme:text-white',
'shadow-lg',
'border border-zinc-200 dark-theme:border-zinc-700'
)
}
}))
</script>

View File

@@ -47,8 +47,8 @@ const containerClasses = computed(() => {
// Variant styles
const variantClasses = {
default: cn(
hasBackground && 'bg-modal-card-background',
hasBorder && 'border border-border-default',
hasBackground && 'bg-white dark-theme:bg-zinc-800',
hasBorder && 'border border-zinc-200 dark-theme:border-zinc-700',
hasShadow && 'shadow-sm',
hasCursor && 'cursor-pointer'
),
@@ -57,9 +57,9 @@ const containerClasses = computed(() => {
'p-2 transition-colors duration-200'
),
outline: cn(
hasBorder && 'border-2 border-border-subtle',
hasBorder && 'border-2 border-zinc-300 dark-theme:border-zinc-600',
hasCursor && 'cursor-pointer',
'hover:border-border-subtle/50 transition-colors'
'hover:border-zinc-400 dark-theme:hover:border-zinc-500 transition-colors'
)
}

View File

@@ -1,5 +1,7 @@
<template>
<div class="line-clamp-2 h-7 text-xs text-muted-foreground">
<div class="line-clamp-2 h-7 text-xs text-zinc-500 dark-theme:text-zinc-400">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -19,7 +19,7 @@ const baseClasses =
const variantStyles = {
dark: 'bg-zinc-500/40 text-white/90',
light: cn('backdrop-blur-[2px] bg-base-background/50 text-base-foreground')
light: 'backdrop-blur-[2px] bg-white/50 text-zinc-900 dark-theme:text-white'
}
const chipClasses = computed(() => {

View File

@@ -12,9 +12,8 @@
</template>
<script setup lang="ts">
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
import type { DeviceStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
device: DeviceStats

View File

@@ -12,7 +12,7 @@
</div>
</div>
<div class="file-action flex flex-row items-center gap-2">
<div class="file-action">
<Button
v-if="status === null || status === 'error'"
class="file-action-button"
@@ -23,13 +23,6 @@
icon="pi pi-download"
@click="triggerDownload"
/>
<Button
v-if="(status === null || status === 'error') && !!props.url"
:label="$t('g.copyURL')"
size="small"
outlined
@click="copyURL"
/>
</div>
</div>
<div
@@ -82,15 +75,14 @@
</template>
<script setup lang="ts">
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
import Button from 'primevue/button'
import ProgressBar from 'primevue/progressbar'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string
@@ -108,7 +100,6 @@ const status = ref<string | null>(null)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const { copyToClipboard } = useCopyToClipboard()
const electronDownloadStore = useElectronDownloadStore()
// @ts-expect-error fixme ts strict error
const [savePath, filename] = props.label.split('/')
@@ -135,8 +126,4 @@ const triggerDownload = async () => {
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
const copyURL = async () => {
await copyToClipboard(props.url)
}
</script>

View File

@@ -43,13 +43,13 @@
</template>
<script setup lang="ts">
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
import Button from 'primevue/button'
import Message from 'primevue/message'
import { computed } from 'vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
url: string

View File

@@ -3,7 +3,7 @@
<div class="flex items-center gap-2">
<div
class="preview-box flex h-16 w-16 items-center justify-center rounded border p-2"
:class="{ 'bg-base-background': !modelValue }"
:class="{ 'bg-smoke-100 dark-theme:bg-smoke-800': !modelValue }"
>
<img
v-if="modelValue"

View File

@@ -23,7 +23,7 @@
/>
<div
v-if="hasError"
class="absolute inset-0 flex items-center justify-center"
class="absolute inset-0 flex items-center justify-center bg-surface-50 text-muted dark-theme:bg-surface-800"
>
<img
src="/assets/images/default-template.png"

View File

@@ -36,7 +36,6 @@
</template>
<script setup lang="ts">
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
import Divider from 'primevue/divider'
import TabPanel from 'primevue/tabpanel'
import TabView from 'primevue/tabview'
@@ -44,6 +43,7 @@ import { computed } from 'vue'
import DeviceInfo from '@/components/common/DeviceInfo.vue'
import type { SystemStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil'
const props = defineProps<{
stats: SystemStats

View File

@@ -23,12 +23,12 @@
</template>
<script setup lang="ts">
import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import { onMounted, ref, watch } from 'vue'
import { isValidUrl } from '@/utils/formatUtil'
import { checkUrlReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'

View File

@@ -18,12 +18,12 @@
</template>
<script setup lang="ts">
import { formatMetronomeCurrency } from '@comfyorg/shared-frontend-utils/formatUtil'
import Skeleton from 'primevue/skeleton'
import Tag from 'primevue/tag'
import { computed } from 'vue'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
const { textClass } = defineProps<{
textClass?: string

View File

@@ -364,7 +364,10 @@
</div>
<!-- Results Summary -->
<div v-if="!isLoading" class="mt-6 px-6 text-sm text-muted">
<div
v-if="!isLoading"
class="mt-6 px-6 text-sm text-neutral-600 dark-theme:text-neutral-400"
>
{{
$t('templateWorkflows.resultsCount', {
count: filteredCount,

View File

@@ -29,10 +29,7 @@
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import { useExternalLink } from '@/composables/useExternalLink'
const { t } = useI18n()
const { buildDocsUrl } = useExternalLink()
const { apiNodeNames, onLogin, onCancel } = defineProps<{
apiNodeNames: string[]
@@ -41,9 +38,6 @@ const { apiNodeNames, onLogin, onCancel } = defineProps<{
}>()
const handleLearnMoreClick = () => {
window.open(
buildDocsUrl('/tutorials/api-nodes/faq', { includeLocale: true }),
'_blank'
)
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
}
</script>

View File

@@ -1,42 +1,39 @@
<template>
<!-- Cloud mode: Learn More + Got It buttons -->
<div
v-if="isCloud"
class="flex w-full items-center justify-between gap-2 py-2 px-4"
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="$t('loadWorkflowWarning.missingNodesTitle')"
:message="$t('loadWorkflowWarning.missingNodesDescription')"
/>
<MissingCoreNodesMessage :missing-core-nodes="missingCoreNodes" />
<ListBox
:options="uniqueNodes"
option-label="label"
scroll-height="100%"
class="comfy-missing-nodes"
:pt="{
list: { class: 'border-none' }
}"
>
<IconTextButton
:label="$t('missingNodes.cloud.learnMore')"
type="transparent"
size="sm"
icon-position="left"
as="a"
href="https://www.comfy.org/cloud"
target="_blank"
rel="noopener noreferrer"
>
<template #icon>
<i class="icon-[lucide--info]"></i>
</template>
</IconTextButton>
<TextButton
:label="$t('missingNodes.cloud.gotIt')"
type="secondary"
size="md"
@click="handleGotItClick"
/>
</div>
<!-- OSS mode: Open Manager + Install All buttons -->
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
<TextButton
:label="$t('g.openManager')"
type="transparent"
size="sm"
@click="openManager"
/>
<template #option="slotProps">
<div class="align-items-center flex">
<span class="node-type">{{ slotProps.option.label }}</span>
<span v-if="slotProps.option.hint" class="node-hint">{{
slotProps.option.hint
}}</span>
<Button
v-if="slotProps.option.action"
:label="slotProps.option.action.text"
size="small"
outlined
@click="slotProps.option.action.callback"
/>
</div>
</template>
</ListBox>
<div v-if="showManagerButtons" class="flex justify-end py-3">
<PackInstallButton
v-if="showInstallAllButton"
type="secondary"
size="md"
:disabled="
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
@@ -49,32 +46,40 @@
: $t('manager.installAllMissingNodes')
"
/>
<Button
:label="$t('g.openManager')"
size="small"
outlined
@click="openManager"
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ListBox from 'primevue/listbox'
import { computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import { isCloud } from '@/platform/distribution/types'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useDialogStore } from '@/stores/dialogStore'
import type { MissingNodeType } from '@/types/comfy'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const dialogStore = useDialogStore()
const { t } = useI18n()
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
const handleGotItClick = () => {
dialogStore.closeDialog({ key: 'global-missing-nodes' })
}
// Get missing node packs from workflow with loading and error states
const { missingNodePacks, isLoading, error, missingCoreNodes } =
useMissingNodes()
const { missingNodePacks, isLoading, error } = useMissingNodes()
const comfyManagerStore = useComfyManagerStore()
const managerState = useManagerState()
@@ -86,6 +91,27 @@ const isInstalling = computed(() => {
)
})
const uniqueNodes = computed(() => {
const seenTypes = new Set()
return props.missingNodeTypes
.filter((node) => {
const type = typeof node === 'object' ? node.type : node
if (seenTypes.has(type)) return false
seenTypes.add(type)
return true
})
.map((node) => {
if (typeof node === 'object') {
return {
label: node.type,
hint: node.hint,
action: node.action
}
}
return { label: node }
})
})
// Show manager buttons unless manager is disabled
const showManagerButtons = computed(() => {
return managerState.shouldShowManagerButtons.value
@@ -103,6 +129,9 @@ const openManager = async () => {
})
}
const { t } = useI18n()
const dialogStore = useDialogStore()
// Computed to check if all missing nodes have been installed
const allMissingNodesInstalled = computed(() => {
return (
@@ -111,14 +140,13 @@ const allMissingNodesInstalled = computed(() => {
missingNodePacks.value?.length === 0
)
})
// Watch for completion and close dialog (OSS mode only)
// Watch for completion and close dialog
watch(allMissingNodesInstalled, async (allInstalled) => {
if (!isCloud && allInstalled && showInstallAllButton.value) {
if (allInstalled && showInstallAllButton.value) {
// Use nextTick to ensure state updates are complete
await nextTick()
dialogStore.closeDialog({ key: 'global-missing-nodes' })
dialogStore.closeDialog({ key: 'global-load-workflow-warning' })
// Show success toast
useToastStore().add({
@@ -130,3 +158,20 @@ watch(allMissingNodesInstalled, async (allInstalled) => {
}
})
</script>
<style scoped>
.comfy-missing-nodes {
max-height: 300px;
overflow-y: auto;
}
.node-hint {
margin-left: 0.5rem;
font-style: italic;
color: var(--text-color-secondary);
}
:deep(.p-button) {
margin-left: auto;
}
</style>

View File

@@ -24,14 +24,16 @@
:key="version"
class="ml-4"
>
<div class="text-sm font-medium text-surface-600">
<div
class="text-sm font-medium text-surface-600 dark-theme:text-surface-400"
>
{{
$t('loadWorkflowWarning.coreNodesFromVersion', {
version: version || 'unknown'
})
}}
</div>
<div class="ml-4 text-sm text-surface-500">
<div class="ml-4 text-sm text-surface-500 dark-theme:text-surface-500">
{{ getUniqueNodeNames(nodes).join(', ') }}
</div>
</div>
@@ -47,14 +49,14 @@ import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
const { missingCoreNodes } = defineProps<{
const props = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]>
}>()
const systemStatsStore = useSystemStatsStore()
const hasMissingCoreNodes = computed(() => {
return Object.keys(missingCoreNodes).length > 0
return Object.keys(props.missingCoreNodes).length > 0
})
// Use computed for reactive version tracking
@@ -64,7 +66,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
})
const sortedMissingCoreNodes = computed(() => {
return Object.entries(missingCoreNodes).sort(([a], [b]) => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first)
return compare(b, a) // Reversed for descending order
})

View File

@@ -1,83 +0,0 @@
<template>
<div
class="flex w-[490px] flex-col border-t-1 border-b-1 border-border-default"
>
<div class="flex h-full w-full flex-col gap-4 p-4">
<!-- Description -->
<div>
<p class="m-0 text-sm leading-4 text-muted-foreground">
{{
isCloud
? $t('missingNodes.cloud.description')
: $t('missingNodes.oss.description')
}}
</p>
</div>
<MissingCoreNodesMessage v-if="!isCloud" :missing-core-nodes />
<!-- Missing Nodes List Wrapper -->
<div
class="comfy-missing-nodes flex flex-col max-h-[256px] rounded-lg py-2 scrollbar-custom bg-secondary-background"
>
<div
v-for="(node, i) in uniqueNodes"
:key="i"
class="flex min-h-8 items-center justify-between px-4 py-2 bg-secondary-background text-muted-foreground"
>
<span class="text-xs">
{{ node.label }}
</span>
<span v-if="node.hint" class="text-xs">{{ node.hint }}</span>
</div>
</div>
<!-- Bottom instruction -->
<div>
<p class="m-0 text-sm leading-4 text-muted-foreground">
{{
isCloud
? $t('missingNodes.cloud.replacementInstruction')
: $t('missingNodes.oss.replacementInstruction')
}}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { isCloud } from '@/platform/distribution/types'
import type { MissingNodeType } from '@/types/comfy'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
// Get missing core nodes for OSS mode
const { missingCoreNodes } = useMissingNodes()
const uniqueNodes = computed(() => {
const seenTypes = new Set()
return props.missingNodeTypes
.filter((node) => {
const type = typeof node === 'object' ? node.type : node
if (seenTypes.has(type)) return false
seenTypes.add(type)
return true
})
.map((node) => {
if (typeof node === 'object') {
return {
label: node.type,
hint: node.hint,
action: node.action
}
}
return { label: node }
})
})
</script>

View File

@@ -1,18 +0,0 @@
<template>
<div class="flex w-full items-center justify-between p-4">
<div class="flex items-center gap-2">
<i class="icon-[lucide--triangle-alert] text-gold-600"></i>
<p class="m-0 text-sm">
{{
isCloud
? $t('missingNodes.cloud.title')
: $t('missingNodes.oss.title')
}}
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { isCloud } from '@/platform/distribution/types'
</script>

View File

@@ -112,7 +112,6 @@
</template>
<script setup lang="ts">
import { formatMetronomeCurrency } from '@comfyorg/shared-frontend-utils/formatUtil'
import Button from 'primevue/button'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
@@ -124,12 +123,12 @@ import { computed, ref, watch } from 'vue'
import UserCredit from '@/components/common/UserCredit.vue'
import UsageLogsTable from '@/components/dialog/content/setting/UsageLogsTable.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useExternalLink } from '@/composables/useExternalLink'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { formatMetronomeCurrency } from '@/utils/formatUtil'
interface CreditHistoryItemData {
title: string
@@ -138,7 +137,6 @@ interface CreditHistoryItemData {
isPositive: boolean
}
const { buildDocsUrl } = useExternalLink()
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
@@ -185,17 +183,12 @@ const handleMessageSupport = async () => {
}
const handleFaqClick = () => {
window.open(
buildDocsUrl('/tutorials/api-nodes/faq', { includeLocale: true }),
'_blank'
)
window.open('https://docs.comfy.org/tutorials/api-nodes/faq', '_blank')
}
const handleOpenPartnerNodesInfo = () => {
window.open(
buildDocsUrl('/tutorials/api-nodes/overview#api-nodes', {
includeLocale: true
}),
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
'_blank'
)
}

View File

@@ -126,7 +126,6 @@
</template>
<script setup lang="ts">
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import { FilterMatchMode } from '@primevue/core/api'
import Button from 'primevue/button'
import Column from 'primevue/column'
@@ -147,6 +146,7 @@ import {
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
import PanelTemplate from './PanelTemplate.vue'
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'

View File

@@ -452,13 +452,10 @@ onMounted(async () => {
'Comfy.CustomColorPalettes'
)
// Restore saved workflow and workflow tabs state
await workflowPersistence.initializeWorkflow()
// Restore workflow and workflow tabs state from storage
await workflowPersistence.restorePreviousWorkflow()
workflowPersistence.restoreWorkflowTabsState()
// Load template from URL if present
await workflowPersistence.loadTemplateFromUrlIfPresent()
// Initialize release store to fetch releases from comfy-api (fire-and-forget)
const { useReleaseStore } = await import(
'@/platform/updates/common/releaseStore'

View File

@@ -10,7 +10,6 @@
</template>
<script setup lang="ts">
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import { useEventListener } from '@vueuse/core'
import { nextTick, ref } from 'vue'
@@ -24,6 +23,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { app as comfyApp } from '@/scripts/app'
import { isDOMWidget } from '@/scripts/domWidget'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
let idleTimeout: number
const nodeDefStore = useNodeDefStore()

Some files were not shown because too many files have changed in this diff Show More