Files
CherryHQ-cherry-studio/packages/ui/scripts/pipeline.ts
2026-05-18 13:44:22 +08:00

209 lines
6.0 KiB
TypeScript

/**
* Icon Pipeline — orchestrates the full icon generation workflow
*
* Steps:
* 1. Vectorize — convert PNG/JPG to SVG (skipped if no raster files or --skip-vectorize)
* 2. Validate — check SVG quality (exits on errors)
* 3. Normalize — normalize SVG viewBox dimensions to 32x32
* 4. Generate — emit light.tsx + dark.tsx + meta.ts per logo
* 5. Avatars — emit avatar.tsx + index.tsx + barrel + catalog
*
* Usage:
* tsx scripts/pipeline.ts --dir=providers
* tsx scripts/pipeline.ts --dir=models --skip-vectorize
* tsx scripts/pipeline.ts --dir=providers --force
*/
import { spawn } from 'child_process'
import fs from 'fs/promises'
import path from 'path'
type SourceDir = 'providers' | 'models'
interface PipelineArgs {
dir: SourceDir
skipVectorize: boolean
force: boolean
}
interface StepResult {
name: string
command: string
status: 'success' | 'skipped' | 'failed'
durationMs: number
exitCode?: number
}
function parseArgs(): PipelineArgs {
const args = process.argv.slice(2)
let dir: SourceDir | undefined
let skipVectorize = false
let force = false
for (const arg of args) {
if (arg.startsWith('--dir=')) {
const value = arg.split('=')[1]
if (value === 'providers' || value === 'models') {
dir = value
} else {
console.error(`Invalid --dir value: ${value}. Use "providers" or "models".`)
process.exit(1)
}
} else if (arg === '--skip-vectorize') {
skipVectorize = true
} else if (arg === '--force') {
force = true
}
}
if (!dir) {
console.error('Usage: tsx scripts/pipeline.ts --dir=providers|models [--skip-vectorize] [--force]')
process.exit(1)
}
return { dir, skipVectorize, force }
}
/**
* Check if a directory contains any raster files (PNG/JPG/JPEG).
*/
async function hasRasterFiles(dir: string): Promise<boolean> {
const rasterExts = new Set(['.png', '.jpg', '.jpeg'])
try {
const entries = await fs.readdir(dir)
return entries.some((f) => rasterExts.has(path.extname(f).toLowerCase()))
} catch {
return false
}
}
/**
* Run a command as a child process, streaming stdout/stderr.
* Returns the exit code.
*/
function runStep(command: string, args: string[], cwd: string): Promise<number> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd,
stdio: 'inherit',
shell: false
})
child.on('error', reject)
child.on('close', (code) => resolve(code ?? 1))
})
}
async function main() {
const { dir, skipVectorize, force } = parseArgs()
const uiRoot = path.join(__dirname, '..')
const iconsDir = path.join(uiRoot, 'icons', dir)
console.log(`\n========================================`)
console.log(` Icon Pipeline: ${dir}`)
console.log(`========================================\n`)
const results: StepResult[] = []
const steps: Array<{
name: string
args: string[]
skip?: () => Promise<boolean> | boolean
skipReason?: string
}> = [
{
name: 'Vectorize',
args: ['scripts/vectorize-logo.ts', `--dir=${dir}`],
skip: async () => skipVectorize || !(await hasRasterFiles(iconsDir)),
skipReason: skipVectorize ? '--skip-vectorize flag' : 'no raster files found'
},
{
name: 'Validate',
args: ['scripts/validate-svgs.ts', `--dir=${dir}`]
},
{
name: 'Normalize',
args: ['scripts/normalize-viewbox.ts', `--dir=${dir}`]
},
{
name: 'Generate',
args: ['scripts/generate-icons.ts', `--type=${dir}`, ...(force ? ['--force'] : [])]
},
{
name: 'Avatars',
args: ['scripts/generate-avatars.ts', `--type=${dir}`]
}
]
const pipelineStart = Date.now()
for (const step of steps) {
const shouldSkip = step.skip ? await step.skip() : false
if (shouldSkip) {
const reason = step.skipReason || 'condition not met'
console.log(`[${step.name}] Skipped (${reason})\n`)
results.push({
name: step.name,
command: `tsx ${step.args.join(' ')}`,
status: 'skipped',
durationMs: 0
})
continue
}
const cmd = `tsx ${step.args.join(' ')}`
console.log(`[${step.name}] Running: ${cmd}`)
console.log('─'.repeat(50))
const stepStart = Date.now()
try {
const exitCode = await runStep('tsx', step.args, uiRoot)
const durationMs = Date.now() - stepStart
if (exitCode !== 0) {
console.log(`\n[${step.name}] Failed with exit code ${exitCode} (${durationMs}ms)\n`)
results.push({ name: step.name, command: cmd, status: 'failed', durationMs, exitCode })
break
}
console.log(`\n[${step.name}] Done (${durationMs}ms)\n`)
results.push({ name: step.name, command: cmd, status: 'success', durationMs })
} catch (error) {
const durationMs = Date.now() - stepStart
console.error(`\n[${step.name}] Error: ${error}\n`)
results.push({ name: step.name, command: cmd, status: 'failed', durationMs, exitCode: 1 })
break
}
}
const totalMs = Date.now() - pipelineStart
// Summary
console.log('\n========================================')
console.log(' Summary')
console.log('========================================\n')
const nameWidth = Math.max(...results.map((r) => r.name.length), 4)
console.log(` ${'Step'.padEnd(nameWidth)} Status Duration`)
console.log(` ${'─'.repeat(nameWidth)} ──────── ────────`)
for (const r of results) {
const statusIcon = r.status === 'success' ? 'OK' : r.status === 'skipped' ? 'SKIP' : 'FAIL'
const duration = r.status === 'skipped' ? '-' : `${r.durationMs}ms`
console.log(` ${r.name.padEnd(nameWidth)} ${statusIcon.padEnd(8)} ${duration}`)
}
console.log(`\n Total: ${totalMs}ms`)
const failed = results.some((r) => r.status === 'failed')
if (failed) {
console.log('\n Pipeline failed. Fix the errors above and re-run.\n')
process.exit(1)
}
console.log('\n Pipeline completed successfully.\n')
}
void main()