451 lines
14 KiB
JavaScript
451 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import { spawn } from 'child_process'
|
||
import { config } from 'dotenv'
|
||
import path from 'path'
|
||
|
||
config({ path: path.resolve(process.cwd(), '.env'), quiet: true })
|
||
|
||
const BUILD_MEMORY_MB = 6144
|
||
const DEFAULT_NODE_OPTIONS = `--max-old-space-size=${BUILD_MEMORY_MB}`
|
||
const HIDDEN_VALUE = '[已隐藏]'
|
||
const SUPPORTED_AGGREGATE_LOGIN_TYPES = [
|
||
'qq',
|
||
'wx',
|
||
'alipay',
|
||
'sina',
|
||
'baidu',
|
||
'douyin',
|
||
'huawei',
|
||
'xiaomi',
|
||
'gitee',
|
||
'gitea',
|
||
'bilibili',
|
||
'kuaishou'
|
||
]
|
||
const SUPPORTED_ENV_VARIABLES = [
|
||
'NODE_ENV',
|
||
'NODE_OPTIONS',
|
||
'NITRO_PRESET',
|
||
'VERCEL',
|
||
'VERCEL_ENV',
|
||
'NETLIFY',
|
||
'PREBUILT',
|
||
'SKIP_INSTALL',
|
||
'SKIP_BUILD',
|
||
'CI',
|
||
'DATABASE_URL',
|
||
'JWT_SECRET',
|
||
'REDIS_URL',
|
||
'DEBUG_SQL',
|
||
'WEBAUTHN_RP_ID',
|
||
'WEBAUTHN_ORIGIN',
|
||
'GITHUB_CLIENT_ID',
|
||
'GITHUB_CLIENT_SECRET',
|
||
'CASDOOR_CLIENT_ID',
|
||
'CASDOOR_CLIENT_SECRET',
|
||
'CASDOOR_ENDPOINT',
|
||
'CASDOOR_ORGANIZATION_NAME',
|
||
'GOOGLE_CLIENT_ID',
|
||
'GOOGLE_CLIENT_SECRET',
|
||
'AGGREGATE_OAUTH_APP_ID',
|
||
'AGGREGATE_OAUTH_APP_KEY',
|
||
'AGGREGATE_OAUTH_LOGIN_TYPE',
|
||
'AGGREGATE_OAUTH_ENDPOINT',
|
||
'OAUTH_REDIRECT_URI',
|
||
'OAUTH_STATE_SECRET',
|
||
'NUXT_PUBLIC_HOST',
|
||
'NUXT_PUBLIC_SITE_TITLE',
|
||
'NUXT_PUBLIC_SITE_DESCRIPTION',
|
||
'NUXT_PUBLIC_SITE_LOGO',
|
||
'NUXT_PUBLIC_SEO_CONFIG',
|
||
'SENTRY_DSN',
|
||
'SENTRY_ENVIRONMENT',
|
||
'SENTRY_RELEASE',
|
||
'SENTRY_TRACES_SAMPLE_RATE',
|
||
'NUXT_PUBLIC_SENTRY_DSN',
|
||
'NUXT_PUBLIC_SENTRY_ENVIRONMENT',
|
||
'NUXT_PUBLIC_SENTRY_RELEASE',
|
||
'NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE',
|
||
'NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE',
|
||
'NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE',
|
||
'VERCEL_GIT_COMMIT_SHA',
|
||
'COMMIT_REF'
|
||
]
|
||
|
||
const colors = {
|
||
reset: '\x1b[0m',
|
||
green: '\x1b[32m',
|
||
yellow: '\x1b[33m',
|
||
cyan: '\x1b[36m',
|
||
dim: '\x1b[2m'
|
||
}
|
||
|
||
function log(message, color = 'reset') {
|
||
console.log(`${colors[color]}${message}${colors.reset}`)
|
||
}
|
||
|
||
function isProvided(value) {
|
||
return typeof value === 'string' && value.trim().length > 0
|
||
}
|
||
|
||
function normalizeBlankEnvironment() {
|
||
for (const name of SUPPORTED_ENV_VARIABLES) {
|
||
const value = process.env[name]
|
||
if (typeof value === 'string' && value.length > 0 && value.trim().length === 0) {
|
||
delete process.env[name]
|
||
}
|
||
}
|
||
}
|
||
|
||
function providedState(value) {
|
||
return isProvided(value) ? '已配置' : '未配置'
|
||
}
|
||
|
||
function strictBoolean(value) {
|
||
return value === 'true'
|
||
}
|
||
|
||
function numberInUnitInterval(value, fallback) {
|
||
if (!isProvided(value)) return { value: fallback, source: '默认值' }
|
||
|
||
const parsed = Number(value)
|
||
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
||
return { value: fallback, source: `无效值 ${JSON.stringify(value)},已回退` }
|
||
}
|
||
|
||
return { value: parsed, source: `来自 ${JSON.stringify(value)}` }
|
||
}
|
||
|
||
function displayValue(value, fallback = '未配置') {
|
||
return isProvided(value) ? JSON.stringify(value) : fallback
|
||
}
|
||
|
||
function hiddenValue(value) {
|
||
return isProvided(value) ? HIDDEN_VALUE : '未配置'
|
||
}
|
||
|
||
function resolveAggregateLoginTypeState(value) {
|
||
if (!isProvided(value)) return '使用 qq(默认值)'
|
||
let values
|
||
try {
|
||
const parsed = JSON.parse(value)
|
||
values = Array.isArray(parsed) ? parsed : [parsed]
|
||
} catch {
|
||
values = value.split(',')
|
||
}
|
||
const normalized = [
|
||
...new Set(
|
||
values
|
||
.filter((item) => typeof item === 'string')
|
||
.map((item) => item.trim().toLowerCase())
|
||
.filter((item) => SUPPORTED_AGGREGATE_LOGIN_TYPES.includes(item))
|
||
)
|
||
]
|
||
return normalized.length > 0
|
||
? '配置有效(具体值已隐藏)'
|
||
: '未识别到受支持的登录方式,导入时回退到 qq'
|
||
}
|
||
|
||
function printItem(name, rawState, result) {
|
||
log(` ${name}: ${rawState} -> ${result}`)
|
||
}
|
||
|
||
function printSensitiveItem(name, value, result) {
|
||
printItem(name, hiddenValue(value), result ?? providedState(value))
|
||
}
|
||
|
||
function resolveNitroPreset() {
|
||
if (process.env.VERCEL) return 'vercel(由 VERCEL 检测)'
|
||
if (process.env.NETLIFY) return 'netlify(由 NETLIFY 检测)'
|
||
return `${process.env.NITRO_PRESET || 'node-server'}${process.env.NITRO_PRESET ? '' : '(默认值)'}`
|
||
}
|
||
|
||
function resolveNodeOptions(value) {
|
||
const options = isProvided(value) ? value.trim() : ''
|
||
const hasMemoryLimit = /(?:^|\s)--max[-_]old[-_]space[-_]size(?:=|\s|$)/.test(options)
|
||
|
||
if (hasMemoryLimit) return options
|
||
return options ? `${options} ${DEFAULT_NODE_OPTIONS}` : DEFAULT_NODE_OPTIONS
|
||
}
|
||
|
||
function resolveSeoConfig() {
|
||
if (!isProvided(process.env.NUXT_PUBLIC_SEO_CONFIG)) {
|
||
return { state: '未配置', result: '使用分项配置或默认值' }
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(process.env.NUXT_PUBLIC_SEO_CONFIG)
|
||
const fields = ['title', 'shortName', 'description', 'logo'].filter((field) =>
|
||
isProvided(parsed?.[field])
|
||
)
|
||
return {
|
||
state: '已配置',
|
||
result: fields.length > 0 ? `解析成功:${fields.join(', ')}` : '解析成功,但未提供有效字段'
|
||
}
|
||
} catch {
|
||
return { state: '已配置', result: '解析失败,将回退到分项配置或默认值' }
|
||
}
|
||
}
|
||
|
||
function printBuildEnvironment(rawNodeOptions) {
|
||
const backendTraces = numberInUnitInterval(process.env.SENTRY_TRACES_SAMPLE_RATE, 0.1)
|
||
const frontendTraces = numberInUnitInterval(
|
||
process.env.NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE || process.env.SENTRY_TRACES_SAMPLE_RATE,
|
||
0.1
|
||
)
|
||
const sessionReplay = numberInUnitInterval(
|
||
process.env.NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE,
|
||
0
|
||
)
|
||
const errorReplay = numberInUnitInterval(
|
||
process.env.NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE,
|
||
0
|
||
)
|
||
const production = process.env.NODE_ENV === 'production'
|
||
const seoConfig = resolveSeoConfig()
|
||
|
||
log('\n🔎 构建环境变量诊断', 'cyan')
|
||
log('仅列出 VoiceHub 支持的变量;敏感值始终隐藏。', 'dim')
|
||
|
||
log('\n构建与部署:', 'cyan')
|
||
printItem('NODE_ENV', displayValue(process.env.NODE_ENV), process.env.NODE_ENV || '由 Nuxt 决定')
|
||
printItem('NODE_OPTIONS', displayValue(rawNodeOptions), process.env.NODE_OPTIONS)
|
||
printItem('NITRO_PRESET', displayValue(process.env.NITRO_PRESET), resolveNitroPreset())
|
||
printItem('VERCEL', displayValue(process.env.VERCEL), process.env.VERCEL ? '已检测' : '未检测')
|
||
printItem('VERCEL_ENV', displayValue(process.env.VERCEL_ENV), process.env.VERCEL_ENV || '未检测')
|
||
printItem('NETLIFY', displayValue(process.env.NETLIFY), process.env.NETLIFY ? '已检测' : '未检测')
|
||
printItem(
|
||
'PREBUILT',
|
||
displayValue(process.env.PREBUILT),
|
||
strictBoolean(process.env.PREBUILT) ? '启用' : '停用'
|
||
)
|
||
printItem(
|
||
'SKIP_INSTALL',
|
||
displayValue(process.env.SKIP_INSTALL),
|
||
strictBoolean(process.env.SKIP_INSTALL) ? '启用' : '停用'
|
||
)
|
||
printItem(
|
||
'SKIP_BUILD',
|
||
displayValue(process.env.SKIP_BUILD),
|
||
strictBoolean(process.env.SKIP_BUILD) ? '启用' : '停用'
|
||
)
|
||
printItem('CI', displayValue(process.env.CI), process.env.CI ? 'CI 环境' : '非 CI 环境')
|
||
|
||
log('\n服务能力:', 'cyan')
|
||
printSensitiveItem('DATABASE_URL', process.env.DATABASE_URL)
|
||
printSensitiveItem('JWT_SECRET', process.env.JWT_SECRET)
|
||
printSensitiveItem(
|
||
'REDIS_URL',
|
||
process.env.REDIS_URL,
|
||
isProvided(process.env.REDIS_URL) ? 'Redis 启用' : 'Redis 停用'
|
||
)
|
||
printItem(
|
||
'DEBUG_SQL',
|
||
displayValue(process.env.DEBUG_SQL),
|
||
strictBoolean(process.env.DEBUG_SQL) ? '启用' : '停用'
|
||
)
|
||
printItem(
|
||
'WEBAUTHN_RP_ID',
|
||
displayValue(process.env.WEBAUTHN_RP_ID),
|
||
providedState(process.env.WEBAUTHN_RP_ID)
|
||
)
|
||
printItem(
|
||
'WEBAUTHN_ORIGIN',
|
||
displayValue(process.env.WEBAUTHN_ORIGIN),
|
||
providedState(process.env.WEBAUTHN_ORIGIN)
|
||
)
|
||
|
||
log('\nOAuth 环境变量(仅用于后台导入):', 'cyan')
|
||
printItem(
|
||
'GitHub CLIENT_ID',
|
||
hiddenValue(process.env.GITHUB_CLIENT_ID),
|
||
isProvided(process.env.GITHUB_CLIENT_ID) ? 'OAuth 启用' : 'OAuth 停用'
|
||
)
|
||
printSensitiveItem('GitHub CLIENT_SECRET', process.env.GITHUB_CLIENT_SECRET)
|
||
printItem(
|
||
'Casdoor CLIENT_ID',
|
||
hiddenValue(process.env.CASDOOR_CLIENT_ID),
|
||
isProvided(process.env.CASDOOR_CLIENT_ID) ? 'OAuth 启用' : 'OAuth 停用'
|
||
)
|
||
printSensitiveItem('Casdoor CLIENT_SECRET', process.env.CASDOOR_CLIENT_SECRET)
|
||
printItem(
|
||
'Google CLIENT_ID',
|
||
hiddenValue(process.env.GOOGLE_CLIENT_ID),
|
||
isProvided(process.env.GOOGLE_CLIENT_ID) ? 'OAuth 启用' : 'OAuth 停用'
|
||
)
|
||
printSensitiveItem('Google CLIENT_SECRET', process.env.GOOGLE_CLIENT_SECRET)
|
||
printItem(
|
||
'聚合登陆 APP_ID',
|
||
hiddenValue(process.env.AGGREGATE_OAUTH_APP_ID),
|
||
isProvided(process.env.AGGREGATE_OAUTH_APP_ID) ? 'OAuth 启用' : 'OAuth 停用'
|
||
)
|
||
printSensitiveItem('聚合登陆 APP_KEY', process.env.AGGREGATE_OAUTH_APP_KEY)
|
||
printSensitiveItem(
|
||
'AGGREGATE_OAUTH_LOGIN_TYPE',
|
||
process.env.AGGREGATE_OAUTH_LOGIN_TYPE,
|
||
resolveAggregateLoginTypeState(process.env.AGGREGATE_OAUTH_LOGIN_TYPE)
|
||
)
|
||
printSensitiveItem(
|
||
'AGGREGATE_OAUTH_ENDPOINT',
|
||
process.env.AGGREGATE_OAUTH_ENDPOINT,
|
||
isProvided(process.env.AGGREGATE_OAUTH_ENDPOINT)
|
||
? '使用自定义接口地址(值已隐藏)'
|
||
: '使用默认接口地址'
|
||
)
|
||
printItem(
|
||
'CASDOOR_ENDPOINT',
|
||
displayValue(process.env.CASDOOR_ENDPOINT),
|
||
providedState(process.env.CASDOOR_ENDPOINT)
|
||
)
|
||
printItem(
|
||
'CASDOOR_ORGANIZATION_NAME',
|
||
displayValue(process.env.CASDOOR_ORGANIZATION_NAME),
|
||
process.env.CASDOOR_ORGANIZATION_NAME || 'built-in(默认值)'
|
||
)
|
||
printSensitiveItem('OAUTH_REDIRECT_URI', process.env.OAUTH_REDIRECT_URI)
|
||
printSensitiveItem('OAUTH_STATE_SECRET', process.env.OAUTH_STATE_SECRET)
|
||
|
||
log('\n站点公开配置:', 'cyan')
|
||
printItem(
|
||
'NUXT_PUBLIC_HOST',
|
||
displayValue(process.env.NUXT_PUBLIC_HOST),
|
||
providedState(process.env.NUXT_PUBLIC_HOST)
|
||
)
|
||
printItem(
|
||
'NUXT_PUBLIC_SITE_TITLE',
|
||
displayValue(process.env.NUXT_PUBLIC_SITE_TITLE),
|
||
providedState(process.env.NUXT_PUBLIC_SITE_TITLE)
|
||
)
|
||
printItem(
|
||
'NUXT_PUBLIC_SITE_DESCRIPTION',
|
||
displayValue(process.env.NUXT_PUBLIC_SITE_DESCRIPTION),
|
||
providedState(process.env.NUXT_PUBLIC_SITE_DESCRIPTION)
|
||
)
|
||
printItem(
|
||
'NUXT_PUBLIC_SITE_LOGO',
|
||
displayValue(process.env.NUXT_PUBLIC_SITE_LOGO),
|
||
providedState(process.env.NUXT_PUBLIC_SITE_LOGO)
|
||
)
|
||
printItem('NUXT_PUBLIC_SEO_CONFIG', seoConfig.state, seoConfig.result)
|
||
|
||
log('\nSentry:', 'cyan')
|
||
printItem(
|
||
'运行状态',
|
||
displayValue(process.env.NODE_ENV),
|
||
production ? '启用(仍受后台遥测开关控制)' : '停用'
|
||
)
|
||
printSensitiveItem(
|
||
'SENTRY_DSN',
|
||
process.env.SENTRY_DSN,
|
||
isProvided(process.env.SENTRY_DSN) ? '使用自定义后端 DSN' : '使用内置后端 DSN'
|
||
)
|
||
printSensitiveItem(
|
||
'NUXT_PUBLIC_SENTRY_DSN',
|
||
process.env.NUXT_PUBLIC_SENTRY_DSN,
|
||
isProvided(process.env.NUXT_PUBLIC_SENTRY_DSN) ? '使用自定义前端 DSN' : '使用内置前端 DSN'
|
||
)
|
||
printItem('SENTRY_TRACES_SAMPLE_RATE', backendTraces.source, backendTraces.value)
|
||
printItem('NUXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE', frontendTraces.source, frontendTraces.value)
|
||
printItem(
|
||
'NUXT_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE',
|
||
sessionReplay.source,
|
||
sessionReplay.value
|
||
)
|
||
printItem(
|
||
'NUXT_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE',
|
||
errorReplay.source,
|
||
errorReplay.value
|
||
)
|
||
printItem(
|
||
'SENTRY_ENVIRONMENT',
|
||
displayValue(process.env.SENTRY_ENVIRONMENT),
|
||
process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || 'development'
|
||
)
|
||
printItem(
|
||
'NUXT_PUBLIC_SENTRY_ENVIRONMENT',
|
||
displayValue(process.env.NUXT_PUBLIC_SENTRY_ENVIRONMENT),
|
||
process.env.NUXT_PUBLIC_SENTRY_ENVIRONMENT ||
|
||
process.env.SENTRY_ENVIRONMENT ||
|
||
process.env.NODE_ENV ||
|
||
'development'
|
||
)
|
||
printItem(
|
||
'SENTRY_RELEASE',
|
||
providedState(process.env.SENTRY_RELEASE),
|
||
process.env.SENTRY_RELEASE ||
|
||
process.env.VERCEL_GIT_COMMIT_SHA ||
|
||
process.env.COMMIT_REF ||
|
||
'未设置'
|
||
)
|
||
printItem(
|
||
'NUXT_PUBLIC_SENTRY_RELEASE',
|
||
providedState(process.env.NUXT_PUBLIC_SENTRY_RELEASE),
|
||
process.env.NUXT_PUBLIC_SENTRY_RELEASE ||
|
||
process.env.SENTRY_RELEASE ||
|
||
process.env.VERCEL_GIT_COMMIT_SHA ||
|
||
process.env.COMMIT_REF ||
|
||
'未设置'
|
||
)
|
||
log('')
|
||
}
|
||
|
||
function runNuxtBuild() {
|
||
return new Promise((resolve) => {
|
||
let settled = false
|
||
const finish = (success) => {
|
||
if (settled) return
|
||
settled = true
|
||
resolve(success)
|
||
}
|
||
let child
|
||
try {
|
||
child = spawn(process.execPath, ['./node_modules/nuxt/bin/nuxt.mjs', 'build'], {
|
||
stdio: 'inherit',
|
||
env: process.env
|
||
})
|
||
} catch {
|
||
finish(false)
|
||
return
|
||
}
|
||
const forwardSignal = (signal) => {
|
||
if (child.exitCode === null && child.signalCode === null) child.kill(signal)
|
||
}
|
||
const onSigterm = () => forwardSignal('SIGTERM')
|
||
const onSigint = () => forwardSignal('SIGINT')
|
||
const cleanup = () => {
|
||
process.off('SIGTERM', onSigterm)
|
||
process.off('SIGINT', onSigint)
|
||
}
|
||
|
||
process.once('SIGTERM', onSigterm)
|
||
process.once('SIGINT', onSigint)
|
||
child.once('error', () => {
|
||
cleanup()
|
||
finish(false)
|
||
})
|
||
child.once('exit', (code) => {
|
||
cleanup()
|
||
finish(code === 0)
|
||
})
|
||
})
|
||
}
|
||
|
||
async function build() {
|
||
const rawNodeOptions = process.env.NODE_OPTIONS
|
||
normalizeBlankEnvironment()
|
||
process.env.NODE_OPTIONS = resolveNodeOptions(rawNodeOptions)
|
||
printBuildEnvironment(rawNodeOptions)
|
||
|
||
if (process.argv.includes('--diagnostics-only')) return
|
||
|
||
log('🔨 开始执行 Nuxt 构建...', 'cyan')
|
||
if (!(await runNuxtBuild())) throw new Error('Nuxt 构建失败')
|
||
log('✅ Nuxt 构建完成', 'green')
|
||
}
|
||
|
||
build().catch((error) => {
|
||
log(`❌ ${error.message}`, 'yellow')
|
||
process.exit(1)
|
||
})
|