Compare commits
6 Commits
feature/as
...
bl-selecti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd20a815e | ||
|
|
9616b91700 | ||
|
|
939d1a0e44 | ||
|
|
df6723415b | ||
|
|
83ff415815 | ||
|
|
87d3111d5c |
@@ -458,15 +458,15 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
|
||||
```bash
|
||||
sleep 10
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
```
|
||||
4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
|
||||
4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
|
||||
- Create a `core/x.yy` branch for the PREVIOUS minor version
|
||||
- Apply branch protection rules
|
||||
- Document the feature freeze policy
|
||||
```bash
|
||||
# Monitor branch creation (for minor/major releases)
|
||||
gh run list --workflow=release-branch-create.yaml --limit=1
|
||||
gh run list --workflow=create-release-candidate-branch.yaml --limit=1
|
||||
```
|
||||
4. If workflow didn't trigger due to [skip ci]:
|
||||
```bash
|
||||
@@ -477,7 +477,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
5. If workflow triggered, monitor execution:
|
||||
```bash
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
gh run watch ${WORKFLOW_RUN_ID}
|
||||
```
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ For each commit:
|
||||
3. Merge the PR: `gh pr merge --merge`
|
||||
4. Monitor release workflow:
|
||||
```bash
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run watch
|
||||
```
|
||||
5. Track progress:
|
||||
|
||||
21
.github/workflows/README.md
vendored
@@ -1,21 +0,0 @@
|
||||
# GitHub Workflows
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Workflow files follow a consistent naming pattern: `<prefix>-<descriptive-name>.yaml`
|
||||
|
||||
### Category Prefixes
|
||||
|
||||
| Prefix | Purpose | Example |
|
||||
| ---------- | ----------------------------------- | ------------------------------------ |
|
||||
| `ci-` | Testing, linting, validation | `ci-tests-e2e.yaml` |
|
||||
| `release-` | Version management, publishing | `release-version-bump.yaml` |
|
||||
| `pr-` | PR automation (triggered by labels) | `pr-claude-review.yaml` |
|
||||
| `api-` | External Api type generation | `api-update-registry-api-types.yaml` |
|
||||
| `i18n-` | Internationalization updates | `i18n-update-core.yaml` |
|
||||
|
||||
## Documentation
|
||||
|
||||
Each workflow file contains comments explaining its purpose, triggers, and behavior. For specific details about what each workflow does, refer to the comments at the top of each `.yaml` file.
|
||||
|
||||
For GitHub Actions documentation, see [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).
|
||||
@@ -1,4 +1,4 @@
|
||||
name: PR Backport
|
||||
name: Auto Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -95,61 +95,41 @@ jobs:
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||
|
||||
- name: Collect backport targets
|
||||
- name: Extract version labels
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: targets
|
||||
id: versions
|
||||
run: |
|
||||
TARGETS=()
|
||||
declare -A SEEN=()
|
||||
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# For manual triggers, get labels from the PR
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
# For automatic triggers, extract from PR event
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
add_target() {
|
||||
local label="$1"
|
||||
local target="$2"
|
||||
|
||||
if [ -z "$target" ]; then
|
||||
return
|
||||
|
||||
for label in $LABELS; do
|
||||
# Match version labels like "1.24" (major.minor only)
|
||||
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# Validate the branch exists before adding to list
|
||||
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
|
||||
VERSIONS="${VERSIONS}${label} "
|
||||
else
|
||||
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
target=$(echo "$target" | xargs)
|
||||
|
||||
if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
|
||||
TARGETS+=("$target")
|
||||
SEEN["$target"]=1
|
||||
else
|
||||
echo "::warning::Label '${label}' references missing branch '${target}'"
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS= read -r label; do
|
||||
[ -z "$label" ] && continue
|
||||
|
||||
if [[ "$label" =~ ^branch:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
add_target "$label" "core/${label}"
|
||||
fi
|
||||
done <<< "$LABELS"
|
||||
|
||||
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
||||
echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
|
||||
if [ -z "$VERSIONS" ]; then
|
||||
echo "::error::No version labels found (e.g., 1.24, 1.22)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
|
||||
echo "Found backport targets: ${TARGETS[*]}"
|
||||
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
|
||||
echo "Found version labels: ${VERSIONS}"
|
||||
|
||||
- name: Backport commits
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
@@ -170,17 +150,16 @@ jobs:
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for target in ${{ steps.targets.outputs.targets }}; do
|
||||
TARGET_BRANCH="${target}"
|
||||
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
|
||||
echo "::group::Backporting to ${TARGET_BRANCH}"
|
||||
TARGET_BRANCH="core/${version}"
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
|
||||
|
||||
# Fetch target branch (fail if doesn't exist)
|
||||
if ! git fetch origin "${TARGET_BRANCH}"; then
|
||||
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
|
||||
FAILED="${FAILED}${version}:branch-missing "
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
@@ -191,7 +170,7 @@ jobs:
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
git checkout main
|
||||
@@ -201,7 +180,7 @@ jobs:
|
||||
git cherry-pick --abort
|
||||
|
||||
echo "::error::Cherry-pick failed due to conflicts"
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
|
||||
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
|
||||
|
||||
# Clean up the failed branch
|
||||
git checkout main
|
||||
@@ -235,13 +214,13 @@ jobs:
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r target branch <<< "${backport}"
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
|
||||
if PR_URL=$(gh pr create \
|
||||
--base "${target}" \
|
||||
--base "core/${version}" \
|
||||
--head "${branch}" \
|
||||
--title "[backport ${target}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--title "[backport ${version}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--label "backport" 2>&1); then
|
||||
|
||||
# Extract PR number from URL
|
||||
@@ -251,9 +230,9 @@ jobs:
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
|
||||
fi
|
||||
else
|
||||
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
|
||||
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
|
||||
# Still try to comment on the original PR about the failure
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -274,16 +253,16 @@ jobs:
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r target reason conflicts <<< "${failure}"
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
|
||||
if [ "${reason}" = "branch-missing" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
|
||||
|
||||
elif [ "${reason}" = "conflicts" ]; then
|
||||
# Convert comma-separated conflicts back to newlines for display
|
||||
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
|
||||
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
||||
fi
|
||||
done
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "PR: Claude Review"
|
||||
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
|
||||
name: Claude PR Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release PyPI Dev
|
||||
name: Create Dev PyPI Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -25,6 +25,17 @@ jobs:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
dist
|
||||
tsconfig.tsbuildinfo
|
||||
key: dev-release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
dev-release-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
@@ -33,7 +44,6 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release Branch Create
|
||||
name: Create Release Branch
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release Draft Create
|
||||
name: Create Release Draft
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -28,6 +28,16 @@ jobs:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
tsconfig.tsbuildinfo
|
||||
key: release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
release-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
@@ -45,7 +55,6 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
@@ -117,7 +126,7 @@ jobs:
|
||||
|
||||
publish_types:
|
||||
needs: build
|
||||
uses: ./.github/workflows/release-npm-types.yaml
|
||||
uses: ./.github/workflows/publish-frontend-types.yaml
|
||||
with:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "CI: Python Validation"
|
||||
description: "Validates Python code in tools/devtools directory"
|
||||
name: Devtools Python Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "CI: Lint Format"
|
||||
description: "Linting and code formatting validation for pull requests"
|
||||
name: Lint and Format
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -33,6 +32,21 @@ jobs:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
.prettierCache
|
||||
.knip-cache
|
||||
key: lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js,mts}', '*.config.*', '.eslintrc.*', '.prettierrc.*', 'tsconfig.json') }}
|
||||
restore-keys: |
|
||||
lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
lint-format-cache-${{ runner.os }}-
|
||||
ci-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
name: "CI: Tests E2E (Deploy for Forks)"
|
||||
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
|
||||
name: PR Playwright Deploy (Forks)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI: Tests E2E"]
|
||||
workflows: ["Tests CI"]
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
@@ -1,9 +1,8 @@
|
||||
name: "CI: Tests Storybook (Deploy for Forks)"
|
||||
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
|
||||
name: PR Storybook Deploy (Forks)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI: Tests Storybook"]
|
||||
workflows: ['Storybook and Chromatic CI']
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
@@ -1,108 +0,0 @@
|
||||
# Setting test expectation screenshots for Playwright
|
||||
name: "PR: Update Playwright Expectations"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
( github.event_name == 'pull_request' && github.event.label.name == 'New Browser Test Expectations' ) ||
|
||||
( github.event.issue.pull_request &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'
|
||||
) &&
|
||||
startsWith(github.event.comment.body, '/update-playwright') )
|
||||
steps:
|
||||
- name: Find Update Comment
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
|
||||
id: "find-update-comment"
|
||||
with:
|
||||
issue-number: ${{ github.event.number || github.event.issue.number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: "Updating Playwright Expectations"
|
||||
|
||||
- name: Add Starting Reaction
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
|
||||
with:
|
||||
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.number || github.event.issue.number }}
|
||||
body: |
|
||||
Updating Playwright Expectations
|
||||
edit-mode: replace
|
||||
reactions: eyes
|
||||
|
||||
- name: Get Branch SHA
|
||||
id: "get-branch"
|
||||
run: echo ::set-output name=branch::$(gh pr view $PR_NO --repo $REPO --json headRefName --jq '.headRefName')
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NO: ${{ github.event.number || github.event.issue.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Initial Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ steps.get-branch.outputs.branch }}
|
||||
- name: Setup Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup ComfyUI Server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Run Playwright tests and update snapshots
|
||||
id: playwright-tests
|
||||
run: pnpm exec playwright test --update-snapshots
|
||||
continue-on-error: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
- name: Debugging info
|
||||
run: |
|
||||
echo "PR: ${{ github.event.issue.number }}"
|
||||
echo "Branch: ${{ steps.get-branch.outputs.branch }}"
|
||||
git status
|
||||
- name: Commit updated expectations
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git add browser_tests
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "[automated] Update test expectations"
|
||||
git push origin ${{ steps.get-branch.outputs.branch }}
|
||||
fi
|
||||
|
||||
- name: Add Done Reaction
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
|
||||
if: github.event_name == 'issue_comment'
|
||||
with:
|
||||
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.number || github.event.issue.number }}
|
||||
reactions: +1
|
||||
reactions-edit-mode: replace
|
||||
|
||||
- name: Remove New Browser Test Expectations label
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "New Browser Test Expectations"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
15
.github/workflows/publish-desktop-ui.yaml
vendored
@@ -44,7 +44,6 @@ jobs:
|
||||
contents: read
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ENABLE_MINIFY: 'true'
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
env:
|
||||
@@ -161,6 +160,20 @@ jobs:
|
||||
echo "publish_dir=$PUBLISH_DIR" >> "$GITHUB_OUTPUT"
|
||||
echo "name=$NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Pack (preview only)
|
||||
shell: bash
|
||||
working-directory: ${{ steps.pkg.outputs.publish_dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm pack --json | tee pack-result.json
|
||||
|
||||
- name: Upload package tarball artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-ui-npm-tarball-${{ inputs.version }}
|
||||
path: ${{ steps.pkg.outputs.publish_dir }}/*.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Check if version already on npm
|
||||
id: check_npm
|
||||
env:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release NPM Types
|
||||
name: Publish Frontend Types
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
52
.github/workflows/size-data.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: size data
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
collect:
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Collect size data
|
||||
run: node scripts/size-collect.js
|
||||
|
||||
- name: Save PR number & base branch
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
echo ${{ github.event.number }} > ./temp/size/number.txt
|
||||
echo ${{ github.base_ref }} > ./temp/size/base.txt
|
||||
|
||||
- name: Upload size data
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: size-data
|
||||
path: temp/size
|
||||
104
.github/workflows/size-report.yml
vendored
@@ -1,104 +0,0 @@
|
||||
name: size report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['size data']
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to report on'
|
||||
required: true
|
||||
type: number
|
||||
run_id:
|
||||
description: 'Size data workflow run ID'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size-report:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
(
|
||||
(github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success') ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
)
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
name: size-data
|
||||
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
|
||||
path: temp/size
|
||||
|
||||
- name: Set PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set base branch
|
||||
id: pr-base
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=main" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download previous size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
branch: ${{ steps.pr-base.outputs.content }}
|
||||
workflow: size-data.yml
|
||||
event: push
|
||||
name: size-data
|
||||
path: temp/size-prev
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Generate size report
|
||||
run: node scripts/size-report.js > size-report.md
|
||||
|
||||
- name: Read size report
|
||||
id: size-report
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./size-report.md
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: actions-cool/maintain-one-comment@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ steps.pr-number.outputs.content }}
|
||||
body: |
|
||||
${{ steps.size-report.outputs.content }}
|
||||
<!-- COMFYUI_FRONTEND_SIZE -->
|
||||
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'
|
||||
@@ -1,5 +1,6 @@
|
||||
name: "CI: Tests Storybook"
|
||||
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
|
||||
name: Storybook and Chromatic CI
|
||||
|
||||
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
@@ -50,6 +51,19 @@ jobs:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
storybook-static
|
||||
tsconfig.tsbuildinfo
|
||||
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
|
||||
restore-keys: |
|
||||
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
storybook-cache-${{ runner.os }}-
|
||||
storybook-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -102,6 +116,19 @@ jobs:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
storybook-static
|
||||
tsconfig.tsbuildinfo
|
||||
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
|
||||
restore-keys: |
|
||||
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
storybook-cache-${{ runner.os }}-
|
||||
storybook-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "CI: Tests E2E"
|
||||
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
|
||||
name: Tests CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -143,7 +142,7 @@ jobs:
|
||||
merge-reports:
|
||||
needs: [playwright-tests-chromium-sharded]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !cancelled() }}
|
||||
if: ${{ always() && !cancelled() }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
@@ -169,6 +168,26 @@ jobs:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
|
||||
|
||||
- name: Build failed screenshot manifest
|
||||
if: ${{ needs.playwright-tests-chromium-sharded.result == 'failure' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! pnpm tsx scripts/cicd/build-failed-screenshot-manifest.ts; then
|
||||
echo "ERROR: Failed to generate screenshot manifest"
|
||||
echo "This may indicate an issue with the Playwright JSON report or the manifest script"
|
||||
exit 1
|
||||
fi
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Upload failed screenshot manifest
|
||||
if: ${{ needs.playwright-tests-chromium-sharded.result == 'failure' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: failed-screenshot-tests
|
||||
path: ComfyUI_frontend/ci-rerun/*.txt
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -1,5 +1,4 @@
|
||||
name: 'Api: Update Registry API Types'
|
||||
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
name: Update Comfy Registry API Types
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
@@ -30,9 +29,26 @@ jobs:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
key: update-registry-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
update-registry-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Cache comfy-api repository
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: comfy-api
|
||||
key: comfy-api-repo-${{ runner.os }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
comfy-api-repo-${{ runner.os }}-
|
||||
|
||||
- name: Checkout comfy-api repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -1,5 +1,4 @@
|
||||
name: 'Api: Update Manager API Types'
|
||||
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
name: Update ComfyUI-Manager API Types
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
@@ -31,9 +30,26 @@ jobs:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
key: update-manager-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
update-manager-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Cache ComfyUI-Manager repository
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ComfyUI-Manager
|
||||
key: comfyui-manager-repo-${{ runner.os }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
comfyui-manager-repo-${{ runner.os }}-
|
||||
|
||||
- name: Checkout ComfyUI-Manager repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -1,5 +1,4 @@
|
||||
name: 'Api: Update Electron API Types'
|
||||
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
name: Update Electron Types
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -26,6 +25,15 @@ jobs:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
key: electron-types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
electron-types-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Update electron types
|
||||
run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: i18n Update Custom Nodes
|
||||
name: Update Locales for given custom node repository
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "i18n: Update Core"
|
||||
description: "Generates and updates translations for core ComfyUI components using OpenAI"
|
||||
name: Update Locales
|
||||
|
||||
on:
|
||||
# Manual dispatch for urgent translation updates
|
||||
@@ -55,5 +54,5 @@ jobs:
|
||||
# Apply the stashed changes if any
|
||||
git stash pop || true
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales"
|
||||
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: i18n Update Nodes
|
||||
name: Update Node Definitions Locales
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
284
.github/workflows/update-playwright-expectations.yaml
vendored
Normal file
@@ -0,0 +1,284 @@
|
||||
# Setting test expectation screenshots for Playwright
|
||||
#
|
||||
# This workflow uses a selective snapshot update strategy:
|
||||
# 1. When tests fail in CI, they generate a manifest of failed test locations (file:line)
|
||||
# 2. This workflow downloads that manifest from the failed test run artifacts
|
||||
# 3. Only the failed tests are re-run with --update-snapshots (much faster than running all tests)
|
||||
# 4. Updated snapshots are committed back to the PR branch
|
||||
#
|
||||
# Trigger: Add label "New Browser Test Expectations" OR comment "/update-playwright" on PR
|
||||
name: Update Playwright Expectations
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
( github.event_name == 'pull_request' && github.event.label.name == 'New Browser Test Expectations' ) ||
|
||||
( github.event.issue.pull_request &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.author_association == 'OWNER' ||
|
||||
github.event.comment.author_association == 'MEMBER' ||
|
||||
github.event.comment.author_association == 'COLLABORATOR'
|
||||
) &&
|
||||
startsWith(github.event.comment.body, '/update-playwright') )
|
||||
steps:
|
||||
- name: Find Update Comment
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
|
||||
id: "find-update-comment"
|
||||
with:
|
||||
issue-number: ${{ github.event.number || github.event.issue.number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: "Updating Playwright Expectations"
|
||||
|
||||
- name: Add Starting Reaction
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
|
||||
with:
|
||||
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.number || github.event.issue.number }}
|
||||
body: |
|
||||
Updating Playwright Expectations
|
||||
edit-mode: replace
|
||||
reactions: eyes
|
||||
|
||||
- name: Get Branch SHA
|
||||
id: "get-branch"
|
||||
run: echo ::set-output name=branch::$(gh pr view $PR_NO --repo $REPO --json headRefName --jq '.headRefName')
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
PR_NO: ${{ github.event.number || github.event.issue.number }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Initial Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ steps.get-branch.outputs.branch }}
|
||||
|
||||
- name: Setup Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
|
||||
- name: Setup ComfyUI Server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
|
||||
- name: Locate failed screenshot manifest artifact
|
||||
id: locate-manifest
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo
|
||||
let headSha = ''
|
||||
if (context.eventName === 'pull_request') {
|
||||
headSha = context.payload.pull_request.head.sha
|
||||
} else if (context.eventName === 'issue_comment') {
|
||||
const prNumber = context.payload.issue.number
|
||||
const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber })
|
||||
headSha = pr.data.head.sha
|
||||
}
|
||||
|
||||
if (!headSha) {
|
||||
core.setOutput('run_id', '')
|
||||
core.setOutput('has_manifest', 'false')
|
||||
return
|
||||
}
|
||||
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: 'tests-ci.yaml',
|
||||
head_sha: headSha,
|
||||
event: 'pull_request',
|
||||
per_page: 1,
|
||||
})
|
||||
const run = data.workflow_runs?.[0]
|
||||
|
||||
let has = 'false'
|
||||
let runId = ''
|
||||
if (run) {
|
||||
runId = String(run.id)
|
||||
const { data: { artifacts = [] } } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner,
|
||||
repo,
|
||||
run_id: run.id,
|
||||
per_page: 100,
|
||||
})
|
||||
if (artifacts.some(a => a.name === 'failed-screenshot-tests' && !a.expired)) has = 'true'
|
||||
}
|
||||
core.setOutput('run_id', runId)
|
||||
core.setOutput('has_manifest', has)
|
||||
|
||||
- name: Download failed screenshot manifest
|
||||
if: steps.locate-manifest.outputs.has_manifest == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
run-id: ${{ steps.locate-manifest.outputs.run_id }}
|
||||
name: failed-screenshot-tests
|
||||
path: ci-rerun
|
||||
|
||||
- name: Re-run failed screenshot tests and update snapshots
|
||||
id: playwright-tests
|
||||
continue-on-error: true
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Selective Snapshot Update"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
# Check if manifest exists
|
||||
if [ ! -d ci-rerun ]; then
|
||||
echo "ERROR: No manifest found in ci-rerun/ directory"
|
||||
echo " This means no failed screenshot tests were detected in the latest CI run."
|
||||
echo " Please ensure tests have been run and failures were recorded."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
files=(ci-rerun/*.txt)
|
||||
|
||||
if [ ${#files[@]} -eq 0 ]; then
|
||||
echo "ERROR: No manifest files found in ci-rerun/"
|
||||
echo " Expected files like: chromium.txt, chromium-2x.txt, mobile-chrome.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found ${#files[@]} project manifest(s):"
|
||||
for f in "${files[@]}"; do
|
||||
project="$(basename "$f" .txt)"
|
||||
count=$(grep -c . "$f" 2>/dev/null || echo "0")
|
||||
echo " - $project: $count failed test(s)"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Re-run tests per project
|
||||
total_tests=0
|
||||
for f in "${files[@]}"; do
|
||||
project="$(basename "$f" .txt)"
|
||||
mapfile -t lines < "$f"
|
||||
filtered=( )
|
||||
|
||||
# Validate and sanitize test paths to prevent command injection
|
||||
for l in "${lines[@]}"; do
|
||||
# Skip empty lines
|
||||
[ -z "$l" ] && continue
|
||||
|
||||
# Validate format: must be browser_tests/...spec.ts:number
|
||||
if [[ "$l" =~ ^browser_tests/.+\.spec\.ts:[0-9]+$ ]]; then
|
||||
filtered+=("$l")
|
||||
else
|
||||
echo "WARNING: Skipping invalid test path: $l"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#filtered[@]} -eq 0 ]; then
|
||||
echo "WARNING: Skipping $project (no valid tests in manifest)"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Updating snapshots for project: $project"
|
||||
echo " Re-running ${#filtered[@]} failed test(s)..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright test --project="$project" --update-snapshots \
|
||||
--reporter=line --reporter=html \
|
||||
"${filtered[@]}"
|
||||
|
||||
total_tests=$((total_tests + ${#filtered[@]}))
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Completed snapshot updates for $total_tests test(s)"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- name: Debugging info
|
||||
run: |
|
||||
echo "PR: ${{ github.event.issue.number }}"
|
||||
echo "Branch: ${{ steps.get-branch.outputs.branch }}"
|
||||
git status
|
||||
|
||||
- name: Commit updated expectations
|
||||
id: commit
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@github.com'
|
||||
git add browser_tests
|
||||
if git diff --cached --quiet; then
|
||||
echo "No expectation updates detected; skipping commit."
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Count changed snapshots
|
||||
changed_count=$(git diff --cached --name-only browser_tests | wc -l)
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "count=$changed_count" >> $GITHUB_OUTPUT
|
||||
|
||||
git commit -m "[automated] Update test expectations"
|
||||
git push origin ${{ steps.get-branch.outputs.branch }}
|
||||
fi
|
||||
|
||||
- name: Generate workflow summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Snapshot Update Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ steps.commit.outputs.changed }}" = "true" ]; then
|
||||
echo "**${{ steps.commit.outputs.count }} snapshot(s) updated**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "<details>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "<summary>View updated files</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
git diff HEAD~1 --name-only browser_tests 2>/dev/null || echo "No git history available" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.commit.outputs.changed }}" = "false" ]; then
|
||||
echo "No snapshot changes detected" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "WARNING: Snapshot update may have failed - check logs above" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "---" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Strategy:** Selective snapshot update (only failed tests re-run)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Add Done Reaction
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
|
||||
if: github.event_name == 'issue_comment'
|
||||
with:
|
||||
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.number || github.event.issue.number }}
|
||||
reactions: +1
|
||||
reactions-edit-mode: replace
|
||||
|
||||
- name: Remove New Browser Test Expectations label
|
||||
if: always() && github.event_name == 'pull_request'
|
||||
run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "New Browser Test Expectations"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "CI: JSON Validation"
|
||||
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
|
||||
name: Validate JSON
|
||||
|
||||
on:
|
||||
push:
|
||||
26
.github/workflows/version-bump-desktop-ui.yaml
vendored
@@ -14,11 +14,6 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version-desktop-ui:
|
||||
@@ -31,25 +26,8 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -86,10 +64,8 @@ jobs:
|
||||
title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
base: main
|
||||
labels: |
|
||||
Release
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "Release: Version Bump"
|
||||
description: "Manual workflow to increment package version with semantic versioning support"
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -15,11 +14,6 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
@@ -31,24 +25,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -82,9 +58,7 @@ jobs:
|
||||
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
base: main
|
||||
labels: |
|
||||
Release
|
||||
@@ -1,5 +1,4 @@
|
||||
name: "CI: Tests Unit"
|
||||
description: "Unit and component testing with Vitest"
|
||||
name: Vitest Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -29,6 +28,19 @@ jobs:
|
||||
node-version: "lts/*"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
coverage
|
||||
.vitest-cache
|
||||
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
|
||||
restore-keys: |
|
||||
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
vitest-cache-${{ runner.os }}-
|
||||
test-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run Knip with cache via package script
|
||||
pnpm knip 1>&2
|
||||
pnpm knip
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>ComfyUI</title>
|
||||
<title>ComfyUI Desktop</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/desktop-ui",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"tags": [
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div
|
||||
class="mx-auto grid h-[40rem] w-full max-w-3xl grid-rows-[1fr_auto_auto_1fr] select-none"
|
||||
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
|
||||
>
|
||||
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
|
||||
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||
{{ $t('install.gpuPicker.title') }}
|
||||
</h2>
|
||||
|
||||
<!-- GPU Selection buttons - takes up remaining space and centers content -->
|
||||
<div class="flex flex-1 items-center justify-center gap-8">
|
||||
<div class="flex-1 flex gap-8 justify-center items-center">
|
||||
<!-- Apple Metal / NVIDIA -->
|
||||
<HardwareOption
|
||||
v-if="platform === 'darwin'"
|
||||
:image-path="'./assets/images/apple-mps-logo.png'"
|
||||
:image-path="'/assets/images/apple-mps-logo.png'"
|
||||
placeholder-text="Apple Metal"
|
||||
subtitle="Apple Metal"
|
||||
:value="'mps'"
|
||||
@@ -21,7 +21,7 @@
|
||||
/>
|
||||
<HardwareOption
|
||||
v-else
|
||||
:image-path="'./assets/images/nvidia-logo-square.jpg'"
|
||||
:image-path="'/assets/images/nvidia-logo-square.jpg'"
|
||||
placeholder-text="NVIDIA"
|
||||
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
|
||||
:value="'nvidia'"
|
||||
@@ -47,17 +47,17 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-16 px-24 pt-12">
|
||||
<div class="pt-12 px-24 h-16">
|
||||
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
|
||||
<Tag
|
||||
:value="$t('install.gpuPicker.recommended')"
|
||||
class="rounded-full bg-neutral-300 px-2 py-[1px] text-sm font-bold text-neutral-900"
|
||||
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
|
||||
/>
|
||||
<i class="icon-[lucide--badge-check] text-lg text-neutral-300" />
|
||||
<i class="icon-[lucide--badge-check] text-neutral-300 text-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-24 text-neutral-300">
|
||||
<div class="text-neutral-300 px-24">
|
||||
<p v-show="descriptionText" class="leading-relaxed">
|
||||
{{ descriptionText }}
|
||||
</p>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
class="task-div relative grid min-h-52 max-w-48"
|
||||
class="task-div max-w-48 min-h-52 grid relative"
|
||||
:class="{ 'opacity-75': isLoading }"
|
||||
>
|
||||
<Card
|
||||
class="relative h-full max-w-48 overflow-hidden"
|
||||
class="max-w-48 relative h-full overflow-hidden"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
<i
|
||||
v-if="runner.state === 'error'"
|
||||
class="pi pi-exclamation-triangle absolute top-0 -right-14 m-2 text-red-500 opacity-15"
|
||||
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
|
||||
style="font-size: 10rem"
|
||||
/>
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="h-full w-full object-contain px-4 pt-4 opacity-25"
|
||||
class="object-contain w-full h-full opacity-25 pt-4 px-4"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
@@ -27,7 +27,7 @@
|
||||
{{ description }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="mt-1 flex gap-4">
|
||||
<div class="flex gap-4 mt-1">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
@@ -73,7 +73,7 @@ defineEmits<{
|
||||
// Bindings
|
||||
const description = computed(() =>
|
||||
runner.value.state === 'error'
|
||||
? (props.task.errorDescription ?? props.task.shortDescription)
|
||||
? props.task.errorDescription ?? props.task.shortDescription
|
||||
: props.task.shortDescription
|
||||
)
|
||||
|
||||
|
||||
@@ -1,163 +1,67 @@
|
||||
// Import only English locale eagerly as the default/fallback
|
||||
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
|
||||
// but these are properly configured in tsconfig.json and resolved by Vite at build time.
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import arCommands from '@frontend-locales/ar/commands.json' with { type: 'json' }
|
||||
import ar from '@frontend-locales/ar/main.json' with { type: 'json' }
|
||||
import arNodes from '@frontend-locales/ar/nodeDefs.json' with { type: 'json' }
|
||||
import arSettings from '@frontend-locales/ar/settings.json' with { type: 'json' }
|
||||
import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import en from '@frontend-locales/en/main.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
|
||||
import esCommands from '@frontend-locales/es/commands.json' with { type: 'json' }
|
||||
import es from '@frontend-locales/es/main.json' with { type: 'json' }
|
||||
import esNodes from '@frontend-locales/es/nodeDefs.json' with { type: 'json' }
|
||||
import esSettings from '@frontend-locales/es/settings.json' with { type: 'json' }
|
||||
import frCommands from '@frontend-locales/fr/commands.json' with { type: 'json' }
|
||||
import fr from '@frontend-locales/fr/main.json' with { type: 'json' }
|
||||
import frNodes from '@frontend-locales/fr/nodeDefs.json' with { type: 'json' }
|
||||
import frSettings from '@frontend-locales/fr/settings.json' with { type: 'json' }
|
||||
import jaCommands from '@frontend-locales/ja/commands.json' with { type: 'json' }
|
||||
import ja from '@frontend-locales/ja/main.json' with { type: 'json' }
|
||||
import jaNodes from '@frontend-locales/ja/nodeDefs.json' with { type: 'json' }
|
||||
import jaSettings from '@frontend-locales/ja/settings.json' with { type: 'json' }
|
||||
import koCommands from '@frontend-locales/ko/commands.json' with { type: 'json' }
|
||||
import ko from '@frontend-locales/ko/main.json' with { type: 'json' }
|
||||
import koNodes from '@frontend-locales/ko/nodeDefs.json' with { type: 'json' }
|
||||
import koSettings from '@frontend-locales/ko/settings.json' with { type: 'json' }
|
||||
import ruCommands from '@frontend-locales/ru/commands.json' with { type: 'json' }
|
||||
import ru from '@frontend-locales/ru/main.json' with { type: 'json' }
|
||||
import ruNodes from '@frontend-locales/ru/nodeDefs.json' with { type: 'json' }
|
||||
import ruSettings from '@frontend-locales/ru/settings.json' with { type: 'json' }
|
||||
import trCommands from '@frontend-locales/tr/commands.json' with { type: 'json' }
|
||||
import tr from '@frontend-locales/tr/main.json' with { type: 'json' }
|
||||
import trNodes from '@frontend-locales/tr/nodeDefs.json' with { type: 'json' }
|
||||
import trSettings from '@frontend-locales/tr/settings.json' with { type: 'json' }
|
||||
import zhTWCommands from '@frontend-locales/zh-TW/commands.json' with { type: 'json' }
|
||||
import zhTW from '@frontend-locales/zh-TW/main.json' with { type: 'json' }
|
||||
import zhTWNodes from '@frontend-locales/zh-TW/nodeDefs.json' with { type: 'json' }
|
||||
import zhTWSettings from '@frontend-locales/zh-TW/settings.json' with { type: 'json' }
|
||||
import zhCommands from '@frontend-locales/zh/commands.json' with { type: 'json' }
|
||||
import zh from '@frontend-locales/zh/main.json' with { type: 'json' }
|
||||
import zhNodes from '@frontend-locales/zh/nodeDefs.json' with { type: 'json' }
|
||||
import zhSettings from '@frontend-locales/zh/settings.json' with { type: 'json' }
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
function buildLocale<
|
||||
M extends Record<string, unknown>,
|
||||
N extends Record<string, unknown>,
|
||||
C extends Record<string, unknown>,
|
||||
S extends Record<string, unknown>
|
||||
>(main: M, nodes: N, commands: C, settings: S) {
|
||||
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
|
||||
return {
|
||||
...main,
|
||||
nodeDefs: nodes,
|
||||
commands: commands,
|
||||
settings: settings
|
||||
} as M & { nodeDefs: N; commands: C; settings: S }
|
||||
}
|
||||
|
||||
// Locale loader map - dynamically import locales only when needed
|
||||
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
|
||||
/* eslint-disable import-x/no-unresolved */
|
||||
const localeLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/main.json'),
|
||||
es: () => import('@frontend-locales/es/main.json'),
|
||||
fr: () => import('@frontend-locales/fr/main.json'),
|
||||
ja: () => import('@frontend-locales/ja/main.json'),
|
||||
ko: () => import('@frontend-locales/ko/main.json'),
|
||||
ru: () => import('@frontend-locales/ru/main.json'),
|
||||
tr: () => import('@frontend-locales/tr/main.json'),
|
||||
zh: () => import('@frontend-locales/zh/main.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
|
||||
}
|
||||
|
||||
const nodeDefsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/nodeDefs.json'),
|
||||
es: () => import('@frontend-locales/es/nodeDefs.json'),
|
||||
fr: () => import('@frontend-locales/fr/nodeDefs.json'),
|
||||
ja: () => import('@frontend-locales/ja/nodeDefs.json'),
|
||||
ko: () => import('@frontend-locales/ko/nodeDefs.json'),
|
||||
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
|
||||
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
|
||||
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
|
||||
}
|
||||
|
||||
const commandsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/commands.json'),
|
||||
es: () => import('@frontend-locales/es/commands.json'),
|
||||
fr: () => import('@frontend-locales/fr/commands.json'),
|
||||
ja: () => import('@frontend-locales/ja/commands.json'),
|
||||
ko: () => import('@frontend-locales/ko/commands.json'),
|
||||
ru: () => import('@frontend-locales/ru/commands.json'),
|
||||
tr: () => import('@frontend-locales/tr/commands.json'),
|
||||
zh: () => import('@frontend-locales/zh/commands.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
|
||||
}
|
||||
|
||||
const settingsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/settings.json'),
|
||||
es: () => import('@frontend-locales/es/settings.json'),
|
||||
fr: () => import('@frontend-locales/fr/settings.json'),
|
||||
ja: () => import('@frontend-locales/ja/settings.json'),
|
||||
ko: () => import('@frontend-locales/ko/settings.json'),
|
||||
ru: () => import('@frontend-locales/ru/settings.json'),
|
||||
tr: () => import('@frontend-locales/tr/settings.json'),
|
||||
zh: () => import('@frontend-locales/zh/settings.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
|
||||
}
|
||||
|
||||
// Track which locales have been loaded
|
||||
const loadedLocales = new Set<string>(['en'])
|
||||
|
||||
// Track locales currently being loaded to prevent race conditions
|
||||
const loadingLocales = new Map<string, Promise<void>>()
|
||||
|
||||
/**
|
||||
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
|
||||
*/
|
||||
export async function loadLocale(locale: string): Promise<void> {
|
||||
if (loadedLocales.has(locale)) {
|
||||
return
|
||||
}
|
||||
|
||||
// If already loading, return the existing promise to prevent duplicate loads
|
||||
const existingLoad = loadingLocales.get(locale)
|
||||
if (existingLoad) {
|
||||
return existingLoad
|
||||
}
|
||||
|
||||
const loader = localeLoaders[locale]
|
||||
const nodeDefsLoader = nodeDefsLoaders[locale]
|
||||
const commandsLoader = commandsLoaders[locale]
|
||||
const settingsLoader = settingsLoaders[locale]
|
||||
|
||||
if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) {
|
||||
console.warn(`Locale "${locale}" is not supported`)
|
||||
return
|
||||
}
|
||||
|
||||
// Create and track the loading promise
|
||||
const loadPromise = (async () => {
|
||||
try {
|
||||
const [main, nodes, commands, settings] = await Promise.all([
|
||||
loader(),
|
||||
nodeDefsLoader(),
|
||||
commandsLoader(),
|
||||
settingsLoader()
|
||||
])
|
||||
|
||||
const messages = buildLocale(
|
||||
main.default,
|
||||
nodes.default,
|
||||
commands.default,
|
||||
settings.default
|
||||
)
|
||||
|
||||
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
|
||||
loadedLocales.add(locale)
|
||||
} catch (error) {
|
||||
console.error(`Failed to load locale "${locale}":`, error)
|
||||
throw error
|
||||
} finally {
|
||||
// Clean up the loading promise once complete
|
||||
loadingLocales.delete(locale)
|
||||
}
|
||||
})()
|
||||
|
||||
loadingLocales.set(locale, loadPromise)
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
// Only include English in the initial bundle
|
||||
const messages = {
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings)
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings),
|
||||
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
|
||||
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
|
||||
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
|
||||
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
|
||||
ko: buildLocale(ko, koNodes, koCommands, koSettings),
|
||||
fr: buildLocale(fr, frNodes, frCommands, frSettings),
|
||||
es: buildLocale(es, esNodes, esCommands, esSettings),
|
||||
ar: buildLocale(ar, arNodes, arCommands, arSettings),
|
||||
tr: buildLocale(tr, trNodes, trCommands, trSettings)
|
||||
}
|
||||
|
||||
// Type for locale messages - inferred from the English locale structure
|
||||
type LocaleMessages = typeof messages.en
|
||||
|
||||
export const i18n = createI18n({
|
||||
// Must set `false`, as Vue I18n Legacy API is for Vue 2
|
||||
legacy: false,
|
||||
|
||||
@@ -66,6 +66,17 @@
|
||||
@click="troubleshoot"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button
|
||||
v-if="!terminalVisible"
|
||||
class="text-sm text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-2 mx-auto"
|
||||
@click="terminalVisible = true"
|
||||
>
|
||||
<i class="pi pi-search"></i>
|
||||
{{ $t('serverStart.showTerminal') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
|
||||
|
||||
@@ -46,10 +46,6 @@ class ComfyMenu {
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
return this.sideToolbar.locator('.side-bar-button')
|
||||
}
|
||||
|
||||
get nodeLibraryTab() {
|
||||
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
|
||||
return this._nodeLibraryTab
|
||||
|
||||
@@ -7,7 +7,7 @@ export class Topbar {
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.menuLocator = page.locator('.comfy-command-menu')
|
||||
this.menuTrigger = page.locator('.comfy-menu-button-wrapper')
|
||||
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
|
||||
}
|
||||
|
||||
async getTabNames(): Promise<string[]> {
|
||||
@@ -105,7 +105,7 @@ export class Topbar {
|
||||
* Close the topbar menu by clicking outside
|
||||
*/
|
||||
async closeTopbarMenu() {
|
||||
await this.page.locator('body').click({ position: { x: 300, y: 10 } })
|
||||
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||
await expect(this.menuLocator).not.toBeVisible()
|
||||
}
|
||||
|
||||
|
||||
@@ -116,10 +116,9 @@ test.describe('Actionbar', () => {
|
||||
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
|
||||
await comfyPage.page.dragAndDrop(
|
||||
'.actionbar .drag-handle',
|
||||
'.actionbar-container',
|
||||
'.comfyui-menu',
|
||||
{
|
||||
targetPosition: { x: 50, y: 20 },
|
||||
force: true
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
}
|
||||
)
|
||||
expect(await comfyPage.actionbar.isDocked()).toBe(true)
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 88 KiB |
@@ -39,15 +39,15 @@ test.describe('Graph Canvas Menu', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('Toggle minimap button is clickable and has correct test id', async ({
|
||||
test('Focus mode button is clickable and has correct test id', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
await expect(minimapButton).toBeVisible()
|
||||
await expect(minimapButton).toBeEnabled()
|
||||
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
|
||||
await expect(focusButton).toBeVisible()
|
||||
await expect(focusButton).toBeEnabled()
|
||||
|
||||
// Test that the button can be clicked without error
|
||||
await minimapButton.click()
|
||||
await focusButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 100 KiB |
@@ -233,7 +233,6 @@ test.describe('Group Node', () => {
|
||||
}
|
||||
|
||||
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
|
||||
.getFolder(GROUP_NODE_CATEGORY)
|
||||
.count()
|
||||
@@ -254,6 +253,8 @@ test.describe('Group Node', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.loadWorkflow(WORKFLOW_NAME)
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
|
||||
groupNode = await comfyPage.getFirstNodeRef()
|
||||
if (!groupNode)
|
||||
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
|
||||
|
||||
@@ -3,10 +3,10 @@ import { expect } from '@playwright/test'
|
||||
import type { Position } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
testComfySnapToGridGridSize
|
||||
} from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -786,25 +786,24 @@ test.describe('Viewport settings', () => {
|
||||
// Screenshot the canvas element
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||
|
||||
// Open zoom controls dropdown first
|
||||
const zoomControlsButton = comfyPage.page.getByTestId(
|
||||
'zoom-controls-button'
|
||||
)
|
||||
await zoomControlsButton.click()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
await toggleButton.click()
|
||||
// close zoom menu
|
||||
await zoomControlsButton.click()
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
|
||||
await comfyPage.nextFrame()
|
||||
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
|
||||
|
||||
// Save workflow as a new file, then zoom out before screen shot
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
await changeTab(tabA)
|
||||
|
||||
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
|
||||
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
await changeTab(tabB)
|
||||
|
||||
await comfyMouse.move(comfyPage.emptySpace)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await comfyMouse.wheel(0, 60)
|
||||
@@ -816,6 +815,9 @@ test.describe('Viewport settings', () => {
|
||||
// Ensure that the screenshots are different due to zoom level
|
||||
expect(screenshotB).not.toBe(screenshotA)
|
||||
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
|
||||
// Go back to Workflow A
|
||||
await changeTab(tabA)
|
||||
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(
|
||||
|
||||
@@ -8,7 +8,9 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
test('Can register sidebar tab', async ({ comfyPage }) => {
|
||||
const initialChildrenCount = await comfyPage.menu.buttons.count()
|
||||
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
window['app'].extensionManager.registerSidebarTab({
|
||||
@@ -24,7 +26,9 @@ test.describe('Menu', () => {
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newChildrenCount = await comfyPage.menu.buttons.count()
|
||||
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
expect(newChildrenCount).toBe(initialChildrenCount + 1)
|
||||
})
|
||||
|
||||
|
||||
@@ -35,6 +35,12 @@ test.describe('Minimap', () => {
|
||||
})
|
||||
|
||||
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||||
// Open zoom controls dropdown first
|
||||
const zoomControlsButton = comfyPage.page.getByTestId(
|
||||
'zoom-controls-button'
|
||||
)
|
||||
await zoomControlsButton.click()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
|
||||
await expect(toggleButton).toBeVisible()
|
||||
@@ -45,6 +51,13 @@ test.describe('Minimap', () => {
|
||||
|
||||
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||
|
||||
// Open zoom controls dropdown first
|
||||
const zoomControlsButton = comfyPage.page.getByTestId(
|
||||
'zoom-controls-button'
|
||||
)
|
||||
await zoomControlsButton.click()
|
||||
|
||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
@@ -54,10 +67,22 @@ test.describe('Minimap', () => {
|
||||
|
||||
await expect(minimapContainer).not.toBeVisible()
|
||||
|
||||
// Open zoom controls dropdown again
|
||||
await zoomControlsButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(toggleButton).toContainText('Show Minimap')
|
||||
|
||||
await toggleButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(minimapContainer).toBeVisible()
|
||||
|
||||
// Open zoom controls dropdown again to verify button text
|
||||
await zoomControlsButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(toggleButton).toContainText('Hide Minimap')
|
||||
})
|
||||
|
||||
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Record Audio Node', () => {
|
||||
test('should add a record audio node and take a screenshot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open the search box by double clicking on the canvas
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
|
||||
// Search for and add the RecordAudio node
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('RecordAudio')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the RecordAudio node was added
|
||||
const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio')
|
||||
expect(recordAudioNodes.length).toBe(1)
|
||||
|
||||
// Take a screenshot of the canvas with the RecordAudio node
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('record_audio_node.png')
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 9.3 KiB |
@@ -12,26 +12,21 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test.fixme(
|
||||
'should allow toggling bypass on a selected node with hotkey',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
test('should allow toggling bypass on a selected node with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
}
|
||||
)
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
})
|
||||
|
||||
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
|
||||
comfyPage
|
||||
|
||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 66 KiB |
755
docs/PLAYWRIGHT_SELECTIVE_RERUN_ALTERNATIVES.md
Normal file
@@ -0,0 +1,755 @@
|
||||
# Playwright Selective Test Rerun Alternatives
|
||||
|
||||
This document analyzes alternatives for selectively re-running only failed Playwright tests for snapshot updates, comparing native Playwright features with the current custom manifest approach used in this project.
|
||||
|
||||
## Table of Contents
|
||||
- [Current Approach](#current-approach)
|
||||
- [Native Playwright Features](#native-playwright-features)
|
||||
- [Playwright Reporter Options](#playwright-reporter-options)
|
||||
- [GitHub Actions Integration Patterns](#github-actions-integration-patterns)
|
||||
- [Third-Party Solutions](#third-party-solutions)
|
||||
- [Comparison and Recommendations](#comparison-and-recommendations)
|
||||
|
||||
---
|
||||
|
||||
## Current Approach
|
||||
|
||||
### Implementation
|
||||
The project currently uses a **custom manifest-based approach** that:
|
||||
|
||||
1. **Generates a manifest** of failed screenshot tests after CI runs
|
||||
- Script: `scripts/cicd/build-failed-screenshot-manifest.ts`
|
||||
- Parses JSON report to find tests with failed screenshot assertions
|
||||
- Creates per-project text files: `ci-rerun/{project}.txt`
|
||||
- Format: `file_path:line_number` (e.g., `browser_tests/menu.test.ts:42`)
|
||||
|
||||
2. **Stores manifest as GitHub artifact**
|
||||
- Artifact name: `failed-screenshot-tests`
|
||||
- Retention: 7 days
|
||||
- Only uploaded when chromium sharded tests fail
|
||||
|
||||
3. **Downloads manifest in update workflow**
|
||||
- Workflow: `.github/workflows/update-playwright-expectations.yaml`
|
||||
- Triggered by: PR label "New Browser Test Expectations" or `/update-playwright` comment
|
||||
- Falls back to full test suite if manifest not found
|
||||
|
||||
4. **Re-runs only failed tests**
|
||||
```bash
|
||||
for f in ci-rerun/*.txt; do
|
||||
project="$(basename "$f" .txt)"
|
||||
mapfile -t lines < "$f"
|
||||
# Filter empty lines
|
||||
pnpm exec playwright test --project="$project" --update-snapshots "${filtered[@]}"
|
||||
done
|
||||
```
|
||||
|
||||
### Advantages
|
||||
- ✅ Works across workflow runs and different trigger mechanisms
|
||||
- ✅ Survives beyond single workflow execution
|
||||
- ✅ Precise control over which tests to re-run
|
||||
- ✅ Supports multiple projects with separate manifests
|
||||
- ✅ Works with sharded test runs (merged report)
|
||||
- ✅ Platform-agnostic approach (works on any CI/CD platform)
|
||||
|
||||
### Disadvantages
|
||||
- ❌ Custom implementation requires maintenance
|
||||
- ❌ Requires parsing JSON report format (could break with Playwright updates)
|
||||
- ❌ Additional artifact storage needed
|
||||
- ❌ More complex than native solutions
|
||||
|
||||
---
|
||||
|
||||
## Native Playwright Features
|
||||
|
||||
### 1. `--last-failed` CLI Flag
|
||||
|
||||
**Availability:** Playwright v1.44.0+ (May 2024)
|
||||
|
||||
#### How It Works
|
||||
```bash
|
||||
# First run - execute all tests
|
||||
npx playwright test
|
||||
|
||||
# Second run - only re-run failed tests
|
||||
npx playwright test --last-failed
|
||||
```
|
||||
|
||||
Playwright maintains a `.last-run.json` file in the `test-results/` directory that tracks failed tests.
|
||||
|
||||
#### CLI Examples
|
||||
```bash
|
||||
# Run only failed tests from last run
|
||||
npx playwright test --last-failed
|
||||
|
||||
# Update snapshots for only failed tests
|
||||
npx playwright test --last-failed --update-snapshots
|
||||
|
||||
# Combine with project filtering
|
||||
npx playwright test --last-failed --project=chromium
|
||||
|
||||
# Debug failed tests
|
||||
npx playwright test --last-failed --debug
|
||||
```
|
||||
|
||||
#### File Location and Format
|
||||
- **Location:** `test-results/.last-run.json`
|
||||
- **Format:** JSON object containing failed test information
|
||||
- **Structure:** Contains a `failedTests: []` array with test identifiers
|
||||
- **Persistence:** Cleared when all tests pass on subsequent run
|
||||
|
||||
#### Advantages
|
||||
- ✅ Built into Playwright (no custom code)
|
||||
- ✅ Simple CLI flag
|
||||
- ✅ Automatically maintained by Playwright
|
||||
- ✅ Works with all Playwright features (debug, UI mode, etc.)
|
||||
|
||||
#### Limitations
|
||||
- ❌ **Not designed for CI/CD distributed testing** (per Playwright maintainers)
|
||||
- ❌ **Intended for local development only** ("inner loop scenario")
|
||||
- ❌ Cleared on new test runs (doesn't persist across clean environments)
|
||||
- ❌ **GitHub Actions starts with clean environment** - `.last-run.json` not available on retry
|
||||
- ❌ **Doesn't work with sharded tests** - each shard creates its own `.last-run.json`
|
||||
- ❌ No native way to merge `.last-run.json` across shards
|
||||
- ❌ Not designed for cross-workflow persistence
|
||||
|
||||
#### CI/CD Workaround (Not Recommended)
|
||||
To use `--last-failed` in GitHub Actions, you would need to:
|
||||
|
||||
```yaml
|
||||
- name: Run Playwright tests
|
||||
id: playwright-test
|
||||
run: npx playwright test
|
||||
|
||||
- name: Upload last run state
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: last-run-state
|
||||
path: test-results/.last-run.json
|
||||
|
||||
# In retry workflow:
|
||||
- name: Download last run state
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: last-run-state
|
||||
path: test-results/
|
||||
|
||||
- name: Rerun failed tests
|
||||
run: npx playwright test --last-failed --update-snapshots
|
||||
```
|
||||
|
||||
**Why This Isn't Ideal:**
|
||||
- Playwright maintainers explicitly state this is not the intended use case
|
||||
- Doesn't work well with sharded tests (multiple `.last-run.json` files)
|
||||
- Requires manual artifact management
|
||||
- More complex than the current custom approach for this use case
|
||||
|
||||
### 2. File:Line Syntax for Specific Tests
|
||||
|
||||
Playwright supports running tests at specific line numbers:
|
||||
|
||||
```bash
|
||||
# Run a specific test at line 42
|
||||
npx playwright test tests/example.spec.ts:42
|
||||
|
||||
# Multiple tests
|
||||
npx playwright test tests/file1.spec.ts:10 tests/file2.spec.ts:25
|
||||
|
||||
# With snapshot updates
|
||||
npx playwright test tests/example.spec.ts:42 --update-snapshots
|
||||
|
||||
# With project selection
|
||||
npx playwright test --project=chromium tests/example.spec.ts:42
|
||||
```
|
||||
|
||||
This is **exactly the format** the current custom manifest uses, making it compatible with Playwright's native CLI.
|
||||
|
||||
### 3. Test Filtering Options
|
||||
|
||||
```bash
|
||||
# Filter by grep pattern
|
||||
npx playwright test -g "screenshot"
|
||||
|
||||
# Inverse grep
|
||||
npx playwright test --grep-invert "mobile"
|
||||
|
||||
# By project
|
||||
npx playwright test --project=chromium
|
||||
|
||||
# Multiple projects
|
||||
npx playwright test --project=chromium --project=firefox
|
||||
|
||||
# Specific directory
|
||||
npx playwright test tests/screenshots/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Playwright Reporter Options
|
||||
|
||||
### 1. JSON Reporter
|
||||
|
||||
**Purpose:** Machine-readable test results
|
||||
|
||||
#### Configuration
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
export default defineConfig({
|
||||
reporter: [
|
||||
['json', { outputFile: 'results.json' }]
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Or via environment variable:
|
||||
```bash
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=results.json npx playwright test --reporter=json
|
||||
```
|
||||
|
||||
#### Output Structure
|
||||
```json
|
||||
{
|
||||
"stats": {
|
||||
"expected": 100,
|
||||
"unexpected": 5,
|
||||
"flaky": 2,
|
||||
"skipped": 3
|
||||
},
|
||||
"suites": [
|
||||
{
|
||||
"title": "Test Suite",
|
||||
"specs": [
|
||||
{
|
||||
"file": "browser_tests/example.test.ts",
|
||||
"line": 42,
|
||||
"tests": [
|
||||
{
|
||||
"projectId": "chromium",
|
||||
"results": [
|
||||
{
|
||||
"status": "failed",
|
||||
"attachments": [
|
||||
{ "contentType": "image/png" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**This is the format** the current `build-failed-screenshot-manifest.ts` script parses.
|
||||
|
||||
#### Advantages
|
||||
- ✅ Stable, documented JSON schema (`@playwright/test/reporter`)
|
||||
- ✅ Includes all test metadata (file, line, project, status, attachments)
|
||||
- ✅ Can be used programmatically
|
||||
- ✅ Supports multiple reporters simultaneously
|
||||
|
||||
#### Current Project Usage
|
||||
```yaml
|
||||
# In tests-ci.yaml
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright test --project=${{ matrix.browser }} \
|
||||
--reporter=list \
|
||||
--reporter=html \
|
||||
--reporter=json
|
||||
```
|
||||
|
||||
### 2. Blob Reporter
|
||||
|
||||
**Purpose:** Merging sharded test reports
|
||||
|
||||
#### Configuration
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
export default defineConfig({
|
||||
reporter: process.env.CI ? 'blob' : 'html'
|
||||
})
|
||||
```
|
||||
|
||||
#### Usage with Sharding
|
||||
```bash
|
||||
# Run sharded test with blob output
|
||||
npx playwright test --shard=1/4 --reporter=blob
|
||||
|
||||
# Merge blob reports
|
||||
npx playwright merge-reports --reporter=html ./all-blob-reports
|
||||
npx playwright merge-reports --reporter=json ./all-blob-reports
|
||||
```
|
||||
|
||||
#### Current Project Usage
|
||||
```yaml
|
||||
# Sharded chromium tests
|
||||
- run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
|
||||
env:
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report
|
||||
|
||||
# Merge reports job
|
||||
- run: |
|
||||
pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
|
||||
```
|
||||
|
||||
#### Advantages
|
||||
- ✅ Designed for distributed testing
|
||||
- ✅ Can merge into any reporter format (HTML, JSON, etc.)
|
||||
- ✅ Preserves all test information across shards
|
||||
|
||||
#### Blob Reporter and `--last-failed`
|
||||
- ❌ Blob reports **do not contain** a merged `.last-run.json`
|
||||
- ❌ Each shard creates its own `.last-run.json` that isn't included in blob
|
||||
- ❌ GitHub issue [#30924](https://github.com/microsoft/playwright/issues/30924) requests this feature (currently unsupported)
|
||||
|
||||
### 3. Multiple Reporters
|
||||
|
||||
You can use multiple reporters simultaneously:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
reporter: [
|
||||
['list'], // Terminal output
|
||||
['html'], // Browse results
|
||||
['json', { outputFile: 'results.json' }], // Programmatic parsing
|
||||
['junit', { outputFile: 'results.xml' }] // CI integration
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Or via CLI:
|
||||
```bash
|
||||
npx playwright test --reporter=list --reporter=html --reporter=json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GitHub Actions Integration Patterns
|
||||
|
||||
### Pattern 1: Comment-Triggered Workflow (JupyterLab Approach)
|
||||
|
||||
**Example:** [jupyterlab/jupyterlab-git](https://github.com/jupyterlab/jupyterlab-git/blob/main/.github/workflows/update-integration-tests.yml)
|
||||
|
||||
```yaml
|
||||
name: Update Playwright Snapshots
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-snapshots:
|
||||
# Only run for authorized users on PRs with specific comment
|
||||
if: >
|
||||
(github.event.issue.author_association == 'OWNER' ||
|
||||
github.event.issue.author_association == 'COLLABORATOR' ||
|
||||
github.event.issue.author_association == 'MEMBER'
|
||||
) && github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, 'please update snapshots')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: React to the triggering comment
|
||||
run: gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions --raw-field 'content=+1'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout PR branch
|
||||
run: gh pr checkout ${{ github.event.issue.number }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup and run tests
|
||||
run: |
|
||||
npm ci
|
||||
npx playwright install --with-deps
|
||||
npx playwright test --update-snapshots
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
git config user.name 'github-actions'
|
||||
git config user.email 'github-actions@github.com'
|
||||
git add .
|
||||
git diff --cached --quiet || git commit -m "Update snapshots"
|
||||
git push
|
||||
```
|
||||
|
||||
#### Advantages
|
||||
- ✅ Simple comment-based trigger
|
||||
- ✅ Visual feedback (reaction on comment)
|
||||
- ✅ Authorization checks built-in
|
||||
- ✅ Auto-commits to PR branch
|
||||
|
||||
#### Limitations
|
||||
- ❌ Runs **all** tests with `--update-snapshots` (not selective)
|
||||
- ❌ No integration with failed test information from CI
|
||||
|
||||
### Pattern 2: Label-Based Trigger + Manifest (Current Approach)
|
||||
|
||||
```yaml
|
||||
name: Update Playwright Expectations
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: >
|
||||
( github.event_name == 'pull_request' &&
|
||||
github.event.label.name == 'New Browser Test Expectations' ) ||
|
||||
( github.event.issue.pull_request &&
|
||||
startsWith(github.event.comment.body, '/update-playwright') )
|
||||
|
||||
steps:
|
||||
# ... setup steps ...
|
||||
|
||||
- name: Locate failed screenshot manifest artifact
|
||||
id: locate-manifest
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo
|
||||
let headSha = ''
|
||||
if (context.eventName === 'pull_request') {
|
||||
headSha = context.payload.pull_request.head.sha
|
||||
} else if (context.eventName === 'issue_comment') {
|
||||
const prNumber = context.payload.issue.number
|
||||
const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber })
|
||||
headSha = pr.data.head.sha
|
||||
}
|
||||
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
owner, repo,
|
||||
workflow_id: 'tests-ci.yaml',
|
||||
head_sha: headSha,
|
||||
per_page: 1,
|
||||
})
|
||||
const run = data.workflow_runs?.[0]
|
||||
|
||||
let has = 'false'
|
||||
if (run) {
|
||||
const { data: { artifacts = [] } } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner, repo, run_id: run.id
|
||||
})
|
||||
if (artifacts.some(a => a.name === 'failed-screenshot-tests' && !a.expired))
|
||||
has = 'true'
|
||||
}
|
||||
core.setOutput('has_manifest', has)
|
||||
|
||||
- name: Download failed screenshot manifest
|
||||
if: steps.locate-manifest.outputs.has_manifest == 'true'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
run-id: ${{ steps.locate-manifest.outputs.run_id }}
|
||||
name: failed-screenshot-tests
|
||||
path: ComfyUI_frontend/ci-rerun
|
||||
|
||||
- name: Re-run failed screenshot tests
|
||||
run: |
|
||||
if [ ! -d ci-rerun ]; then
|
||||
echo "No manifest found; running full suite"
|
||||
pnpm exec playwright test --update-snapshots
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for f in ci-rerun/*.txt; do
|
||||
project="$(basename "$f" .txt)"
|
||||
mapfile -t lines < "$f"
|
||||
filtered=()
|
||||
for l in "${lines[@]}"; do
|
||||
[ -n "$l" ] && filtered+=("$l")
|
||||
done
|
||||
|
||||
if [ ${#filtered[@]} -gt 0 ]; then
|
||||
echo "Re-running ${#filtered[@]} tests for project $project"
|
||||
pnpm exec playwright test --project="$project" --update-snapshots "${filtered[@]}"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
#### Advantages
|
||||
- ✅ **Selective** - only re-runs failed screenshot tests
|
||||
- ✅ Works across different trigger mechanisms (label or comment)
|
||||
- ✅ Fallback to full suite if manifest not found
|
||||
- ✅ Per-project manifests support multiple browser configurations
|
||||
- ✅ Handles sharded tests via merged report
|
||||
|
||||
### Pattern 3: WordPress/Openverse Approach (Always Update)
|
||||
|
||||
Proposed pattern (not fully implemented):
|
||||
1. CI always runs with `--update-snapshots` flag
|
||||
2. If snapshots change, create/update a secondary branch
|
||||
3. Open PR targeting the original PR branch
|
||||
4. Developer reviews snapshot changes before merging
|
||||
|
||||
#### Advantages
|
||||
- ✅ Always generates correct snapshots
|
||||
- ✅ Snapshot changes are visible in separate PR
|
||||
- ✅ No test failures due to mismatched snapshots
|
||||
|
||||
#### Limitations
|
||||
- ❌ Creates multiple PRs
|
||||
- ❌ More complex merge workflow
|
||||
- ❌ Potential for snapshot changes to mask real issues
|
||||
|
||||
### Pattern 4: Manual Workflow Dispatch
|
||||
|
||||
```yaml
|
||||
name: Update Snapshots
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
update-snapshots:
|
||||
description: 'Update snapshots'
|
||||
type: boolean
|
||||
default: false
|
||||
test-pattern:
|
||||
description: 'Test pattern (optional)'
|
||||
type: string
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup
|
||||
run: |
|
||||
npm ci
|
||||
npx playwright install --with-deps
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ "${{ inputs.update-snapshots }}" = "true" ]; then
|
||||
FLAGS="--update-snapshots"
|
||||
fi
|
||||
|
||||
PATTERN="${{ inputs.test-pattern }}"
|
||||
npx playwright test ${PATTERN} ${FLAGS}
|
||||
```
|
||||
|
||||
#### Advantages
|
||||
- ✅ Full manual control
|
||||
- ✅ Can specify test patterns
|
||||
- ✅ Simple to understand
|
||||
|
||||
#### Limitations
|
||||
- ❌ Requires manual triggering
|
||||
- ❌ Not integrated with CI failures
|
||||
|
||||
---
|
||||
|
||||
## Third-Party Solutions
|
||||
|
||||
### Currents.dev - Last Failed GitHub Action
|
||||
|
||||
**Repository:** [currents-dev/playwright-last-failed](https://github.com/currents-dev/playwright-last-failed)
|
||||
|
||||
#### Purpose
|
||||
Helps run last failed Playwright tests using Currents' cloud-based caching service.
|
||||
|
||||
#### Usage
|
||||
```yaml
|
||||
- name: Playwright Last Failed action
|
||||
id: last-failed-action
|
||||
uses: currents-dev/playwright-last-failed@v1
|
||||
with:
|
||||
pw-output-dir: test-results
|
||||
matrix-index: ${{ matrix.shard }}
|
||||
matrix-total: ${{ strategy.job-total }}
|
||||
```
|
||||
|
||||
#### How It Works
|
||||
- Uses Currents' cloud service to persist failed test information
|
||||
- Supports sharded tests via matrix parameters
|
||||
- Enables selective rerun of failed tests across workflow retries
|
||||
|
||||
#### Advantages
|
||||
- ✅ Works with sharded tests
|
||||
- ✅ Persists across workflow runs
|
||||
- ✅ Supports GitHub Actions retry mechanism
|
||||
- ✅ Handles distributed testing
|
||||
|
||||
#### Limitations
|
||||
- ❌ **Requires Currents subscription** (third-party paid service)
|
||||
- ❌ Dependency on external service
|
||||
- ❌ Data sent to third-party cloud
|
||||
- ❌ Additional cost
|
||||
- ❌ Vendor lock-in
|
||||
|
||||
#### Recommendation
|
||||
**Not suitable for this project** due to:
|
||||
- External service dependency
|
||||
- Cost implications
|
||||
- The current custom solution is already working well
|
||||
|
||||
---
|
||||
|
||||
## Comparison and Recommendations
|
||||
|
||||
### Feature Matrix
|
||||
|
||||
| Feature | Current Approach | `--last-failed` | Currents | Comment Trigger Only |
|
||||
|---------|-----------------|-----------------|----------|---------------------|
|
||||
| Works with sharded tests | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
|
||||
| Persists across workflows | ✅ Yes | ❌ No | ✅ Yes | N/A |
|
||||
| Selective reruns | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No (runs all) |
|
||||
| No external dependencies | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes |
|
||||
| Simple implementation | ⚠️ Medium | ✅ Simple | ✅ Simple | ✅ Simple |
|
||||
| Maintenance overhead | ⚠️ Medium | ✅ Low | ✅ Low | ✅ Low |
|
||||
| Works in CI/CD | ✅ Yes | ⚠️ Workaround | ✅ Yes | ✅ Yes |
|
||||
| Cost | ✅ Free | ✅ Free | ❌ Paid | ✅ Free |
|
||||
| Supports multiple projects | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
|
||||
|
||||
### Why `--last-failed` Isn't Suitable (Currently)
|
||||
|
||||
1. **Not designed for CI/CD:** Playwright maintainers explicitly state it's for "inner loop scenario (local development)"
|
||||
2. **Doesn't work with sharded tests:** Each shard creates its own `.last-run.json` with no native merge
|
||||
3. **Clean environment issue:** GitHub Actions starts fresh, losing `.last-run.json`
|
||||
4. **Feature request pending:** GitHub issue [#30924](https://github.com/microsoft/playwright/issues/30924) requests blob report integration (not yet implemented)
|
||||
|
||||
### Recommendations
|
||||
|
||||
#### Short Term: Keep Current Approach
|
||||
**Verdict: The current custom manifest approach is the best solution for this project's needs.**
|
||||
|
||||
**Reasons:**
|
||||
1. ✅ **Works perfectly with sharded tests** - merges results across 8 shards
|
||||
2. ✅ **Persists across workflows** - artifact storage for 7 days
|
||||
3. ✅ **Selective reruns** - only failed screenshot tests
|
||||
4. ✅ **No external dependencies** - fully self-contained
|
||||
5. ✅ **Uses stable Playwright JSON format** - typed via `@playwright/test/reporter`
|
||||
6. ✅ **Already working well** - proven in production
|
||||
|
||||
**Minor Improvements:**
|
||||
```typescript
|
||||
// Add version check to warn if JSON schema changes
|
||||
import { version } from '@playwright/test/package.json'
|
||||
if (major(version) !== 1) {
|
||||
console.warn('Playwright major version changed - verify JSON schema compatibility')
|
||||
}
|
||||
|
||||
// Add more robust error handling
|
||||
try {
|
||||
const report: JSONReport = JSON.parse(raw)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse Playwright JSON report: ${error.message}`)
|
||||
}
|
||||
|
||||
// Consider adding tests for the manifest builder
|
||||
// e.g., tests/cicd/build-failed-screenshot-manifest.test.ts
|
||||
```
|
||||
|
||||
#### Long Term: Monitor Playwright Development
|
||||
|
||||
**Watch for these features:**
|
||||
1. **Blob report + `.last-run.json` merge** - GitHub issue [#30924](https://github.com/microsoft/playwright/issues/30924)
|
||||
2. **Native CI/CD support for `--last-failed`** - may never happen (by design)
|
||||
3. **Report merging improvements** - GitHub issue [#33094](https://github.com/microsoft/playwright/issues/33094)
|
||||
|
||||
**Migration path if native support improves:**
|
||||
```yaml
|
||||
# Future potential approach (if Playwright adds this feature)
|
||||
- name: Merge reports with last-run
|
||||
run: |
|
||||
npx playwright merge-reports --reporter=html ./all-blob-reports
|
||||
npx playwright merge-reports --reporter=last-failed ./all-blob-reports
|
||||
|
||||
- name: Upload merged last-run
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: last-run-state
|
||||
path: test-results/.last-run.json
|
||||
|
||||
# In update workflow
|
||||
- name: Download last-run state
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: last-run-state
|
||||
path: test-results/
|
||||
|
||||
- name: Update snapshots for failed tests
|
||||
run: npx playwright test --last-failed --update-snapshots
|
||||
```
|
||||
|
||||
**However, this is speculative** - Playwright maintainers have indicated `--last-failed` is not intended for CI/CD.
|
||||
|
||||
#### Alternative: Simplify to Full Suite Reruns
|
||||
|
||||
If the custom manifest becomes too complex to maintain, consider:
|
||||
|
||||
```yaml
|
||||
- name: Re-run ALL screenshot tests
|
||||
run: |
|
||||
# Simple grep-based filtering for screenshot tests
|
||||
npx playwright test -g "screenshot" --update-snapshots
|
||||
```
|
||||
|
||||
**Trade-offs:**
|
||||
- ✅ Much simpler
|
||||
- ✅ No custom scripts
|
||||
- ❌ Slower (runs all screenshot tests, not just failed ones)
|
||||
- ❌ Potentially updates snapshots that weren't actually failing
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The current custom manifest approach is **well-designed** and **appropriate** for this project's requirements:
|
||||
|
||||
1. **Handles sharded tests** - critical for CI performance
|
||||
2. **Selective reruns** - saves time and resources
|
||||
3. **Stable implementation** - uses documented Playwright JSON schema
|
||||
4. **No external dependencies** - fully controlled
|
||||
|
||||
While `--last-failed` is a nice feature for **local development**, Playwright's own documentation and maintainer comments confirm it's **not suitable for distributed CI/CD testing**, which is exactly what this project needs.
|
||||
|
||||
The only potentially better solution (Currents) requires a paid external service, which adds cost and complexity without significant benefits over the current approach.
|
||||
|
||||
**Recommendation: Keep the current implementation**, with minor improvements to error handling and documentation. Monitor Playwright development for native improvements, but don't expect `--last-failed` to become a viable alternative for this use case.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Official Playwright Documentation
|
||||
- [Command Line](https://playwright.dev/docs/test-cli)
|
||||
- [Reporters](https://playwright.dev/docs/test-reporters)
|
||||
- [Test Sharding](https://playwright.dev/docs/test-sharding)
|
||||
- [CI/CD Setup](https://playwright.dev/docs/ci-intro)
|
||||
|
||||
### Community Resources
|
||||
- [Playwright Solutions: How to Run Failures Only](https://playwrightsolutions.com/how-to-run-failures-only-from-the-last-playwright-run/)
|
||||
- [Medium: How to Run Only Last Failed Tests](https://medium.com/@testerstalk/how-to-run-only-last-failed-tests-in-playwright-e5e41472594a)
|
||||
- [Medium: Streamlining Visual Regression Testing](https://medium.com/@haleywardo/streamlining-playwright-visual-regression-testing-with-github-actions-e077fd33c27c)
|
||||
|
||||
### GitHub Issues
|
||||
- [#30924 - Last-failed with blob reports](https://github.com/microsoft/playwright/issues/30924)
|
||||
- [#33094 - Merging main run with --last-failed](https://github.com/microsoft/playwright/issues/33094)
|
||||
- [#28254 - Feature request for --last-failed](https://github.com/microsoft/playwright/issues/28254)
|
||||
|
||||
### Example Implementations
|
||||
- [JupyterLab Git - Update Integration Tests](https://github.com/jupyterlab/jupyterlab-git/blob/main/.github/workflows/update-integration-tests.yml)
|
||||
- [WordPress Openverse - Discussion #4535](https://github.com/WordPress/openverse/issues/4535)
|
||||
|
||||
### Third-Party Tools
|
||||
- [Currents - Playwright Last Failed Action](https://github.com/currents-dev/playwright-last-failed)
|
||||
- [Currents - Re-run Only Failed Tests](https://docs.currents.dev/guides/re-run-only-failed-tests)
|
||||
482
docs/SNAPSHOT_UPDATE_FROM_ACTUALS.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Snapshot Update from Actual Files (Fast Approach)
|
||||
|
||||
**Date:** 2025-10-08
|
||||
**Status:** Proposed Optimization
|
||||
|
||||
## Overview
|
||||
|
||||
When Playwright snapshot tests fail, Playwright **already generates the new ("actual") snapshots**. Instead of re-running tests with `--update-snapshots`, we can extract these actual snapshots from the `test-results/` directory and copy them to overwrite the expected snapshots.
|
||||
|
||||
**Performance improvement:** ~1-2 minutes → **~10-30 seconds**
|
||||
|
||||
## How Playwright Stores Snapshots
|
||||
|
||||
### Expected (Baseline) Snapshots
|
||||
|
||||
Stored in: `<test-file>-snapshots/<snapshot-name>-<project>-<platform>.png`
|
||||
|
||||
**Example:**
|
||||
```
|
||||
browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png
|
||||
```
|
||||
|
||||
### Failed Test Artifacts
|
||||
|
||||
When a snapshot test fails, Playwright creates:
|
||||
|
||||
```
|
||||
test-results/<test-hash>/
|
||||
├── <snapshot-name>-actual.png # The NEW screenshot
|
||||
├── <snapshot-name>-expected.png # Copy of baseline
|
||||
└── <snapshot-name>-diff.png # Visual diff
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
test-results/interaction-default-chromium-67af3c/
|
||||
├── default-1-actual.png
|
||||
├── default-1-expected.png
|
||||
└── default-1-diff.png
|
||||
```
|
||||
|
||||
## Current Approach vs. Proposed Approach
|
||||
|
||||
### Current: Re-run Tests with `--update-snapshots`
|
||||
|
||||
```yaml
|
||||
# Current workflow (.github/workflows/update-playwright-expectations.yaml)
|
||||
- name: Re-run failed screenshot tests and update snapshots
|
||||
run: |
|
||||
# Download manifest of failed tests
|
||||
# For each project: chromium, chromium-2x, etc.
|
||||
# Run: playwright test --project="$project" --update-snapshots test1.spec.ts:42 test2.spec.ts:87 ...
|
||||
```
|
||||
|
||||
**Time:** ~2-5 minutes (depends on # of failed tests)
|
||||
|
||||
**Why slow:**
|
||||
- Re-executes tests (browser startup, navigation, interactions)
|
||||
- Waits for elements, animations, etc.
|
||||
- Generates HTML report
|
||||
- Each test takes 5-15 seconds
|
||||
|
||||
### Proposed: Copy Actual → Expected
|
||||
|
||||
```yaml
|
||||
# Proposed workflow
|
||||
- name: Download test artifacts (includes test-results/)
|
||||
- name: Copy actual snapshots to expected locations
|
||||
run: pnpm tsx scripts/cicd/update-snapshots-from-actuals.ts
|
||||
- name: Commit and push
|
||||
```
|
||||
|
||||
**Time:** ~10-30 seconds (just file operations)
|
||||
|
||||
**Why fast:**
|
||||
- No test execution
|
||||
- No browser startup
|
||||
- Just file copying
|
||||
- Parallel file operations
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Modify tests-ci.yaml
|
||||
|
||||
Currently, test artifacts upload only the `playwright-report/` directory.
|
||||
|
||||
**Add test-results/ to artifacts:**
|
||||
|
||||
```yaml
|
||||
# .github/workflows/tests-ci.yaml
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-results-${{ matrix.browser }} # New artifact
|
||||
path: |
|
||||
ComfyUI_frontend/test-results/**/*-actual.png
|
||||
ComfyUI_frontend/test-results/**/*-expected.png
|
||||
ComfyUI_frontend/test-results/**/*-diff.png
|
||||
retention-days: 7
|
||||
```
|
||||
|
||||
**Optimization:** Only upload actual snapshots for failed tests (saves artifact storage)
|
||||
|
||||
### Step 2: Create Script to Map Actuals → Expected
|
||||
|
||||
**File:** `scripts/cicd/update-snapshots-from-actuals.ts`
|
||||
|
||||
```typescript
|
||||
import type { JSONReport, JSONReportTestResult } from '@playwright/test/reporter'
|
||||
import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
interface SnapshotMapping {
|
||||
actualPath: string // test-results/.../snapshot-1-actual.png
|
||||
expectedPath: string // browser_tests/tests/foo.spec.ts-snapshots/snapshot-chromium-linux.png
|
||||
testFile: string
|
||||
testName: string
|
||||
project: string
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reportPath = path.join('playwright-report', 'report.json')
|
||||
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
console.log('No report.json found - no failed tests to update')
|
||||
return
|
||||
}
|
||||
|
||||
const raw = await fsp.readFile(reportPath, 'utf8')
|
||||
const report: JSONReport = JSON.parse(raw)
|
||||
|
||||
const mappings: SnapshotMapping[] = []
|
||||
|
||||
// Parse JSON report to extract snapshot paths
|
||||
function collectFailedSnapshots(suite: any) {
|
||||
if (!suite) return
|
||||
|
||||
for (const childSuite of suite.suites ?? []) {
|
||||
collectFailedSnapshots(childSuite)
|
||||
}
|
||||
|
||||
for (const spec of suite.specs ?? []) {
|
||||
for (const test of spec.tests) {
|
||||
const lastResult = test.results[test.results.length - 1]
|
||||
|
||||
if (lastResult?.status !== 'failed') continue
|
||||
|
||||
// Check if test has image attachments (indicates screenshot test)
|
||||
const imageAttachments = lastResult.attachments.filter(
|
||||
(att: any) => att?.contentType?.startsWith('image/')
|
||||
)
|
||||
|
||||
if (imageAttachments.length === 0) continue
|
||||
|
||||
// Extract snapshot mapping from attachments
|
||||
for (const attachment of imageAttachments) {
|
||||
const attachmentPath = attachment.path
|
||||
|
||||
if (!attachmentPath || !attachmentPath.includes('-actual.png')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse test-results path to determine expected location
|
||||
// test-results/interaction-default-chromium-67af3c/default-1-actual.png
|
||||
// → browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png
|
||||
|
||||
const actualPath = attachmentPath
|
||||
const expectedPath = inferExpectedPath(actualPath, spec.file, test.projectId)
|
||||
|
||||
if (expectedPath) {
|
||||
mappings.push({
|
||||
actualPath,
|
||||
expectedPath,
|
||||
testFile: spec.file,
|
||||
testName: test.annotations[0]?.description || test.title,
|
||||
project: test.projectId
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectFailedSnapshots(report)
|
||||
|
||||
if (mappings.length === 0) {
|
||||
console.log('No failed snapshot tests found')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Found ${mappings.length} snapshots to update`)
|
||||
|
||||
// Copy actual → expected
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
for (const mapping of mappings) {
|
||||
try {
|
||||
if (!fs.existsSync(mapping.actualPath)) {
|
||||
console.warn(`⚠️ Actual file not found: ${mapping.actualPath}`)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure expected directory exists
|
||||
const expectedDir = path.dirname(mapping.expectedPath)
|
||||
await fsp.mkdir(expectedDir, { recursive: true })
|
||||
|
||||
// Copy actual → expected
|
||||
await fsp.copyFile(mapping.actualPath, mapping.expectedPath)
|
||||
|
||||
console.log(`✓ Updated: ${path.basename(mapping.expectedPath)}`)
|
||||
successCount++
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to update ${mapping.expectedPath}:`, error)
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Successfully updated ${successCount} snapshots`)
|
||||
if (errorCount > 0) {
|
||||
console.log(`⚠️ Failed to update ${errorCount} snapshots`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the expected snapshot path from the actual path
|
||||
*
|
||||
* Actual: test-results/interaction-default-chromium-67af3c/default-1-actual.png
|
||||
* Expected: browser_tests/tests/interaction.spec.ts-snapshots/default-chromium-linux.png
|
||||
*/
|
||||
function inferExpectedPath(actualPath: string, testFile: string, projectId: string): string | null {
|
||||
try {
|
||||
// Extract snapshot name from actual path
|
||||
// "default-1-actual.png" → "default"
|
||||
const actualFilename = path.basename(actualPath)
|
||||
const snapshotName = actualFilename.replace(/-\d+-actual\.png$/, '')
|
||||
|
||||
// Determine platform (linux, darwin, win32)
|
||||
const platform = process.platform === 'linux' ? 'linux'
|
||||
: process.platform === 'darwin' ? 'darwin'
|
||||
: 'win32'
|
||||
|
||||
// Build expected path
|
||||
const testDir = path.dirname(testFile)
|
||||
const testBasename = path.basename(testFile)
|
||||
const snapshotsDir = path.join(testDir, `${testBasename}-snapshots`)
|
||||
const expectedFilename = `${snapshotName}-${projectId}-${platform}.png`
|
||||
|
||||
return path.join(snapshotsDir, expectedFilename)
|
||||
} catch (error) {
|
||||
console.error(`Failed to infer expected path for ${actualPath}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Failed to update snapshots:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
```
|
||||
|
||||
### Step 3: Better Approach - Use Playwright's Attachment Metadata
|
||||
|
||||
The JSON reporter actually includes the **expected snapshot path** in the attachments!
|
||||
|
||||
**Simplified script:**
|
||||
|
||||
```typescript
|
||||
async function main() {
|
||||
const report: JSONReport = JSON.parse(await fsp.readFile('playwright-report/report.json', 'utf8'))
|
||||
|
||||
const updates: Array<{ actual: string; expected: string }> = []
|
||||
|
||||
for (const result of getAllTestResults(report)) {
|
||||
if (result.status !== 'failed') continue
|
||||
|
||||
for (const attachment of result.attachments) {
|
||||
// Playwright includes both actual and expected in attachments
|
||||
if (attachment.name?.includes('-actual') && attachment.path) {
|
||||
const actualPath = attachment.path
|
||||
|
||||
// Find corresponding expected attachment
|
||||
const expectedAttachment = result.attachments.find(
|
||||
att => att.name === attachment.name.replace('-actual', '-expected')
|
||||
)
|
||||
|
||||
if (expectedAttachment?.path) {
|
||||
// The expected path in attachment points to the test-results copy
|
||||
// But we can infer the real expected path from the attachment metadata
|
||||
const expectedPath = inferRealExpectedPath(expectedAttachment)
|
||||
updates.push({ actual: actualPath, expected: expectedPath })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy files
|
||||
for (const { actual, expected } of updates) {
|
||||
await fsp.copyFile(actual, expected)
|
||||
console.log(`✓ Updated: ${path.relative(process.cwd(), expected)}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Update GitHub Actions Workflow
|
||||
|
||||
```yaml
|
||||
# .github/workflows/update-playwright-expectations.yaml
|
||||
name: Update Playwright Expectations
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-snapshots') &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'),
|
||||
github.event.comment.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: React to comment
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
github.rest.reactions.createForIssueComment({
|
||||
comment_id: context.payload.comment.id,
|
||||
content: '+1'
|
||||
})
|
||||
|
||||
- name: Checkout PR
|
||||
run: gh pr checkout ${{ github.event.issue.number }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Get latest failed test run
|
||||
id: get-run
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.issue.number
|
||||
})
|
||||
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'tests-ci.yaml',
|
||||
head_sha: pr.data.head.sha,
|
||||
per_page: 1
|
||||
})
|
||||
|
||||
core.setOutput('run_id', runs.data.workflow_runs[0]?.id || '')
|
||||
|
||||
- name: Download test results
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
run-id: ${{ steps.get-run.outputs.run_id }}
|
||||
pattern: playwright-results-*
|
||||
path: ComfyUI_frontend/test-results
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download JSON report
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
run-id: ${{ steps.get-run.outputs.run_id }}
|
||||
pattern: playwright-report-*
|
||||
path: ComfyUI_frontend/playwright-report
|
||||
merge-multiple: true
|
||||
|
||||
- name: Update snapshots from actuals
|
||||
working-directory: ComfyUI_frontend
|
||||
run: pnpm tsx scripts/cicd/update-snapshots-from-actuals.ts
|
||||
|
||||
- name: Commit and push
|
||||
working-directory: ComfyUI_frontend
|
||||
run: |
|
||||
git config user.name 'github-actions'
|
||||
git config user.email 'github-actions@github.com'
|
||||
git add browser_tests/**/*-snapshots/*.png
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
echo "No snapshot changes detected"
|
||||
else
|
||||
git commit -m "[automated] Update test expectations"
|
||||
git push
|
||||
fi
|
||||
```
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
### Current Approach: Re-run Tests
|
||||
|
||||
| Step | Time |
|
||||
|------|------|
|
||||
| Download manifest | 5s |
|
||||
| Install Playwright browsers | 20s |
|
||||
| Re-run 50 failed tests | 2-3 min |
|
||||
| Generate report | 10s |
|
||||
| Commit and push | 10s |
|
||||
| **Total** | **~3-4 min** |
|
||||
|
||||
### Proposed Approach: Copy Actuals
|
||||
|
||||
| Step | Time |
|
||||
|------|------|
|
||||
| Download test-results artifacts | 10s |
|
||||
| Download JSON report | 2s |
|
||||
| Run copy script | 5s |
|
||||
| Commit and push | 10s |
|
||||
| **Total** | **~30s** |
|
||||
|
||||
**Speedup: 6-8x faster** ⚡
|
||||
|
||||
## Advantages
|
||||
|
||||
✅ **Much faster** - No test re-execution
|
||||
✅ **Simpler** - No need for manifest generation
|
||||
✅ **Fewer dependencies** - No Playwright browser install needed
|
||||
✅ **Less resource usage** - No ComfyUI server, no browser processes
|
||||
✅ **More reliable** - File operations are deterministic
|
||||
✅ **Already tested** - The snapshots were generated during the actual test run
|
||||
|
||||
## Disadvantages / Edge Cases
|
||||
|
||||
❌ **New snapshots** - If a test creates a snapshot for the first time, there's no existing expected file. This is rare and can be handled by fallback to re-running.
|
||||
|
||||
❌ **Deleted tests** - Old snapshots won't be cleaned up automatically. Could add a cleanup step.
|
||||
|
||||
❌ **Multiple projects** - Each project (chromium, chromium-2x, mobile-chrome) generates separate actuals. The script needs to handle all of them.
|
||||
|
||||
❌ **Artifact storage** - Storing test-results/ increases artifact size. Mitigation: Only upload `-actual.png` files, not traces/videos.
|
||||
|
||||
## Hybrid Approach (Recommended)
|
||||
|
||||
Use the fast copy approach **with fallback**:
|
||||
|
||||
```yaml
|
||||
- name: Update snapshots
|
||||
run: |
|
||||
# Try fast approach first
|
||||
if pnpm tsx scripts/cicd/update-snapshots-from-actuals.ts; then
|
||||
echo "✓ Updated snapshots from actuals"
|
||||
else
|
||||
echo "⚠ Fast update failed, falling back to re-running tests"
|
||||
# Fallback to current approach
|
||||
pnpm exec playwright test --update-snapshots --project=chromium ...
|
||||
fi
|
||||
```
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] Create `scripts/cicd/update-snapshots-from-actuals.ts`
|
||||
- [ ] Update `tests-ci.yaml` to upload `test-results/` artifacts
|
||||
- [ ] Update `update-playwright-expectations.yaml` to use new script
|
||||
- [ ] Add fallback logic for edge cases
|
||||
- [ ] Test with actual PR
|
||||
- [ ] Update documentation
|
||||
- [ ] Consider switching from label trigger → comment trigger (`/update-snapshots`)
|
||||
|
||||
## Related Links
|
||||
|
||||
- **Playwright snapshot docs:** https://playwright.dev/docs/test-snapshots
|
||||
- **JSON reporter types:** `@playwright/test/reporter`
|
||||
- **GitHub Actions artifacts:** https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts
|
||||
- **Issue #22064:** Playwright feature request for better snapshot file alignment
|
||||
|
||||
## Conclusion
|
||||
|
||||
This approach is **significantly faster** and **simpler** than re-running tests. The main trade-off is artifact storage size, but this can be mitigated by only uploading actual snapshots (not traces/videos).
|
||||
|
||||
**Recommendation:** Implement this as the primary approach with fallback to re-running tests for edge cases.
|
||||
@@ -1,7 +1,6 @@
|
||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||
import pluginJs from '@eslint/js'
|
||||
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
@@ -24,17 +23,10 @@ const commonGlobals = {
|
||||
} as const
|
||||
|
||||
const settings = {
|
||||
'import-x/resolver-next': [
|
||||
createTypeScriptImportResolver({
|
||||
alwaysTryTypes: true,
|
||||
project: [
|
||||
'./tsconfig.json',
|
||||
'./apps/*/tsconfig.json',
|
||||
'./packages/*/tsconfig.json'
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
],
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
node: true
|
||||
},
|
||||
tailwindcss: {
|
||||
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
|
||||
functions: ['cn', 'clsx', 'tw']
|
||||
@@ -60,7 +52,6 @@ export default defineConfig([
|
||||
'**/vite.config.*.timestamp*',
|
||||
'**/vitest.config.*.timestamp*',
|
||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
||||
'public/auth-sw.js',
|
||||
'src/extensions/core/*',
|
||||
'src/scripts/*',
|
||||
'src/types/generatedManagerTypes.ts',
|
||||
@@ -77,7 +68,9 @@ export default defineConfig([
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts'
|
||||
'vite.types.config.mts',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts'
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -255,17 +248,5 @@ export default defineConfig([
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
6
global.d.ts
vendored
@@ -4,12 +4,6 @@ declare const __SENTRY_DSN__: string
|
||||
declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
declare const __USE_PROD_CONFIG__: boolean
|
||||
declare const __MIXPANEL_TOKEN__: string
|
||||
|
||||
type BuildFeatureFlags = {
|
||||
REQUIRE_SUBSCRIPTION: boolean
|
||||
}
|
||||
declare const __BUILD_FLAGS__: BuildFeatureFlags
|
||||
|
||||
interface Navigator {
|
||||
/**
|
||||
|
||||
@@ -12,10 +12,6 @@ const config: KnipConfig = {
|
||||
],
|
||||
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
|
||||
},
|
||||
'apps/desktop-ui': {
|
||||
entry: ['src/main.ts', 'src/i18n.ts'],
|
||||
project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}']
|
||||
},
|
||||
'packages/tailwind-utils': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
},
|
||||
@@ -43,9 +39,7 @@ 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',
|
||||
// Service worker - registered at runtime via navigator.serviceWorker.register()
|
||||
'public/auth-sw.js'
|
||||
'src/scripts/ui/components/splitButton.ts'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.30.1",
|
||||
"version": "1.29.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -11,10 +11,7 @@
|
||||
"build:desktop": "nx build @comfyorg/desktop-ui",
|
||||
"build-storybook": "storybook build",
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build",
|
||||
"size:collect": "node scripts/size-collect.js",
|
||||
"size:report": "node scripts/size-report.js",
|
||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||
"dev:desktop": "nx dev @comfyorg/desktop-ui",
|
||||
"dev:electron": "nx serve --config vite.electron.config.mts",
|
||||
@@ -88,14 +85,9 @@
|
||||
"jsdom": "catalog:",
|
||||
"knip": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"pretty-bytes": "catalog:",
|
||||
"rollup-plugin-visualizer": "catalog:",
|
||||
"storybook": "catalog:",
|
||||
"stylelint": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
|
||||
@@ -24,14 +24,11 @@
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 8px;
|
||||
|
||||
/* Font Families */
|
||||
--font-inter: 'Inter', sans-serif;
|
||||
|
||||
/* Palette Colors */
|
||||
--color-charcoal-100: #171718;
|
||||
--color-charcoal-100: #55565e;
|
||||
--color-charcoal-200: #494a50;
|
||||
--color-charcoal-300: #3c3d42;
|
||||
--color-charcoal-400: #313235;
|
||||
@@ -63,7 +60,6 @@
|
||||
--color-sand-200: #d6cfc2;
|
||||
--color-sand-300: #888682;
|
||||
|
||||
--color-pure-black: #000000;
|
||||
--color-pure-white: #ffffff;
|
||||
|
||||
--color-slate-100: #9c9eab;
|
||||
@@ -89,10 +85,6 @@
|
||||
--color-bypass: #6a246a;
|
||||
--color-error: #962a2a;
|
||||
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
--text-xxxs: 0.5625rem;
|
||||
--text-xxxs--line-height: calc(1 / 0.5625);
|
||||
|
||||
--color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3);
|
||||
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
|
||||
@@ -145,9 +137,6 @@
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
--button-surface: var(--color-pure-white);
|
||||
--button-surface-contrast: var(--color-pure-black);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
--code-bg-color: rgb(96 165 250 / 0.2);
|
||||
@@ -157,19 +146,8 @@
|
||||
|
||||
--accent-primary: var(--color-charcoal-700);
|
||||
--backdrop: var(--color-white);
|
||||
--button-hover-surface: var(--color-gray-200);
|
||||
--button-active-surface: var(--color-gray-400);
|
||||
--button-icon: var(--color-gray-600);
|
||||
--dialog-surface: var(--color-neutral-200);
|
||||
--interface-menu-component-surface-hovered: var(--color-gray-200);
|
||||
--interface-menu-component-surface-selected: var(--color-gray-400);
|
||||
--interface-menu-keybind-surface-default: var(--color-gray-500);
|
||||
--interface-panel-surface: var(--color-pure-white);
|
||||
--interface-stroke: var(--color-gray-300);
|
||||
--nav-background: var(--color-pure-white);
|
||||
--node-border: var(--color-gray-300);
|
||||
--node-component-border: var(--color-gray-400);
|
||||
--node-component-disabled: var(--color-alpha-stone-100-20);
|
||||
--node-component-executing: var(--color-blue-500);
|
||||
--node-component-header: var(--fg-color);
|
||||
--node-component-header-icon: var(--color-stone-200);
|
||||
@@ -181,7 +159,7 @@
|
||||
--node-component-slot-dot-outline: var(--color-black);
|
||||
--node-component-slot-text: var(--color-stone-200);
|
||||
--node-component-surface-highlight: var(--color-stone-100);
|
||||
--node-component-surface-hovered: var(--color-gray-200);
|
||||
--node-component-surface-hovered: var(--color-charcoal-400);
|
||||
--node-component-surface-selected: var(--color-charcoal-200);
|
||||
--node-component-surface: var(--color-white);
|
||||
--node-component-tooltip: var(--color-charcoal-700);
|
||||
@@ -192,33 +170,18 @@
|
||||
from var(--color-zinc-500) r g b / 10%
|
||||
);
|
||||
--node-component-widget-skeleton-surface: var(--color-zinc-300);
|
||||
--node-divider: var(--color-sand-100);
|
||||
--node-component-disabled: var(--color-alpha-stone-100-20);
|
||||
--node-icon-disabled: var(--color-alpha-gray-500-50);
|
||||
--node-stroke: var(--color-gray-400);
|
||||
--node-stroke-selected: var(--color-accent-primary);
|
||||
--node-stroke-error: var(--color-error);
|
||||
--node-stroke-executing: var(--color-blue-100);
|
||||
--text-secondary: var(--color-stone-100);
|
||||
--text-primary: var(--color-charcoal-700);
|
||||
--input-surface: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
--accent-primary: var(--color-pure-white);
|
||||
--backdrop: var(--color-neutral-900);
|
||||
--button-surface: var(--color-charcoal-600);
|
||||
--button-surface-contrast: var(--color-pure-white);
|
||||
--button-hover-surface: var(--color-charcoal-600);
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-gray-800);
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
|
||||
--interface-menu-component-surface-selected: var(--color-charcoal-300);
|
||||
--interface-menu-keybind-surface-default: var(--color-charcoal-200);
|
||||
--interface-panel-surface: var(--color-charcoal-100);
|
||||
--interface-stroke: var(--color-charcoal-400);
|
||||
--nav-background: var(--color-charcoal-100);
|
||||
--node-border: var(--color-charcoal-500);
|
||||
--node-component-border: var(--color-stone-200);
|
||||
--node-component-border-error: var(--color-danger-100);
|
||||
--node-component-border-executing: var(--color-blue-500);
|
||||
@@ -231,7 +194,7 @@
|
||||
--node-component-slot-dot-outline: var(--color-white);
|
||||
--node-component-slot-text: var(--color-slate-200);
|
||||
--node-component-surface-highlight: var(--color-slate-100);
|
||||
--node-component-surface-hovered: var(--color-charcoal-600);
|
||||
--node-component-surface-hovered: var(--color-charcoal-400);
|
||||
--node-component-surface-selected: var(--color-charcoal-200);
|
||||
--node-component-surface: var(--color-charcoal-800);
|
||||
--node-component-tooltip: var(--color-white);
|
||||
@@ -239,32 +202,16 @@
|
||||
--node-component-tooltip-surface: var(--color-charcoal-800);
|
||||
--node-component-widget-skeleton-surface: var(--color-zinc-800);
|
||||
--node-component-disabled: var(--color-alpha-charcoal-600-30);
|
||||
--node-divider: var(--color-charcoal-500);
|
||||
--node-icon-disabled: var(--color-alpha-stone-100-20);
|
||||
--node-stroke: var(--color-stone-200);
|
||||
--node-stroke-selected: var(--color-pure-white);
|
||||
--node-stroke-error: var(--color-error);
|
||||
--node-stroke-executing: var(--color-blue-100);
|
||||
--text-secondary: var(--color-slate-100);
|
||||
--text-primary: var(--color-pure-white);
|
||||
--input-surface: rgba(130, 130, 130, 0.1);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-backdrop: var(--backdrop);
|
||||
--color-button-active-surface: var(--button-active-surface);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-icon: var(--button-icon);
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-interface-menu-component-surface-hovered: var(--interface-menu-component-surface-hovered);
|
||||
--color-interface-menu-component-surface-selected: var(--interface-menu-component-surface-selected);
|
||||
--color-interface-menu-keybind-surface-default: var(--interface-menu-keybind-surface-default);
|
||||
--color-interface-panel-surface: var(--interface-panel-surface);
|
||||
--color-interface-stroke: var(--interface-stroke);
|
||||
--color-nav-background: var(--nav-background);
|
||||
--color-node-border: var(--node-border);
|
||||
--color-node-component-border: var(--node-component-border);
|
||||
--color-node-component-executing: var(--node-component-executing);
|
||||
--color-node-component-header: var(--node-component-header);
|
||||
@@ -297,15 +244,11 @@
|
||||
--node-component-widget-skeleton-surface
|
||||
);
|
||||
--color-node-component-disabled: var(--node-component-disabled);
|
||||
--color-node-divider: var(--node-divider);
|
||||
--color-node-icon-disabled: var(--node-icon-disabled);
|
||||
--color-node-stroke: var(--node-stroke);
|
||||
--color-node-stroke-selected: var(--node-stroke-selected);
|
||||
--color-node-stroke-error: var(--node-stroke-error);
|
||||
--color-node-stroke-executing: var(--node-stroke-executing);
|
||||
--color-text-secondary: var(--text-secondary);
|
||||
--color-text-primary: var(--text-primary);
|
||||
--color-input-surface: var(--input-surface);
|
||||
}
|
||||
|
||||
@custom-variant dark-theme {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 9.99996L11.9427 7.94263C11.6926 7.69267 11.3536 7.55225 11 7.55225C10.6464 7.55225 10.3074 7.69267 10.0573 7.94263L9 9M8 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.51377 12.671L4.77612 14.3921C4.67222 14.6346 4.32853 14.6346 4.22463 14.3921L3.48699 12.671C3.45664 12.6002 3.40022 12.5437 3.32942 12.5134L1.60825 11.7757C1.36581 11.6718 1.36581 11.3282 1.60825 11.2243L3.32942 10.4866C3.40022 10.4563 3.45664 10.3998 3.48699 10.329L4.22463 8.60787C4.32853 8.36544 4.67222 8.36544 4.77612 8.60787L5.51377 10.329C5.54411 10.3998 5.60053 10.4563 5.67134 10.4866L7.39251 11.2243C7.63494 11.3282 7.63494 11.6718 7.39251 11.7757L5.67134 12.5134C5.60053 12.5437 5.54411 12.6002 5.51377 12.671Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5 5H5.0001" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,14 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 16 16" fill="none">
|
||||
<g clip-path="url(#clip0_704_2695)">
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="currentColor" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="#9C9EAB" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_704_2695">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 964 B After Width: | Height: | Size: 938 B |
@@ -474,93 +474,3 @@ export function formatDuration(milliseconds: number): string {
|
||||
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
// Module scope constants to avoid re-initialization on every call
|
||||
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']
|
||||
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi']
|
||||
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac']
|
||||
const THREE_D_EXTENSIONS = ['obj', 'fbx', 'gltf', 'glb']
|
||||
|
||||
/**
|
||||
* Truncates a filename while preserving the extension
|
||||
* @param filename The filename to truncate
|
||||
* @param maxLength Maximum length for the filename without extension
|
||||
* @returns Truncated filename with extension preserved
|
||||
*/
|
||||
export function truncateFilename(
|
||||
filename: string,
|
||||
maxLength: number = 20
|
||||
): string {
|
||||
if (!filename || filename.length <= maxLength) {
|
||||
return filename
|
||||
}
|
||||
|
||||
const lastDotIndex = filename.lastIndexOf('.')
|
||||
const nameWithoutExt =
|
||||
lastDotIndex > -1 ? filename.substring(0, lastDotIndex) : filename
|
||||
const extension = lastDotIndex > -1 ? filename.substring(lastDotIndex) : ''
|
||||
|
||||
// If the name without extension is short enough, return as is
|
||||
if (nameWithoutExt.length <= maxLength) {
|
||||
return filename
|
||||
}
|
||||
|
||||
// Calculate how to split the truncation
|
||||
const halfLength = Math.floor((maxLength - 3) / 2) // -3 for '...'
|
||||
const start = nameWithoutExt.substring(0, halfLength)
|
||||
const end = nameWithoutExt.substring(nameWithoutExt.length - halfLength)
|
||||
|
||||
return `${start}...${end}${extension}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the media type from a filename's extension (singular form)
|
||||
* @param filename The filename to analyze
|
||||
* @returns The media type: 'image', 'video', 'audio', or '3D'
|
||||
*/
|
||||
export function getMediaTypeFromFilename(
|
||||
filename: string
|
||||
): 'image' | 'video' | 'audio' | '3D' {
|
||||
if (!filename) return 'image'
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
if (!ext) return 'image'
|
||||
|
||||
if (IMAGE_EXTENSIONS.includes(ext)) return 'image'
|
||||
if (VIDEO_EXTENSIONS.includes(ext)) return 'video'
|
||||
if (AUDIO_EXTENSIONS.includes(ext)) return 'audio'
|
||||
if (THREE_D_EXTENSIONS.includes(ext)) return '3D'
|
||||
|
||||
return 'image'
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getMediaTypeFromFilename instead - returns plural form for legacy compatibility
|
||||
* @param filename The filename to analyze
|
||||
* @returns The media type in plural form: 'images', 'videos', 'audios', '3D'
|
||||
*/
|
||||
export function getMediaTypeFromFilenamePlural(filename: string): string {
|
||||
const type = getMediaTypeFromFilename(filename)
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return 'images'
|
||||
case 'video':
|
||||
return 'videos'
|
||||
case 'audio':
|
||||
return 'audios'
|
||||
case '3D':
|
||||
return '3D'
|
||||
default:
|
||||
return 'images'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getMediaTypeFromFilename instead - kept for backward compatibility
|
||||
* @param filename The filename to analyze
|
||||
* @returns The media kind: 'image', 'video', 'audio', or '3D'
|
||||
*/
|
||||
export function getMediaKindFromFilename(
|
||||
filename: string
|
||||
): 'image' | 'video' | 'audio' | '3D' {
|
||||
return getMediaTypeFromFilename(filename)
|
||||
}
|
||||
|
||||
252
pnpm-lock.yaml
generated
@@ -183,18 +183,9 @@ catalogs:
|
||||
lint-staged:
|
||||
specifier: ^15.2.7
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
mixpanel-browser:
|
||||
specifier: ^2.71.0
|
||||
version: 2.71.0
|
||||
nx:
|
||||
specifier: 21.4.1
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
pinia:
|
||||
specifier: ^2.1.7
|
||||
version: 2.2.2
|
||||
@@ -202,20 +193,14 @@ catalogs:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
primeicons:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
primevue:
|
||||
specifier: ^4.2.5
|
||||
version: 4.2.5
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4
|
||||
storybook:
|
||||
specifier: ^9.1.6
|
||||
version: 9.1.6
|
||||
@@ -266,7 +251,7 @@ catalogs:
|
||||
version: 3.5.13
|
||||
vue-component-type-helpers:
|
||||
specifier: ^3.0.7
|
||||
version: 3.1.1
|
||||
version: 3.1.0
|
||||
vue-eslint-parser:
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
@@ -488,7 +473,7 @@ importers:
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/storybook':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
'@nx/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vitest@3.2.4)
|
||||
@@ -500,19 +485,19 @@ importers:
|
||||
version: 1.52.0
|
||||
'@storybook/addon-docs':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/vue3':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/vue3-vite':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.12(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.0
|
||||
@@ -560,10 +545,10 @@ importers:
|
||||
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
|
||||
eslint-plugin-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2)
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0-beta.0(tailwindcss@4.1.12)
|
||||
@@ -597,33 +582,18 @@ importers:
|
||||
lint-staged:
|
||||
specifier: 'catalog:'
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.4
|
||||
mixpanel-browser:
|
||||
specifier: 'catalog:'
|
||||
version: 2.71.0
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
postcss-html:
|
||||
specifier: 'catalog:'
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
rollup-plugin-visualizer:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.4(rollup@4.22.4)
|
||||
version: 3.3.2
|
||||
storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
stylelint:
|
||||
specifier: 'catalog:'
|
||||
version: 16.24.0(typescript@5.9.2)
|
||||
@@ -671,7 +641,7 @@ importers:
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-type-helpers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.1
|
||||
version: 3.1.0
|
||||
vue-eslint-parser:
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
|
||||
@@ -2207,21 +2177,6 @@ packages:
|
||||
'@microsoft/tsdoc@0.15.1':
|
||||
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
|
||||
|
||||
'@mixpanel/rrdom@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-vX/tbnS14ZzzatC7vOyvAm9tOLU8tof0BuppBlphzEx1YHTSw8DQiAmyAc0AmXidchLV0W+cUHV/WsehPLh2hQ==}
|
||||
|
||||
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-2kSnjZZ3QZ9zOz/isOt8s54mXUUDgXk/u0eEi/rE0xBWDeuA0NHrBcqiMc+w4F/yWWUpo5F5zcuPeYpc6ufAsw==}
|
||||
|
||||
'@mixpanel/rrweb-types@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-ucIYe1mfJ2UksvXW+d3bOySTB2/0yUSqQJlUydvbBz6OO2Bhq3nJHyLXV9ExkgUMZm1ZyDcvvmNUd1+5tAXlpA==}
|
||||
|
||||
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-OomKIB6GTx5xvCLJ7iic2khT/t/tnCJUex13aEqsbSqIT/UzUUsqf+LTrgUK5ex+f6odmkCNjre2y5jvpNqn+g==}
|
||||
|
||||
'@mixpanel/rrweb@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-J3dVTEu6Z4p8di7y9KKvUooNuBjX97DdG6XGWoPEPi07A9512h9M8MEtvlY3mK0PGfuC0Mz5Pv/Ws6gjGYfKQg==}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@@ -3041,9 +2996,6 @@ packages:
|
||||
'@types/chai@5.2.2':
|
||||
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
|
||||
|
||||
'@types/css-font-loading-module@0.0.7':
|
||||
resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
@@ -3542,9 +3494,6 @@ packages:
|
||||
'@webgpu/types@0.1.51':
|
||||
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
|
||||
|
||||
'@xstate/fsm@1.6.5':
|
||||
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
|
||||
|
||||
'@xterm/addon-fit@0.10.0':
|
||||
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
|
||||
peerDependencies:
|
||||
@@ -3827,10 +3776,6 @@ packages:
|
||||
balanced-match@2.0.0:
|
||||
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
|
||||
|
||||
base64-arraybuffer@1.0.2:
|
||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
@@ -4941,6 +4886,9 @@ packages:
|
||||
get-tsconfig@4.10.1:
|
||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -6018,9 +5966,6 @@ packages:
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mixpanel-browser@2.71.0:
|
||||
resolution: {integrity: sha512-jKmDXe68/oQFgk/9ns9Z36bA0CJ31PH8Y77XTLLGfJvhsUPbvu+7Se9e281NejZF6+OMqx7cE+zFxToozYyNrA==}
|
||||
|
||||
mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6048,6 +5993,11 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.5:
|
||||
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
||||
engines: {node: ^18 || >=20}
|
||||
@@ -6395,6 +6345,10 @@ packages:
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.5.1:
|
||||
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -6407,15 +6361,11 @@ packages:
|
||||
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
prettier@3.3.2:
|
||||
resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-bytes@7.1.0:
|
||||
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
@@ -6718,19 +6668,6 @@ packages:
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
rollup-plugin-visualizer@6.0.4:
|
||||
resolution: {integrity: sha512-q8Q7J/6YofkmaGW1sH/fPRAz37x/+pd7VBuaUU7lwvOS/YikuiiEU9jeb9PH8XHiq50XFrUsBbOxeAMYQ7KZkg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
rolldown: 1.x || ^1.0.0-beta
|
||||
rollup: 2.x || 3.x || 4.x
|
||||
peerDependenciesMeta:
|
||||
rolldown:
|
||||
optional: true
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
rollup@4.22.4:
|
||||
resolution: {integrity: sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -6885,10 +6822,6 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map@0.7.6:
|
||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
speakingurl@14.0.1:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7517,6 +7450,9 @@ packages:
|
||||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-component-type-helpers@3.1.0:
|
||||
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
|
||||
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
@@ -9533,29 +9469,6 @@ snapshots:
|
||||
|
||||
'@microsoft/tsdoc@0.15.1': {}
|
||||
|
||||
'@mixpanel/rrdom@2.0.0-alpha.18.2':
|
||||
dependencies:
|
||||
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
|
||||
|
||||
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
|
||||
'@mixpanel/rrweb-types@2.0.0-alpha.18.2': {}
|
||||
|
||||
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2': {}
|
||||
|
||||
'@mixpanel/rrweb@2.0.0-alpha.18.2':
|
||||
dependencies:
|
||||
'@mixpanel/rrdom': 2.0.0-alpha.18.2
|
||||
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
|
||||
'@mixpanel/rrweb-types': 2.0.0-alpha.18.2
|
||||
'@mixpanel/rrweb-utils': 2.0.0-alpha.18.2
|
||||
'@types/css-font-loading-module': 0.0.7
|
||||
'@xstate/fsm': 1.6.5
|
||||
base64-arraybuffer: 1.0.2
|
||||
mitt: 3.0.1
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.4.5
|
||||
@@ -9732,7 +9645,7 @@ snapshots:
|
||||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@nx/cypress': 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/devkit': 21.4.1(nx@21.4.1)
|
||||
@@ -9740,7 +9653,7 @@ snapshots:
|
||||
'@nx/js': 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)
|
||||
'@phenomnomnominal/tsquery': 5.0.1(typescript@5.9.2)
|
||||
semver: 7.7.2
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
@@ -10091,29 +10004,29 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.0(@types/react@19.1.9)(react@19.1.1)
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
unplugin: 1.16.1
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
@@ -10123,19 +10036,19 @@ snapshots:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
find-package-json: 1.2.0
|
||||
magic-string: 0.30.19
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
typescript: 5.9.2
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-meta: 2.2.12(typescript@5.9.2)
|
||||
@@ -10143,10 +10056,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.1
|
||||
@@ -10411,7 +10324,7 @@ snapshots:
|
||||
'@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
|
||||
'@tiptap/pm': 2.10.4
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)':
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.28.3
|
||||
'@babel/parser': 7.28.4
|
||||
@@ -10419,7 +10332,7 @@ snapshots:
|
||||
'@babel/types': 7.28.4
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: 4.17.21
|
||||
prettier: 3.6.2
|
||||
prettier: 3.3.2
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
transitivePeerDependencies:
|
||||
@@ -10444,8 +10357,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
|
||||
'@types/css-font-loading-module@0.0.7': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@@ -11039,8 +10950,6 @@ snapshots:
|
||||
|
||||
'@webgpu/types@0.1.51': {}
|
||||
|
||||
'@xstate/fsm@1.6.5': {}
|
||||
|
||||
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 5.5.0
|
||||
@@ -11357,8 +11266,6 @@ snapshots:
|
||||
|
||||
balanced-match@2.0.0: {}
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-opn@3.0.2:
|
||||
@@ -12203,20 +12110,20 @@ snapshots:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
prettier: 3.6.2
|
||||
prettier: 3.3.2
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.11.11
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.8(eslint@9.35.0(jiti@2.4.2))
|
||||
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -12655,6 +12562,10 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -13927,10 +13838,6 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mixpanel-browser@2.71.0:
|
||||
dependencies:
|
||||
'@mixpanel/rrweb': 2.0.0-alpha.18.2
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
mlly@1.8.0:
|
||||
@@ -13953,6 +13860,8 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
nanoid@5.1.5: {}
|
||||
|
||||
napi-postinstall@0.3.3: {}
|
||||
@@ -14332,14 +14241,14 @@ snapshots:
|
||||
dependencies:
|
||||
htmlparser2: 8.0.2
|
||||
js-tokens: 9.0.1
|
||||
postcss: 8.5.6
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.6)
|
||||
postcss: 8.5.1
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.1)
|
||||
|
||||
postcss-resolve-nested-selector@0.1.6: {}
|
||||
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.6):
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.1):
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
postcss: 8.5.1
|
||||
|
||||
postcss-safe-parser@7.0.1(postcss@8.5.6):
|
||||
dependencies:
|
||||
@@ -14357,6 +14266,12 @@ snapshots:
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.5.1:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@@ -14369,9 +14284,7 @@ snapshots:
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
|
||||
prettier@3.6.2: {}
|
||||
|
||||
pretty-bytes@7.1.0: {}
|
||||
prettier@3.3.2: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
@@ -14820,15 +14733,6 @@ snapshots:
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rollup-plugin-visualizer@6.0.4(rollup@4.22.4):
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 4.0.3
|
||||
source-map: 0.7.6
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
rollup: 4.22.4
|
||||
|
||||
rollup@4.22.4:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
@@ -15020,8 +14924,6 @@ snapshots:
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
source-map@0.7.6: {}
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
sprintf-js@1.0.3: {}
|
||||
@@ -15048,7 +14950,7 @@ snapshots:
|
||||
internal-slot: 1.1.0
|
||||
optional: true
|
||||
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@testing-library/jest-dom': 6.6.4
|
||||
@@ -15063,7 +14965,7 @@ snapshots:
|
||||
semver: 7.7.2
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
prettier: 3.6.2
|
||||
prettier: 3.3.2
|
||||
transitivePeerDependencies:
|
||||
- '@testing-library/dom'
|
||||
- bufferutil
|
||||
@@ -15385,7 +15287,7 @@ snapshots:
|
||||
tsx@4.19.4:
|
||||
dependencies:
|
||||
esbuild: 0.25.5
|
||||
get-tsconfig: 4.10.1
|
||||
get-tsconfig: 4.7.5
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
@@ -15737,7 +15639,7 @@ snapshots:
|
||||
vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.6
|
||||
postcss: 8.5.1
|
||||
rollup: 4.22.4
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.10
|
||||
@@ -15802,6 +15704,8 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-component-type-helpers@3.1.0: {}
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
|
||||
@@ -62,16 +62,12 @@ catalog:
|
||||
jsdom: ^26.1.0
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
markdown-table: ^3.0.4
|
||||
nx: 21.4.1
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.6.2
|
||||
pretty-bytes: ^7.1.0
|
||||
prettier: ^3.3.2
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
rollup-plugin-visualizer: ^6.0.4
|
||||
storybook: ^9.1.6
|
||||
stylelint: ^16.24.0
|
||||
tailwindcss: ^4.1.12
|
||||
@@ -98,10 +94,12 @@ catalog:
|
||||
zod: ^3.23.8
|
||||
zod-to-json-schema: ^3.24.1
|
||||
zod-validation-error: ^3.3.0
|
||||
mixpanel-browser: ^2.71.0
|
||||
|
||||
cleanupUnusedCatalogs: true
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@firebase/util'
|
||||
- protobufjs
|
||||
@@ -116,6 +114,3 @@ onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- nx
|
||||
- oxc-resolver
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Authentication Service Worker
|
||||
* Intercepts /api/view requests and adds Firebase authentication headers.
|
||||
* Required for browser-native requests (img, video, audio) that cannot send custom headers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AuthHeader
|
||||
* @property {string} Authorization - Bearer token for authentication
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CachedAuth
|
||||
* @property {AuthHeader|null} header
|
||||
* @property {number} expiresAt - Timestamp when cache expires
|
||||
*/
|
||||
|
||||
const CACHE_TTL_MS = 50 * 60 * 1000 // 50 minutes (Firebase tokens expire in 1 hour)
|
||||
|
||||
/** @type {CachedAuth|null} */
|
||||
let authCache = null
|
||||
|
||||
/** @type {Promise<AuthHeader|null>|null} */
|
||||
let authRequestInFlight = null
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'INVALIDATE_AUTH_HEADER') {
|
||||
authCache = null
|
||||
authRequestInFlight = null
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url)
|
||||
|
||||
if (
|
||||
!url.pathname.startsWith('/api/view') &&
|
||||
!url.pathname.startsWith('/api/viewvideo')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
const authHeader = await getAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
return fetch(event.request)
|
||||
}
|
||||
|
||||
const headers = new Headers(event.request.headers)
|
||||
for (const [key, value] of Object.entries(authHeader)) {
|
||||
headers.set(key, value)
|
||||
}
|
||||
|
||||
return fetch(
|
||||
new Request(event.request.url, {
|
||||
method: event.request.method,
|
||||
headers: headers,
|
||||
mode: 'same-origin',
|
||||
credentials: event.request.credentials,
|
||||
cache: 'no-store',
|
||||
redirect: event.request.redirect,
|
||||
referrer: event.request.referrer,
|
||||
integrity: event.request.integrity
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[Auth SW] Request failed:', error)
|
||||
return fetch(event.request)
|
||||
}
|
||||
})()
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Gets auth header from cache or requests from main thread
|
||||
* @returns {Promise<AuthHeader|null>}
|
||||
*/
|
||||
async function getAuthHeader() {
|
||||
// Return cached value if valid
|
||||
if (authCache && authCache.expiresAt > Date.now()) {
|
||||
return authCache.header
|
||||
}
|
||||
|
||||
// Clear expired cache
|
||||
if (authCache) {
|
||||
authCache = null
|
||||
}
|
||||
|
||||
// Deduplicate concurrent requests
|
||||
if (authRequestInFlight) {
|
||||
return authRequestInFlight
|
||||
}
|
||||
|
||||
authRequestInFlight = requestAuthHeaderFromMainThread()
|
||||
const header = await authRequestInFlight
|
||||
authRequestInFlight = null
|
||||
|
||||
// Cache the result
|
||||
if (header) {
|
||||
authCache = {
|
||||
header,
|
||||
expiresAt: Date.now() + CACHE_TTL_MS
|
||||
}
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests auth header from main thread via MessageChannel
|
||||
* @returns {Promise<AuthHeader|null>}
|
||||
*/
|
||||
async function requestAuthHeaderFromMainThread() {
|
||||
const clients = await self.clients.matchAll()
|
||||
if (clients.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const messageChannel = new MessageChannel()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let timeoutId
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
clearTimeout(timeoutId)
|
||||
resolve(event.data.authHeader)
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
console.error(
|
||||
'[Auth SW] Timeout waiting for auth header from main thread'
|
||||
)
|
||||
resolve(null)
|
||||
}, 1000)
|
||||
|
||||
clients[0].postMessage({ type: 'REQUEST_AUTH_HEADER' }, [
|
||||
messageChannel.port2
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
@@ -1,130 +0,0 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Bundle categorization configuration
|
||||
*
|
||||
* This file defines how bundles are categorized in size reports.
|
||||
* Categories help identify which parts of the application are growing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleCategory
|
||||
* @property {string} name - Display name of the category
|
||||
* @property {string} description - Description of what this category includes
|
||||
* @property {RegExp[]} patterns - Regex patterns to match bundle files
|
||||
* @property {number} order - Sort order for display (lower = first)
|
||||
*/
|
||||
|
||||
/** @type {BundleCategory[]} */
|
||||
export const BUNDLE_CATEGORIES = [
|
||||
{
|
||||
name: 'App Entry Points',
|
||||
description: 'Main entry bundles and manifests',
|
||||
patterns: [/^index-.*\.js$/i, /^manifest-.*\.js$/i],
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
name: 'Graph Workspace',
|
||||
description: 'Graph editor runtime, canvas, workflow orchestration',
|
||||
patterns: [
|
||||
/Graph(View|State)?-.*\.js$/i,
|
||||
/(Canvas|Workflow|History|NodeGraph|Compositor)-.*\.js$/i
|
||||
],
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
name: 'Views & Navigation',
|
||||
description: 'Top-level views, pages, and routed surfaces',
|
||||
patterns: [/.*(View|Page|Layout|Screen|Route)-.*\.js$/i],
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
name: 'Panels & Settings',
|
||||
description: 'Configuration panels, inspectors, and settings screens',
|
||||
patterns: [/.*(Panel|Settings|Config|Preferences|Manager)-.*\.js$/i],
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
name: 'User & Accounts',
|
||||
description: 'Authentication, profile, and account management bundles',
|
||||
patterns: [
|
||||
/.*((User(Panel|Select|Auth|Account|Profile|Settings|Preferences|Manager|List|Menu|Modal))|Account|Auth|Profile|Login|Signup|Password).*-.+\.js$/i
|
||||
],
|
||||
order: 5
|
||||
},
|
||||
{
|
||||
name: 'Editors & Dialogs',
|
||||
description: 'Modals, dialogs, drawers, and in-app editors',
|
||||
patterns: [/.*(Modal|Dialog|Drawer|Editor)-.*\.js$/i],
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
name: 'UI Components',
|
||||
description: 'Reusable component library chunks',
|
||||
patterns: [
|
||||
/.*(Button|Avatar|Badge|Dropdown|Tabs|Table|List|Card|Form|Input|Toggle|Menu|Toolbar|Sidebar)-.*\.js$/i,
|
||||
/.*\.vue_vue_type_script_setup_true_lang-.*\.js$/i
|
||||
],
|
||||
order: 7
|
||||
},
|
||||
{
|
||||
name: 'Data & Services',
|
||||
description: 'Stores, services, APIs, and repositories',
|
||||
patterns: [/.*(Service|Store|Api|Repository)-.*\.js$/i],
|
||||
order: 8
|
||||
},
|
||||
{
|
||||
name: 'Utilities & Hooks',
|
||||
description: 'Helpers, composables, and utility bundles',
|
||||
patterns: [
|
||||
/.*(Util|Utils|Helper|Composable|Hook)-.*\.js$/i,
|
||||
/use[A-Z].*\.js$/
|
||||
],
|
||||
order: 9
|
||||
},
|
||||
{
|
||||
name: 'Vendor & Third-Party',
|
||||
description: 'External libraries and shared vendor chunks',
|
||||
patterns: [
|
||||
/^(chunk|vendor|prime|three|lodash|chart|firebase|yjs|axios|uuid)-.*\.js$/i
|
||||
],
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
description: 'Bundles that do not match a named category',
|
||||
patterns: [/.*/],
|
||||
order: 99
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Categorize a bundle file based on its name
|
||||
*
|
||||
* @param {string} fileName - The bundle file name (e.g., "assets/GraphView-BnV6iF9h.js")
|
||||
* @returns {string} - The category name
|
||||
*/
|
||||
export function categorizeBundle(fileName) {
|
||||
// Extract just the file name without path
|
||||
const baseName = fileName.split('/').pop() || fileName
|
||||
|
||||
// Find the first matching category
|
||||
for (const category of BUNDLE_CATEGORIES) {
|
||||
for (const pattern of category.patterns) {
|
||||
if (pattern.test(baseName)) {
|
||||
return category.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category metadata by name
|
||||
*
|
||||
* @param {string} categoryName - The category name
|
||||
* @returns {BundleCategory | undefined} - The category metadata
|
||||
*/
|
||||
export function getCategoryMetadata(categoryName) {
|
||||
return BUNDLE_CATEGORIES.find((cat) => cat.name === categoryName)
|
||||
}
|
||||
87
scripts/cicd/build-failed-screenshot-manifest.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type {
|
||||
JSONReport,
|
||||
JSONReportSpec,
|
||||
JSONReportSuite,
|
||||
JSONReportTestResult
|
||||
} from '@playwright/test/reporter'
|
||||
import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
const getArg = (flag: string, fallback: string) => {
|
||||
const i = argv.indexOf(flag)
|
||||
if (i >= 0 && i + 1 < argv.length) return argv[i + 1]
|
||||
return fallback
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Defaults mirror the workflow layout
|
||||
const reportPath = getArg(
|
||||
'--report',
|
||||
path.join('playwright-report', 'report.json')
|
||||
)
|
||||
const outDir = getArg('--out', path.join('ci-rerun'))
|
||||
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
throw Error(`Report not found at ${reportPath}`)
|
||||
}
|
||||
|
||||
const raw = await fsp.readFile(reportPath, 'utf8')
|
||||
|
||||
let data: JSONReport
|
||||
try {
|
||||
data = JSON.parse(raw)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse Playwright JSON report at ${reportPath}. ` +
|
||||
`The report file may be corrupted or incomplete. ` +
|
||||
`Error: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
|
||||
const hasScreenshotSignal = (r: JSONReportTestResult) => {
|
||||
return r.attachments.some((att) => att?.contentType?.startsWith('image/'))
|
||||
}
|
||||
|
||||
const out = new Map<string, Set<string>>()
|
||||
|
||||
const collectFailedScreenshots = (suite?: JSONReportSuite) => {
|
||||
if (!suite) return
|
||||
const childSuites = suite.suites ?? []
|
||||
for (const childSuite of childSuites) collectFailedScreenshots(childSuite)
|
||||
const specs: JSONReportSpec[] = suite.specs ?? []
|
||||
for (const spec of specs) {
|
||||
const file = spec.file
|
||||
const line = spec.line
|
||||
const loc = `${file}:${line}`
|
||||
for (const test of spec.tests) {
|
||||
const project = test.projectId
|
||||
const last = test.results[test.results.length - 1]
|
||||
const failedScreenshot =
|
||||
last && last.status === 'failed' && hasScreenshotSignal(last)
|
||||
if (!failedScreenshot) continue
|
||||
if (!out.has(project)) out.set(project, new Set())
|
||||
const projectSet = out.get(project)
|
||||
if (projectSet) {
|
||||
projectSet.add(loc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const report: JSONReport = data
|
||||
const rootSuites = report.suites ?? []
|
||||
for (const suite of rootSuites) collectFailedScreenshots(suite)
|
||||
|
||||
await fsp.mkdir(outDir, { recursive: true })
|
||||
for (const [project, set] of out.entries()) {
|
||||
const f = path.join(outDir, `${project}.txt`)
|
||||
await fsp.writeFile(f, Array.from(set).join('\n') + '\n', 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Manifest generation failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,90 +0,0 @@
|
||||
// @ts-check
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { brotliCompressSync, gzipSync } from 'node:zlib'
|
||||
import pico from 'picocolors'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { categorizeBundle } from './bundle-categories.js'
|
||||
|
||||
const distDir = path.resolve('dist')
|
||||
const sizeDir = path.resolve('temp/size')
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {string} file
|
||||
* @property {string} category
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main function to collect bundle size data
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(distDir)) {
|
||||
console.error(pico.red('Error: dist directory does not exist'))
|
||||
console.error(pico.yellow('Please run "pnpm build" first'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(pico.blue('\nCollecting bundle size data...\n'))
|
||||
|
||||
// Collect main bundle files from dist/assets
|
||||
const assetsDir = path.join(distDir, 'assets')
|
||||
const bundles = []
|
||||
|
||||
if (existsSync(assetsDir)) {
|
||||
const files = await readdir(assetsDir)
|
||||
const jsFiles = files.filter(
|
||||
(file) => file.endsWith('.js') && !file.includes('legacy')
|
||||
)
|
||||
|
||||
for (const file of jsFiles) {
|
||||
const filePath = path.join(assetsDir, file)
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
const size = Buffer.byteLength(content)
|
||||
const gzip = gzipSync(content).length
|
||||
const brotli = brotliCompressSync(content).length
|
||||
const fileName = `assets/${file}`
|
||||
const category = categorizeBundle(fileName)
|
||||
|
||||
bundles.push({
|
||||
file: fileName,
|
||||
category,
|
||||
size,
|
||||
gzip,
|
||||
brotli
|
||||
})
|
||||
|
||||
console.log(
|
||||
`${pico.green(file)} ${pico.dim(`[${category}]`)} - ` +
|
||||
`Size: ${prettyBytes(size)} / ` +
|
||||
`Gzip: ${prettyBytes(gzip)} / ` +
|
||||
`Brotli: ${prettyBytes(brotli)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create temp/size directory
|
||||
await mkdir(sizeDir, { recursive: true })
|
||||
|
||||
// Write individual bundle files
|
||||
for (const bundle of bundles) {
|
||||
const fileName = bundle.file.replace(/[/\\]/g, '_').replace('.js', '.json')
|
||||
await writeFile(
|
||||
path.join(sizeDir, fileName),
|
||||
JSON.stringify(bundle, null, 2),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
pico.green(`\n✓ Collected size data for ${bundles.length} bundles\n`)
|
||||
)
|
||||
console.log(pico.blue(`Data saved to: ${sizeDir}\n`))
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
// @ts-check
|
||||
import { markdownTable } from 'markdown-table'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { getCategoryMetadata } from './bundle-categories.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeMetrics
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SizeResult & { file: string, category?: string }} BundleResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'added' | 'removed' | 'increased' | 'decreased' | 'unchanged'} BundleStatus
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleDiff
|
||||
* @property {string} fileName
|
||||
* @property {BundleResult | undefined} curr
|
||||
* @property {BundleResult | undefined} prev
|
||||
* @property {SizeMetrics} diff
|
||||
* @property {BundleStatus} status
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CountSummary
|
||||
* @property {number} added
|
||||
* @property {number} removed
|
||||
* @property {number} increased
|
||||
* @property {number} decreased
|
||||
* @property {number} unchanged
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CategoryReport
|
||||
* @property {string} name
|
||||
* @property {string | undefined} description
|
||||
* @property {number} order
|
||||
* @property {{ current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }} metrics
|
||||
* @property {CountSummary} counts
|
||||
* @property {BundleDiff[]} bundles
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleReport
|
||||
* @property {CategoryReport[]} categories
|
||||
* @property {{ currentBundles: number, baselineBundles: number, metrics: { current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }, counts: CountSummary }} overall
|
||||
* @property {boolean} hasBaseline
|
||||
*/
|
||||
|
||||
const currDir = path.resolve('temp/size')
|
||||
const prevDir = path.resolve('temp/size-prev')
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main entry for generating the size report
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(currDir)) {
|
||||
console.error('Error: temp/size directory does not exist')
|
||||
console.error('Please run "pnpm size:collect" first')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const report = await buildBundleReport()
|
||||
const output = renderReport(report)
|
||||
process.stdout.write(output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build bundle comparison data from current and baseline artifacts
|
||||
* @returns {Promise<BundleReport>}
|
||||
*/
|
||||
async function buildBundleReport() {
|
||||
/**
|
||||
* @param {string[]} files
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
|
||||
|
||||
const currFiles = filterFiles(await readdir(currDir))
|
||||
const baselineFiles = existsSync(prevDir)
|
||||
? filterFiles(await readdir(prevDir))
|
||||
: []
|
||||
const fileList = new Set([...currFiles, ...baselineFiles])
|
||||
|
||||
/** @type {Map<string, CategoryReport>} */
|
||||
const categories = new Map()
|
||||
|
||||
const overall = {
|
||||
currentBundles: 0,
|
||||
baselineBundles: 0,
|
||||
metrics: {
|
||||
current: createMetrics(),
|
||||
baseline: createMetrics(),
|
||||
diff: createMetrics()
|
||||
},
|
||||
counts: createCounts()
|
||||
}
|
||||
|
||||
for (const file of fileList) {
|
||||
const currPath = path.resolve(currDir, file)
|
||||
const prevPath = path.resolve(prevDir, file)
|
||||
|
||||
const curr = await importJSON(currPath)
|
||||
const prev = await importJSON(prevPath)
|
||||
const fileName = curr?.file || prev?.file
|
||||
if (!fileName) continue
|
||||
|
||||
const categoryName = curr?.category || prev?.category || 'Other'
|
||||
const category = ensureCategoryEntry(categories, categoryName)
|
||||
|
||||
const currMetrics = toMetrics(curr)
|
||||
const baselineMetrics = toMetrics(prev)
|
||||
const diffMetrics = subtractMetrics(currMetrics, baselineMetrics)
|
||||
const status = getStatus(curr, prev, diffMetrics.size)
|
||||
|
||||
if (curr) {
|
||||
overall.currentBundles++
|
||||
}
|
||||
if (prev) {
|
||||
overall.baselineBundles++
|
||||
}
|
||||
|
||||
addMetrics(overall.metrics.current, currMetrics)
|
||||
addMetrics(overall.metrics.baseline, baselineMetrics)
|
||||
addMetrics(overall.metrics.diff, diffMetrics)
|
||||
incrementStatus(overall.counts, status)
|
||||
|
||||
addMetrics(category.metrics.current, currMetrics)
|
||||
addMetrics(category.metrics.baseline, baselineMetrics)
|
||||
addMetrics(category.metrics.diff, diffMetrics)
|
||||
incrementStatus(category.counts, status)
|
||||
|
||||
category.bundles.push({
|
||||
fileName,
|
||||
curr,
|
||||
prev,
|
||||
diff: diffMetrics,
|
||||
status
|
||||
})
|
||||
}
|
||||
|
||||
const sortedCategories = Array.from(categories.values()).sort(
|
||||
(a, b) => a.order - b.order
|
||||
)
|
||||
|
||||
return {
|
||||
categories: sortedCategories,
|
||||
overall,
|
||||
hasBaseline: baselineFiles.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the complete report in markdown
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderReport(report) {
|
||||
const parts = ['## Bundle Size Report\n']
|
||||
|
||||
parts.push(renderSummary(report))
|
||||
|
||||
if (report.categories.length > 0) {
|
||||
const glance = renderCategoryGlance(report)
|
||||
if (glance) {
|
||||
parts.push('\n' + glance)
|
||||
}
|
||||
parts.push('\n' + renderCategoryDetails(report))
|
||||
}
|
||||
|
||||
return (
|
||||
parts
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trimEnd() + '\n'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render overall summary bullets
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderSummary(report) {
|
||||
const { overall, hasBaseline } = report
|
||||
const lines = ['**Summary**']
|
||||
|
||||
const rawLineParts = [
|
||||
`- Raw size: ${prettyBytes(overall.metrics.current.size)}`
|
||||
]
|
||||
if (hasBaseline) {
|
||||
rawLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.size)}`)
|
||||
rawLineParts.push(`— ${formatDiffIndicator(overall.metrics.diff.size)}`)
|
||||
}
|
||||
lines.push(rawLineParts.join(' '))
|
||||
|
||||
const gzipLineParts = [`- Gzip: ${prettyBytes(overall.metrics.current.gzip)}`]
|
||||
if (hasBaseline) {
|
||||
gzipLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.gzip)}`)
|
||||
gzipLineParts.push(`— ${formatDiffIndicator(overall.metrics.diff.gzip)}`)
|
||||
}
|
||||
lines.push(gzipLineParts.join(' '))
|
||||
|
||||
const brotliLineParts = [
|
||||
`- Brotli: ${prettyBytes(overall.metrics.current.brotli)}`
|
||||
]
|
||||
if (hasBaseline) {
|
||||
brotliLineParts.push(
|
||||
`baseline ${prettyBytes(overall.metrics.baseline.brotli)}`
|
||||
)
|
||||
brotliLineParts.push(
|
||||
`— ${formatDiffIndicator(overall.metrics.diff.brotli)}`
|
||||
)
|
||||
}
|
||||
lines.push(brotliLineParts.join(' '))
|
||||
|
||||
const bundleStats = [`${overall.currentBundles} current`]
|
||||
if (hasBaseline) {
|
||||
bundleStats.push(`${overall.baselineBundles} baseline`)
|
||||
}
|
||||
|
||||
const statusParts = []
|
||||
if (overall.counts.added) statusParts.push(`${overall.counts.added} added`)
|
||||
if (overall.counts.removed)
|
||||
statusParts.push(`${overall.counts.removed} removed`)
|
||||
if (overall.counts.increased)
|
||||
statusParts.push(`${overall.counts.increased} grew`)
|
||||
if (overall.counts.decreased)
|
||||
statusParts.push(`${overall.counts.decreased} shrank`)
|
||||
|
||||
let bundlesLine = `- Bundles: ${bundleStats.join(' • ')}`
|
||||
if (statusParts.length > 0) {
|
||||
bundlesLine += ` • ${statusParts.join(' / ')}`
|
||||
}
|
||||
lines.push(bundlesLine)
|
||||
|
||||
if (!hasBaseline) {
|
||||
lines.push(
|
||||
'_Baseline artifact not found; showing current bundle sizes only._'
|
||||
)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a compact category glance line
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderCategoryGlance(report) {
|
||||
const { categories, hasBaseline } = report
|
||||
const relevant = categories.filter(
|
||||
(category) =>
|
||||
category.metrics.current.size > 0 ||
|
||||
(hasBaseline && category.metrics.baseline.size > 0)
|
||||
)
|
||||
|
||||
if (relevant.length === 0) return ''
|
||||
|
||||
const sorted = relevant.slice().sort((a, b) => {
|
||||
if (hasBaseline) {
|
||||
return (
|
||||
Math.abs(b.metrics.diff.size) - Math.abs(a.metrics.diff.size) ||
|
||||
b.metrics.current.size - a.metrics.current.size
|
||||
)
|
||||
}
|
||||
return b.metrics.current.size - a.metrics.current.size
|
||||
})
|
||||
|
||||
const limit = 6
|
||||
const trimmed = sorted.slice(0, limit)
|
||||
const parts = trimmed.map((category) => {
|
||||
const currentStr = prettyBytes(category.metrics.current.size)
|
||||
if (hasBaseline) {
|
||||
return `${category.name} ${formatDiffIndicator(category.metrics.diff.size)} (${currentStr})`
|
||||
}
|
||||
return `${category.name} ${currentStr}`
|
||||
})
|
||||
|
||||
if (sorted.length > limit) {
|
||||
parts.push(`+ ${sorted.length - limit} more`)
|
||||
}
|
||||
|
||||
return `**Category Glance**\n${parts.join(' · ')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render per-category detail tables wrapped in collapsible sections
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderCategoryDetails(report) {
|
||||
const lines = ['<details>', '<summary>Per-category breakdown</summary>', '']
|
||||
|
||||
for (const category of report.categories) {
|
||||
lines.push(renderCategoryBlock(category, report.hasBaseline))
|
||||
}
|
||||
|
||||
lines.push('</details>')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single category block with its table
|
||||
* @param {CategoryReport} category
|
||||
* @param {boolean} hasBaseline
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderCategoryBlock(category, hasBaseline) {
|
||||
const lines = ['<details>']
|
||||
const currentStr = prettyBytes(category.metrics.current.size)
|
||||
const summaryParts = [`<summary>${category.name} — ${currentStr}`]
|
||||
|
||||
if (hasBaseline) {
|
||||
summaryParts.push(
|
||||
` (baseline ${prettyBytes(category.metrics.baseline.size)}) • ${formatDiffIndicator(category.metrics.diff.size)}`
|
||||
)
|
||||
}
|
||||
|
||||
summaryParts.push('</summary>')
|
||||
lines.push(summaryParts.join(''))
|
||||
|
||||
if (category.description) {
|
||||
lines.push(`_${category.description}_`)
|
||||
}
|
||||
|
||||
if (category.bundles.length === 0) {
|
||||
lines.push('No bundles matched this category.\n')
|
||||
lines.push('</details>\n')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
const headers = hasBaseline
|
||||
? ['File', 'Before', 'After', 'Δ Raw', 'Δ Gzip', 'Δ Brotli']
|
||||
: ['File', 'Size', 'Gzip', 'Brotli']
|
||||
|
||||
const rows = category.bundles
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const diffMagnitude = Math.abs(b.diff.size) - Math.abs(a.diff.size)
|
||||
if (diffMagnitude !== 0) return diffMagnitude
|
||||
return a.fileName.localeCompare(b.fileName)
|
||||
})
|
||||
.map((bundle) => {
|
||||
if (hasBaseline) {
|
||||
return [
|
||||
formatFileLabel(bundle),
|
||||
formatSize(bundle.prev?.size),
|
||||
formatSize(bundle.curr?.size),
|
||||
formatDiffIndicator(bundle.diff.size),
|
||||
formatDiffIndicator(bundle.diff.gzip),
|
||||
formatDiffIndicator(bundle.diff.brotli)
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
formatFileLabel(bundle),
|
||||
formatSize(bundle.curr?.size),
|
||||
formatSize(bundle.curr?.gzip),
|
||||
formatSize(bundle.curr?.brotli)
|
||||
]
|
||||
})
|
||||
|
||||
lines.push(markdownTable([headers, ...rows]))
|
||||
|
||||
const statusParts = []
|
||||
if (category.counts.added) statusParts.push(`${category.counts.added} added`)
|
||||
if (category.counts.removed)
|
||||
statusParts.push(`${category.counts.removed} removed`)
|
||||
if (category.counts.increased)
|
||||
statusParts.push(`${category.counts.increased} grew`)
|
||||
if (category.counts.decreased)
|
||||
statusParts.push(`${category.counts.decreased} shrank`)
|
||||
|
||||
if (statusParts.length > 0) {
|
||||
lines.push(`\n_Status:_ ${statusParts.join(' / ')}`)
|
||||
}
|
||||
|
||||
lines.push('</details>\n')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a category entry exists in the map
|
||||
* @param {Map<string, CategoryReport>} categories
|
||||
* @param {string} categoryName
|
||||
* @returns {CategoryReport}
|
||||
*/
|
||||
function ensureCategoryEntry(categories, categoryName) {
|
||||
if (!categories.has(categoryName)) {
|
||||
const meta = getCategoryMetadata(categoryName)
|
||||
categories.set(categoryName, {
|
||||
name: categoryName,
|
||||
description: meta?.description,
|
||||
order: meta?.order ?? 99,
|
||||
metrics: {
|
||||
current: createMetrics(),
|
||||
baseline: createMetrics(),
|
||||
diff: createMetrics()
|
||||
},
|
||||
counts: createCounts(),
|
||||
bundles: []
|
||||
})
|
||||
}
|
||||
// @ts-expect-error - ensured by check above
|
||||
return categories.get(categoryName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bundle result to metrics
|
||||
* @param {BundleResult | undefined} bundle
|
||||
* @returns {SizeMetrics}
|
||||
*/
|
||||
function toMetrics(bundle) {
|
||||
if (!bundle) return createMetrics()
|
||||
return {
|
||||
size: bundle.size,
|
||||
gzip: bundle.gzip,
|
||||
brotli: bundle.brotli
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty metrics object
|
||||
* @returns {SizeMetrics}
|
||||
*/
|
||||
function createMetrics() {
|
||||
return { size: 0, gzip: 0, brotli: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add source metrics into target metrics
|
||||
* @param {SizeMetrics} target
|
||||
* @param {SizeMetrics} source
|
||||
*/
|
||||
function addMetrics(target, source) {
|
||||
target.size += source.size
|
||||
target.gzip += source.gzip
|
||||
target.brotli += source.brotli
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract baseline metrics from current metrics
|
||||
* @param {SizeMetrics} current
|
||||
* @param {SizeMetrics} baseline
|
||||
* @returns {SizeMetrics}
|
||||
*/
|
||||
function subtractMetrics(current, baseline) {
|
||||
return {
|
||||
size: current.size - baseline.size,
|
||||
gzip: current.gzip - baseline.gzip,
|
||||
brotli: current.brotli - baseline.brotli
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty counts object
|
||||
* @returns {CountSummary}
|
||||
*/
|
||||
function createCounts() {
|
||||
return { added: 0, removed: 0, increased: 0, decreased: 0, unchanged: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment status counters
|
||||
* @param {CountSummary} counts
|
||||
* @param {BundleStatus} status
|
||||
*/
|
||||
function incrementStatus(counts, status) {
|
||||
counts[status] += 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine bundle status for reporting
|
||||
* @param {BundleResult | undefined} curr
|
||||
* @param {BundleResult | undefined} prev
|
||||
* @param {number} sizeDiff
|
||||
* @returns {BundleStatus}
|
||||
*/
|
||||
function getStatus(curr, prev, sizeDiff) {
|
||||
if (curr && prev) {
|
||||
if (sizeDiff > 0) return 'increased'
|
||||
if (sizeDiff < 0) return 'decreased'
|
||||
return 'unchanged'
|
||||
}
|
||||
if (curr && !prev) return 'added'
|
||||
if (!curr && prev) return 'removed'
|
||||
return 'unchanged'
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file label with status hints
|
||||
* @param {BundleDiff} bundle
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatFileLabel(bundle) {
|
||||
if (bundle.status === 'added') {
|
||||
return `**${bundle.fileName}** _(new)_`
|
||||
}
|
||||
if (bundle.status === 'removed') {
|
||||
return `~~${bundle.fileName}~~ _(removed)_`
|
||||
}
|
||||
return bundle.fileName
|
||||
}
|
||||
|
||||
/**
|
||||
* Format size for table output
|
||||
* @param {number | undefined} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatSize(value) {
|
||||
if (value === undefined) return '—'
|
||||
return prettyBytes(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a diff with an indicator emoji
|
||||
* @param {number} diff
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDiffIndicator(diff) {
|
||||
if (diff > 0) {
|
||||
return `:red_circle: +${prettyBytes(diff)}`
|
||||
}
|
||||
if (diff < 0) {
|
||||
return `:green_circle: -${prettyBytes(Math.abs(diff))}`
|
||||
}
|
||||
return ':white_circle: 0 B'
|
||||
}
|
||||
|
||||
/**
|
||||
* Import JSON data if it exists
|
||||
* @template T
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<T | undefined>}
|
||||
*/
|
||||
async function importJSON(filePath) {
|
||||
if (!existsSync(filePath)) return undefined
|
||||
return (await import(filePath, { with: { type: 'json' } })).default
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
"comfy_base": {
|
||||
"fg-color": "#222",
|
||||
"bg-color": "#DDD",
|
||||
"comfy-menu-bg": "#FFFFFF",
|
||||
"comfy-menu-bg": "#F5F5F5",
|
||||
"comfy-menu-hover-bg": "#ccc",
|
||||
"comfy-menu-secondary-bg": "#EEE",
|
||||
"comfy-input-bg": "#C9C9C9",
|
||||
|
||||
@@ -1,88 +1,48 @@
|
||||
<template>
|
||||
<div class="splitter-overlay-root pointer-events-none flex flex-col">
|
||||
<slot name="workflow-tabs" />
|
||||
|
||||
<div
|
||||
class="pointer-events-none flex flex-1 overflow-hidden"
|
||||
:class="{
|
||||
'flex-row': sidebarLocation === 'left',
|
||||
'flex-row-reverse': sidebarLocation === 'right'
|
||||
}"
|
||||
<Splitter
|
||||
:key="sidebarStateKey"
|
||||
class="splitter-overlay-root splitter-overlay"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<div class="side-toolbar-container">
|
||||
<slot name="side-toolbar" />
|
||||
</div>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel :size="100">
|
||||
<Splitter
|
||||
key="main-splitter-stable"
|
||||
class="splitter-overlay flex-1 overflow-hidden"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey || 'main-splitter'"
|
||||
class="splitter-overlay max-w-full"
|
||||
layout="vertical"
|
||||
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
|
||||
state-key="bottom-panel-splitter"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'left'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel :size="80" class="flex flex-col">
|
||||
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
|
||||
|
||||
<Splitter
|
||||
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
|
||||
layout="vertical"
|
||||
:pt:gutter="
|
||||
'rounded-tl-lg rounded-tr-lg ' +
|
||||
(bottomPanelVisible ? '' : 'hidden')
|
||||
"
|
||||
state-key="bottom-panel-splitter"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
v-show="bottomPanelVisible"
|
||||
class="bottom-panel pointer-events-auto rounded-lg"
|
||||
>
|
||||
<slot name="bottom-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'right'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
|
||||
<slot name="bottom-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -114,11 +74,7 @@ const activeSidebarTabId = computed(
|
||||
)
|
||||
|
||||
const sidebarStateKey = computed(() => {
|
||||
if (unifiedWidth.value) {
|
||||
return 'unified-sidebar'
|
||||
}
|
||||
// When no tab is active, use a default key to maintain state
|
||||
return activeSidebarTabId.value ?? 'default-sidebar'
|
||||
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -137,17 +93,12 @@ const sidebarStateKey = computed(() => {
|
||||
|
||||
.side-bar-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.splitter-overlay-bottom :deep(.p-splitter-gutter) {
|
||||
transform: translateY(5px);
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.splitter-overlay {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="workspaceState.focusMode"
|
||||
class="comfy-menu-hamburger no-drag top-0 right-0"
|
||||
class="comfy-menu-hamburger no-drag"
|
||||
:style="positionCSS"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
@@ -14,13 +15,14 @@
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div class="window-actions-spacer" />
|
||||
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { watchEffect } from 'vue'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -43,6 +45,15 @@ watchEffect(() => {
|
||||
app.ui.menuContainer.style.display = 'block'
|
||||
}
|
||||
})
|
||||
|
||||
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const positionCSS = computed<CSSProperties>(() =>
|
||||
// 'Bottom' menuSetting shows the hamburger button in the bottom right corner
|
||||
// 'Disabled', 'Top' menuSetting shows the hamburger button in the top right corner
|
||||
menuSetting.value === 'Bottom'
|
||||
? { bottom: '0px', right: '0px' }
|
||||
: { top: '0px', right: '0px' }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto mx-2 flex h-12 items-center rounded-lg px-2 shadow-md"
|
||||
>
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<LoginButton v-if="!isLoggedIn" />
|
||||
<CurrentUserButton v-else class="shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
onMounted(() => {
|
||||
if (legacyCommandsContainerRef.value) {
|
||||
app.menu.element.style.width = 'fit-content'
|
||||
legacyCommandsContainerRef.value.appendChild(app.menu.element)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actionbar-container {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
</style>
|
||||