mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 14:01:30 +08:00
209 lines
6.0 KiB
TypeScript
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()
|