203 lines
5.4 KiB
JavaScript
203 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import { execSync } from 'child_process'
|
||
import fs from 'fs'
|
||
import { config } from 'dotenv'
|
||
import path from 'path'
|
||
|
||
// 加载环境变量(从项目根目录)
|
||
config({ path: path.resolve(process.cwd(), '.env') })
|
||
|
||
// 如果设置了 PREBUILT=true,则自动跳过安装和构建
|
||
if (process.env.PREBUILT === 'true') {
|
||
process.env.SKIP_INSTALL = 'true'
|
||
process.env.SKIP_BUILD = 'true'
|
||
}
|
||
|
||
// 颜色输出函数
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
red: '\x1b[31m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
cyan: '\x1b[36m'
|
||
}
|
||
const BUILD_MEMORY_MB = 1024
|
||
const DEFAULT_NODE_OPTIONS = `--max-old-space-size=${BUILD_MEMORY_MB}`
|
||
|
||
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 fileExists(filePath) {
|
||
try {
|
||
return fs.existsSync(filePath)
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 安全执行命令
|
||
function safeExec(command, options = {}) {
|
||
try {
|
||
execSync(command, { stdio: 'inherit', ...options })
|
||
return true
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 检查环境变量
|
||
function checkEnvironment() {
|
||
logStep('🔍', '检查环境配置...')
|
||
|
||
const requiredEnvVars = ['DATABASE_URL']
|
||
const missingVars = []
|
||
|
||
requiredEnvVars.forEach((varName) => {
|
||
if (!process.env[varName]) {
|
||
missingVars.push(varName)
|
||
}
|
||
})
|
||
|
||
if (missingVars.length > 0) {
|
||
logError(`缺少必需的环境变量: ${missingVars.join(', ')}`)
|
||
logError('请通过以下方式之一设置环境变量:')
|
||
logError('1. 创建 .env 文件并配置 DATABASE_URL')
|
||
logError('2. 使用 docker run -e DATABASE_URL=xxx 传递环境变量')
|
||
logError('3. 在 docker-compose.yml 中配置 environment')
|
||
throw new Error('环境变量配置不完整')
|
||
} else {
|
||
logSuccess('环境变量检查通过')
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// 主部署流程
|
||
async function deploy() {
|
||
log('🚀 开始部署...', 'cyan')
|
||
|
||
try {
|
||
// 0. 检查环境(必须通过)
|
||
checkEnvironment()
|
||
|
||
// 1. 安装依赖
|
||
if (process.env.SKIP_INSTALL === 'true') {
|
||
logStep('📦', '跳过依赖安装 (SKIP_INSTALL=true)...')
|
||
} else {
|
||
logStep('📦', '安装依赖...')
|
||
let installed = false
|
||
|
||
// 优先使用 npm(用户要求:禁止 pnpm)—— 优先 package-lock.json 冻结安装
|
||
if (fileExists('package-lock.json')) {
|
||
log('尝试使用 npm ci(基于 package-lock.json 冻结安装)...', 'cyan')
|
||
if (safeExec('npm ci --no-audit --no-fund')) {
|
||
installed = true
|
||
logSuccess('依赖安装完成 (npm ci)')
|
||
} else {
|
||
logWarning('npm ci 失败,准备回退到 npm install...')
|
||
}
|
||
} else {
|
||
logWarning('未检测到 package-lock.json,直接使用 npm install...')
|
||
}
|
||
|
||
// 兜底用 npm install
|
||
if (!installed) {
|
||
log('正在使用 npm install 安装...', 'cyan')
|
||
if (!safeExec('npm install --no-audit --no-fund')) {
|
||
throw new Error('依赖安装失败')
|
||
}
|
||
logSuccess('依赖安装完成 (npm install)')
|
||
}
|
||
}
|
||
|
||
// 2. 检查 Drizzle 配置
|
||
if (
|
||
!fileExists('drizzle.config.ts') ||
|
||
!fileExists('app/drizzle/schema.ts') ||
|
||
!fileExists('app/drizzle/db.ts')
|
||
) {
|
||
throw new Error('Drizzle 配置文件不完整')
|
||
}
|
||
|
||
// 2.1. 确保迁移目录存在
|
||
if (!fileExists('app/drizzle/migrations')) {
|
||
fs.mkdirSync('app/drizzle/migrations', { recursive: true })
|
||
}
|
||
|
||
// 3. 数据库同步
|
||
logStep('🗄️', '同步数据库...')
|
||
let dbSyncSuccess = false
|
||
if (process.env.DATABASE_URL) {
|
||
const nonInteractiveEnv = {
|
||
...process.env,
|
||
DRIZZLE_KIT_FORCE: 'true',
|
||
CI: 'true',
|
||
NODE_ENV: 'production'
|
||
}
|
||
if (safeExec('node scripts/db-sync.js', { env: nonInteractiveEnv })) {
|
||
logSuccess('数据库同步成功')
|
||
dbSyncSuccess = true
|
||
} else {
|
||
logWarning('数据库同步失败')
|
||
}
|
||
} else {
|
||
logWarning('未设置 DATABASE_URL,跳过数据库迁移')
|
||
}
|
||
|
||
// 4. 创建管理员账户
|
||
if (fileExists('scripts/create-admin.js') && dbSyncSuccess) {
|
||
logStep('👤', '检查管理员账户...')
|
||
// 优先 npm run create-admin,兜底直接用 node_modules/.bin/tsx 调用(用户禁用 pnpm)
|
||
if (!safeExec('npm run create-admin')) {
|
||
safeExec('./node_modules/.bin/tsx scripts/create-admin.js')
|
||
}
|
||
}
|
||
|
||
// 5. 构建应用
|
||
if (process.env.SKIP_BUILD === 'true') {
|
||
logStep('🔨', '跳过应用构建 (SKIP_BUILD=true)...')
|
||
} else {
|
||
logStep('🔨', '构建应用...')
|
||
const buildEnv = {
|
||
...process.env,
|
||
NODE_OPTIONS: process.env.NODE_OPTIONS || DEFAULT_NODE_OPTIONS
|
||
}
|
||
// 用 npm run build(= nuxt build),避免用 pnpm exec
|
||
if (!safeExec('npm run build', { env: buildEnv })) {
|
||
throw new Error('应用构建失败')
|
||
}
|
||
logSuccess('应用构建完成')
|
||
}
|
||
|
||
log('🎉 部署完成!', 'green')
|
||
} catch (error) {
|
||
logError(`部署失败: ${error.message}`)
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
// 运行部署
|
||
deploy().catch((error) => {
|
||
logError(`未预期的错误: ${error.message}`)
|
||
process.exit(1)
|
||
})
|