Merge pull request #455 from alibaba/chore/parallel-build-scripts
chore(scripts): add parallel build scripts
This commit is contained in:
@@ -28,8 +28,8 @@
|
||||
"start": "npm run dev --workspace=@page-agent/website",
|
||||
"dev:ext": "npm run dev -w @page-agent/ext",
|
||||
"dev:demo": "npm run dev:demo --workspace=page-agent",
|
||||
"build": "npm run build:libs && npm run build:website",
|
||||
"build:libs": "npm run build --workspaces --if-present",
|
||||
"build": "node scripts/build.js",
|
||||
"build:libs": "node scripts/build-libs.js",
|
||||
"build:website": "npm run build:website --workspace=@page-agent/website",
|
||||
"build:ext": "npm run zip -w @page-agent/ext",
|
||||
"version": "node scripts/sync-version.js",
|
||||
|
||||
25
scripts/build-libs.js
Normal file
25
scripts/build-libs.js
Normal 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
45
scripts/build.js
Normal 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
115
scripts/parallel-task.js
Normal 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 `)}`)
|
||||
}
|
||||
Reference in New Issue
Block a user