diff --git a/.claude/commands/create-frontend-release.md b/.claude/commands/create-frontend-release.md new file mode 100644 index 000000000..2a9dd7445 --- /dev/null +++ b/.claude/commands/create-frontend-release.md @@ -0,0 +1,654 @@ +# Create Frontend Release + +This command guides you through creating a comprehensive frontend release with semantic versioning analysis, automated change detection, security scanning, and multi-stage human verification. + + +Create a frontend release with version type: $ARGUMENTS + +Expected format: Version increment type and optional description +Examples: +- `patch` - Bug fixes only +- `minor` - New features, backward compatible +- `major` - Breaking changes +- `prerelease` - Alpha/beta/rc releases +- `patch "Critical security fixes"` - With custom description +- `minor --skip-changelog` - Skip automated changelog generation +- `minor --dry-run` - Simulate release without executing + +If no arguments provided, the command will analyze recent changes and recommend the appropriate version type. + + +## Prerequisites + +Before starting, ensure: +- You have push access to the repository +- GitHub CLI (`gh`) is authenticated +- You're on a clean main branch working tree +- All intended changes are merged to main +- You understand the scope of changes being released + +## Critical Checks Before Starting + +### 1. Check Current Version Status +```bash +# Get current version and check if it's a pre-release +CURRENT_VERSION=$(node -p "require('./package.json').version") +if [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then + echo "⚠️ Current version $CURRENT_VERSION is a pre-release" + echo "Consider promoting to stable (e.g., 1.24.0-1 → 1.24.0) first" +fi +``` + +### 2. Find Last Stable Release +```bash +# Get last stable release tag (no pre-release suffix) +LAST_STABLE=$(git tag -l "v*" | grep -v "\-" | sort -V | tail -1) +echo "Last stable release: $LAST_STABLE" +``` + +## Configuration Options + +**Environment Variables:** +- `RELEASE_SKIP_SECURITY_SCAN=true` - Skip security audit +- `RELEASE_AUTO_APPROVE=true` - Skip some confirmation prompts +- `RELEASE_DRY_RUN=true` - Simulate release without executing + +## Release Process + +### Step 1: Environment Safety Check + +1. Verify clean working directory: + ```bash + git status --porcelain + ``` +2. Confirm on main branch: + ```bash + git branch --show-current + ``` +3. Pull latest changes: + ```bash + git pull origin main + ``` +4. Check GitHub CLI authentication: + ```bash + gh auth status + ``` +5. Verify npm/PyPI publishing access (dry run) +6. **CONFIRMATION REQUIRED**: Environment ready for release? + +### Step 2: Analyze Recent Changes + +1. Get current version from package.json +2. **IMPORTANT**: Determine correct base for comparison: + ```bash + # If current version is pre-release, use last stable release + if [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then + BASE_TAG=$LAST_STABLE + else + BASE_TAG=$(git describe --tags --abbrev=0) + fi + ``` +3. Find commits since base release (CRITICAL: use --first-parent): + ```bash + git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent + ``` +4. Count total commits: + ```bash + COMMIT_COUNT=$(git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent | wc -l) + echo "Found $COMMIT_COUNT commits since $BASE_TAG" + ``` +5. Analyze commits for: + - Breaking changes (BREAKING CHANGE, !, feat()) + - New features (feat:, feature:) + - Bug fixes (fix:, bugfix:) + - Documentation changes (docs:) + - Dependency updates +6. **VERIFY PR TARGET BRANCHES**: + ```bash + # Get merged PRs and verify they were merged to main + gh pr list --state merged --limit 50 --json number,title,baseRefName,mergedAt | \ + jq -r '.[] | select(.baseRefName == "main") | "\(.number): \(.title)"' + ``` +7. **HUMAN ANALYSIS**: Review change summary and verify scope + +### Step 3: Semantic Version Determination + +Based on analysis, determine version type: + +**Pre-release Handling:** +- If current version is pre-release (e.g., 1.24.0-1): + - Consider promoting to stable (1.24.0) instead of new version + - Or create new minor/major if significant changes added + +**Automatic Detection:** +- **MAJOR**: Breaking changes detected (`BREAKING CHANGE`, `!` in commits) +- **MINOR**: New features without breaking changes (`feat:` commits) +- **PATCH**: Only bug fixes, docs, or dependency updates + +**Version Workflow Limitations:** +- ⚠️ Cannot use "stable" as version_type - not in allowed values +- Allowed values: patch, minor, major, prepatch, preminor, premajor, prerelease +- For pre-release → stable promotion, must manually update version + +**Manual Override Options:** +- If arguments provided, validate against detected changes +- **CONFIRMATION REQUIRED**: Version type correct for these changes? +- **WARNING**: If manual override conflicts with detected breaking changes + +**Version Preview:** +- Current: `${CURRENT_VERSION}` +- Proposed: Show exact version number +- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`? + +### Step 4: Security and Dependency Audit + +1. Run security audit: + ```bash + npm audit --audit-level moderate + ``` +2. Check for known vulnerabilities in dependencies +3. Scan for hardcoded secrets or credentials: + ```bash + git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found" + ``` +4. Verify no sensitive data in recent commits +5. **SECURITY REVIEW**: Address any critical findings before proceeding? + +### Step 5: Pre-Release Testing + +1. Run complete test suite: + ```bash + npm run test:unit + npm run test:component + npm run test:browser + ``` +2. Run type checking: + ```bash + npm run typecheck + ``` +3. Run linting (may have issues with missing packages): + ```bash + npm run lint || echo "Lint issues - verify if critical" + ``` +4. Test build process: + ```bash + npm run build + npm run build:types + ``` +5. **QUALITY GATE**: All tests and builds passing? + +### Step 6: Breaking Change Analysis + +For minor/major releases: +1. Analyze API changes in: + - Public TypeScript interfaces + - Extension APIs + - Component props + - CLAUDE.md guidelines +2. Check for: + - Removed public functions/classes + - Changed function signatures + - Deprecated feature removals + - Configuration changes +3. Generate breaking change summary +4. **COMPATIBILITY REVIEW**: Breaking changes documented and justified? + +### Step 7: Generate and Save Changelog + +1. Extract commit messages since base release: + ```bash + git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent > commits.txt + ``` +2. **CRITICAL**: Verify PR inclusion by checking merge location: + ```bash + # For each significant PR mentioned, verify it's on main + for PR in ${SIGNIFICANT_PRS}; do + COMMIT=$(gh pr view $PR --json mergeCommit -q .mergeCommit.oid) + git branch -r --contains $COMMIT | grep -q "origin/main" || \ + echo "WARNING: PR #$PR not on main branch!" + done + ``` +3. Group by type: + - 🚀 **Features** (feat:) + - 🐛 **Bug Fixes** (fix:) + - 💥 **Breaking Changes** (BREAKING CHANGE) + - 📚 **Documentation** (docs:) + - 🔧 **Maintenance** (chore:, refactor:) + - ⬆️ **Dependencies** (deps:, dependency updates) +4. Include PR numbers and links +5. Add issue references (Fixes #123) +6. **Save changelog locally:** + ```bash + # Save to dated file for history + echo "$CHANGELOG" > release-notes-${NEW_VERSION}-$(date +%Y%m%d).md + + # Save to current for easy access + echo "$CHANGELOG" > CURRENT_RELEASE_NOTES.md + ``` +7. **CHANGELOG REVIEW**: Verify all PRs listed are actually on main branch + +### Step 8: Create Enhanced Release Notes + +1. Create comprehensive user-facing release notes including: + - **What's New**: Major features and improvements + - **Bug Fixes**: User-visible fixes + - **Breaking Changes**: Migration guide if applicable + - **Dependencies**: Major dependency updates + - **Performance**: Notable performance improvements + - **Contributors**: Thank contributors for their work +2. Reference related documentation updates +3. Include screenshots for UI changes (if available) +4. **Save release notes:** + ```bash + # Enhanced release notes for GitHub + echo "$RELEASE_NOTES" > github-release-notes-${NEW_VERSION}.md + ``` +5. **CONTENT REVIEW**: Release notes clear and helpful for users? + +### Step 9: Create Version Bump PR + +**For standard version bumps (patch/minor/major):** +```bash +# Trigger the workflow +gh workflow run version-bump.yaml -f version_type=${VERSION_TYPE} + +# Workflow runs quickly - usually creates PR within 30 seconds +echo "Workflow triggered. Waiting for PR creation..." +``` + +**For pre-release → stable promotion:** +1. Must manually create branch and update version: + ```bash + git checkout -b version-bump-${NEW_VERSION} + # Edit package.json to remove pre-release suffix + git add package.json + git commit -m "${NEW_VERSION}" + git push origin version-bump-${NEW_VERSION} + ``` + +2. Wait for PR creation (if using workflow) or create manually: + ```bash + # For workflow-created PRs - wait and find it + sleep 30 + # Look for PR from comfy-pr-bot (not github-actions) + PR_NUMBER=$(gh pr list --author comfy-pr-bot --limit 1 --json number --jq '.[0].number') + + # Verify we got the PR + if [ -z "$PR_NUMBER" ]; then + echo "PR not found yet. Checking recent PRs..." + gh pr list --limit 5 --json number,title,author + fi + + # For manual PRs + gh pr create --title "${NEW_VERSION}" \ + --body-file enhanced-pr-description.md \ + --label "Release" + ``` +3. **Create enhanced PR description:** + ```bash + cat > enhanced-pr-description.md << EOF + # Release v${NEW_VERSION} + + ## Version Change + \`${CURRENT_VERSION}\` → \`${NEW_VERSION}\` (${VERSION_TYPE}) + + ## Changelog + ${CHANGELOG} + + ## Breaking Changes + ${BREAKING_CHANGES} + + ## Testing Performed + - ✅ Full test suite (unit, component, browser) + - ✅ TypeScript compilation + - ✅ Linting checks + - ✅ Build verification + - ✅ Security audit + + ## Distribution Channels + - GitHub Release (with dist.zip) + - PyPI Package (comfyui-frontend-package) + - npm Package (@comfyorg/comfyui-frontend-types) + + ## Post-Release Tasks + - [ ] Verify all distribution channels + - [ ] Update external documentation + - [ ] Monitor for issues + EOF + ``` +4. Update PR with enhanced description: + ```bash + gh pr edit ${PR_NUMBER} --body-file enhanced-pr-description.md + ``` +5. Add changelog as comment for easy reference: + ```bash + gh pr comment ${PR_NUMBER} --body-file CURRENT_RELEASE_NOTES.md + ``` +6. **PR REVIEW**: Version bump PR created and enhanced correctly? + +### Step 11: Critical Release PR Verification + +1. **CRITICAL**: Verify PR has "Release" label: + ```bash + gh pr view ${PR_NUMBER} --json labels | jq -r '.labels[].name' | grep -q "Release" || \ + echo "ERROR: Release label missing! Add it immediately!" + ``` +2. Check for update-locales commits: + ```bash + # WARNING: update-locales may add [skip ci] which blocks release workflow! + gh pr view ${PR_NUMBER} --json commits | grep -q "skip ci" && \ + echo "WARNING: [skip ci] detected - release workflow may not trigger!" + ``` +3. Verify version number in package.json +4. Review all changed files +5. Ensure no unintended changes included +6. Wait for required PR checks: + ```bash + gh pr checks ${PR_NUMBER} --watch + ``` +7. **FINAL CODE REVIEW**: Release label present and no [skip ci]? + +### Step 12: Pre-Merge Validation + +1. **Review Requirements**: Release PRs require approval +2. Monitor CI checks - watch for update-locales +3. **CRITICAL WARNING**: If update-locales adds [skip ci], the release workflow won't trigger! +4. Check no new commits to main since PR creation +5. **DEPLOYMENT READINESS**: Ready to merge? + +### Step 13: Execute Release + +1. **FINAL CONFIRMATION**: Merge PR to trigger release? +2. Merge the Release PR: + ```bash + gh pr merge ${PR_NUMBER} --merge + ``` +3. **IMMEDIATELY CHECK**: Did release workflow trigger? + ```bash + sleep 10 + gh run list --workflow=release.yaml --limit=1 + ``` +4. If workflow didn't trigger due to [skip ci]: + ```bash + echo "ERROR: Release workflow didn't trigger!" + echo "Options:" + echo "1. Create patch release (e.g., 1.24.1) to trigger workflow" + echo "2. Investigate manual release options" + ``` +5. If workflow triggered, monitor execution: + ```bash + WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId') + gh run watch ${WORKFLOW_RUN_ID} + ``` + +### Step 14: Enhance GitHub Release + +1. Wait for automatic release creation: + ```bash + # Wait for release to be created + while ! gh release view v${NEW_VERSION} >/dev/null 2>&1; do + echo "Waiting for release creation..." + sleep 10 + done + ``` + +2. **Enhance the GitHub release:** + ```bash + # Update release with our enhanced notes + gh release edit v${NEW_VERSION} \ + --title "🚀 ComfyUI Frontend v${NEW_VERSION}" \ + --notes-file github-release-notes-${NEW_VERSION}.md \ + --latest + + # Add any additional assets if needed + # gh release upload v${NEW_VERSION} additional-assets.zip + ``` + +3. **Verify release details:** + ```bash + gh release view v${NEW_VERSION} + ``` + +### Step 15: Verify Multi-Channel Distribution + +1. **GitHub Release:** + ```bash + gh release view v${NEW_VERSION} --json assets,body,createdAt,tagName + ``` + - ✅ Check release notes + - ✅ Verify dist.zip attachment + - ✅ Confirm release marked as latest (for main branch) + +2. **PyPI Package:** + ```bash + # Check PyPI availability (may take a few minutes) + for i in {1..10}; do + if curl -s https://pypi.org/pypi/comfyui-frontend-package/json | jq -r '.releases | keys[]' | grep -q ${NEW_VERSION}; then + echo "✅ PyPI package available" + break + fi + echo "⏳ Waiting for PyPI package... (attempt $i/10)" + sleep 30 + done + ``` + +3. **npm Package:** + ```bash + # Check npm availability + for i in {1..10}; do + if npm view @comfyorg/comfyui-frontend-types@${NEW_VERSION} version >/dev/null 2>&1; then + echo "✅ npm package available" + break + fi + echo "⏳ Waiting for npm package... (attempt $i/10)" + sleep 30 + done + ``` + +4. **DISTRIBUTION VERIFICATION**: All channels published successfully? + +### Step 16: Post-Release Monitoring Setup + +1. **Monitor immediate release health:** + ```bash + # Check for immediate issues + gh issue list --label "bug" --state open --limit 5 --json title,number,createdAt + + # Monitor download metrics (if accessible) + gh release view v${NEW_VERSION} --json assets --jq '.assets[].downloadCount' + ``` + +2. **Update documentation tracking:** + ```bash + cat > post-release-checklist.md << EOF + # Post-Release Checklist for v${NEW_VERSION} + + ## Immediate Tasks (Next 24 hours) + - [ ] Monitor error rates and user feedback + - [ ] Watch for critical issues + - [ ] Verify documentation is up to date + - [ ] Check community channels for questions + + ## Short-term Tasks (Next week) + - [ ] Update external integration guides + - [ ] Monitor adoption metrics + - [ ] Gather user feedback + - [ ] Plan next release cycle + + ## Long-term Tasks + - [ ] Analyze release process improvements + - [ ] Update release templates based on learnings + - [ ] Document any new patterns discovered + + ## Key Metrics to Track + - Download counts: GitHub, PyPI, npm + - Issue reports related to v${NEW_VERSION} + - Community feedback and adoption + - Performance impact measurements + EOF + ``` + +3. **Create release summary:** + ```bash + cat > release-summary-${NEW_VERSION}.md << EOF + # Release Summary: ComfyUI Frontend v${NEW_VERSION} + + **Released:** $(date) + **Type:** ${VERSION_TYPE} + **Duration:** ~${RELEASE_DURATION} minutes + **Release Commit:** ${RELEASE_COMMIT} + + ## Metrics + - **Commits Included:** ${COMMITS_COUNT} + - **Contributors:** ${CONTRIBUTORS_COUNT} + - **Files Changed:** ${FILES_CHANGED} + - **Lines Added/Removed:** +${LINES_ADDED}/-${LINES_REMOVED} + + ## Distribution Status + - ✅ GitHub Release: Published + - ✅ PyPI Package: Available + - ✅ npm Types: Available + + ## Next Steps + - Monitor for 24-48 hours + - Address any critical issues immediately + - Plan next release cycle + + ## Files Generated + - \`release-notes-${NEW_VERSION}-$(date +%Y%m%d).md\` - Detailed changelog + - \`github-release-notes-${NEW_VERSION}.md\` - GitHub release notes + - \`post-release-checklist.md\` - Follow-up tasks + EOF + ``` + +4. **RELEASE COMPLETION**: All post-release setup completed? + +## Advanced Safety Features + +### Rollback Procedures + +**Pre-Merge Rollback:** +```bash +# Close version bump PR and reset +gh pr close ${PR_NUMBER} +git reset --hard origin/main +git clean -fd +``` + +**Post-Merge Rollback:** +```bash +# Create immediate patch release with reverts +git revert ${RELEASE_COMMIT} +# Follow this command again with patch version +``` + +**Emergency Procedures:** +```bash +# Document incident +cat > release-incident-${NEW_VERSION}.md << EOF +# Release Incident Report + +**Version:** ${NEW_VERSION} +**Issue:** [Describe the problem] +**Impact:** [Severity and scope] +**Resolution:** [Steps taken] +**Prevention:** [Future improvements] +EOF + +# Contact package registries for critical issues +echo "For critical security issues, consider:" +echo "- PyPI: Contact support for package yanking" +echo "- npm: Use 'npm unpublish' within 72 hours" +echo "- GitHub: Update release with warning notes" +``` + +### Quality Gates Summary + +The command implements multiple quality gates: + +1. **🔒 Security Gate**: Vulnerability scanning, secret detection +2. **🧪 Quality Gate**: Full test suite, linting, type checking +3. **📋 Content Gate**: Changelog accuracy, release notes quality +4. **🔄 Process Gate**: Release timing verification +5. **✅ Verification Gate**: Multi-channel publishing confirmation +6. **📊 Monitoring Gate**: Post-release health tracking + +## Common Scenarios + +### Scenario 1: Regular Feature Release +```bash +/project:create-frontend-release minor +``` +- Analyzes features since last release +- Generates changelog automatically +- Creates comprehensive release notes + +### Scenario 2: Critical Security Patch +```bash +/project:create-frontend-release patch "Security fixes for CVE-2024-XXXX" +``` +- Expedited security scanning +- Enhanced monitoring setup + +### Scenario 3: Major Version with Breaking Changes +```bash +/project:create-frontend-release major +``` +- Comprehensive breaking change analysis +- Migration guide generation + +### Scenario 4: Pre-release Testing +```bash +/project:create-frontend-release prerelease +``` +- Creates alpha/beta/rc versions +- Draft release status + +## Common Issues and Solutions + +### Issue: Pre-release Version Confusion +**Problem**: Not sure whether to promote pre-release or create new version +**Solution**: +- If no new commits since pre-release: promote to stable +- If new commits exist: consider new minor version + +### Issue: Wrong Commit Count +**Problem**: Changelog includes commits from other branches +**Solution**: Always use `--first-parent` flag with git log + +### Issue: Release Workflow Doesn't Trigger +**Problem**: update-locales adds [skip ci] to PR +**Solution**: +1. Create patch release to trigger workflow +2. Alternative: Revert version and re-run version bump workflow +3. Fix update-locales to skip [skip ci] for Release PRs + +**Update**: Sometimes update-locales doesn't add [skip ci] - always verify! + +### Issue: Version Workflow Limitations +**Problem**: Cannot use "stable" as version_type +**Solution**: Manually create PR for pre-release → stable promotion + +### Issue: Missing PRs in Changelog +**Problem**: PR was merged to different branch +**Solution**: Verify PR merge target with: +```bash +gh pr view ${PR_NUMBER} --json baseRefName +``` + +### Issue: Release Failed Due to [skip ci] +**Problem**: Release workflow didn't trigger after merge +**Recovery Strategy**: +1. Revert version in a new PR (e.g., 1.24.0 → 1.24.0-1) +2. Merge the revert PR +3. Run version bump workflow again +4. This creates a fresh PR without [skip ci] +Benefits: Cleaner than creating extra version numbers + +## Key Learnings & Notes + +1. **PR Author**: Version bump PRs are created by `comfy-pr-bot`, not `github-actions` +2. **Workflow Speed**: Version bump workflow typically completes in ~20-30 seconds +3. **Update-locales Behavior**: Inconsistent - sometimes adds [skip ci], sometimes doesn't +4. **Recovery Options**: Reverting version is cleaner than creating extra versions + diff --git a/.claude/commands/create-hotfix-release.md b/.claude/commands/create-hotfix-release.md new file mode 100644 index 000000000..b1e521a29 --- /dev/null +++ b/.claude/commands/create-hotfix-release.md @@ -0,0 +1,222 @@ +# Create Hotfix Release + +This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step. + + +Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS + +Expected format: Comma-separated list of commits or PR numbers +Examples: +- `abc123,def456,ghi789` (commits) +- `#1234,#5678` (PRs) +- `abc123,#1234,def456` (mixed) + +If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs. + + +## Prerequisites + +Before starting, ensure: +- You have push access to the repository +- GitHub CLI (`gh`) is authenticated +- You're on a clean working tree +- You understand the commits/PRs you're cherry-picking + +## Hotfix Release Process + +### Step 1: Identify Target Core Branch + +1. Fetch the current ComfyUI requirements.txt from master branch: + ```bash + curl -s https://raw.githubusercontent.com/comfyanonymous/ComfyUI/master/requirements.txt | grep "comfyui-frontend-package" + ``` +2. Extract the `comfyui-frontend-package` version (e.g., `comfyui-frontend-package==1.23.4`) +3. Parse version to get major.minor (e.g., `1.23.4` → `1.23`) +4. Determine core branch: `core/.` (e.g., `core/1.23`) +5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*` +6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch? + +### Step 2: Parse and Validate Arguments + +1. Parse the comma-separated list of commits/PRs +2. For each item: + - If starts with `#`: Treat as PR number + - Otherwise: Treat as commit hash +3. For PR numbers: + - Fetch PR details using `gh pr view ` + - Extract the merge commit if PR is merged + - If PR has multiple commits, list them all + - **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits? +4. Validate all commit hashes exist in the repository + +### Step 3: Analyze Target Changes + +1. For each commit/PR to cherry-pick: + - Display commit hash, author, date + - Show PR title and number (if applicable) + - Display commit message + - Show files changed and diff statistics + - Check if already in core branch: `git branch --contains ` +2. Identify potential conflicts by checking changed files +3. **CONFIRMATION REQUIRED**: Proceed with these commits? + +### Step 4: Create Hotfix Branch + +1. Checkout the core branch (e.g., `core/1.23`) +2. Pull latest changes: `git pull origin core/X.Y` +3. Display current version from package.json +4. Create hotfix branch: `hotfix/-` + - Example: `hotfix/1.23.4-20241120` +5. **CONFIRMATION REQUIRED**: Created branch correctly? + +### Step 5: Cherry-pick Changes + +For each commit: +1. Attempt cherry-pick: `git cherry-pick ` +2. If conflicts occur: + - Display conflict details + - Show conflicting sections + - Provide resolution guidance + - **CONFIRMATION REQUIRED**: Conflicts resolved correctly? +3. After successful cherry-pick: + - Show the changes: `git show HEAD` + - Run validation: `npm run typecheck && npm run lint` +4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid? + +### Step 6: Create PR to Core Branch + +1. Push the hotfix branch: `git push origin hotfix/-` +2. Create PR using gh CLI: + ```bash + gh pr create --base core/X.Y --head hotfix/- \ + --title "[Hotfix] Cherry-pick fixes to core/X.Y" \ + --body "Cherry-picked commits: ..." + ``` +3. Add appropriate labels (but NOT "Release" yet) +4. PR body should include: + - List of cherry-picked commits/PRs + - Original issue references + - Testing instructions + - Impact assessment +5. **CONFIRMATION REQUIRED**: PR created correctly? + +### Step 7: Wait for Tests + +1. Monitor PR checks: `gh pr checks` +2. Display test results as they complete +3. If any tests fail: + - Show failure details + - Analyze if related to cherry-picks + - **DECISION REQUIRED**: Fix and continue, or abort? +4. Wait for all required checks to pass +5. **CONFIRMATION REQUIRED**: All tests passing? + +### Step 8: Merge Hotfix PR + +1. Verify all checks have passed +2. Check for required approvals +3. Merge the PR: `gh pr merge --merge` +4. Delete the hotfix branch +5. **CONFIRMATION REQUIRED**: PR merged successfully? + +### Step 9: Create Version Bump + +1. Checkout the core branch: `git checkout core/X.Y` +2. Pull latest changes: `git pull origin core/X.Y` +3. Read current version from package.json +4. Determine patch version increment: + - Current: `1.23.4` → New: `1.23.5` +5. Create release branch named with new version: `release/1.23.5` +6. Update version in package.json to `1.23.5` +7. Commit: `git commit -m "[release] Bump version to 1.23.5"` +8. **CONFIRMATION REQUIRED**: Version bump correct? + +### Step 10: Create Release PR + +1. Push release branch: `git push origin release/1.23.5` +2. Create PR with Release label: + ```bash + gh pr create --base core/X.Y --head release/1.23.5 \ + --title "[Release] v1.23.5" \ + --body "..." \ + --label "Release" + ``` +3. **CRITICAL**: Verify "Release" label is added +4. PR description should include: + - Version: `1.23.4` → `1.23.5` + - Included fixes (link to previous PR) + - Release notes for users +5. **CONFIRMATION REQUIRED**: Release PR has "Release" label? + +### Step 11: Monitor Release Process + +1. Wait for PR checks to pass +2. **FINAL CONFIRMATION**: Ready to trigger release by merging? +3. Merge the PR: `gh pr merge --merge` +4. Monitor release workflow: + ```bash + gh run list --workflow=release.yaml --limit=1 + gh run watch + ``` +5. Track progress: + - GitHub release draft/publication + - PyPI upload + - npm types publication + +### Step 12: Post-Release Verification + +1. Verify GitHub release: + ```bash + gh release view v1.23.5 + ``` +2. Check PyPI package: + ```bash + pip index versions comfyui-frontend-package | grep 1.23.5 + ``` +3. Verify npm package: + ```bash + npm view @comfyorg/comfyui-frontend-types@1.23.5 + ``` +4. Generate release summary with: + - Version released + - Commits included + - Issues fixed + - Distribution status +5. **CONFIRMATION REQUIRED**: Release completed successfully? + +## Safety Checks + +Throughout the process: +- Always verify core branch matches ComfyUI's requirements.txt +- For PRs: Ensure using correct commits (merge vs individual) +- Check version numbers follow semantic versioning +- **Critical**: "Release" label must be on version bump PR +- Validate cherry-picks don't break core branch stability +- Keep audit trail of all operations + +## Rollback Procedures + +If something goes wrong: +- Before push: `git reset --hard origin/core/X.Y` +- After PR creation: Close PR and start over +- After failed release: Create new patch version with fixes +- Document any issues for future reference + +## Important Notes + +- Core branch version will be behind main - this is expected +- The "Release" label triggers the PyPI/npm publication +- PR numbers must include the `#` prefix +- Mixed commits/PRs are supported but review carefully +- Always wait for full test suite before proceeding + +## Expected Timeline + +- Step 1-3: ~10 minutes (analysis) +- Steps 4-6: ~15-30 minutes (cherry-picking) +- Step 7: ~10-20 minutes (tests) +- Steps 8-10: ~10 minutes (version bump) +- Step 11-12: ~15-20 minutes (release) +- Total: ~60-90 minutes + +This process ensures a safe, verified hotfix release with multiple confirmation points and clear tracking of what changes are being released. \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index a19f49ff7..a8866207a 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,9 @@ if [[ "$OS" == "Windows_NT" ]]; then npx.cmd lint-staged + # Check for unused i18n keys in staged files + npx.cmd tsx scripts/check-unused-i18n-keys.ts else npx lint-staged + # Check for unused i18n keys in staged files + npx tsx scripts/check-unused-i18n-keys.ts fi diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 7b27108fe..01a04cd97 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -9,9 +9,10 @@ module.exports = defineConfig({ entry: 'src/locales/en', entryLocale: 'en', output: 'src/locales', - outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'], + outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'], reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream. 'latent' is the short form of 'latent space'. 'mask' is in the context of image processing. + Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters. ` }); diff --git a/README.md b/README.md index 0004bdecf..d490ae628 100644 --- a/README.md +++ b/README.md @@ -529,7 +529,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat! ### Prerequisites & Technology Stack - **Required Software**: - - Node.js (v16 or later) and npm + - Node.js (v16 or later; v20/v22 strongly recommended) and npm - Git for version control - A running ComfyUI backend instance diff --git a/browser_tests/README.md b/browser_tests/README.md index 88bd865f8..1aeaa6e54 100644 --- a/browser_tests/README.md +++ b/browser_tests/README.md @@ -14,7 +14,7 @@ Clone to your `custom_nodes` dir _ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._ ### Node.js & Playwright Prerequisites -Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver: +Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver: ```bash npx playwright install chromium --with-deps ``` diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png index f133bdf6f..9f713b4a1 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png index 2adb3ad8d..3d0809c96 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/primitive-node-connected-chromium-linux.png differ diff --git a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png index 0341dac37..588674402 100644 Binary files a/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png and b/browser_tests/tests/primitiveNode.spec.ts-snapshots/static-primitive-connected-chromium-linux.png differ diff --git a/browser_tests/tests/releaseNotifications.spec.ts b/browser_tests/tests/releaseNotifications.spec.ts index 5e5e58001..19d09327d 100644 --- a/browser_tests/tests/releaseNotifications.spec.ts +++ b/browser_tests/tests/releaseNotifications.spec.ts @@ -130,4 +130,239 @@ test.describe('Release Notifications', () => { whatsNewSection.locator('text=No recent releases') ).toBeVisible() }) + + test('should hide "What\'s New" section when notifications are disabled', async ({ + comfyPage + }) => { + // Disable version update notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Mock release API with test data + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 1, + project: 'comfyui', + version: 'v0.3.44', + attention: 'high', + content: '## New Features\n\n- Added awesome feature', + published_at: new Date().toISOString() + } + ]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify help center menu appears + const helpMenu = comfyPage.page.locator('.help-center-menu') + await expect(helpMenu).toBeVisible() + + // Verify "What's New?" section is hidden + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).not.toBeVisible() + + // Should not show any popups or toasts + await expect(comfyPage.page.locator('.whats-new-popup')).not.toBeVisible() + await expect( + comfyPage.page.locator('.release-notification-toast') + ).not.toBeVisible() + }) + + test('should not make API calls when notifications are disabled', async ({ + comfyPage + }) => { + // Disable version update notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Track API calls + let apiCallCount = 0 + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + apiCallCount++ + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Wait a bit to ensure any potential API calls would have been made + await comfyPage.page.waitForTimeout(1000) + + // Verify no API calls were made + expect(apiCallCount).toBe(0) + }) + + test('should show "What\'s New" section when notifications are enabled', async ({ + comfyPage + }) => { + // Enable version update notifications (default behavior) + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + + // Mock release API with test data + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 1, + project: 'comfyui', + version: 'v0.3.44', + attention: 'medium', + content: '## New Features\n\n- Added awesome feature', + published_at: new Date().toISOString() + } + ]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify help center menu appears + const helpMenu = comfyPage.page.locator('.help-center-menu') + await expect(helpMenu).toBeVisible() + + // Verify "What's New?" section is visible + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).toBeVisible() + + // Should show the release + await expect( + whatsNewSection.locator('text=Comfy v0.3.44 Release') + ).toBeVisible() + }) + + test('should toggle "What\'s New" section when setting changes', async ({ + comfyPage + }) => { + // Mock release API with test data + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { + id: 1, + project: 'comfyui', + version: 'v0.3.44', + attention: 'low', + content: '## Bug Fixes\n\n- Fixed minor issue', + published_at: new Date().toISOString() + } + ]) + }) + } else { + await route.continue() + } + }) + + // Start with notifications enabled + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify "What's New?" section is visible + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).toBeVisible() + + // Close help center + await comfyPage.page.click('.help-center-backdrop') + + // Disable notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Reopen help center + await helpCenterButton.click() + + // Verify "What's New?" section is now hidden + await expect(whatsNewSection).not.toBeVisible() + }) + + test('should handle edge case with empty releases and disabled notifications', async ({ + comfyPage + }) => { + // Disable notifications + await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + + // Mock empty releases + await comfyPage.page.route('**/releases**', async (route) => { + const url = route.request().url() + if ( + url.includes('api.comfy.org') || + url.includes('stagingapi.comfy.org') + ) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([]) + }) + } else { + await route.continue() + } + }) + + await comfyPage.setup({ mockReleases: false }) + + // Open help center + const helpCenterButton = comfyPage.page.locator('.comfy-help-center-btn') + await helpCenterButton.waitFor({ state: 'visible' }) + await helpCenterButton.click() + + // Verify help center still works + const helpMenu = comfyPage.page.locator('.help-center-menu') + await expect(helpMenu).toBeVisible() + + // Section should be hidden regardless of empty releases + const whatsNewSection = comfyPage.page.locator('.whats-new-section') + await expect(whatsNewSection).not.toBeVisible() + }) }) diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts new file mode 100644 index 000000000..69a40ced9 --- /dev/null +++ b/browser_tests/tests/useSettingSearch.spec.ts @@ -0,0 +1,289 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.describe('Settings Search functionality', () => { + test.beforeEach(async ({ comfyPage }) => { + // Register test settings to verify hidden/deprecated filtering + await comfyPage.page.evaluate(() => { + window['app'].registerExtension({ + name: 'TestSettingsExtension', + settings: [ + { + id: 'TestHiddenSetting', + name: 'Test Hidden Setting', + type: 'hidden', + defaultValue: 'hidden_value', + category: ['Test', 'Hidden'] + }, + { + id: 'TestDeprecatedSetting', + name: 'Test Deprecated Setting', + type: 'text', + defaultValue: 'deprecated_value', + deprecated: true, + category: ['Test', 'Deprecated'] + }, + { + id: 'TestVisibleSetting', + name: 'Test Visible Setting', + type: 'text', + defaultValue: 'visible_value', + category: ['Test', 'Visible'] + } + ] + }) + }) + }) + + test('can open settings dialog and use search box', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await expect(searchBox).toBeVisible() + + // Verify search box has the correct placeholder + await expect(searchBox).toHaveAttribute( + 'placeholder', + expect.stringContaining('Search') + ) + }) + + test('search box is functional and accepts input', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find and interact with the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Comfy') + + // Verify the input was accepted + await expect(searchBox).toHaveValue('Comfy') + }) + + test('search box clears properly', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find and interact with the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('test') + await expect(searchBox).toHaveValue('test') + + // Clear the search box + await searchBox.clear() + await expect(searchBox).toHaveValue('') + }) + + test('settings categories are visible in sidebar', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Check that the sidebar has categories + const categories = comfyPage.page.locator( + '.settings-sidebar .p-listbox-option' + ) + expect(await categories.count()).toBeGreaterThan(0) + + // Check that at least one category is visible + await expect(categories.first()).toBeVisible() + }) + + test('can select different categories in sidebar', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Get categories and click on different ones + const categories = comfyPage.page.locator( + '.settings-sidebar .p-listbox-option' + ) + const categoryCount = await categories.count() + + if (categoryCount > 1) { + // Click on the second category + await categories.nth(1).click() + + // Verify the category is selected + await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/) + } + }) + + test('settings content area is visible', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Check that the content area is visible + const contentArea = comfyPage.page.locator('.settings-content') + await expect(contentArea).toBeVisible() + + // Check that tab panels are visible + const tabPanels = comfyPage.page.locator('.settings-tab-panels') + await expect(tabPanels).toBeVisible() + }) + + test('search functionality affects UI state', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Find the search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + + // Type in search box + await searchBox.fill('graph') + await comfyPage.page.waitForTimeout(200) // Wait for debounce + + // Verify that the search input is handled + await expect(searchBox).toHaveValue('graph') + }) + + test('settings dialog can be closed', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Close with escape key + await comfyPage.page.keyboard.press('Escape') + + // Verify dialog is closed + await expect(settingsDialog).not.toBeVisible() + }) + + test('search box has proper debouncing behavior', async ({ comfyPage }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Type rapidly in search box + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('a') + await searchBox.fill('ab') + await searchBox.fill('abc') + await searchBox.fill('abcd') + + // Wait for debounce + await comfyPage.page.waitForTimeout(200) + + // Verify final value + await expect(searchBox).toHaveValue('abcd') + }) + + test('search excludes hidden settings from results', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Search for our test settings + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Test') + await comfyPage.page.waitForTimeout(300) // Wait for debounce + + // Get all settings content + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Should show visible setting but not hidden setting + await expect(settingsContent).toContainText('Test Visible Setting') + await expect(settingsContent).not.toContainText('Test Hidden Setting') + }) + + test('search excludes deprecated settings from results', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Search for our test settings + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Test') + await comfyPage.page.waitForTimeout(300) // Wait for debounce + + // Get all settings content + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Should show visible setting but not deprecated setting + await expect(settingsContent).toContainText('Test Visible Setting') + await expect(settingsContent).not.toContainText('Test Deprecated Setting') + }) + + test('search shows visible settings but excludes hidden and deprecated', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + // Search for our test settings + const searchBox = comfyPage.page.locator('.settings-search-box input') + await searchBox.fill('Test') + await comfyPage.page.waitForTimeout(300) // Wait for debounce + + // Get all settings content + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Should only show the visible setting + await expect(settingsContent).toContainText('Test Visible Setting') + + // Should not show hidden or deprecated settings + await expect(settingsContent).not.toContainText('Test Hidden Setting') + await expect(settingsContent).not.toContainText('Test Deprecated Setting') + }) + + test('search by setting name excludes hidden and deprecated', async ({ + comfyPage + }) => { + // Open settings dialog + await comfyPage.page.keyboard.press('Control+,') + const settingsDialog = comfyPage.page.locator('.settings-container') + await expect(settingsDialog).toBeVisible() + + const searchBox = comfyPage.page.locator('.settings-search-box input') + const settingsContent = comfyPage.page.locator('.settings-tab-panels') + + // Search specifically for hidden setting by name + await searchBox.clear() + await searchBox.fill('Hidden') + await comfyPage.page.waitForTimeout(300) + + // Should not show the hidden setting even when searching by name + await expect(settingsContent).not.toContainText('Test Hidden Setting') + + // Search specifically for deprecated setting by name + await searchBox.clear() + await searchBox.fill('Deprecated') + await comfyPage.page.waitForTimeout(300) + + // Should not show the deprecated setting even when searching by name + await expect(settingsContent).not.toContainText('Test Deprecated Setting') + + // Search for visible setting by name - should work + await searchBox.clear() + await searchBox.fill('Visible') + await comfyPage.page.waitForTimeout(300) + + // Should show the visible setting + await expect(settingsContent).toContainText('Test Visible Setting') + }) +}) diff --git a/build/plugins/addElementVnodeExportPlugin.ts b/build/plugins/addElementVnodeExportPlugin.ts deleted file mode 100644 index 1266d13e2..000000000 --- a/build/plugins/addElementVnodeExportPlugin.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Plugin } from 'vite' - -/** - * Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode. - * - * This plugin addresses compatibility issues where some components or libraries - * might be using the older createElementVNode function name instead of createBaseVNode. - * It modifies the Vue vendor chunk during build to add the alias export. - * - * @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports - */ -export function addElementVnodeExportPlugin(): Plugin { - return { - name: 'add-element-vnode-export-plugin', - - renderChunk(code, chunk, _options) { - if (chunk.name.startsWith('vendor-vue')) { - const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/ - const match = code.match(exportRegex) - - if (match) { - const existingExports = match[2].trim() - const exportsArray = existingExports - .split(',') - .map((e) => e.trim()) - .filter(Boolean) - - const hasCreateBaseVNode = exportsArray.some((e) => - e.startsWith('createBaseVNode') - ) - const hasCreateElementVNode = exportsArray.some((e) => - e.includes('createElementVNode') - ) - - if (hasCreateBaseVNode && !hasCreateElementVNode) { - const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}` - const newCode = code.replace(exportRegex, newExportStatement) - - console.log( - `[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.` - ) - - return { code: newCode, map: null } - } else if (!hasCreateBaseVNode) { - console.warn( - `[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.` - ) - } - } else { - console.warn( - `[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.` - ) - } - } - - return null - } - } -} diff --git a/build/plugins/generateImportMapPlugin.ts b/build/plugins/generateImportMapPlugin.ts index c6661a811..80ccb6c9f 100644 --- a/build/plugins/generateImportMapPlugin.ts +++ b/build/plugins/generateImportMapPlugin.ts @@ -1,9 +1,24 @@ -import type { OutputOptions } from 'rollup' -import { HtmlTagDescriptor, Plugin } from 'vite' +import glob from 'fast-glob' +import fs from 'fs-extra' +import { dirname, join } from 'node:path' +import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite' -interface VendorLibrary { +interface ImportMapSource { name: string - pattern: RegExp + pattern: string | RegExp + entry: string + recursiveDependence?: boolean + override?: Record> +} + +const parseDeps = (root: string, pkg: string) => { + const pkgPath = join(root, 'node_modules', pkg, 'package.json') + if (fs.existsSync(pkgPath)) { + const content = fs.readFileSync(pkgPath, 'utf-8') + const pkg = JSON.parse(content) + return Object.keys(pkg.dependencies || {}) + } + return [] } /** @@ -23,53 +38,89 @@ interface VendorLibrary { * @returns {Plugin} A Vite plugin that generates and injects an import map */ export function generateImportMapPlugin( - vendorLibraries: VendorLibrary[] + importMapSources: ImportMapSource[] ): Plugin { const importMapEntries: Record = {} + const resolvedImportMapSources: Map = new Map() + const assetDir = 'assets/lib' + let root: string return { name: 'generate-import-map-plugin', // Configure manual chunks during the build process configResolved(config) { + root = config.root + if (config.build) { // Ensure rollupOptions exists if (!config.build.rollupOptions) { config.build.rollupOptions = {} } - const outputOptions: OutputOptions = { - manualChunks: (id: string) => { - for (const lib of vendorLibraries) { - if (lib.pattern.test(id)) { - return `vendor-${lib.name}` - } + for (const source of importMapSources) { + resolvedImportMapSources.set(source.name, source) + if (source.recursiveDependence) { + const deps = parseDeps(root, source.name) + + while (deps.length) { + const dep = deps.shift()! + const depSource = Object.assign({}, source, { + name: dep, + pattern: dep, + ...source.override?.[dep] + }) + resolvedImportMapSources.set(depSource.name, depSource) + + const _deps = parseDeps(root, depSource.name) + deps.unshift(..._deps) } - return null - }, - // Disable minification of internal exports to preserve function names - minifyInternalExports: false + } } - config.build.rollupOptions.output = outputOptions + + const external: (string | RegExp)[] = [] + for (const [, source] of resolvedImportMapSources) { + external.push(source.pattern) + } + config.build.rollupOptions.external = external } }, - generateBundle(_options, bundle) { - for (const fileName in bundle) { - const chunk = bundle[fileName] - if (chunk.type === 'chunk' && !chunk.isEntry) { - // Find matching vendor library by chunk name - const vendorLib = vendorLibraries.find( - (lib) => chunk.name === `vendor-${lib.name}` - ) + generateBundle(_options) { + for (const [, source] of resolvedImportMapSources) { + if (source.entry) { + const moduleFile = join(source.name, source.entry) + const sourceFile = join(root, 'node_modules', moduleFile) + const targetFile = join(root, 'dist', assetDir, moduleFile) - if (vendorLib) { - const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}` - importMapEntries[vendorLib.name] = relativePath + importMapEntries[source.name] = + './' + normalizePath(join(assetDir, moduleFile)) - console.log( - `[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'` - ) + const targetDir = dirname(targetFile) + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + fs.copyFileSync(sourceFile, targetFile) + } + + if (source.recursiveDependence) { + const files = glob.sync(['**/*.{js,mjs}'], { + cwd: join(root, 'node_modules', source.name) + }) + + for (const file of files) { + const moduleFile = join(source.name, file) + const sourceFile = join(root, 'node_modules', moduleFile) + const targetFile = join(root, 'dist', assetDir, moduleFile) + + importMapEntries[normalizePath(join(source.name, dirname(file)))] = + './' + normalizePath(join(assetDir, moduleFile)) + + const targetDir = dirname(targetFile) + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) + } + fs.copyFileSync(sourceFile, targetFile) } } } diff --git a/build/plugins/index.ts b/build/plugins/index.ts index c67473f7a..f8c2d695c 100644 --- a/build/plugins/index.ts +++ b/build/plugins/index.ts @@ -1,3 +1,2 @@ -export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin' export { comfyAPIPlugin } from './comfyAPIPlugin' export { generateImportMapPlugin } from './generateImportMapPlugin' diff --git a/package-lock.json b/package-lock.json index 6cae51239..b3443857d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "@comfyorg/comfyui-frontend", - "version": "1.24.0-0", + "version": "1.24.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@comfyorg/comfyui-frontend", - "version": "1.24.0-0", + "version": "1.24.0", "license": "GPL-3.0-only", "dependencies": { "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "^0.4.43", - "@comfyorg/litegraph": "^0.16.3", + "@comfyorg/litegraph": "^0.16.6", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", @@ -949,9 +949,9 @@ "license": "GPL-3.0-only" }, "node_modules/@comfyorg/litegraph": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.3.tgz", - "integrity": "sha512-dst29g8+aZW8sWTYxj3LK1W4lX07elBPWFB1L4HLTkYgkzQoyBkHR1O2lSvAn+7bKagi0Q5PjIcZnWG+JAi0lg==", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.16.6.tgz", + "integrity": "sha512-pRmJYZ39rIpGIaJAaOLicRFe3KyeNTXNAAB0+Thz8cPGpu2dBv8W6PlOu94VYNRc+pBhEwV+jJVlXb5YyAvBXQ==", "license": "MIT" }, "node_modules/@cspotcode/source-map-support": { diff --git a/package.json b/package.json index ba674deb4..3993a8697 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.24.0-0", + "version": "1.24.0", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -77,7 +77,7 @@ "@alloc/quick-lru": "^5.2.0", "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "^0.4.43", - "@comfyorg/litegraph": "^0.16.3", + "@comfyorg/litegraph": "^0.16.6", "@primevue/forms": "^4.2.5", "@primevue/themes": "^4.2.5", "@sentry/vue": "^8.48.0", diff --git a/scripts/check-unused-i18n-keys.ts b/scripts/check-unused-i18n-keys.ts new file mode 100755 index 000000000..f459b8c23 --- /dev/null +++ b/scripts/check-unused-i18n-keys.ts @@ -0,0 +1,192 @@ +#!/usr/bin/env tsx +import { execSync } from 'child_process' +import * as fs from 'fs' +import { globSync } from 'glob' + +interface LocaleData { + [key: string]: any +} + +// Configuration +const SOURCE_PATTERNS = ['src/**/*.{js,ts,vue}', '!src/locales/**/*'] +const IGNORE_PATTERNS = [ + // Keys that might be dynamically constructed + /^commands\./, // Command definitions are loaded dynamically + /^settings\..*\.options\./, // Setting options are rendered dynamically + /^nodeDefs\./, // Node definitions are loaded from backend + /^templateWorkflows\./, // Template workflows are loaded dynamically + /^dataTypes\./, // Data types might be referenced dynamically + /^contextMenu\./, // Context menu items might be dynamic + /^color\./, // Color names might be used dynamically + // Auto-generated categories from collect-i18n-general.ts + /^menuLabels\./, // Menu labels generated from command labels + /^settingsCategories\./, // Settings categories generated from setting definitions + /^serverConfigItems\./, // Server config items generated from SERVER_CONFIG_ITEMS + /^serverConfigCategories\./, // Server config categories generated from config categories + /^nodeCategories\./, // Node categories generated from node definitions + // Setting option values that are dynamically generated + /\.options\./ // All setting options are rendered dynamically +] + +// Get list of staged locale files +function getStagedLocaleFiles(): string[] { + try { + const output = execSync('git diff --cached --name-only --diff-filter=AM', { + encoding: 'utf-8' + }) + return output + .split('\n') + .filter( + (file) => file.startsWith('src/locales/') && file.endsWith('.json') + ) + } catch { + return [] + } +} + +// Extract all keys from a nested object +function extractKeys(obj: any, prefix = ''): string[] { + const keys: string[] = [] + + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + keys.push(...extractKeys(value, fullKey)) + } else { + keys.push(fullKey) + } + } + + return keys +} + +// Get new keys added in staged files +function getNewKeysFromStagedFiles(stagedFiles: string[]): Set { + const newKeys = new Set() + + for (const file of stagedFiles) { + try { + // Get the staged content + const stagedContent = execSync(`git show :${file}`, { encoding: 'utf-8' }) + const stagedData: LocaleData = JSON.parse(stagedContent) + const stagedKeys = new Set(extractKeys(stagedData)) + + // Get the current HEAD content (if file exists) + let headKeys = new Set() + try { + const headContent = execSync(`git show HEAD:${file}`, { + encoding: 'utf-8' + }) + const headData: LocaleData = JSON.parse(headContent) + headKeys = new Set(extractKeys(headData)) + } catch { + // File is new, all keys are new + } + + // Find keys that are in staged but not in HEAD + stagedKeys.forEach((key) => { + if (!headKeys.has(key)) { + newKeys.add(key) + } + }) + } catch (error) { + console.error(`Error processing ${file}:`, error) + } + } + + return newKeys +} + +// Check if a key should be ignored +function shouldIgnoreKey(key: string): boolean { + return IGNORE_PATTERNS.some((pattern) => pattern.test(key)) +} + +// Search for key usage in source files +function isKeyUsed(key: string, sourceFiles: string[]): boolean { + // Escape special regex characters + const escapeRegex = (str: string) => + str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const escapedKey = escapeRegex(key) + const lastPart = key.split('.').pop() + const escapedLastPart = lastPart ? escapeRegex(lastPart) : '' + + // Common patterns for i18n key usage + const patterns = [ + // Direct usage: $t('key'), t('key'), i18n.t('key') + new RegExp(`[t$]\\s*\\(\\s*['"\`]${escapedKey}['"\`]`, 'g'), + // With namespace: $t('g.key'), t('namespace.key') + new RegExp(`[t$]\\s*\\(\\s*['"\`][^'"]+\\.${escapedLastPart}['"\`]`, 'g'), + // Dynamic keys might reference parts of the key + new RegExp(`['"\`]${escapedKey}['"\`]`, 'g') + ] + + for (const file of sourceFiles) { + const content = fs.readFileSync(file, 'utf-8') + + for (const pattern of patterns) { + if (pattern.test(content)) { + return true + } + } + } + + return false +} + +// Main function +async function checkNewUnusedKeys() { + const stagedLocaleFiles = getStagedLocaleFiles() + + if (stagedLocaleFiles.length === 0) { + // No locale files staged, nothing to check + process.exit(0) + } + + // Get all new keys from staged files + const newKeys = getNewKeysFromStagedFiles(stagedLocaleFiles) + + if (newKeys.size === 0) { + // Silent success - no output needed + process.exit(0) + } + + // Get all source files + const sourceFiles = globSync(SOURCE_PATTERNS) + + // Check each new key + const unusedNewKeys: string[] = [] + + newKeys.forEach((key) => { + if (!shouldIgnoreKey(key) && !isKeyUsed(key, sourceFiles)) { + unusedNewKeys.push(key) + } + }) + + // Report results + if (unusedNewKeys.length > 0) { + console.log('\n⚠️ Warning: Found unused NEW i18n keys:\n') + + for (const key of unusedNewKeys.sort()) { + console.log(` - ${key}`) + } + + console.log(`\n✨ Total unused new keys: ${unusedNewKeys.length}`) + console.log( + '\nThese keys were added but are not used anywhere in the codebase.' + ) + console.log('Consider using them or removing them in a future update.') + + // Changed from process.exit(1) to process.exit(0) for warning only + process.exit(0) + } else { + // Silent success - no output needed + } +} + +// Run the check +checkNewUnusedKeys().catch((err) => { + console.error('Error checking unused keys:', err) + process.exit(1) +}) diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 4189c02a2..c6aa2aea7 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -77,6 +77,7 @@ import { app as comfyApp } from '@/scripts/app' import { ChangeTracker } from '@/scripts/changeTracker' import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets' import { useColorPaletteService } from '@/services/colorPaletteService' +import { newUserService } from '@/services/newUserService' import { useWorkflowService } from '@/services/workflowService' import { useCommandStore } from '@/stores/commandStore' import { useExecutionStore } from '@/stores/executionStore' @@ -305,6 +306,9 @@ onMounted(async () => { CORE_SETTINGS.forEach((setting) => { settingStore.addSetting(setting) }) + + await newUserService().initializeIfNewUser(settingStore) + // @ts-expect-error fixme ts strict error await comfyApp.setup(canvasRef.value) canvasStore.canvas = comfyApp.canvas diff --git a/src/components/helpcenter/HelpCenterMenuContent.vue b/src/components/helpcenter/HelpCenterMenuContent.vue index a7e180529..45a6da119 100644 --- a/src/components/helpcenter/HelpCenterMenuContent.vue +++ b/src/components/helpcenter/HelpCenterMenuContent.vue @@ -4,6 +4,7 @@