Files
ClassHelper/packages/server/src/config/env.ts
T

78 lines
2.9 KiB
TypeScript
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.
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { config as loadEnv } from 'dotenv';
import { z } from 'zod';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
/**
* packages/server 根目录。
* 源码运行时为 src/config -> ../..;编译后为 dist/config -> ../..,两者一致。
*/
export const serverRoot = path.resolve(currentDir, '..', '..');
// 显式按包根目录加载 .env,避免受启动时 cwd 影响
loadEnv({ path: path.join(serverRoot, '.env') });
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.coerce.number().int().min(1).max(65535).default(4000),
DATABASE_PROVIDER: z.enum(['sqlite', 'mysql']).default('sqlite'),
DATABASE_URL: z.string().min(1).default('file:./prisma/dev.db'),
JWT_SECRET: z.string().min(16),
JWT_EXPIRES_IN: z.string().default('7d'),
CORS_ORIGIN: z.string().default('*'),
BCRYPT_ROUNDS: z.coerce.number().int().min(4).max(15).default(10),
DEFAULT_STUDENT_PASSWORD: z.string().min(6).default('123456'),
TERM_START_DATE: z.string().default(''),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
const issues = parsed.error.issues
.map((issue) => ` - ${issue.path.join('.') || '(root)'}: ${issue.message}`)
.join('\n');
throw new Error(`环境变量校验失败,请检查 packages/server/.env(可从 .env.example 复制):\n${issues}`);
}
const raw = parsed.data;
/** 把 file:./prisma/dev.db 解析为绝对路径,保证任何 cwd 下都指向同一个数据库文件 */
function resolveSqliteUrl(url: string): string {
if (!url.startsWith('file:')) return url;
const filePath = url.slice('file:'.length);
const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(serverRoot, filePath);
return `file:${absolute.replace(/\\/g, '/')}`;
}
const isSqlite = raw.DATABASE_PROVIDER === 'sqlite';
export const env = {
nodeEnv: raw.NODE_ENV,
isProduction: raw.NODE_ENV === 'production',
isDevelopment: raw.NODE_ENV === 'development',
port: raw.PORT,
databaseProvider: raw.DATABASE_PROVIDER,
/** 已规范化为绝对路径(SQLite)/ 原始连接串(MySQL) */
databaseUrl: isSqlite ? resolveSqliteUrl(raw.DATABASE_URL) : raw.DATABASE_URL,
rawDatabaseUrl: raw.DATABASE_URL,
jwtSecret: raw.JWT_SECRET,
jwtExpiresIn: raw.JWT_EXPIRES_IN,
/** true 表示允许所有来源,否则为白名单数组 */
corsOrigins:
raw.CORS_ORIGIN === '*'
? true
: raw.CORS_ORIGIN.split(',')
.map((item) => item.trim())
.filter(Boolean),
bcryptRounds: raw.BCRYPT_ROUNDS,
/** 新增学生时未指定密码时的初始密码 */
defaultStudentPassword: raw.DEFAULT_STUDENT_PASSWORD,
termStartDate: raw.TERM_START_DATE,
logLevel: raw.LOG_LEVEL,
serverRoot,
} as const;
export type AppEnv = typeof env;