feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from 'child_process'
|
||||
import fs from 'fs'
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m'
|
||||
}
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logStep(step, message) {
|
||||
log(`${step} ${message}`, 'cyan')
|
||||
}
|
||||
|
||||
function logSuccess(message) {
|
||||
log(`✅ ${message}`, 'green')
|
||||
}
|
||||
|
||||
function logWarning(message) {
|
||||
log(`⚠️ ${message}`, 'yellow')
|
||||
}
|
||||
|
||||
function logError(message) {
|
||||
log(`❌ ${message}`, 'red')
|
||||
}
|
||||
|
||||
function execAsync(command, args = [], options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const { signal, ...spawnOptions } = options
|
||||
let abortHandler
|
||||
let settled = false
|
||||
const finish = (success) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
if (signal && abortHandler) signal.removeEventListener('abort', abortHandler)
|
||||
resolve(success)
|
||||
}
|
||||
let child
|
||||
try {
|
||||
child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
detached: Boolean(signal) && process.platform !== 'win32',
|
||||
...spawnOptions
|
||||
})
|
||||
} catch {
|
||||
finish(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (signal) {
|
||||
abortHandler = () => {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return
|
||||
if (process.platform === 'win32') {
|
||||
const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
})
|
||||
killer.once('error', () => child.kill('SIGTERM'))
|
||||
killer.once('exit', (code) => {
|
||||
if (code !== 0 && child.exitCode === null) child.kill('SIGTERM')
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGTERM')
|
||||
} catch {
|
||||
child.kill('SIGTERM')
|
||||
}
|
||||
}
|
||||
}
|
||||
signal.addEventListener('abort', abortHandler, { once: true })
|
||||
if (signal.aborted) abortHandler()
|
||||
}
|
||||
|
||||
child.once('error', () => finish(false))
|
||||
child.once('exit', (code) => finish(code === 0))
|
||||
})
|
||||
}
|
||||
|
||||
function settleTask(task) {
|
||||
return task.then(
|
||||
() => ({ success: true }),
|
||||
(error) => ({ success: false, error })
|
||||
)
|
||||
}
|
||||
|
||||
async function syncDatabase() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
logWarning('未设置 DATABASE_URL')
|
||||
return
|
||||
}
|
||||
|
||||
logStep('🗄️', '同步数据库...')
|
||||
const env = { ...process.env, CI: 'true', DRIZZLE_KIT_FORCE: 'true', NODE_ENV: 'production' }
|
||||
if (!(await execAsync(process.execPath, ['scripts/db-sync.js'], { env }))) {
|
||||
throw new Error('数据库同步失败,已终止构建以避免运行时schema不一致')
|
||||
}
|
||||
logSuccess('数据库同步成功')
|
||||
|
||||
if (fileExists('scripts/create-admin.js')) {
|
||||
logStep('👤', '检查管理员账户...')
|
||||
if (!(await execAsync('pnpm run create-admin', [], { env, shell: true }))) {
|
||||
throw new Error('创建或检查管理员账户失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function buildApplication(signal) {
|
||||
logStep('🔨', '构建应用...')
|
||||
if (!(await execAsync(process.execPath, ['scripts/build.js'], { env: process.env, signal }))) {
|
||||
throw new Error('构建失败')
|
||||
}
|
||||
logSuccess('构建完成')
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
return fs.existsSync(filePath)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Netlify 构建流程
|
||||
async function netlifyBuild() {
|
||||
log('🚀 Netlify 构建', 'cyan')
|
||||
|
||||
try {
|
||||
// 1. 设置环境变量
|
||||
process.env.NETLIFY = 'true'
|
||||
process.env.NITRO_PRESET = 'netlify'
|
||||
|
||||
// Netlify 会在 build.command 前完成依赖安装,保留平台缓存可显著缩短后续部署。
|
||||
logStep('📦', '使用 Netlify 已安装的依赖和构建缓存...')
|
||||
|
||||
// 2. 检查 Drizzle 配置
|
||||
if (!fileExists('drizzle.config.ts') || !fileExists('app/drizzle/schema.ts')) {
|
||||
throw new Error('Drizzle 配置文件不完整')
|
||||
}
|
||||
|
||||
// 3. 确保迁移目录存在
|
||||
if (!fileExists('app/drizzle/migrations')) {
|
||||
fs.mkdirSync('app/drizzle/migrations', { recursive: true })
|
||||
}
|
||||
|
||||
// 数据库操作主要等待网络,与 CPU 密集的 Nuxt 构建并行可缩短 serverless 部署时间。
|
||||
const buildController = new AbortController()
|
||||
const databaseTask = syncDatabase().catch((error) => {
|
||||
buildController.abort()
|
||||
throw error
|
||||
})
|
||||
|
||||
// 数据库失败时主动取消构建;构建失败时等待迁移安全结束,避免中断数据库事务。
|
||||
const [databaseResult, buildResult] = await Promise.all([
|
||||
settleTask(databaseTask),
|
||||
settleTask(buildApplication(buildController.signal))
|
||||
])
|
||||
if (!databaseResult.success) throw databaseResult.error
|
||||
if (!buildResult.success) throw buildResult.error
|
||||
|
||||
// 5. 验证构建输出
|
||||
const hasNetlifyFunctions = fileExists('.netlify/functions-internal/server')
|
||||
const hasOutputPublic = fileExists('.output/public')
|
||||
|
||||
if (hasNetlifyFunctions) {
|
||||
logSuccess('Netlify Functions 生成成功')
|
||||
}
|
||||
|
||||
if (hasOutputPublic) {
|
||||
logSuccess('静态资源生成成功')
|
||||
}
|
||||
|
||||
if (!hasNetlifyFunctions && !hasOutputPublic) {
|
||||
throw new Error('构建输出目录不存在')
|
||||
}
|
||||
|
||||
log('🎉 构建完成!', 'green')
|
||||
} catch (error) {
|
||||
logError(`构建失败: ${error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行构建
|
||||
netlifyBuild().catch((error) => {
|
||||
logError(`未预期的错误: ${error.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user