Files
BYW-VoiceHub/scripts/build.js
T

451 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)
})