fix: resolve merge conflicts in .gitignore and knip.config.ts

Merged changes from main branch:
- Keep both /output.txt and .pages/ ignores in .gitignore
- Keep desktop-ui workspace and auth-sw.js ignore in knip.config.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
snomiao
2025-11-23 07:00:09 +00:00
15 changed files with 1104 additions and 6 deletions

View File

@@ -3,7 +3,7 @@ description: "End-to-end testing with Playwright across multiple browsers, deplo
on:
push:
branches: [main, master, core/*, desktop/*]
branches: [main, master, core/*, desktop/*, sno-deploy-ghpage]
pull_request:
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]

View File

@@ -4,7 +4,9 @@ description: "Builds Storybook and runs visual regression testing via Chromatic,
on:
workflow_dispatch: # Allow manual triggering
pull_request:
branches: [main]
branches: [main, sno-deploy-ghpage]
push:
branches: [sno-deploy-ghpage]
jobs:
# Post starting comment for non-forked PRs
@@ -31,7 +33,7 @@ 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 }}

View File

@@ -3,7 +3,7 @@ description: "Unit and component testing with Vitest"
on:
push:
branches: [main, master, dev*, core/*, desktop/*]
branches: [main, master, dev*, core/*, desktop/*, sno-deploy-ghpage]
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
@@ -34,3 +34,20 @@ jobs:
- name: Run Vitest tests
run: pnpm test:unit
- name: Generate test reports (on main or debug branch)
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/sno-deploy-ghpage'
run: |
mkdir -p ./vitest-reports
pnpm exec vitest \
--reporter=json --outputFile.json="./vitest-reports/results.json" \
--reporter=html --outputFile.html="./vitest-reports/index.html" \
--run
- name: Upload Vitest reports artifact (on main or debug branch)
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/sno-deploy-ghpage'
uses: actions/upload-artifact@v4
with:
name: vitest-reports
path: vitest-reports/
retention-days: 7

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

6
.gitignore vendored
View File

@@ -96,3 +96,9 @@ vitest.config.*.timestamp*
# Weekly docs check output
/output.txt
# Generated reports in .pages (exclude generated, keep HTML templates)
/.pages/*/**/*
/.pages-dist/
.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

@@ -41,7 +41,10 @@ const config: KnipConfig = {
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts'
'src/scripts/ui/components/splitButton.ts',
'.pages/vite.config.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

View File

@@ -35,6 +35,8 @@
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
"lint": "eslint src --cache",
"locale": "lobe-i18n locale",
"pages:dev": "vite --config ./.pages/vite.config.ts",
"pages:build": "bash scripts/build-pages.sh && vite build --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 2>/dev/null | sed 's/^\[log\] //' > ".pages/knip/report.md" && [ -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

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

View File

@@ -23,8 +23,10 @@ export default defineConfig({
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
],
coverage: {
reporter: ['text', 'json', 'html']
reporter: ['text', 'json', 'html'],
reportsDirectory: './.vitest/coverage'
},
reporters: ['html', 'json'],
exclude: [
'**/node_modules/**',
'**/dist/**',