feat: add GitHub Pages deployment for development tools and reports

- Add GitHub Pages workflow (release-pages.yml) to deploy Storybook, Knip reports, and Playwright test reports
- Create .pages directory structure for GitHub Pages content
- Add build scripts for generating pages artifacts (build-pages.sh, create-playwright-index.js)
- Update CI workflows to support pages deployment and artifact handling
- Configure Knip and TypeScript for new .pages structure
- Add HTML viewers for Knip and Playwright reports

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
snomiao
2026-02-12 06:40:52 +00:00
parent 553ea63357
commit 2a466af154
14 changed files with 1456 additions and 31 deletions

View File

@@ -3,7 +3,7 @@ name: 'CI: Tests E2E'
on:
push:
branches: [main, master, core/*, desktop/*]
branches: [main, master, core/*, desktop/*, sno-deploy-ghpage]
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
workflow_dispatch:

View File

@@ -1,9 +1,13 @@
# Description: Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages
name: 'CI: Tests Storybook'
name: Storybook and Chromatic CI
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
on:
workflow_dispatch: # Allow manual triggering
pull_request:
branches: [main, sno-deploy-ghpage]
push:
branches: [sno-deploy-ghpage]
jobs:
# Post starting comment for non-forked PRs
@@ -14,7 +18,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Post starting comment
env:
@@ -30,16 +34,40 @@ jobs:
# Build Storybook for all PRs (free Cloudflare deployment)
storybook-build:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'push'
outputs:
conclusion: ${{ steps.job-status.outputs.conclusion }}
workflow-url: ${{ steps.workflow-url.outputs.url }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
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
- name: Build Storybook
run: pnpm build-storybook
@@ -58,7 +86,7 @@ jobs:
- name: Upload Storybook build
if: success() && github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: storybook-static
path: storybook-static/
@@ -75,16 +103,40 @@ jobs:
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
fetch-depth: 0 # Required for Chromatic baseline
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
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
- name: Build Storybook and run Chromatic
id: chromatic
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
@@ -114,11 +166,11 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v7
uses: actions/download-artifact@v4
with:
name: storybook-static
path: storybook-static
@@ -148,7 +200,7 @@ jobs:
pull-requests: write
steps:
- name: Update comment with Chromatic URLs
uses: actions/github-script@v8
uses: actions/github-script@v7
with:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';

186
.github/workflows/release-pages.yml vendored Normal file
View File

@@ -0,0 +1,186 @@
name: Deploy to GitHub Pages
description: Build and deploy to GitHub Pages and Vercel on successful completion of tests and builds
on:
# Triggers when any of these workflows complete on main branch
# Runs ONCE per workflow completion (e.g., if "Vitest Tests" completes, this workflow runs once)
workflow_run:
workflows: ['Storybook and Chromatic CI', 'Vitest Tests', 'Tests CI']
types: [completed]
# Allow direct pushes to the debug branch to kick off the full pipeline
push:
branches: [sno-deploy-ghpage]
# Keep manual trigger for debugging
workflow_dispatch:
jobs:
incremental-build:
runs-on: ubuntu-latest
# Only run if the triggering workflow succeeded (or manual dispatch/push)
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '24'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
.pages
storybook-static
coverage
key: build-cache-${{ github.ref_name }}-${{ github.run_id }}-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}
restore-keys: |
build-cache-${{ github.ref_name }}-
build-cache-main-
- name: Download Storybook artifact (source run)
id: fetch_storybook_trigger
continue-on-error: true
if: github.event_name == 'workflow_run' && github.event.workflow_run.name == 'Storybook and Chromatic CI'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: storybook-and-chromatic-ci.yaml
name: storybook-static
run_id: ${{ github.event.workflow_run.id }}
path: storybook-static
- name: Download Storybook artifact (latest successful run on main)
continue-on-error: true
if: steps.fetch_storybook_trigger.outcome != 'success'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: storybook-and-chromatic-ci.yaml
name: storybook-static
branch: main
workflow_conclusion: success
path: storybook-static
- name: Download Vitest reports (source run)
id: fetch_vitest_trigger
continue-on-error: true
if: github.event_name == 'workflow_run' && github.event.workflow_run.name == 'Vitest Tests'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: vitest-tests.yaml
name: vitest-reports
run_id: ${{ github.event.workflow_run.id }}
path: ./.pages/vitest-reports
- name: Download Vitest reports (latest successful run on main)
continue-on-error: true
if: steps.fetch_vitest_trigger.outcome != 'success'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: vitest-tests.yaml
name: vitest-reports
branch: main
workflow_conclusion: success
path: ./.pages/vitest-reports
- name: Download Playwright E2E reports (source run)
id: fetch_playwright_trigger
continue-on-error: true
if: github.event_name == 'workflow_run' && github.event.workflow_run.name == 'Tests CI'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: ci-tests-e2e.yaml
name_is_regexp: true
name: playwright-report-.*
run_id: ${{ github.event.workflow_run.id }}
path: ./playwright-reports-temp
- name: Download Playwright E2E reports (latest successful run on main)
continue-on-error: true
if: steps.fetch_playwright_trigger.outcome != 'success'
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: ci-tests-e2e.yaml
name_is_regexp: true
name: playwright-report-.*
branch: main
workflow_conclusion: success
path: ./playwright-reports-temp
- name: Organize Playwright reports by browser
if: always()
run: |
mkdir -p ./.pages/playwright-reports
# Move each browser report to its own directory
if [ -d "./playwright-reports-temp" ]; then
for dir in ./playwright-reports-temp/playwright-report-*; do
if [ -d "$dir" ]; then
browser_name=$(basename "$dir" | sed 's/playwright-report-//')
mkdir -p "./.pages/playwright-reports/${browser_name}"
cp -r "$dir"/* "./.pages/playwright-reports/${browser_name}/"
fi
done
fi
- name: Build static assets (with artifact reuse)
run: ./scripts/build-pages.sh
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload built pages as cache
uses: actions/upload-pages-artifact@v4
with:
name: built-pages
path: '.pages'
deploy-vercel-app:
runs-on: ubuntu-latest
needs: incremental-build
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: download built pages
uses: actions/download-artifact@v4
with:
name: built-pages
path: ./artifact
- name: Extract artifact
run: |
mkdir -p ./.pages
cd ./artifact
tar -xf artifact.tar -C ../.pages
# debug ls of ./.pages
- name: List ./.pages contents
run: ls -la ./.pages
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
working-directory: .pages
vercel-args: ${{ github.ref_name == 'main' && '--prod' || '' }}
github-comment: true
alias-domains: |
${{ github.ref_name }}-comfyui-frontend-reports.vercel.app

10
.gitignore vendored
View File

@@ -18,7 +18,6 @@ yarn.lock
.stylelintcache
node_modules
.pnpm-store
dist
dist-ssr
*.local
@@ -79,7 +78,7 @@ templates_repo/
vite.config.mts.timestamp-*.mjs
# Linux core dumps
/core
core
*storybook.log
storybook-static
@@ -94,7 +93,8 @@ storybook-static
vite.config.*.timestamp*
vitest.config.*.timestamp*
# Weekly docs check output
/output.txt
# Generated reports in .pages (exclude generated, keep HTML templates)
/.pages/*/**/*
/.pages-dist/
.amp
.vercel

160
.pages/README.md Normal file
View File

@@ -0,0 +1,160 @@
# GitHub Pages Deployment
This document describes the GitHub Pages deployment setup for ComfyUI Frontend development tools.
## Overview
The project automatically deploys the following development tools to GitHub Pages on every merge to the `main` branch:
- **Storybook** - Interactive component library and design system documentation
- **Nx Dependency Graph** - Visual representation of project dependencies
- **Test Coverage Reports** - Code coverage from Vitest unit tests
- **Vitest Results** - Interactive test results and reports
- **Knip Report** - Unused code and dependency analysis
## Accessing the Tools
Once deployed, all tools are accessible from a single landing page at:
```
https://comfy-org.github.io/ComfyUI_frontend/
```
## Primary Use Case: Storybook for Design Team
The primary motivation for this deployment is to provide the design team with a consistent, bookmarkable URL to reference the latest component system state. Instead of sharing PR-specific Storybook builds, the design team can always access the latest approved components from the main branch.
## Deployment Workflow
The deployment is managed by the `.github/workflows/release-pages.yml` workflow, which:
1. **Triggers on**:
- Push to `main` branch
- Manual workflow dispatch
2. **Build Process**:
- Installs dependencies with pnpm
- Runs `scripts/build-pages.sh` to generate Storybook, Nx dependency graph, Vitest reports, coverage, and Knip analysis
- Creates a landing page with links to all tools
3. **Deployment**:
- Uses GitHub Pages deploy action
- Deploys to `gh-pages` branch
- Available at the GitHub Pages URL
## Workflow Details
### Build Steps
The build script handles optional tooling gracefully—if an individual tool fails to build, the remainder of the deployment still proceeds and the failure is logged as a warning.
#### Storybook (Required)
```bash
pnpm build-storybook --output-dir dist/storybook
```
#### Nx Graph (Optional)
```bash
pnpm nx graph --file=dist/nx-graph/index.html
```
#### Test Coverage (Optional)
```bash
pnpm exec vitest --run --coverage --coverage.reporter=html
```
#### Vitest Results (Optional)
```bash
pnpm exec vitest --run --reporter=html --outputFile dist/vitest-ui/index.html
```
#### Knip Report (Optional)
```bash
pnpm knip --reporter json
```
### Permissions
The workflow requires the following permissions:
```yaml
permissions:
contents: read
pages: write
id-token: write
```
## Manual Deployment
You can manually trigger a deployment from the GitHub Actions tab:
1. Go to Actions → Deploy to GitHub Pages
2. Click "Run workflow"
3. Select the `main` branch
4. Click "Run workflow"
## Troubleshooting
### Storybook Build Fails
If the Storybook build fails:
1. Check that all Storybook stories are syntactically correct
2. Verify that all components can be imported
3. Run `pnpm build-storybook` locally to reproduce the issue
### Other Tools Fail
Since all tools except Storybook are marked with `continue-on-error: true`, they will not prevent deployment. If a tool consistently fails:
1. Check the GitHub Actions logs for the specific error
2. Test the build command locally
3. Consider adjusting the build command in the workflow
### GitHub Pages Not Updating
If changes aren't reflected on the live site:
1. Check the workflow run in the Actions tab
2. Verify that the deployment step succeeded
3. GitHub Pages can take a few minutes to update
4. Clear your browser cache or try an incognito window
## Maintenance
### Adding New Tools
To add a new development tool to the deployment:
1. Add a new build step in `.github/workflows/release-pages.yml`
2. Ensure the output goes to a subdirectory of `dist/`
3. Add `continue-on-error: true` if the tool is optional
4. Update the landing page `dist/index.html` with a link to the new tool
### Removing Tools
To remove a tool from deployment:
1. Remove the build step from the workflow
2. Remove the corresponding link from the landing page
## Cost Considerations
GitHub Pages is free for public repositories and includes:
- 1 GB storage
- 100 GB bandwidth per month
- 10 builds per hour
This should be more than sufficient for the development tools deployment.
## Security
The deployment only includes static, built artifacts:
- No source code is directly exposed
- No secrets or credentials are included
- All content is publicly accessible (appropriate for public repo)
## Related Documentation
- [GitHub Pages Documentation](https://docs.github.com/en/pages)
- [Storybook Documentation](https://storybook.js.org/docs)
- [Nx Documentation](https://nx.dev)
- [Vitest Documentation](https://vitest.dev)
- [Knip Documentation](https://knip.dev)

211
.pages/index.html Normal file
View File

@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComfyUI Frontend - Development Tools</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #52b2bb 100%);
}
.container {
max-width: 1200px;
width: 100%;
}
.header {
text-align: center;
color: white;
margin-bottom: 3rem;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
font-weight: 700;
}
.header p {
font-size: 1.125rem;
opacity: 0.9;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
p,h1,h2,h3,h4,h5,h6 {
transform: matrix(1, 0, -0.25, 1, 0, 0);
}
.card {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
display: block;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
}
.card-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #1a202c;
}
.card-description {
color: #718096;
line-height: 1.6;
}
.card-status {
display: inline-block;
margin-top: 1rem;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
.status-available {
background: #c6f6d5;
color: #22543d;
}
.status-loading {
background: #e9d8fd;
color: #44337a;
}
.status-unavailable {
background: #fed7d7;
color: #742a2a;
}
.footer {
text-align: center;
color: white;
margin-top: 3rem;
opacity: 0.8;
}
.footer a {
color: white;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎨 ComfyUI Frontend</h1>
<p>Development Tools & Documentation</p>
</div>
<div class="grid">
<a href="./storybook/index.html" class="card" data-status="storybook" data-fetch="./storybook/index.html">
<div class="card-icon">📚</div>
<h2 class="card-title">Storybook</h2>
<p class="card-description">Interactive component library and design system documentation</p>
<span class="card-status status-loading" data-status-indicator>Checking…</span>
</a>
<a href="./nx-graph/index.html" class="card" data-status="nx-graph" data-fetch="./nx-graph/index.html">
<div class="card-icon">🔗</div>
<h2 class="card-title">Nx Dependency Graph</h2>
<p class="card-description">Visual representation of project dependencies and build structure</p>
<span class="card-status status-loading" data-status-indicator>Checking…</span>
</a>
<a href="./coverage/index.html" class="card" data-status="coverage" data-fetch="./coverage/index.html">
<div class="card-icon">📊</div>
<h2 class="card-title">Test Coverage</h2>
<p class="card-description">Code coverage reports from Vitest unit tests</p>
<span class="card-status status-loading" data-status-indicator>Checking…</span>
</a>
<a href="./playwright-reports/index.html" class="card" data-status="playwright" data-fetch="./playwright-reports/index.html">
<div class="card-icon">🎭</div>
<h2 class="card-title">Playwright E2E</h2>
<p class="card-description">Browser end-to-end test reports generated by Playwright</p>
<span class="card-status status-loading" data-status-indicator>Checking…</span>
</a>
<a href="./vitest-reports/index.html" class="card" data-status="vitest-reports" data-fetch="./vitest-reports/index.html">
<div class="card-icon">🧪</div>
<h2 class="card-title">Vitest Results</h2>
<p class="card-description">Interactive test results and reports</p>
<span class="card-status status-loading" data-status-indicator>Checking…</span>
</a>
<a href="./knip.html" class="card" data-status="knip" data-fetch="./knip/report.md">
<div class="card-icon">🔍</div>
<h2 class="card-title">Knip Report</h2>
<p class="card-description">Unused code and dependency analysis</p>
<span class="card-status status-loading" data-status-indicator>Checking…</span>
</a>
</div>
<div class="footer">
<p>
Built from the <strong>main</strong> branch &bull;
<a href="https://github.com/Comfy-Org/ComfyUI_frontend" target="_blank">GitHub Repository</a> &bull;
<a href="https://docs.comfy.org" target="_blank">Official Documentation</a>
</p>
</div>
</div>
<script>
const cards = Array.from(document.querySelectorAll('.card[data-status]'))
const setStatus = (card, variant, text) => {
const indicator = card.querySelector('[data-status-indicator]')
if (!indicator) return
indicator.classList.remove('status-loading', 'status-available', 'status-unavailable')
indicator.classList.add(`status-${variant}`)
indicator.textContent = text
}
const absoluteUrl = (href) => new URL(href, window.location.href).toString()
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000)
cards.forEach((card) => {
const href = card.dataset.fetch || card.getAttribute('href')
if (!href) {
setStatus(card, 'unavailable', 'No link configured')
return
}
fetch(absoluteUrl(href), { cache: 'no-store', signal: controller.signal })
.then((response) => {
if (response.ok) {
setStatus(card, 'available', 'Available')
} else {
setStatus(card, 'unavailable', `Unavailable (${response.status})`)
}
})
.catch((error) => {
const reason = error.name === 'AbortError' ? 'Timed out' : 'Unavailable'
setStatus(card, 'unavailable', reason)
})
})
window.addEventListener('beforeunload', () => {
clearTimeout(timeout)
controller.abort()
})
</script>
</body>
</html>

87
.pages/knip.html Normal file
View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Knip Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
padding: 2rem;
background: #1a1a1a;
color: #f0f0f0;
line-height: 1.6;
}
h1 {
color: #4a9eff;
border-bottom: 3px solid #4a9eff;
padding-bottom: 1rem;
}
.status {
display: inline-block;
padding: 0.5rem 1rem;
margin: 1rem 0;
border-radius: 6px;
font-weight: 500;
}
.error {
background: #d32f2f;
color: #ffebee;
}
.loading {
background: #f57c00;
color: #fff3e0;
}
#content {
background: #2a2a2a;
padding: 1rem;
border-radius: 8px;
border: 1px solid #404040;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<h1>🧹 Knip Code Quality Report</h1>
<div id="status" class="status loading">Loading report...</div>
<div id="content"></div>
<script>
async function loadReport() {
const statusEl = document.getElementById('status')
const contentEl = document.getElementById('content')
try {
const response = await fetch('./knip/report.md')
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const reportText = await response.text()
statusEl.style.display = 'none'
// Wait for marked to be available
if (typeof marked !== 'undefined') {
contentEl.innerHTML = marked.parse(reportText)
} else {
contentEl.innerHTML = `<pre>${reportText}</pre>`
}
} catch (error) {
statusEl.className = 'status error'
statusEl.textContent = `Failed to load report: ${error.message}`
contentEl.innerHTML = '<p>The Knip report could not be loaded. This might happen if:</p><ul><li>The report generation failed during build</li><li>No unused code was detected</li><li>Network connectivity issues</li></ul>'
}
}
// Wait for marked library to load before running
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadReport)
} else {
loadReport()
}
</script>
</body>
</html>

View File

@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playwright E2E Test Reports</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.status {
display: inline-block;
padding: 0.5rem 1rem;
margin: 1rem auto;
border-radius: 6px;
font-weight: 500;
text-align: center;
width: 100%;
max-width: 400px;
display: block;
}
.error {
background: #d32f2f;
color: #ffebee;
}
.loading {
background: #f57c00;
color: #fff3e0;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
display: block;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #f0f0f0;
}
.card h2 {
color: #333;
font-size: 1.5rem;
text-transform: capitalize;
}
.pass-rate {
background: #10b981;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: bold;
font-size: 1.1rem;
}
.pass-rate.has-failures {
background: #ef4444;
}
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat .label {
font-size: 0.875rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat .value {
font-size: 1.75rem;
font-weight: bold;
}
.stat.passed .value {
color: #10b981;
}
.stat.failed .value {
color: #ef4444;
}
.stat.skipped .value {
color: #f59e0b;
}
.stat.flaky .value {
color: #8b5cf6;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.cards-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🎭 Playwright E2E Test Reports</h1>
<div id="status" class="status loading">Loading reports...</div>
<div id="content" class="cards-grid"></div>
</div>
<script>
async function getTestStats(browserName) {
try {
const reportJsonPath = `./playwright-reports/${browserName}/report.json`
const response = await fetch(reportJsonPath)
if (!response.ok) {
console.warn(`No report.json found for ${browserName}`)
return null
}
const reportData = await response.json()
let passed = 0
let failed = 0
let skipped = 0
let flaky = 0
// Parse Playwright JSON report format
if (reportData.suites) {
const countResults = (suites) => {
for (const suite of suites) {
if (suite.specs) {
for (const spec of suite.specs) {
if (!spec.tests || spec.tests.length === 0) continue
const test = spec.tests[0]
const results = test.results || []
// Check if test is flaky (has both pass and fail results)
const hasPass = results.some((r) => r.status === 'passed')
const hasFail = results.some((r) => r.status === 'failed')
if (hasPass && hasFail) {
flaky++
} else if (results.some((r) => r.status === 'passed')) {
passed++
} else if (results.some((r) => r.status === 'failed')) {
failed++
} else if (results.some((r) => r.status === 'skipped')) {
skipped++
}
}
}
if (suite.suites) {
countResults(suite.suites)
}
}
}
countResults(reportData.suites)
}
return { passed, failed, skipped, flaky }
} catch (error) {
console.error(`Error reading report for ${browserName}:`, error.message)
return null
}
}
function createCard(browserName, stats) {
if (!stats) return ''
const total = stats.passed + stats.failed + stats.skipped + stats.flaky
const passRate = total > 0 ? ((stats.passed / total) * 100).toFixed(1) : 0
return `
<a href="./playwright-reports/${browserName}/index.html" class="card">
<div class="card-header">
<h2>${browserName}</h2>
<span class="pass-rate ${stats.failed > 0 ? 'has-failures' : ''}">${passRate}%</span>
</div>
<div class="stats">
<div class="stat passed">
<span class="label">Passed</span>
<span class="value">${stats.passed}</span>
</div>
<div class="stat failed">
<span class="label">Failed</span>
<span class="value">${stats.failed}</span>
</div>
<div class="stat skipped">
<span class="label">Skipped</span>
<span class="value">${stats.skipped}</span>
</div>
<div class="stat flaky">
<span class="label">Flaky</span>
<span class="value">${stats.flaky}</span>
</div>
</div>
</a>
`
}
async function loadReports() {
const statusEl = document.getElementById('status')
const contentEl = document.getElementById('content')
try {
// Known browser configurations from the workflow
const browsers = ['chromium', 'chromium-2x', 'chromium-0.5x', 'mobile-chrome']
const cards = []
for (const browser of browsers) {
const stats = await getTestStats(browser)
if (stats) {
cards.push(createCard(browser, stats))
console.log(`✓ Found report for ${browser}:`, stats)
}
}
if (cards.length === 0) {
throw new Error('No valid browser reports found')
}
statusEl.style.display = 'none'
contentEl.innerHTML = cards.join('')
} catch (error) {
statusEl.className = 'status error'
statusEl.textContent = `Failed to load reports: ${error.message}`
contentEl.innerHTML = '<p style="color: white; text-align: center;">The Playwright reports could not be loaded. This might happen if:</p><ul style="color: white; text-align: center; list-style: none;"><li>The report generation failed during build</li><li>No test reports are available yet</li><li>Network connectivity issues</li></ul>'
}
}
// Load reports when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadReports)
} else {
loadReports()
}
</script>
</body>
</html>

46
.pages/vite.config.ts Normal file
View File

@@ -0,0 +1,46 @@
import fs from 'node:fs'
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
const rootDir = __dirname
const outDir = resolve(rootDir, '../.pages-dist')
const discoverHtmlEntries = () => {
const entries = new Map<string, string>()
const topLevel = resolve(rootDir, 'index.html')
if (fs.existsSync(topLevel)) entries.set('index', topLevel)
for (const dirent of fs.readdirSync(rootDir, { withFileTypes: true })) {
if (!dirent.isDirectory() || dirent.name.startsWith('.')) continue
const candidate = resolve(rootDir, dirent.name, 'index.html')
if (fs.existsSync(candidate)) entries.set(dirent.name, candidate)
}
return entries.size > 0 ? Object.fromEntries(entries) : undefined
}
export default defineConfig({
root: rootDir,
base: '/ComfyUI_frontend/',
appType: 'mpa',
logLevel: 'info',
publicDir: false,
server: {
open: '/index.html',
fs: {
allow: [rootDir],
strict: false
}
},
preview: {
open: '/index.html'
},
build: {
emptyOutDir: false,
outDir,
copyPublicDir: false,
rollupOptions: {
input: discoverHtmlEntries()
}
}
})

View File

@@ -8,38 +8,53 @@ const config: KnipConfig = {
'src/assets/css/style.css',
'src/main.ts',
'src/scripts/ui/menu/index.ts',
'src/types/index.ts',
'src/storybook/mocks/**/*.ts'
'src/types/index.ts'
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
},
'apps/desktop-ui': {
entry: ['src/main.ts', 'src/i18n.ts'],
project: ['src/**/*.{js,ts,vue}']
},
'packages/tailwind-utils': {
project: ['src/**/*.{js,ts}']
},
'packages/design-system': {
entry: ['src/**/*.ts'],
project: ['src/**/*.{js,ts}', '*.{js,ts,mts}']
},
'packages/registry-types': {
project: ['src/**/*.{js,ts}']
}
},
ignoreBinaries: ['python3', 'gh'],
ignoreBinaries: ['python3'],
ignoreDependencies: [
// Weird importmap things
'@iconify-json/lucide',
'@iconify/json',
'@primeuix/forms',
'@primeuix/styled',
'@primeuix/utils',
'@primevue/icons'
'@primevue/icons',
// Dev
'@trivago/prettier-plugin-sort-imports',
// 3D and collaboration libraries
'three',
'@types/three',
'yjs'
],
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',
'.pages/vite.config.ts',
// Utility files with exports that may be used by extensions or future features
'src/constants/uvMirrors.ts',
'src/lib/litegraph/src/measure.ts',
'src/lib/litegraph/src/widgets/DisconnectedWidget.ts',
'src/renderer/extensions/vueNodes/widgets/utils/audioUtils.ts',
'src/utils/electronMirrorCheck.ts',
'src/renderer/extensions/vueNodes/composables/slotLinkDragContext.ts',
'src/types/spatialIndex.ts',
'src/lib/litegraph/src/litegraph.ts',
'src/utils/vintageClipboard.ts'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -36,6 +36,8 @@
"lint:desktop": "nx run @comfyorg/desktop-ui:lint",
"locale": "lobe-i18n locale",
"oxlint": "oxlint src --type-aware",
"pages:build": "bash scripts/build-pages.sh && vite build --config ./.pages/vite.config.ts",
"pages:dev": "vite --config ./.pages/vite.config.ts",
"preinstall": "pnpm dlx only-allow pnpm",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",

75
scripts/build-pages.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
# Build or reuse Storybook
echo "[build-pages] Setting up Storybook"
rm -rf ".pages/storybook"
if [ -d "./storybook-static" ] && [ "$(find ./storybook-static -name '*.html' | wc -l)" -gt 0 ]; then
echo "✅ Reusing downloaded Storybook build"
cp -r "./storybook-static" ".pages/storybook"
else
echo "🔨 Building Storybook from source"
pnpm build-storybook && cp -r "storybook-static" ".pages/storybook"
fi
echo "[build-pages] Generating Nx dependency graph"
rm -rf ".pages/nx-graph" && mkdir -p ".pages/nx-graph"
pnpm nx graph --file=".pages/nx-graph/index.html"
# Generate or reuse Vitest test reports
echo "[build-pages] Setting up Vitest test reports"
rm -rf ".pages/vitest-reports" && mkdir -p ".pages/vitest-reports"
if [ -d ".page/vitest-reports" ]; then
echo "✅ Reusing downloaded Vitest reports"
cp -r ".page/vitest-reports/"* ".pages/vitest-reports/" 2>/dev/null || echo "⚠️ No vitest reports to copy"
else
echo "🔨 Generating Vitest reports from source"
pnpm exec vitest \
--reporter=json --outputFile.json=".pages/vitest-reports/results.json" \
--reporter=html --outputFile.html=".pages/vitest-reports/index.html" \
--run
fi
# Set up Playwright test reports if available
echo "[build-pages] Setting up Playwright test reports"
if [ -d ".page/playwright-reports" ]; then
echo "✅ Reusing downloaded Playwright reports"
mkdir -p ".pages/playwright-reports"
cp -r ".page/playwright-reports/"* ".pages/playwright-reports/" 2>/dev/null || echo "⚠️ No playwright reports to copy"
fi
echo "[build-pages] Generating coverage report"
mkdir -p ".pages/coverage"
if pnpm exec vitest --run --coverage --coverage.reporter=html --coverage.reportsDirectory=".pages/coverage"; then
echo "✅ Coverage report completed"
else
echo "⚠️ Coverage report failed, continuing..."
fi
echo "[build-pages] Generating Knip report"
mkdir -p ".pages/knip"
rm -f ".pages/knip/report.md"
if pnpm knip --reporter markdown --no-progress --no-exit-code > ".pages/knip/report.md" 2>/dev/null && [ -s ".pages/knip/report.md" ]; then
echo "✅ Knip report generated at .pages/knip/report.md"
else
echo "⚠️ Knip report failed, creating placeholder..."
cat > ".pages/knip/report.md" <<'EOF'
# Knip report
> ⚠️ Knip report unavailable.
>
> Generation failed during build. See CI logs for details.
EOF
fi
echo "[build-pages] Landing page already exists at .pages/index.html"
echo "[build-pages] Build artifacts ready in ./.pages"
echo "[build-pages] Note: For local dev, you can develop the .pages/index.html using:
pnpm exec vite .pages
"

View File

@@ -0,0 +1,290 @@
#!/usr/bin/env node
/**
* Creates an index page for Playwright test reports with test statistics
* Reads JSON reports from each browser and creates a landing page with cards
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const reportsDir = path.join(__dirname, '..', '.pages', 'playwright-reports')
function getTestStats(reportPath) {
try {
const reportJsonPath = path.join(reportPath, 'report.json')
if (!fs.existsSync(reportJsonPath)) {
console.warn(`No report.json found at ${reportJsonPath}`)
return null
}
const reportData = JSON.parse(fs.readFileSync(reportJsonPath, 'utf-8'))
let passed = 0
let failed = 0
let skipped = 0
let flaky = 0
// Parse Playwright JSON report format
if (reportData.suites) {
const countResults = (suites) => {
for (const suite of suites) {
if (suite.specs) {
for (const spec of suite.specs) {
if (!spec.tests || spec.tests.length === 0) continue
const test = spec.tests[0]
const results = test.results || []
// Check if test is flaky (has both pass and fail results)
const hasPass = results.some((r) => r.status === 'passed')
const hasFail = results.some((r) => r.status === 'failed')
if (hasPass && hasFail) {
flaky++
} else if (results.some((r) => r.status === 'passed')) {
passed++
} else if (results.some((r) => r.status === 'failed')) {
failed++
} else if (results.some((r) => r.status === 'skipped')) {
skipped++
}
}
}
if (suite.suites) {
countResults(suite.suites)
}
}
}
countResults(reportData.suites)
}
return { passed, failed, skipped, flaky }
} catch (error) {
console.error(`Error reading report at ${reportPath}:`, error.message)
return null
}
}
function generateIndexHtml(browsers) {
const cards = browsers
.map((browser) => {
const { name, stats } = browser
if (!stats) return ''
const total = stats.passed + stats.failed + stats.skipped + stats.flaky
const passRate = total > 0 ? ((stats.passed / total) * 100).toFixed(1) : 0
return `
<a href="./${name}/index.html" class="card">
<div class="card-header">
<h2>${name}</h2>
<span class="pass-rate ${stats.failed > 0 ? 'has-failures' : ''}">${passRate}%</span>
</div>
<div class="stats">
<div class="stat passed">
<span class="label">Passed</span>
<span class="value">${stats.passed}</span>
</div>
<div class="stat failed">
<span class="label">Failed</span>
<span class="value">${stats.failed}</span>
</div>
<div class="stat skipped">
<span class="label">Skipped</span>
<span class="value">${stats.skipped}</span>
</div>
<div class="stat flaky">
<span class="label">Flaky</span>
<span class="value">${stats.flaky}</span>
</div>
</div>
</a>
`
})
.join('')
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playwright E2E Test Reports</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
text-decoration: none;
color: inherit;
display: block;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #f0f0f0;
}
.card h2 {
color: #333;
font-size: 1.5rem;
text-transform: capitalize;
}
.pass-rate {
background: #10b981;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: bold;
font-size: 1.1rem;
}
.pass-rate.has-failures {
background: #ef4444;
}
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat .label {
font-size: 0.875rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat .value {
font-size: 1.75rem;
font-weight: bold;
}
.stat.passed .value {
color: #10b981;
}
.stat.failed .value {
color: #ef4444;
}
.stat.skipped .value {
color: #f59e0b;
}
.stat.flaky .value {
color: #8b5cf6;
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.cards-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🎭 Playwright E2E Test Reports</h1>
<div class="cards-grid">
${cards}
</div>
</div>
</body>
</html>`
}
function main() {
if (!fs.existsSync(reportsDir)) {
console.log(
'No playwright reports directory found, skipping index creation'
)
return
}
const browsers = []
const browserDirs = fs.readdirSync(reportsDir, { withFileTypes: true })
for (const dirent of browserDirs) {
if (dirent.isDirectory()) {
const browserName = dirent.name
const browserPath = path.join(reportsDir, browserName)
const stats = getTestStats(browserPath)
if (stats) {
browsers.push({ name: browserName, stats })
console.log(`✓ Found report for ${browserName}:`, stats)
}
}
}
if (browsers.length === 0) {
console.warn('No valid browser reports found')
return
}
const html = generateIndexHtml(browsers)
const indexPath = path.join(reportsDir, 'index.html')
fs.writeFileSync(indexPath, html, 'utf-8')
console.log(`✓ Created index page at ${indexPath}`)
}
main()

View File

@@ -35,6 +35,7 @@
"rootDir": "./"
},
"include": [
".pages/*.ts",
".storybook/**/*",
"eslint.config.ts",
"global.d.ts",