feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
This commit is contained in:
102
packages/shared-ts/src/env-loader/index.ts
Normal file
102
packages/shared-ts/src/env-loader/index.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* .env 文件加载器(dev 模式专用).
|
||||
*
|
||||
* 背景:
|
||||
* - NestJS 服务的 main.ts 直接 `process.env` 读取环境变量,无 dotenv 自动加载。
|
||||
* - 在 PowerShell + pnpm dev 链中,部分变量可能丢失(Start-Process 子进程继承问题)。
|
||||
* - 本模块在每个 NestJS 服务的 main.ts 顶部最先调用,从 monorepo 根 .env 加载。
|
||||
*
|
||||
* 行为:
|
||||
* - 从调用方 cwd 向上查找 .env(最多 5 层),加载第一个找到的文件。
|
||||
* - 仅在 `process.env[KEY]` 为空/未定义时设置,不覆盖真实环境变量。
|
||||
* - 支持 `KEY=VALUE`、`KEY="VALUE"`、`KEY='VALUE'`,忽略注释与空行。
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* import "@edu/shared-ts/env-loader"; // 副作用导入,main.ts 第一行
|
||||
* ```
|
||||
*
|
||||
* 仲裁依据:
|
||||
* - coord-final-decisions §1 G4(pino 结构化日志)
|
||||
* - DEV_MODE=true 旁路鉴权(ADR-019)
|
||||
*/
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
/**
|
||||
* 解析单个 .env 文件内容为 [key, value] 数组。
|
||||
* @internal
|
||||
*/
|
||||
function parseEnvContent(content: string): Array<[string, string]> {
|
||||
const entries: Array<[string, string]> = [];
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const eqIdx = line.indexOf("=");
|
||||
if (eqIdx === -1) continue;
|
||||
const key = line.substring(0, eqIdx).trim();
|
||||
if (!key) continue;
|
||||
let val = line.substring(eqIdx + 1).trim();
|
||||
// 移除引号
|
||||
if (
|
||||
(val.startsWith('"') && val.endsWith('"')) ||
|
||||
(val.startsWith("'") && val.endsWith("'"))
|
||||
) {
|
||||
val = val.substring(1, val.length - 1);
|
||||
}
|
||||
entries.push([key, val]);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 startDir 向上查找 .env 文件,最多向上 maxDepth 层。
|
||||
* @internal
|
||||
*/
|
||||
function findEnvFile(startDir: string, maxDepth = 5): string | null {
|
||||
let current = startDir;
|
||||
for (let i = 0; i < maxDepth; i++) {
|
||||
const candidate = join(current, ".env");
|
||||
if (existsSync(candidate)) return candidate;
|
||||
const parent = resolve(current, "..");
|
||||
if (parent === current) break; // 到达根目录
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 .env 文件到 process.env(仅 dev 模式)。
|
||||
*
|
||||
* - 当 NODE_ENV === "production" 时跳过(生产环境必须用真实环境变量)。
|
||||
* - 当 DEV_MODE === "false" 时不跳过(DEV_MODE 仅控制鉴权旁路,不影响 .env 加载)。
|
||||
*
|
||||
* @returns 加载的变量数量(已存在的不计)
|
||||
*/
|
||||
export function loadEnvFile(): number {
|
||||
if (process.env.NODE_ENV === "production") return 0;
|
||||
|
||||
const envPath = findEnvFile(process.cwd());
|
||||
if (!envPath) return 0;
|
||||
|
||||
let loaded = 0;
|
||||
try {
|
||||
const content = readFileSync(envPath, "utf-8");
|
||||
const entries = parseEnvContent(content);
|
||||
for (const [key, val] of entries) {
|
||||
// 仅在未设置或空时填充,不覆盖真实环境变量
|
||||
const existing = process.env[key];
|
||||
if (existing === undefined || existing === "") {
|
||||
process.env[key] = val;
|
||||
loaded++;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 静默失败,env.ts 的 zod 校验会给出明确错误
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
// 模块导入时自动加载一次(副作用导入模式)
|
||||
loadEnvFile();
|
||||
Reference in New Issue
Block a user