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:
SpecialX
2026-07-17 16:10:05 +08:00
parent f7e52b5b7f
commit 9cedf0c437
140 changed files with 10872 additions and 3192 deletions

View 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 G4pino 结构化日志)
* - 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();