chore(scripts): add parallel build scripts

Replace sequential `npm run build --workspaces --if-present` with
concurrent execution via a reusable `parallelTask` utility. Full
`npm run build` now runs cleanup then all 7 build tasks in parallel.
This commit is contained in:
Simon
2026-04-15 17:44:37 +08:00
parent cc27ff9305
commit 9af3a3f73e
4 changed files with 187 additions and 2 deletions

25
scripts/build-libs.js Normal file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
/**
* Equivalent to: npm run build --workspaces --if-present
*
* Reads the workspace list from root package.json, filters to those with a
* "build" script, and runs them all concurrently via parallelTask.
*/
import { readFileSync } from 'fs'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { parallelTask } from './parallel-task.js'
const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..')
const rootPkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'))
const tasks = rootPkg.workspaces
.map((ws) => {
const dir = join(rootDir, ws)
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'))
return pkg.scripts?.build ? { label: pkg.name, command: 'npm run build', cwd: dir } : null
})
.filter(Boolean)
await parallelTask(tasks, { timeoutMs: 120_000 })

45
scripts/build.js Normal file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env node
/**
* Full build pipeline. Equivalent to:
* npm run cleanup && npm run build --workspaces --if-present
* && npm run build:website -w @page-agent/website
* && npm run zip -w @page-agent/ext
*
* 1. cleanup
* 2. build everything in parallel (libs + website + extension)
*/
import chalk from 'chalk'
import { execSync } from 'child_process'
import { readFileSync } from 'fs'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { parallelTask } from './parallel-task.js'
const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..')
const rootPkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'))
// Step 1: cleanup
console.log(chalk.bgBlue.white.bold(' ▸ cleanup '))
execSync('npm run cleanup', { cwd: rootDir, stdio: 'inherit' })
// Step 2: build all in parallel
console.log(chalk.bgBlue.white.bold(' ▸ build '))
const tasks = rootPkg.workspaces
.map((ws) => {
const dir = join(rootDir, ws)
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'))
return pkg.scripts?.build ? { label: pkg.name, command: 'npm run build', cwd: dir } : null
})
.filter(Boolean)
tasks.push(
{
label: '@page-agent/website',
command: 'npm run build:website',
cwd: join(rootDir, 'packages/website'),
},
{ label: '@page-agent/ext', command: 'npm run zip', cwd: join(rootDir, 'packages/extension') }
)
await parallelTask(tasks, { timeoutMs: 120_000 })

115
scripts/parallel-task.js Normal file
View File

@@ -0,0 +1,115 @@
import chalk from 'chalk'
import { spawn } from 'child_process'
/**
* Run multiple shell commands in parallel with progress reporting.
*
* @param {{ label: string, command: string, cwd?: string }[]} tasks
* @param {{ timeoutMs?: number }} options - Default timeout 30s per task
* @returns {Promise<void>} Rejects (process.exit) if any task fails
*/
export async function parallelTask(tasks, options = {}) {
const { timeoutMs = 30_000 } = options
const total = tasks.length
const bgColors = [
chalk.bgCyan,
chalk.bgMagenta,
chalk.bgBlue,
chalk.bgYellow,
chalk.bgGreenBright,
]
const fgOnBg = [chalk.black, chalk.white, chalk.white, chalk.black, chalk.black]
let done = 0
let failed = 0
const spinner = ['◐', '◓', '◑', '◒']
let tick = 0
const printProgress = () => {
const running = total - done - failed
const s = spinner[tick++ % spinner.length]
const status = failed
? `${running} running, ${done} done, ${chalk.bgRed.white.bold(` ${failed} failed `)}`
: `${running} running, ${done} done`
process.stderr.write(`\r${chalk.bgCyan.black.bold(` ${s} ${done}/${total} `)} ${status} `)
}
const timer = setInterval(printProgress, 1000)
printProgress()
/** @type {{ label: string, output: string, exitCode: number | null, timedOut: boolean }[]} */
const results = await Promise.all(
tasks.map(
(task) =>
new Promise((resolve) => {
const chunks = /** @type {Buffer[]} */ ([])
const child = spawn('sh', ['-c', task.command], {
cwd: task.cwd,
env: { ...process.env, FORCE_COLOR: '1', NO_COLOR: '' },
stdio: ['ignore', 'pipe', 'pipe'],
})
child.stdout.on('data', (d) => chunks.push(d))
child.stderr.on('data', (d) => chunks.push(d))
const timeout = setTimeout(() => {
child.kill('SIGTERM')
}, timeoutMs)
child.on('close', (code, signal) => {
clearTimeout(timeout)
const timedOut = signal === 'SIGTERM'
const exitCode = timedOut ? 1 : code
if (exitCode === 0) done++
else failed++
resolve({
label: task.label,
output: Buffer.concat(chunks).toString(),
exitCode,
timedOut,
})
})
})
)
)
clearInterval(timer)
process.stderr.write('\r\x1b[K')
const failedTasks = /** @type {typeof results} */ ([])
for (let i = 0; i < results.length; i++) {
const r = results[i]
if (r.exitCode !== 0) {
failedTasks.push(r)
continue
}
const bg = bgColors[i % bgColors.length]
const fg = fgOnBg[i % fgOnBg.length]
const banner = bg(fg.bold(`${r.label} `))
console.log(`\n${banner}`)
if (r.output.trim()) process.stdout.write(r.output)
}
if (failedTasks.length) {
for (const r of failedTasks) {
const banner = chalk.bgRed(
chalk.white.bold(`${r.label} ${r.timedOut ? '· timed out' : '· failed'} `)
)
console.log(`\n${banner}`)
if (r.output.trim()) process.stdout.write(r.output)
}
const summary = failedTasks.map((t) => t.label).join(', ')
console.error(
`\n${chalk.bgRed.white.bold(`${failedTasks.length}/${total} failed: ${summary} `)}`
)
process.exit(1)
}
console.log(`\n${chalk.bgGreen.black.bold(` ✔ All ${total} tasks completed `)}`)
}