mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-05 03:59:09 +00:00
Compare commits
20 Commits
test/error
...
api-change
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
842515c2be | ||
|
|
e36e25ebd6 | ||
|
|
f844d3e95b | ||
|
|
98ed124b02 | ||
|
|
e933b5c357 | ||
|
|
cb32562f04 | ||
|
|
9c94a4818f | ||
|
|
779f539b0e | ||
|
|
95c815f17c | ||
|
|
30dcf8c133 | ||
|
|
f1193a2f86 | ||
|
|
75daf2e4d2 | ||
|
|
11922709a9 | ||
|
|
3742a76cfb | ||
|
|
c84144581d | ||
|
|
8b88f8ccae | ||
|
|
0aab7cba4b | ||
|
|
608874a312 | ||
|
|
5cf6ac07ac | ||
|
|
ff60bdf1bc |
234
.github/workflows/README-manual-api-changelog.md
vendored
Normal file
234
.github/workflows/README-manual-api-changelog.md
vendored
Normal 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>
|
||||
```
|
||||
255
.github/workflows/manual-api-changelog.yaml
vendored
Normal file
255
.github/workflows/manual-api-changelog.yaml
vendored
Normal 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
|
||||
203
.github/workflows/release-api-changelogs.yaml
vendored
Normal file
203
.github/workflows/release-api-changelogs.yaml
vendored
Normal 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
|
||||
49
demo-snapshots/CHANGELOG-DEMO.md
Normal file
49
demo-snapshots/CHANGELOG-DEMO.md
Normal 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
188
demo-snapshots/README.md
Normal 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
58
demo-snapshots/v1.29.0.d.ts
vendored
Normal 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
192
demo-snapshots/v1.29.0.json
Normal 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
92
demo-snapshots/v1.30.2.d.ts
vendored
Normal 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
311
demo-snapshots/v1.30.2.json
Normal 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
40
docs/API-CHANGELOG.md
Normal 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`
|
||||
|
||||
---
|
||||
|
||||
@@ -58,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/*'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
444
scripts/compare-api-snapshots.js
Normal file
444
scripts/compare-api-snapshots.js
Normal 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
313
scripts/snapshot-api.js
Normal 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))
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": false,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
||||
Reference in New Issue
Block a user