Compare commits
68 Commits
v1.30.1
...
vueNodes-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c33e21390 | ||
|
|
671e33cf09 | ||
|
|
68f98e2624 | ||
|
|
8120ed9dfa | ||
|
|
89ff8255bd | ||
|
|
f14a6beda5 | ||
|
|
aeabc24bf2 | ||
|
|
56a6ad5660 | ||
|
|
4e5eba6c54 | ||
|
|
647e62d4b7 | ||
|
|
4555656bf2 | ||
|
|
1e85902242 | ||
|
|
c436552ade | ||
|
|
449a51b82d | ||
|
|
bd3f02065a | ||
|
|
fb66637765 | ||
|
|
97f7c2149a | ||
|
|
f63d0f3289 | ||
|
|
e5a0466e40 | ||
|
|
c71ec35a5f | ||
|
|
7b213f135e | ||
|
|
8b7b580ed4 | ||
|
|
bfe53d7721 | ||
|
|
6368647cde | ||
|
|
69d37e8949 | ||
|
|
dc5d41642d | ||
|
|
2f00893b27 | ||
|
|
a5c29a9826 | ||
|
|
b44a39569e | ||
|
|
8e8a45c496 | ||
|
|
187f59eed3 | ||
|
|
9cd7d06a6d | ||
|
|
4ad8ae2634 | ||
|
|
e4b52e5329 | ||
|
|
2346ba1af0 | ||
|
|
45cefda6e1 | ||
|
|
cc73c42f76 | ||
|
|
f8490d0939 | ||
|
|
2e8e1366bd | ||
|
|
6ef5c2602f | ||
|
|
9cf3a318eb | ||
|
|
668e95501a | ||
|
|
d8860c87e8 | ||
|
|
6f8789b9aa | ||
|
|
9ae66c778d | ||
|
|
80013bcd5c | ||
|
|
aa943ac565 | ||
|
|
8eac19d06e | ||
|
|
55d2b300a6 | ||
|
|
32ed446285 | ||
|
|
4066fbd2d7 | ||
|
|
26f587c956 | ||
|
|
7ad1112535 | ||
|
|
522656a2dc | ||
|
|
15d223ef9b | ||
|
|
c8146ffc64 | ||
|
|
c263f6da25 | ||
|
|
1f5191847a | ||
|
|
fc69924c4a | ||
|
|
5b5151f41f | ||
|
|
2018f1e671 | ||
|
|
8822f186e0 | ||
|
|
0ff1837ced | ||
|
|
7e1e8e3b65 | ||
|
|
d83e34d0fc | ||
|
|
c6b528b8be | ||
|
|
37d2f25d2f | ||
|
|
3dcabaeb0a |
@@ -5,6 +5,10 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
|
||||
|
||||
# Proxy target of the local development server
|
||||
# Note: localhost:8188 does not work.
|
||||
# Cloud auto-detection: Setting this to any *.comfy.org URL automatically enables
|
||||
# cloud mode (DISTRIBUTION=cloud) without needing to set DISTRIBUTION separately.
|
||||
# Examples: https://testcloud.comfy.org/, https://stagingcloud.comfy.org/,
|
||||
# https://pr-123.testenvs.comfy.org/, https://cloud.comfy.org/
|
||||
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
|
||||
|
||||
# Allow dev server access from remote IP addresses.
|
||||
|
||||
@@ -26,15 +26,6 @@ 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
|
||||
|
||||
|
||||
@@ -31,26 +31,9 @@ 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:
|
||||
|
||||
@@ -30,26 +30,9 @@ 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:
|
||||
|
||||
19
.github/workflows/ci-lint-format.yaml
vendored
@@ -33,27 +33,15 @@ 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
|
||||
|
||||
- name: Run ESLint with auto-fix
|
||||
run: pnpm lint:fix
|
||||
|
||||
- name: Run Stylelint with auto-fix
|
||||
run: pnpm stylelint:fix
|
||||
|
||||
- name: Run Prettier with auto-format
|
||||
run: pnpm format
|
||||
|
||||
@@ -78,6 +66,7 @@ jobs:
|
||||
- name: Final validation
|
||||
run: |
|
||||
pnpm lint
|
||||
pnpm stylelint
|
||||
pnpm format:check
|
||||
pnpm knip
|
||||
|
||||
|
||||
26
.github/workflows/ci-tests-storybook.yaml
vendored
@@ -50,19 +50,6 @@ 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
|
||||
|
||||
@@ -115,19 +102,6 @@ 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
|
||||
|
||||
|
||||
13
.github/workflows/ci-tests-unit.yaml
vendored
@@ -29,19 +29,6 @@ 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
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
setup:
|
||||
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.issue.pull_request &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(
|
||||
github.event.comment.author_association == 'OWNER' ||
|
||||
@@ -24,12 +24,25 @@ jobs:
|
||||
github.event.comment.author_association == 'COLLABORATOR'
|
||||
) &&
|
||||
startsWith(github.event.comment.body, '/update-playwright') )
|
||||
outputs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
pr-number: ${{ steps.pr-info.outputs.pr-number }}
|
||||
branch: ${{ steps.pr-info.outputs.branch }}
|
||||
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
|
||||
steps:
|
||||
- name: Get PR info
|
||||
id: pr-info
|
||||
run: |
|
||||
echo "pr-number=${{ github.event.number || github.event.issue.number }}" >> $GITHUB_OUTPUT
|
||||
echo "branch=$(gh pr view ${{ github.event.number || github.event.issue.number }} --repo ${{ github.repository }} --json headRefName --jq '.headRefName')" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Find Update Comment
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
|
||||
id: "find-update-comment"
|
||||
with:
|
||||
issue-number: ${{ github.event.number || github.event.issue.number }}
|
||||
issue-number: ${{ steps.pr-info.outputs.pr-number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: "Updating Playwright Expectations"
|
||||
|
||||
@@ -37,49 +50,176 @@ jobs:
|
||||
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 }}
|
||||
issue-number: ${{ steps.pr-info.outputs.pr-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
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ steps.get-branch.outputs.branch }}
|
||||
- name: Setup Frontend
|
||||
ref: ${{ steps.pr-info.outputs.branch }}
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup ComfyUI Server
|
||||
# Save expensive build artifacts (Python env, built frontend, node_modules)
|
||||
# Source code will be checked out fresh in sharded jobs
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
with:
|
||||
path: |
|
||||
ComfyUI
|
||||
dist
|
||||
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
|
||||
|
||||
# Sharded snapshot updates
|
||||
update-snapshots-sharded:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4]
|
||||
shardTotal: [4]
|
||||
steps:
|
||||
# Checkout source code fresh (not cached)
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch }}
|
||||
|
||||
# Restore expensive build artifacts from setup job
|
||||
- name: Restore cached artifacts
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
with:
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
ComfyUI
|
||||
dist
|
||||
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
|
||||
|
||||
- name: Setup ComfyUI server (from cache)
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
|
||||
- name: Setup nodejs, pnpm, reuse built frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Run Playwright tests and update snapshots
|
||||
|
||||
# Run sharded tests with snapshot updates
|
||||
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
id: playwright-tests
|
||||
run: pnpm exec playwright test --update-snapshots
|
||||
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
continue-on-error: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
# Upload updated snapshots from this shard
|
||||
- name: Upload updated snapshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
name: snapshots-shard-${{ matrix.shardIndex }}
|
||||
path: browser_tests/**/*-snapshots/
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload test report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-shard-${{ matrix.shardIndex }}
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# Merge snapshots and commit
|
||||
merge-and-commit:
|
||||
needs: [setup, update-snapshots-sharded]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch }}
|
||||
|
||||
# Download all snapshot artifacts from shards
|
||||
- name: Download all snapshots
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: snapshots-shard-*
|
||||
path: ./downloaded-snapshots
|
||||
merge-multiple: false
|
||||
|
||||
# Merge snapshots from all shards into browser_tests directory
|
||||
- name: Merge snapshots
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Merging snapshots from all shards..."
|
||||
|
||||
# Verify target directory exists
|
||||
if [ ! -d "browser_tests" ]; then
|
||||
echo "::error::Target directory 'browser_tests' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# List all downloaded artifacts for debugging
|
||||
echo "Available shard artifacts:"
|
||||
ls -la ./downloaded-snapshots/ || echo "No downloaded-snapshots directory found"
|
||||
|
||||
merged_count=0
|
||||
skipped_count=0
|
||||
|
||||
for shard_dir in ./downloaded-snapshots/snapshots-shard-*; do
|
||||
echo "Processing: $shard_dir"
|
||||
|
||||
# Check if glob matched any directories
|
||||
if [ ! -e "$shard_dir" ]; then
|
||||
echo "::warning::No shard directories found matching pattern"
|
||||
break
|
||||
fi
|
||||
|
||||
if [ ! -d "$shard_dir" ]; then
|
||||
echo "::warning::Expected directory but found file: $shard_dir"
|
||||
skipped_count=$((skipped_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if directory has content (empty is OK - shard might have no changes)
|
||||
if [ -z "$(ls -A "$shard_dir" 2>/dev/null)" ]; then
|
||||
echo "Shard directory $shard_dir is empty (no snapshots updated in this shard)"
|
||||
skipped_count=$((skipped_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Merging snapshots from $shard_dir..."
|
||||
|
||||
# The artifact path 'browser_tests/**/*-snapshots/' uploads relative to browser_tests/
|
||||
# So downloaded artifacts contain 'tests/' directly, not 'browser_tests/tests/'
|
||||
# We need to merge the artifact contents into browser_tests/
|
||||
if ! rsync -a "$shard_dir/" browser_tests/; then
|
||||
echo "::error::Failed to merge snapshots from $shard_dir/ to browser_tests/"
|
||||
echo "Contents of $shard_dir:"
|
||||
find "$shard_dir" -type f | head -20
|
||||
exit 1
|
||||
fi
|
||||
merged_count=$((merged_count + 1))
|
||||
echo "Successfully merged shard $merged_count"
|
||||
done
|
||||
|
||||
echo "Snapshot merge complete. Merged: $merged_count shard(s), Skipped: $skipped_count shard(s)"
|
||||
|
||||
- name: Debugging info
|
||||
run: |
|
||||
echo "PR: ${{ github.event.issue.number }}"
|
||||
echo "Branch: ${{ steps.get-branch.outputs.branch }}"
|
||||
echo "PR: ${{ needs.setup.outputs.pr-number }}"
|
||||
echo "Branch: ${{ needs.setup.outputs.branch }}"
|
||||
git status
|
||||
|
||||
- name: Commit updated expectations
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
@@ -89,20 +229,20 @@ jobs:
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "[automated] Update test expectations"
|
||||
git push origin ${{ steps.get-branch.outputs.branch }}
|
||||
git push origin ${{ needs.setup.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 }}
|
||||
comment-id: ${{ needs.setup.outputs.comment-id }}
|
||||
issue-number: ${{ needs.setup.outputs.pr-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"
|
||||
run: gh pr edit ${{ needs.setup.outputs.pr-number }} --remove-label "New Browser Test Expectations"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
14
.github/workflows/publish-desktop-ui.yaml
vendored
@@ -161,20 +161,6 @@ 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:
|
||||
|
||||
10
.github/workflows/release-draft-create.yaml
vendored
@@ -28,16 +28,6 @@ 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
|
||||
|
||||
11
.github/workflows/release-pypi-dev.yaml
vendored
@@ -25,17 +25,6 @@ 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
|
||||
|
||||
28
.github/workflows/release-version-bump.yaml
vendored
@@ -15,6 +15,11 @@ 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:
|
||||
@@ -26,6 +31,24 @@ 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
|
||||
@@ -36,7 +59,6 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Bump version
|
||||
id: bump-version
|
||||
@@ -59,7 +81,9 @@ 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: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
26
.github/workflows/version-bump-desktop-ui.yaml
vendored
@@ -14,6 +14,11 @@ 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:
|
||||
@@ -26,8 +31,25 @@ 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:
|
||||
@@ -64,8 +86,10 @@ 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: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
|
||||
2
.gitignore
vendored
@@ -78,7 +78,7 @@ templates_repo/
|
||||
vite.config.mts.timestamp-*.mjs
|
||||
|
||||
# Linux core dumps
|
||||
./core
|
||||
/core
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run Knip with cache via package script
|
||||
pnpm knip
|
||||
pnpm knip 1>&2
|
||||
|
||||
|
||||
@@ -7,12 +7,5 @@
|
||||
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
|
||||
"options": {
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"]
|
||||
}
|
||||
}
|
||||
]
|
||||
"plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>ComfyUI Desktop</title>
|
||||
<title>ComfyUI</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.1",
|
||||
"version": "0.0.3",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"tags": [
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
|
||||
class="mx-auto grid h-[40rem] w-full max-w-3xl grid-rows-[1fr_auto_auto_1fr] select-none"
|
||||
>
|
||||
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
|
||||
{{ $t('install.gpuPicker.title') }}
|
||||
</h2>
|
||||
|
||||
<!-- GPU Selection buttons - takes up remaining space and centers content -->
|
||||
<div class="flex-1 flex gap-8 justify-center items-center">
|
||||
<div class="flex flex-1 items-center justify-center gap-8">
|
||||
<!-- 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="pt-12 px-24 h-16">
|
||||
<div class="h-16 px-24 pt-12">
|
||||
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
|
||||
<Tag
|
||||
:value="$t('install.gpuPicker.recommended')"
|
||||
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
|
||||
class="rounded-full bg-neutral-300 px-2 py-[1px] text-sm font-bold text-neutral-900"
|
||||
/>
|
||||
<i class="icon-[lucide--badge-check] text-neutral-300 text-lg" />
|
||||
<i class="icon-[lucide--badge-check] text-lg text-neutral-300" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-neutral-300 px-24">
|
||||
<div class="px-24 text-neutral-300">
|
||||
<p v-show="descriptionText" class="leading-relaxed">
|
||||
{{ descriptionText }}
|
||||
</p>
|
||||
|
||||
@@ -1,67 +1,163 @@
|
||||
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 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 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, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
|
||||
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) {
|
||||
return {
|
||||
...main,
|
||||
nodeDefs: nodes,
|
||||
commands: commands,
|
||||
settings: settings
|
||||
}
|
||||
} as M & { nodeDefs: N; commands: C; settings: S }
|
||||
}
|
||||
|
||||
const messages = {
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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,17 +66,6 @@
|
||||
@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) -->
|
||||
|
||||
@@ -13,7 +13,7 @@ export class ComfyActionbar {
|
||||
|
||||
async isDocked() {
|
||||
const className = await this.root.getAttribute('class')
|
||||
return className?.includes('is-docked') ?? false
|
||||
return className?.includes('static') ?? false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -301,7 +301,9 @@ test.describe('Settings', () => {
|
||||
})
|
||||
|
||||
test.describe('Support', () => {
|
||||
test('Should open external zendesk link', async ({ comfyPage }) => {
|
||||
test('Should open external zendesk link with OSS tag', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
const pagePromise = comfyPage.page.context().waitForEvent('page')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
|
||||
@@ -309,6 +311,10 @@ test.describe('Support', () => {
|
||||
|
||||
await newPage.waitForLoadState('networkidle')
|
||||
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
|
||||
|
||||
const url = new URL(newPage.url())
|
||||
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
@@ -12,24 +12,26 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('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.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)
|
||||
|
||||
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 comfyPage.page.mouse.click(400, 300)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
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: 96 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 82 KiB |
@@ -60,6 +60,7 @@ 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',
|
||||
|
||||
6
global.d.ts
vendored
@@ -4,6 +4,12 @@ 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,6 +12,10 @@ 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}']
|
||||
},
|
||||
@@ -30,16 +34,16 @@ const config: KnipConfig = {
|
||||
'@primeuix/forms',
|
||||
'@primeuix/styled',
|
||||
'@primeuix/utils',
|
||||
'@primevue/icons',
|
||||
// Dev
|
||||
'@trivago/prettier-plugin-sort-imports'
|
||||
'@primevue/icons'
|
||||
],
|
||||
ignore: [
|
||||
// Auto generated manager types
|
||||
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
|
||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
||||
// Used by a custom node (that should move off of this)
|
||||
'src/scripts/ui/components/splitButton.ts'
|
||||
'src/scripts/ui/components/splitButton.ts',
|
||||
// Service worker - registered at runtime via navigator.serviceWorker.register()
|
||||
'public/auth-sw.js'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -8,8 +8,10 @@ export default {
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
// Convert absolute paths to relative paths for better ESLint resolution
|
||||
const relativePaths = fileNames.map((f) => f.replace(process.cwd() + '/', ''))
|
||||
return [
|
||||
`pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
|
||||
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
|
||||
`pnpm exec eslint --cache --fix ${relativePaths.join(' ')}`,
|
||||
`pnpm exec prettier --cache --write ${relativePaths.join(' ')}`
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.30.1",
|
||||
"version": "1.30.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -55,6 +55,7 @@
|
||||
"@nx/vite": "catalog:",
|
||||
"@pinia/testing": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@prettier/plugin-oxc": "catalog:",
|
||||
"@storybook/addon-docs": "catalog:",
|
||||
"@storybook/vue3": "catalog:",
|
||||
"@storybook/vue3-vite": "catalog:",
|
||||
@@ -89,6 +90,7 @@
|
||||
"knip": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
--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);
|
||||
--input-surface: rgb(0 0 0 / 0.15);
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
@@ -247,7 +247,7 @@
|
||||
--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);
|
||||
--input-surface: rgb(130 130 130 / 0.1);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -258,9 +258,15 @@
|
||||
--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-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);
|
||||
@@ -324,7 +330,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Everything below here to be cleaned up over time. */
|
||||
|
||||
/* ===================== Scrollbar Utilities (Tailwind) =====================
|
||||
Usage: Add `scrollbar-custom` class to scrollable containers.
|
||||
The scrollbar styling adapts to light/dark theme automatically.
|
||||
============================================================================ */
|
||||
|
||||
@utility scrollbar-custom {
|
||||
overflow-y: auto;
|
||||
/* Firefox */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--dialog-surface) transparent;
|
||||
|
||||
/* WebKit */
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--dialog-surface);
|
||||
border-radius: 9999px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--dialog-surface);
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
/* =================== End Custom Scrollbar (cross-browser) =================== */
|
||||
|
||||
/* Everthing below here to be cleaned up over time. */
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
@@ -1139,7 +1180,7 @@ audio.comfy-audio.empty-audio-widget {
|
||||
}
|
||||
|
||||
.isLOD .lg-node-header {
|
||||
border-radius: 0px;
|
||||
border-radius: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
246
pnpm-lock.yaml
generated
@@ -45,6 +45,9 @@ catalogs:
|
||||
'@playwright/test':
|
||||
specifier: ^1.52.0
|
||||
version: 1.52.0
|
||||
'@prettier/plugin-oxc':
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@primeuix/forms':
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2
|
||||
@@ -186,6 +189,9 @@ catalogs:
|
||||
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
|
||||
@@ -495,6 +501,9 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: 'catalog:'
|
||||
version: 1.52.0
|
||||
'@prettier/plugin-oxc':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.4
|
||||
'@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)))
|
||||
@@ -597,6 +606,9 @@ importers:
|
||||
markdown-table:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.4
|
||||
mixpanel-browser:
|
||||
specifier: 'catalog:'
|
||||
version: 2.71.0
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1
|
||||
@@ -2201,6 +2213,21 @@ 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==}
|
||||
|
||||
@@ -2327,6 +2354,98 @@ packages:
|
||||
'@one-ini/wasm@0.1.1':
|
||||
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
||||
|
||||
'@oxc-parser/binding-android-arm64@0.74.0':
|
||||
resolution: {integrity: sha512-lgq8TJq22eyfojfa2jBFy2m66ckAo7iNRYDdyn9reXYA3I6Wx7tgGWVx1JAp1lO+aUiqdqP/uPlDaETL9tqRcg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxc-parser/binding-darwin-arm64@0.74.0':
|
||||
resolution: {integrity: sha512-xbY/io/hkARggbpYEMFX6CwFzb7f4iS6WuBoBeZtdqRWfIEi7sm/uYWXfyVeB8uqOATvJ07WRFC2upI8PSI83g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-parser/binding-darwin-x64@0.74.0':
|
||||
resolution: {integrity: sha512-FIj2gAGtFaW0Zk+TnGyenMUoRu1ju+kJ/h71D77xc1owOItbFZFGa+4WSVck1H8rTtceeJlK+kux+vCjGFCl9Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-parser/binding-freebsd-x64@0.74.0':
|
||||
resolution: {integrity: sha512-W1I+g5TJg0TRRMHgEWNWsTIfe782V3QuaPgZxnfPNmDMywYdtlzllzclBgaDq6qzvZCCQc/UhvNb37KWTCTj8A==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxc-parser/binding-linux-arm-gnueabihf@0.74.0':
|
||||
resolution: {integrity: sha512-gxqkyRGApeVI8dgvJ19SYe59XASW3uVxF1YUgkE7peW/XIg5QRAOVTFKyTjI9acYuK1MF6OJHqx30cmxmZLtiQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-arm-musleabihf@0.74.0':
|
||||
resolution: {integrity: sha512-jpnAUP4Fa93VdPPDzxxBguJmldj/Gpz7wTXKFzpAueqBMfZsy9KNC+0qT2uZ9HGUDMzNuKw0Se3bPCpL/gfD2Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-gnu@0.74.0':
|
||||
resolution: {integrity: sha512-fcWyM7BNfCkHqIf3kll8fJctbR/PseL4RnS2isD9Y3FFBhp4efGAzhDaxIUK5GK7kIcFh1P+puIRig8WJ6IMVQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.74.0':
|
||||
resolution: {integrity: sha512-AMY30z/C77HgiRRJX7YtVUaelKq1ex0aaj28XoJu4SCezdS8i0IftUNTtGS1UzGjGZB8zQz5SFwVy4dRu4GLwg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.74.0':
|
||||
resolution: {integrity: sha512-/RZAP24TgZo4vV/01TBlzRqs0R7E6xvatww4LnmZEBBulQBU/SkypDywfriFqWuFoa61WFXPV7sLcTjJGjim/w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.74.0':
|
||||
resolution: {integrity: sha512-620J1beNAlGSPBD+Msb3ptvrwxu04B8iULCH03zlf0JSLy/5sqlD6qBs0XUVkUJv1vbakUw1gfVnUQqv0UTuEg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.74.0':
|
||||
resolution: {integrity: sha512-WBFgQmGtFnPNzHyLKbC1wkYGaRIBxXGofO0+hz1xrrkPgbxbJS1Ukva1EB8sPaVBBQ52Bdc2GjLSp721NWRvww==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.74.0':
|
||||
resolution: {integrity: sha512-y4mapxi0RGqlp3t6Sm+knJlAEqdKDYrEue2LlXOka/F2i4sRN0XhEMPiSOB3ppHmvK4I2zY2XBYTsX1Fel0fAg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-wasm32-wasi@0.74.0':
|
||||
resolution: {integrity: sha512-yDS9bRDh5ymobiS2xBmjlrGdUuU61IZoJBaJC5fELdYT5LJNBXlbr3Yc6m2PWfRJwkH6Aq5fRvxAZ4wCbkGa8w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@oxc-parser/binding-win32-arm64-msvc@0.74.0':
|
||||
resolution: {integrity: sha512-XFWY52Rfb4N5wEbMCTSBMxRkDLGbAI9CBSL24BIDywwDJMl31gHEVlmHdCDRoXAmanCI6gwbXYTrWe0HvXJ7Aw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-parser/binding-win32-x64-msvc@0.74.0':
|
||||
resolution: {integrity: sha512-1D3x6iU2apLyfTQHygbdaNbX3nZaHu4yaXpD7ilYpoLo7f0MX0tUuoDrqJyJrVGqvyXgc0uz4yXz9tH9ZZhvvg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-project/types@0.74.0':
|
||||
resolution: {integrity: sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ==}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.6.1':
|
||||
resolution: {integrity: sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==}
|
||||
cpu: [arm]
|
||||
@@ -2460,6 +2579,10 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@prettier/plugin-oxc@0.0.4':
|
||||
resolution: {integrity: sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@primeuix/forms@0.0.2':
|
||||
resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==}
|
||||
engines: {node: '>=12.11.0'}
|
||||
@@ -3020,6 +3143,9 @@ 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==}
|
||||
|
||||
@@ -3518,6 +3644,9 @@ 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:
|
||||
@@ -3800,6 +3929,10 @@ 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==}
|
||||
|
||||
@@ -5987,6 +6120,9 @@ 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'}
|
||||
@@ -6173,6 +6309,10 @@ packages:
|
||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
oxc-parser@0.74.0:
|
||||
resolution: {integrity: sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
oxc-resolver@11.6.1:
|
||||
resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==}
|
||||
|
||||
@@ -9499,6 +9639,29 @@ 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
|
||||
@@ -9739,6 +9902,55 @@ snapshots:
|
||||
|
||||
'@one-ini/wasm@0.1.1': {}
|
||||
|
||||
'@oxc-parser/binding-android-arm64@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-darwin-arm64@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-darwin-x64@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-freebsd-x64@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm-gnueabihf@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm-musleabihf@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-gnu@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-wasm32-wasi@0.74.0':
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 0.2.12
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-win32-arm64-msvc@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-win32-x64-msvc@0.74.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-project/types@0.74.0': {}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.6.1':
|
||||
optional: true
|
||||
|
||||
@@ -9834,6 +10046,10 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@prettier/plugin-oxc@0.0.4':
|
||||
dependencies:
|
||||
oxc-parser: 0.74.0
|
||||
|
||||
'@primeuix/forms@0.0.2':
|
||||
dependencies:
|
||||
'@primeuix/utils': 0.3.2
|
||||
@@ -10387,6 +10603,8 @@ 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
|
||||
@@ -10980,6 +11198,8 @@ 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
|
||||
@@ -11296,6 +11516,8 @@ snapshots:
|
||||
|
||||
balanced-match@2.0.0: {}
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-opn@3.0.2:
|
||||
@@ -13864,6 +14086,10 @@ 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:
|
||||
@@ -14106,6 +14332,26 @@ snapshots:
|
||||
safe-push-apply: 1.0.0
|
||||
optional: true
|
||||
|
||||
oxc-parser@0.74.0:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.74.0
|
||||
optionalDependencies:
|
||||
'@oxc-parser/binding-android-arm64': 0.74.0
|
||||
'@oxc-parser/binding-darwin-arm64': 0.74.0
|
||||
'@oxc-parser/binding-darwin-x64': 0.74.0
|
||||
'@oxc-parser/binding-freebsd-x64': 0.74.0
|
||||
'@oxc-parser/binding-linux-arm-gnueabihf': 0.74.0
|
||||
'@oxc-parser/binding-linux-arm-musleabihf': 0.74.0
|
||||
'@oxc-parser/binding-linux-arm64-gnu': 0.74.0
|
||||
'@oxc-parser/binding-linux-arm64-musl': 0.74.0
|
||||
'@oxc-parser/binding-linux-riscv64-gnu': 0.74.0
|
||||
'@oxc-parser/binding-linux-s390x-gnu': 0.74.0
|
||||
'@oxc-parser/binding-linux-x64-gnu': 0.74.0
|
||||
'@oxc-parser/binding-linux-x64-musl': 0.74.0
|
||||
'@oxc-parser/binding-wasm32-wasi': 0.74.0
|
||||
'@oxc-parser/binding-win32-arm64-msvc': 0.74.0
|
||||
'@oxc-parser/binding-win32-x64-msvc': 0.74.0
|
||||
|
||||
oxc-resolver@11.6.1:
|
||||
dependencies:
|
||||
napi-postinstall: 0.3.3
|
||||
|
||||
@@ -16,6 +16,7 @@ catalog:
|
||||
'@nx/vite': 21.4.1
|
||||
'@pinia/testing': ^0.1.5
|
||||
'@playwright/test': ^1.52.0
|
||||
'@prettier/plugin-oxc': ^0.0.4
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
'@primeuix/utils': ^0.3.2
|
||||
@@ -98,6 +99,7 @@ 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
|
||||
|
||||
|
||||
BIN
public/assets/images/cloud-subscription.webm
Normal file
147
public/auth-sw.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @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())
|
||||
})
|
||||
@@ -18,44 +18,81 @@
|
||||
export const BUNDLE_CATEGORIES = [
|
||||
{
|
||||
name: 'App Entry Points',
|
||||
description: 'Main application bundles',
|
||||
patterns: [/^index-.*\.js$/],
|
||||
description: 'Main entry bundles and manifests',
|
||||
patterns: [/^index-.*\.js$/i, /^manifest-.*\.js$/i],
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
name: 'Core Views',
|
||||
description: 'Major application views and screens',
|
||||
patterns: [/GraphView-.*\.js$/, /UserSelectView-.*\.js$/],
|
||||
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: 'UI Panels',
|
||||
description: 'Settings and configuration panels',
|
||||
patterns: [/.*Panel-.*\.js$/],
|
||||
name: 'Views & Navigation',
|
||||
description: 'Top-level views, pages, and routed surfaces',
|
||||
patterns: [/.*(View|Page|Layout|Screen|Route)-.*\.js$/i],
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
name: 'UI Components',
|
||||
description: 'Reusable UI components',
|
||||
patterns: [/Avatar-.*\.js$/, /Badge-.*\.js$/],
|
||||
name: 'Panels & Settings',
|
||||
description: 'Configuration panels, inspectors, and settings screens',
|
||||
patterns: [/.*(Panel|Settings|Config|Preferences|Manager)-.*\.js$/i],
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
name: 'Services',
|
||||
description: 'Business logic and services',
|
||||
patterns: [/.*Service-.*\.js$/, /.*Store-.*\.js$/],
|
||||
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: 'Utilities',
|
||||
description: 'Helper functions and utilities',
|
||||
patterns: [/.*[Uu]til.*\.js$/],
|
||||
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: 'Uncategorized bundles',
|
||||
patterns: [/.*/], // Catch-all pattern
|
||||
description: 'Bundles that do not match a named category',
|
||||
patterns: [/.*/],
|
||||
order: 99
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,6 +7,13 @@ 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
|
||||
@@ -18,15 +25,52 @@ import { getCategoryMetadata } from './bundle-categories.js'
|
||||
* @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')
|
||||
let output = '## Bundle Size Report\n\n'
|
||||
const sizeHeaders = ['Size', 'Gzip', 'Brotli']
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main function to generate the size report
|
||||
* Main entry for generating the size report
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(currDir)) {
|
||||
@@ -35,27 +79,41 @@ async function run() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await renderFiles()
|
||||
const report = await buildBundleReport()
|
||||
const output = renderReport(report)
|
||||
process.stdout.write(output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders file sizes and diffs between current and previous versions
|
||||
* Build bundle comparison data from current and baseline artifacts
|
||||
* @returns {Promise<BundleReport>}
|
||||
*/
|
||||
async function renderFiles() {
|
||||
async function buildBundleReport() {
|
||||
/**
|
||||
* @param {string[]} files
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
|
||||
|
||||
const curr = filterFiles(await readdir(currDir))
|
||||
const prev = existsSync(prevDir) ? filterFiles(await readdir(prevDir)) : []
|
||||
const fileList = new Set([...curr, ...prev])
|
||||
const currFiles = filterFiles(await readdir(currDir))
|
||||
const baselineFiles = existsSync(prevDir)
|
||||
? filterFiles(await readdir(prevDir))
|
||||
: []
|
||||
const fileList = new Set([...currFiles, ...baselineFiles])
|
||||
|
||||
// Group bundles by category
|
||||
/** @type {Map<string, Array<{fileName: string, curr: BundleResult | undefined, prev: BundleResult | undefined}>>} */
|
||||
const bundlesByCategory = new Map()
|
||||
/** @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)
|
||||
@@ -63,100 +121,440 @@ async function renderFiles() {
|
||||
|
||||
const curr = await importJSON(currPath)
|
||||
const prev = await importJSON(prevPath)
|
||||
const fileName = curr?.file || prev?.file || ''
|
||||
const category = curr?.category || prev?.category || 'Other'
|
||||
const fileName = curr?.file || prev?.file
|
||||
if (!fileName) continue
|
||||
|
||||
if (!bundlesByCategory.has(category)) {
|
||||
bundlesByCategory.set(category, [])
|
||||
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++
|
||||
}
|
||||
|
||||
// @ts-expect-error - get is valid
|
||||
bundlesByCategory.get(category).push({ fileName, curr, prev })
|
||||
}
|
||||
addMetrics(overall.metrics.current, currMetrics)
|
||||
addMetrics(overall.metrics.baseline, baselineMetrics)
|
||||
addMetrics(overall.metrics.diff, diffMetrics)
|
||||
incrementStatus(overall.counts, status)
|
||||
|
||||
// Sort categories by their order
|
||||
const sortedCategories = Array.from(bundlesByCategory.keys()).sort((a, b) => {
|
||||
const metaA = getCategoryMetadata(a)
|
||||
const metaB = getCategoryMetadata(b)
|
||||
return (metaA?.order ?? 99) - (metaB?.order ?? 99)
|
||||
})
|
||||
addMetrics(category.metrics.current, currMetrics)
|
||||
addMetrics(category.metrics.baseline, baselineMetrics)
|
||||
addMetrics(category.metrics.diff, diffMetrics)
|
||||
incrementStatus(category.counts, status)
|
||||
|
||||
let totalSize = 0
|
||||
let totalCount = 0
|
||||
|
||||
// Render each category
|
||||
for (const category of sortedCategories) {
|
||||
const bundles = bundlesByCategory.get(category) || []
|
||||
if (bundles.length === 0) continue
|
||||
|
||||
const categoryMeta = getCategoryMetadata(category)
|
||||
output += `### ${category}\n\n`
|
||||
if (categoryMeta?.description) {
|
||||
output += `_${categoryMeta.description}_\n\n`
|
||||
}
|
||||
|
||||
const rows = []
|
||||
let categorySize = 0
|
||||
|
||||
for (const { fileName, curr, prev } of bundles) {
|
||||
if (!curr) {
|
||||
// File was deleted
|
||||
rows.push([`~~${fileName}~~`])
|
||||
} else {
|
||||
rows.push([
|
||||
fileName,
|
||||
`${prettyBytes(curr.size)}${getDiff(curr.size, prev?.size)}`,
|
||||
`${prettyBytes(curr.gzip)}${getDiff(curr.gzip, prev?.gzip)}`,
|
||||
`${prettyBytes(curr.brotli)}${getDiff(curr.brotli, prev?.brotli)}`
|
||||
])
|
||||
categorySize += curr.size
|
||||
totalSize += curr.size
|
||||
totalCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Sort rows by file name within category
|
||||
rows.sort((a, b) => {
|
||||
const fileA = a[0].replace(/~~/g, '')
|
||||
const fileB = b[0].replace(/~~/g, '')
|
||||
return fileA.localeCompare(fileB)
|
||||
category.bundles.push({
|
||||
fileName,
|
||||
curr,
|
||||
prev,
|
||||
diff: diffMetrics,
|
||||
status
|
||||
})
|
||||
|
||||
output += markdownTable([['File', ...sizeHeaders], ...rows])
|
||||
output += `\n\n**Category Total:** ${prettyBytes(categorySize)}\n\n`
|
||||
}
|
||||
|
||||
// Add overall summary
|
||||
if (totalCount > 0) {
|
||||
output += '---\n\n'
|
||||
output += `**Overall Total Size:** ${prettyBytes(totalSize)}\n`
|
||||
output += `**Total Bundle Count:** ${totalCount}\n`
|
||||
const sortedCategories = Array.from(categories.values()).sort(
|
||||
(a, b) => a.order - b.order
|
||||
)
|
||||
|
||||
return {
|
||||
categories: sortedCategories,
|
||||
overall,
|
||||
hasBaseline: baselineFiles.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports JSON data from a specified path
|
||||
*
|
||||
* 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 - Path to the JSON file
|
||||
* @returns {Promise<T | undefined>} The JSON content or undefined if the file does not exist
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<T | undefined>}
|
||||
*/
|
||||
async function importJSON(filePath) {
|
||||
if (!existsSync(filePath)) return undefined
|
||||
return (await import(filePath, { with: { type: 'json' } })).default
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the difference between the current and previous sizes
|
||||
*
|
||||
* @param {number} curr - The current size
|
||||
* @param {number} [prev] - The previous size
|
||||
* @returns {string} The difference in pretty format
|
||||
*/
|
||||
function getDiff(curr, prev) {
|
||||
if (prev === undefined) return ''
|
||||
const diff = curr - prev
|
||||
if (diff === 0) return ''
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
return ` (**${sign}${prettyBytes(diff)}**)`
|
||||
}
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
/**
|
||||
* Utility functions for downloading files
|
||||
*/
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
// Constants
|
||||
const DEFAULT_DOWNLOAD_FILENAME = 'download.png'
|
||||
|
||||
/**
|
||||
* Trigger a download by creating a temporary anchor element
|
||||
* @param href - The URL or blob URL to download
|
||||
* @param filename - The filename to suggest to the browser
|
||||
*/
|
||||
function triggerLinkDownload(href: string, filename: string): void {
|
||||
const link = document.createElement('a')
|
||||
link.href = href
|
||||
link.download = filename
|
||||
link.style.display = 'none'
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from a URL by creating a temporary anchor element
|
||||
* @param url - The URL of the file to download (must be a valid URL string)
|
||||
* @param filename - Optional filename override (will use URL filename or default if not provided)
|
||||
* @throws {Error} If the URL is invalid or empty
|
||||
*/
|
||||
export const downloadFile = (url: string, filename?: string): void => {
|
||||
export function downloadFile(url: string, filename?: string): void {
|
||||
if (!url || typeof url !== 'string' || url.trim().length === 0) {
|
||||
throw new Error('Invalid URL provided for download')
|
||||
}
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download =
|
||||
|
||||
const inferredFilename =
|
||||
filename || extractFilenameFromUrl(url) || DEFAULT_DOWNLOAD_FILENAME
|
||||
|
||||
// Trigger download
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
if (isCloud) {
|
||||
// Assets from cross-origin (e.g., GCS) cannot be downloaded this way
|
||||
void downloadViaBlobFetch(url, inferredFilename).catch((error) => {
|
||||
console.error('Failed to download file', error)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
triggerLinkDownload(url, inferredFilename)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a Blob by creating a temporary object URL and anchor element
|
||||
* @param filename - The filename to suggest to the browser
|
||||
* @param blob - The Blob to download
|
||||
*/
|
||||
export function downloadBlob(filename: string, blob: Blob): void {
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
triggerLinkDownload(url, filename)
|
||||
|
||||
// Revoke on the next microtask to give the browser time to start the download
|
||||
queueMicrotask(() => URL.revokeObjectURL(url))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,3 +74,15 @@ const extractFilenameFromUrl = (url: string): string | null => {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const downloadViaBlobFetch = async (
|
||||
href: string,
|
||||
filename: string
|
||||
): Promise<void> => {
|
||||
const response = await fetch(href)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${href}: ${response.status}`)
|
||||
}
|
||||
const blob = await response.blob()
|
||||
downloadBlob(filename, blob)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
|
||||
|
||||
<Splitter
|
||||
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
|
||||
class="splitter-overlay splitter-overlay-bottom mr-1 mb-1 ml-1 flex-1"
|
||||
layout="vertical"
|
||||
:pt:gutter="
|
||||
'rounded-tl-lg rounded-tr-lg ' +
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-2">
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-1">
|
||||
<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"
|
||||
class="actionbar-container pointer-events-auto mx-1 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
|
||||
@@ -13,8 +13,10 @@
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<LoginButton v-if="!isLoggedIn" />
|
||||
<CurrentUserButton v-else class="shrink-0" />
|
||||
<template v-if="isDesktop">
|
||||
<LoginButton v-if="!isLoggedIn" />
|
||||
<CurrentUserButton v-else class="shrink-0" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,9 +31,11 @@ import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const isDesktop = isElectron()
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
<div class="flex h-full items-center">
|
||||
<div
|
||||
v-if="isDragging && !isDocked"
|
||||
class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
|
||||
:class="{
|
||||
'drop-zone-active': isMouseOverDropZone
|
||||
}"
|
||||
:class="actionbarClass"
|
||||
@mouseenter="onMouseEnterDropZone"
|
||||
@mouseleave="onMouseLeaveDropZone"
|
||||
>
|
||||
@@ -13,18 +10,15 @@
|
||||
</div>
|
||||
|
||||
<Panel
|
||||
class="actionbar"
|
||||
class="pointer-events-auto z-1000"
|
||||
:style="style"
|
||||
:class="{
|
||||
fixed: !isDocked,
|
||||
'is-dragging': isDragging,
|
||||
'is-docked static mr-2 border-none bg-transparent p-0': isDocked
|
||||
:class="panelClass"
|
||||
:pt="{
|
||||
header: { class: 'hidden' },
|
||||
content: { class: isDocked ? 'p-0' : 'p-1' }
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="panelRef"
|
||||
class="actionbar-content flex items-center select-none"
|
||||
>
|
||||
<div ref="panelRef" class="flex items-center select-none">
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
@@ -34,7 +28,8 @@
|
||||
)
|
||||
"
|
||||
/>
|
||||
<ComfyQueueButton />
|
||||
|
||||
<ComfyRunButton />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
@@ -55,7 +50,7 @@ import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
import ComfyRunButton from './ComfyRunButton'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
|
||||
@@ -250,45 +245,20 @@ watch(isDragging, (dragging) => {
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
})
|
||||
const actionbarClass = computed(() =>
|
||||
cn(
|
||||
'w-[265px] border-dashed border-blue-500 opacity-80',
|
||||
'm-1.5 flex items-center justify-center self-stretch',
|
||||
'rounded-md before:w-50 before:-ml-50 before:h-full',
|
||||
isMouseOverDropZone.value &&
|
||||
'border-[3px] opacity-100 scale-105 shadow-[0_0_20px] shadow-blue-500'
|
||||
)
|
||||
)
|
||||
const panelClass = computed(() =>
|
||||
cn(
|
||||
'actionbar pointer-events-auto z1000',
|
||||
isDragging.value && 'select-none pointer-events-none',
|
||||
isDocked.value ? 'p-0 static mr-2 border-none bg-transparent' : 'fixed'
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.actionbar {
|
||||
pointer-events: all;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.actionbar-drop-zone {
|
||||
width: 265px;
|
||||
border: 2px dashed var(--p-primary-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.actionbar-drop-zone.drop-zone-active {
|
||||
background: var(--p-highlight-background-focus);
|
||||
border-color: var(--p-primary-color);
|
||||
border-width: 3px;
|
||||
box-shadow: 0 0 20px var(--p-primary-color);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.actionbar.is-dragging {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.p-panel-content) {
|
||||
@apply p-1;
|
||||
}
|
||||
|
||||
.is-docked :deep(.p-panel-content) {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
:deep(.p-panel-header) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<component
|
||||
:is="currentButton"
|
||||
:key="isActiveSubscription ? 'queue' : 'subscribe'"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueButton.vue'
|
||||
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
|
||||
const currentButton = computed(() =>
|
||||
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
|
||||
)
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
showDelay: 600
|
||||
}"
|
||||
class="comfyui-queue-button"
|
||||
:label="activeQueueModeMenuItem.label"
|
||||
:label="String(activeQueueModeMenuItem?.label ?? '')"
|
||||
severity="primary"
|
||||
size="small"
|
||||
:model="queueModeMenuItems"
|
||||
@@ -33,7 +33,7 @@
|
||||
value: item.tooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
:label="String(item.label)"
|
||||
:label="String(item.label ?? '')"
|
||||
:icon="item.icon"
|
||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
@@ -82,10 +82,13 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
useQueuePendingTaskCountStore,
|
||||
@@ -93,43 +96,52 @@ import {
|
||||
} from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import BatchCountEdit from './BatchCountEdit.vue'
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const { t } = useI18n()
|
||||
const queueModeMenuItemLookup = computed(() => ({
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
instant: {
|
||||
key: 'instant',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'instant'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'change'
|
||||
const queueModeMenuItemLookup = computed(() => {
|
||||
const items: Record<string, MenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
if (!isCloud) {
|
||||
items.instant = {
|
||||
key: 'instant',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'instant'
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const activeQueueModeMenuItem = computed(
|
||||
() => queueModeMenuItemLookup.value[queueMode.value]
|
||||
)
|
||||
const activeQueueModeMenuItem = computed(() => {
|
||||
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
|
||||
return (
|
||||
queueModeMenuItemLookup.value[queueMode.value] ||
|
||||
queueModeMenuItemLookup.value.disabled
|
||||
)
|
||||
})
|
||||
const queueModeMenuItems = computed(() =>
|
||||
Object.values(queueModeMenuItemLookup.value)
|
||||
)
|
||||
@@ -141,10 +153,15 @@ const hasPendingTasks = computed(
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const queuePrompt = async (e: Event) => {
|
||||
const commandId =
|
||||
'shiftKey' in e && e.shiftKey
|
||||
? 'Comfy.QueuePromptFront'
|
||||
: 'Comfy.QueuePrompt'
|
||||
const isShiftPressed = 'shiftKey' in e && e.shiftKey
|
||||
const commandId = isShiftPressed
|
||||
? 'Comfy.QueuePromptFront'
|
||||
: 'Comfy.QueuePrompt'
|
||||
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
|
||||
}
|
||||
|
||||
await commandStore.execute(commandId)
|
||||
}
|
||||
</script>
|
||||
7
src/components/actionbar/ComfyRunButton/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
export default isCloud && __BUILD_FLAGS__.REQUIRE_SUBSCRIPTION
|
||||
? defineAsyncComponent(() => import('./CloudRunButtonWrapper.vue'))
|
||||
: defineAsyncComponent(() => import('./ComfyQueueButton.vue'))
|
||||
@@ -18,7 +18,7 @@
|
||||
class="w-fit rounded-lg p-0"
|
||||
:model="items"
|
||||
:pt="{ item: { class: 'pointer-events-auto' } }"
|
||||
aria-label="Graph navigation"
|
||||
:aria-label="$t('g.graphNavigation')"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<SubgraphBreadcrumbItem
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
value: item.label,
|
||||
showDelay: 512
|
||||
}"
|
||||
draggable="false"
|
||||
href="#"
|
||||
class="p-breadcrumb-item-link h-12 cursor-pointer px-2"
|
||||
:class="{
|
||||
|
||||
@@ -46,7 +46,12 @@
|
||||
: $t('manager.installAllMissingNodes')
|
||||
"
|
||||
/>
|
||||
<Button label="Open Manager" size="small" outlined @click="openManager" />
|
||||
<Button
|
||||
:label="$t('g.openManager')"
|
||||
size="small"
|
||||
outlined
|
||||
@click="openManager"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<img
|
||||
src="/assets/images/comfy-logo-mono.svg"
|
||||
class="mr-2 h-5 w-5"
|
||||
alt="Comfy"
|
||||
:alt="$t('g.comfy')"
|
||||
/>
|
||||
{{ t('auth.login.useApiKey') }}
|
||||
</Button>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
ref="keybindingInput"
|
||||
class="mb-2 text-center"
|
||||
:model-value="newBindingKeyCombo?.toString() ?? ''"
|
||||
placeholder="Press keys for new binding"
|
||||
:placeholder="$t('g.pressKeysForNewBinding')"
|
||||
autocomplete="off"
|
||||
fluid
|
||||
@keydown.stop.prevent="captureKeybinding"
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
/>
|
||||
<Button
|
||||
v-if="!isApiKeyLogin"
|
||||
class="w-32"
|
||||
class="w-fit"
|
||||
variant="text"
|
||||
severity="danger"
|
||||
:label="$t('auth.deleteAccount.deleteAccount')"
|
||||
icon="pi pi-trash"
|
||||
@click="handleDeleteAccount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="px-2 py-4">
|
||||
<img
|
||||
src="/assets/images/comfy-logo-single.svg"
|
||||
alt="ComfyOrg Logo"
|
||||
:alt="$t('g.comfyOrgLogoAlt')"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
|
||||
@@ -420,9 +420,7 @@ onMounted(async () => {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
CORE_SETTINGS.forEach((setting) => {
|
||||
settingStore.addSetting(setting)
|
||||
})
|
||||
CORE_SETTINGS.forEach(settingStore.addSetting)
|
||||
|
||||
await newUserService().initializeIfNewUser(settingStore)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
class="absolute right-2 bottom-2 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||
:style="stringifiedMinimapStyles.buttonGroupStyles"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="absolute right-2 bottom-[66px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
|
||||
class="absolute right-0 bottom-[62px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
|
||||
>
|
||||
<div
|
||||
class="w-4/5 rounded-lg border border-node-border bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
|
||||
|
||||
@@ -243,7 +243,7 @@ const pt = computed(() => ({
|
||||
},
|
||||
listContainer: () => ({
|
||||
style: { maxHeight: listMaxHeight },
|
||||
class: 'overflow-y-auto scrollbar-hide'
|
||||
class: 'scrollbar-custom'
|
||||
}),
|
||||
list: {
|
||||
class: 'flex flex-col gap-0 p-0 m-0 list-none border-none text-sm'
|
||||
|
||||
@@ -159,7 +159,7 @@ const pt = computed(() => ({
|
||||
},
|
||||
listContainer: () => ({
|
||||
style: `max-height: ${listMaxHeight}`,
|
||||
class: 'overflow-y-auto scrollbar-hide'
|
||||
class: 'scrollbar-custom'
|
||||
}),
|
||||
list: {
|
||||
class:
|
||||
|
||||
@@ -180,7 +180,7 @@ onMounted(() => {
|
||||
* but need to reference sidebar dimensions for proper positioning.
|
||||
*/
|
||||
:root {
|
||||
--sidebar-padding: 8px;
|
||||
--sidebar-padding: 4px;
|
||||
--sidebar-icon-size: 1rem;
|
||||
|
||||
--sidebar-default-floating-width: 56px;
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
<teleport v-if="isHovered" to="#node-library-node-preview-container">
|
||||
<div class="node-lib-node-preview" :style="nodePreviewStyle">
|
||||
<NodePreview ref="previewRef" :node-def="nodeDef" />
|
||||
<NodePreview :node-def="nodeDef" />
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
@@ -132,11 +132,12 @@ function deleteBlueprint() {
|
||||
void useSubgraphStore().deleteBlueprint(props.node.data.name)
|
||||
}
|
||||
|
||||
const previewRef = ref<InstanceType<typeof NodePreview> | null>(null)
|
||||
const nodePreviewStyle = ref<CSSProperties>({
|
||||
position: 'absolute',
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
left: '0px'
|
||||
left: '0px',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1001
|
||||
})
|
||||
|
||||
const handleNodeHover = async () => {
|
||||
@@ -144,19 +145,15 @@ const handleNodeHover = async () => {
|
||||
if (!hoverTarget) return
|
||||
|
||||
const targetRect = hoverTarget.getBoundingClientRect()
|
||||
const margin = 40
|
||||
|
||||
const previewHeight = previewRef.value?.$el.offsetHeight || 0
|
||||
const availableSpaceBelow = window.innerHeight - targetRect.bottom
|
||||
|
||||
nodePreviewStyle.value.top =
|
||||
previewHeight > availableSpaceBelow
|
||||
? `${Math.max(0, targetRect.top - (previewHeight - availableSpaceBelow) - 20)}px`
|
||||
: `${targetRect.top - 40}px`
|
||||
if (sidebarLocation.value === 'left') {
|
||||
nodePreviewStyle.value.left = `${targetRect.right}px`
|
||||
} else {
|
||||
nodePreviewStyle.value.left = `${targetRect.left - 400}px`
|
||||
}
|
||||
nodePreviewStyle.value.top = `${targetRect.top}px`
|
||||
nodePreviewStyle.value.left =
|
||||
sidebarLocation.value === 'left'
|
||||
? `${targetRect.right + margin}px`
|
||||
: `${targetRect.left - margin}px`
|
||||
nodePreviewStyle.value.transform =
|
||||
sidebarLocation.value === 'right' ? 'translateX(-100%)' : undefined
|
||||
}
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -64,7 +64,7 @@ function updateToastPosition() {
|
||||
|
||||
styleElement.textContent = `
|
||||
.p-toast.p-component.p-toast-top-right {
|
||||
top: ${rect.top + 20}px !important;
|
||||
top: ${rect.top + 100}px !important;
|
||||
right: ${window.innerWidth - (rect.left + rect.width) + 20}px !important;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2 bg-comfy-menu-secondary px-3">
|
||||
<div
|
||||
class="flex items-center gap-2 bg-comfy-menu-secondary"
|
||||
:class="[{ 'flex-row-reverse': reverseOrder }, noPadding ? '' : 'px-3']"
|
||||
>
|
||||
<div
|
||||
v-if="badge.label"
|
||||
class="rounded-full bg-white px-1.5 py-0.5 text-xxxs font-semibold text-black"
|
||||
:class="labelClass"
|
||||
>
|
||||
{{ badge.label }}
|
||||
</div>
|
||||
<div class="font-inter text-sm font-extrabold text-slate-100">
|
||||
<div
|
||||
class="font-inter text-sm font-extrabold text-slate-100"
|
||||
:class="textClass"
|
||||
>
|
||||
{{ badge.text }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +21,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { TopbarBadge } from '@/types/comfy'
|
||||
|
||||
defineProps<{
|
||||
badge: TopbarBadge
|
||||
}>()
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
badge: TopbarBadge
|
||||
reverseOrder?: boolean
|
||||
noPadding?: boolean
|
||||
labelClass?: string
|
||||
textClass?: string
|
||||
}>(),
|
||||
{
|
||||
reverseOrder: false,
|
||||
noPadding: false,
|
||||
labelClass: '',
|
||||
textClass: ''
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
v-for="badge in topbarBadgeStore.badges"
|
||||
:key="badge.text"
|
||||
:badge
|
||||
:reverse-order="reverseOrder"
|
||||
:no-padding="noPadding"
|
||||
:label-class="labelClass"
|
||||
:text-class="textClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,5 +17,20 @@ import { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'
|
||||
|
||||
import TopbarBadge from './TopbarBadge.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
reverseOrder?: boolean
|
||||
noPadding?: boolean
|
||||
labelClass?: string
|
||||
textClass?: string
|
||||
}>(),
|
||||
{
|
||||
reverseOrder: false,
|
||||
noPadding: false,
|
||||
labelClass: '',
|
||||
textClass: ''
|
||||
}
|
||||
)
|
||||
|
||||
const topbarBadgeStore = useTopbarBadgeStore()
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { FirebaseError } from 'firebase/app'
|
||||
import { AuthErrorCodes } from 'firebase/auth'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
@@ -54,6 +56,16 @@ export const useFirebaseAuthActions = () => {
|
||||
detail: t('auth.signOut.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
|
||||
if (isCloud) {
|
||||
try {
|
||||
const router = useRouter()
|
||||
await router.push({ name: 'cloud-login' })
|
||||
} catch (error) {
|
||||
// needed for local development until we bring in cloud login pages.
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}, reportError)
|
||||
|
||||
const sendPasswordReset = wrapWithErrorHandlingAsync(
|
||||
@@ -208,6 +220,7 @@ export const useFirebaseAuthActions = () => {
|
||||
signUpWithEmail,
|
||||
updatePassword,
|
||||
deleteAccount,
|
||||
accessError
|
||||
accessError,
|
||||
reportError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,14 +126,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
// Extract safe widget data
|
||||
const slotMetadata = new Map<string, WidgetSlotMetadata>()
|
||||
|
||||
node.inputs?.forEach((input, index) => {
|
||||
if (!input?.widget?.name) return
|
||||
slotMetadata.set(input.widget.name, {
|
||||
index,
|
||||
linked: input.link != null
|
||||
})
|
||||
})
|
||||
|
||||
const reactiveWidgets = shallowReactive<IBaseWidget[]>(node.widgets ?? [])
|
||||
Object.defineProperty(node, 'widgets', {
|
||||
get() {
|
||||
@@ -144,8 +136,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
})
|
||||
|
||||
const safeWidgets = reactiveComputed<SafeWidgetData[]>(
|
||||
() =>
|
||||
const safeWidgets = reactiveComputed<SafeWidgetData[]>(() => {
|
||||
node.inputs?.forEach((input, index) => {
|
||||
if (!input?.widget?.name) return
|
||||
slotMetadata.set(input.widget.name, {
|
||||
index,
|
||||
linked: input.link != null
|
||||
})
|
||||
})
|
||||
return (
|
||||
node.widgets?.map((widget) => {
|
||||
try {
|
||||
// TODO: Use widget.getReactiveData() once TypeScript types are updated
|
||||
@@ -183,7 +182,8 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
}
|
||||
}) ?? []
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const nodeType =
|
||||
node.type ||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'
|
||||
|
||||
const clipboardHTMLWrapper = [
|
||||
'<meta charset="utf-8"><div><span data-metadata="',
|
||||
'"></span></div><span style="white-space:pre-wrap;">Text</span>'
|
||||
]
|
||||
|
||||
/**
|
||||
* Adds a handler on copy that serializes selected nodes to JSON
|
||||
@@ -9,28 +15,19 @@ export const useCopy = () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
useEventListener(document, 'copy', (e) => {
|
||||
if (!(e.target instanceof Element)) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
(e.target instanceof HTMLTextAreaElement &&
|
||||
e.target.type === 'textarea') ||
|
||||
(e.target instanceof HTMLInputElement && e.target.type === 'text')
|
||||
) {
|
||||
if (shouldIgnoreCopyPaste(e.target)) {
|
||||
// Default system copy
|
||||
return
|
||||
}
|
||||
const isTargetInGraph =
|
||||
e.target.classList.contains('litegraph') ||
|
||||
e.target.classList.contains('graph-canvas-container') ||
|
||||
e.target.id === 'graph-canvas'
|
||||
|
||||
// copy nodes and clear clipboard
|
||||
const canvas = canvasStore.canvas
|
||||
if (isTargetInGraph && canvas?.selectedItems) {
|
||||
canvas.copyToClipboard()
|
||||
if (canvas?.selectedItems) {
|
||||
const serializedData = canvas.copyToClipboard()
|
||||
// clearData doesn't remove images from clipboard
|
||||
e.clipboardData?.setData('text', ' ')
|
||||
e.clipboardData?.setData(
|
||||
'text/html',
|
||||
clipboardHTMLWrapper.join(btoa(serializedData))
|
||||
)
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
return false
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
import type { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { SUPPORT_URL } from '@/platform/support/config'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -451,6 +454,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
category: 'essentials' as const,
|
||||
function: async () => {
|
||||
const batchCount = useQueueSettingsStore().batchCount
|
||||
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackWorkflowExecution()
|
||||
}
|
||||
|
||||
await app.queuePrompt(0, batchCount)
|
||||
}
|
||||
},
|
||||
@@ -462,6 +470,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
category: 'essentials' as const,
|
||||
function: async () => {
|
||||
const batchCount = useQueueSettingsStore().batchCount
|
||||
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackWorkflowExecution()
|
||||
}
|
||||
|
||||
await app.queuePrompt(-1, batchCount)
|
||||
}
|
||||
},
|
||||
@@ -763,7 +776,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Contact Support',
|
||||
versionAdded: '1.17.8',
|
||||
function: () => {
|
||||
window.open('https://support.comfy.org/', '_blank')
|
||||
window.open(SUPPORT_URL, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,22 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
|
||||
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'
|
||||
|
||||
function pasteClipboardItems(data: DataTransfer): boolean {
|
||||
const rawData = data.getData('text/html')
|
||||
const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1]
|
||||
if (!match) return false
|
||||
try {
|
||||
useCanvasStore()
|
||||
.getCanvas()
|
||||
._deserializeItems(JSON.parse(atob(match)), {})
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
|
||||
@@ -38,15 +54,10 @@ export const usePaste = () => {
|
||||
}
|
||||
|
||||
useEventListener(document, 'paste', async (e) => {
|
||||
const isTargetInGraph =
|
||||
e.target instanceof Element &&
|
||||
(e.target.classList.contains('litegraph') ||
|
||||
e.target.classList.contains('graph-canvas-container') ||
|
||||
e.target.id === 'graph-canvas')
|
||||
|
||||
// If the target is not in the graph, we don't want to handle the paste event
|
||||
if (!isTargetInGraph) return
|
||||
|
||||
if (shouldIgnoreCopyPaste(e.target)) {
|
||||
// Default system copy
|
||||
return
|
||||
}
|
||||
// ctrl+shift+v is used to paste nodes with connections
|
||||
// this is handled by litegraph
|
||||
if (workspaceStore.shiftDown) return
|
||||
@@ -109,6 +120,7 @@ export const usePaste = () => {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (pasteClipboardItems(data)) return
|
||||
|
||||
// No image found. Look for node data
|
||||
data = data.getData('text/plain')
|
||||
|
||||
1
src/config/subscriptionPricesConfig.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MONTHLY_SUBSCRIPTION_PRICE = 20
|
||||
24
src/extensions/core/cloudSubscription.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.CloudSubscription',
|
||||
|
||||
setup: async () => {
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const { requireActiveSubscription } = useSubscription()
|
||||
|
||||
const checkSubscriptionStatus = () => {
|
||||
if (!isLoggedIn.value) return
|
||||
|
||||
void requireActiveSubscription()
|
||||
}
|
||||
|
||||
watch(() => isLoggedIn.value, checkSubscriptionStatus, {
|
||||
immediate: true
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -26,4 +26,8 @@ import './widgetInputs'
|
||||
|
||||
if (isCloud) {
|
||||
import('./cloudBadge')
|
||||
|
||||
if (__BUILD_FLAGS__.REQUIRE_SUBSCRIPTION) {
|
||||
import('./cloudSubscription')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter'
|
||||
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter'
|
||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
|
||||
|
||||
import { downloadBlob } from '@/base/common/downloadUtil'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
@@ -38,13 +39,7 @@ export class ModelExporter {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = desiredFilename
|
||||
link.click()
|
||||
|
||||
URL.revokeObjectURL(link.href)
|
||||
downloadBlob(desiredFilename, blob)
|
||||
} catch (error) {
|
||||
console.error('Error downloading from URL:', error)
|
||||
useToastStore().addAlert(t('toastMessages.failedToDownloadFile'))
|
||||
@@ -152,19 +147,11 @@ export class ModelExporter {
|
||||
|
||||
private static saveArrayBuffer(buffer: ArrayBuffer, filename: string): void {
|
||||
const blob = new Blob([buffer], { type: 'application/octet-stream' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
downloadBlob(filename, blob)
|
||||
}
|
||||
|
||||
private static saveString(text: string, filename: string): void {
|
||||
const blob = new Blob([text], { type: 'text/plain' })
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(link.href)
|
||||
downloadBlob(filename, blob)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { downloadBlob } from '@/base/common/downloadUtil'
|
||||
|
||||
import { type EventManagerInterface } from './interfaces'
|
||||
|
||||
export class RecordingManager {
|
||||
@@ -149,17 +151,7 @@ export class RecordingManager {
|
||||
|
||||
try {
|
||||
const blob = new Blob(this.recordedChunks, { type: 'video/webm' })
|
||||
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
document.body.appendChild(a)
|
||||
a.style.display = 'none'
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.click()
|
||||
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
downloadBlob(filename, blob)
|
||||
|
||||
this.eventManager.emitEvent('recordingExported', null)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { downloadBlob } from '@/base/common/downloadUtil'
|
||||
import { t } from '@/i18n'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
@@ -145,18 +146,7 @@ class ManageTemplates extends ComfyDialog {
|
||||
|
||||
const json = JSON.stringify({ templates: this.templates }, null, 2) // convert the data to a JSON string
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = $el('a', {
|
||||
href: url,
|
||||
download: 'node_templates.json',
|
||||
style: { display: 'none' },
|
||||
parent: document.body
|
||||
})
|
||||
a.click()
|
||||
setTimeout(function () {
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
downloadBlob('node_templates.json', blob)
|
||||
}
|
||||
|
||||
override show() {
|
||||
@@ -298,19 +288,9 @@ class ManageTemplates extends ComfyDialog {
|
||||
const blob = new Blob([json], {
|
||||
type: 'application/json'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = $el('a', {
|
||||
href: url,
|
||||
// @ts-expect-error fixme ts strict error
|
||||
download: (nameInput.value || t.name) + '.json',
|
||||
style: { display: 'none' },
|
||||
parent: document.body
|
||||
})
|
||||
a.click()
|
||||
setTimeout(function () {
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const name = (nameInput.value || t.name) + '.json'
|
||||
downloadBlob(name, blob)
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
|
||||
189
src/i18n.ts
@@ -1,68 +1,159 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import arCommands from './locales/ar/commands.json' with { type: 'json' }
|
||||
import ar from './locales/ar/main.json' with { type: 'json' }
|
||||
import arNodes from './locales/ar/nodeDefs.json' with { type: 'json' }
|
||||
import arSettings from './locales/ar/settings.json' with { type: 'json' }
|
||||
// ESLint cannot statically resolve dynamic imports with relative paths in template strings,
|
||||
// but these are valid ES module imports that Vite processes correctly at build time.
|
||||
|
||||
// Import only English locale eagerly as the default/fallback
|
||||
import enCommands from './locales/en/commands.json' with { type: 'json' }
|
||||
import en from './locales/en/main.json' with { type: 'json' }
|
||||
import enNodes from './locales/en/nodeDefs.json' with { type: 'json' }
|
||||
import enSettings from './locales/en/settings.json' with { type: 'json' }
|
||||
import esCommands from './locales/es/commands.json' with { type: 'json' }
|
||||
import es from './locales/es/main.json' with { type: 'json' }
|
||||
import esNodes from './locales/es/nodeDefs.json' with { type: 'json' }
|
||||
import esSettings from './locales/es/settings.json' with { type: 'json' }
|
||||
import frCommands from './locales/fr/commands.json' with { type: 'json' }
|
||||
import fr from './locales/fr/main.json' with { type: 'json' }
|
||||
import frNodes from './locales/fr/nodeDefs.json' with { type: 'json' }
|
||||
import frSettings from './locales/fr/settings.json' with { type: 'json' }
|
||||
import jaCommands from './locales/ja/commands.json' with { type: 'json' }
|
||||
import ja from './locales/ja/main.json' with { type: 'json' }
|
||||
import jaNodes from './locales/ja/nodeDefs.json' with { type: 'json' }
|
||||
import jaSettings from './locales/ja/settings.json' with { type: 'json' }
|
||||
import koCommands from './locales/ko/commands.json' with { type: 'json' }
|
||||
import ko from './locales/ko/main.json' with { type: 'json' }
|
||||
import koNodes from './locales/ko/nodeDefs.json' with { type: 'json' }
|
||||
import koSettings from './locales/ko/settings.json' with { type: 'json' }
|
||||
import ruCommands from './locales/ru/commands.json' with { type: 'json' }
|
||||
import ru from './locales/ru/main.json' with { type: 'json' }
|
||||
import ruNodes from './locales/ru/nodeDefs.json' with { type: 'json' }
|
||||
import ruSettings from './locales/ru/settings.json' with { type: 'json' }
|
||||
import trCommands from './locales/tr/commands.json' with { type: 'json' }
|
||||
import tr from './locales/tr/main.json' with { type: 'json' }
|
||||
import trNodes from './locales/tr/nodeDefs.json' with { type: 'json' }
|
||||
import trSettings from './locales/tr/settings.json' with { type: 'json' }
|
||||
import zhTWCommands from './locales/zh-TW/commands.json' with { type: 'json' }
|
||||
import zhTW from './locales/zh-TW/main.json' with { type: 'json' }
|
||||
import zhTWNodes from './locales/zh-TW/nodeDefs.json' with { type: 'json' }
|
||||
import zhTWSettings from './locales/zh-TW/settings.json' with { type: 'json' }
|
||||
import zhCommands from './locales/zh/commands.json' with { type: 'json' }
|
||||
import zh from './locales/zh/main.json' with { type: 'json' }
|
||||
import zhNodes from './locales/zh/nodeDefs.json' with { type: 'json' }
|
||||
import zhSettings from './locales/zh/settings.json' with { type: 'json' }
|
||||
|
||||
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
|
||||
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) {
|
||||
return {
|
||||
...main,
|
||||
nodeDefs: nodes,
|
||||
commands: commands,
|
||||
settings: settings
|
||||
}
|
||||
} as M & { nodeDefs: N; commands: C; settings: S }
|
||||
}
|
||||
|
||||
const messages = {
|
||||
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)
|
||||
// Locale loader map - dynamically import locales only when needed
|
||||
const localeLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('./locales/ar/main.json'),
|
||||
es: () => import('./locales/es/main.json'),
|
||||
fr: () => import('./locales/fr/main.json'),
|
||||
ja: () => import('./locales/ja/main.json'),
|
||||
ko: () => import('./locales/ko/main.json'),
|
||||
ru: () => import('./locales/ru/main.json'),
|
||||
tr: () => import('./locales/tr/main.json'),
|
||||
zh: () => import('./locales/zh/main.json'),
|
||||
'zh-TW': () => import('./locales/zh-TW/main.json')
|
||||
}
|
||||
|
||||
const nodeDefsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('./locales/ar/nodeDefs.json'),
|
||||
es: () => import('./locales/es/nodeDefs.json'),
|
||||
fr: () => import('./locales/fr/nodeDefs.json'),
|
||||
ja: () => import('./locales/ja/nodeDefs.json'),
|
||||
ko: () => import('./locales/ko/nodeDefs.json'),
|
||||
ru: () => import('./locales/ru/nodeDefs.json'),
|
||||
tr: () => import('./locales/tr/nodeDefs.json'),
|
||||
zh: () => import('./locales/zh/nodeDefs.json'),
|
||||
'zh-TW': () => import('./locales/zh-TW/nodeDefs.json')
|
||||
}
|
||||
|
||||
const commandsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('./locales/ar/commands.json'),
|
||||
es: () => import('./locales/es/commands.json'),
|
||||
fr: () => import('./locales/fr/commands.json'),
|
||||
ja: () => import('./locales/ja/commands.json'),
|
||||
ko: () => import('./locales/ko/commands.json'),
|
||||
ru: () => import('./locales/ru/commands.json'),
|
||||
tr: () => import('./locales/tr/commands.json'),
|
||||
zh: () => import('./locales/zh/commands.json'),
|
||||
'zh-TW': () => import('./locales/zh-TW/commands.json')
|
||||
}
|
||||
|
||||
const settingsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('./locales/ar/settings.json'),
|
||||
es: () => import('./locales/es/settings.json'),
|
||||
fr: () => import('./locales/fr/settings.json'),
|
||||
ja: () => import('./locales/ja/settings.json'),
|
||||
ko: () => import('./locales/ko/settings.json'),
|
||||
ru: () => import('./locales/ru/settings.json'),
|
||||
tr: () => import('./locales/tr/settings.json'),
|
||||
zh: () => import('./locales/zh/settings.json'),
|
||||
'zh-TW': () => import('./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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -1665,7 +1665,7 @@ export class LGraph
|
||||
continue
|
||||
}
|
||||
|
||||
const input = subgraphNode.findInputSlotByType(link.type, true, true)
|
||||
const input = subgraphNode.inputs[i - 1]
|
||||
outputNode.connectSlots(output, subgraphNode, input, link.parentId)
|
||||
}
|
||||
|
||||
|
||||