chore(deno): migrate toolchain from Node 24 + pnpm to Deno 2.9.7 + TypeScript 7.0.2 (W1.5)
- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock; remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports) - tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions), test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types - tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl - server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting; drop @hono/node-server, tsx, esbuild build.mjs - logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests - relative imports get explicit extensions (sites/mp-sync paths left for follow-up) - crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas - single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml - docs: README, runbook, ADR-001 superseded, plan §1 validation row Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
d9f2af2f3c
commit
304feea141
226 files changed
+6601
-8620
No files matched your search
@@ -17,6 +17,7 @@ export function redact(value: unknown, depth = 0): unknown {
|
||||
if (depth > 8) return value;
|
||||
if (typeof value === "string") return redactString(value);
|
||||
if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));
|
||||
if (value instanceof Error) return redactError(value, depth);
|
||||
if (value && typeof value === "object") {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
@@ -28,6 +29,20 @@ export function redact(value: unknown, depth = 0): unknown {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error 的 message/stack 是不可枚举属性,按普通对象处理会变成 `{}`;
|
||||
* 这里先按 pino 标准形状(type/message/stack + 自有字段 + cause)展开再脱敏。
|
||||
*/
|
||||
function redactError(err: Error, depth: number): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = { type: err.name, message: redactString(err.message) };
|
||||
if (err.stack) out.stack = redactString(err.stack);
|
||||
for (const [k, v] of Object.entries(err)) {
|
||||
out[k] = SENSITIVE_KEY.test(k) && typeof v !== "number" && v != null ? REDACTED : redact(v, depth + 1);
|
||||
}
|
||||
if (err.cause !== undefined) out.cause = redact(err.cause, depth + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface LoggerOptions {
|
||||
level?: string;
|
||||
pretty?: boolean;
|
||||
@@ -41,7 +56,11 @@ export function createLogger(opts: LoggerOptions = {}): Logger {
|
||||
formatters: {
|
||||
// 所有结构化字段(含 err 序列化结果)统一过一遍脱敏
|
||||
log: (obj) => redact(obj) as Record<string, unknown>,
|
||||
// 根 logger 的 base 绑定;child() 的绑定 pino 会跳过这里,由 redactChildBindings 处理
|
||||
bindings: (bindings) => redact(bindings) as Record<string, unknown>,
|
||||
},
|
||||
// err 已由 formatters.log 按 message/stack 展开并脱敏;默认 err 序列化器会把 type 改写成 "Object"
|
||||
serializers: { err: (v: unknown) => v },
|
||||
hooks: {
|
||||
// msg 文本也脱敏
|
||||
logMethod(args, method) {
|
||||
@@ -50,9 +69,26 @@ export function createLogger(opts: LoggerOptions = {}): Logger {
|
||||
},
|
||||
},
|
||||
};
|
||||
if (opts.destination) return pino(options, opts.destination);
|
||||
if (opts.destination) return redactChildBindings(pino(options, opts.destination));
|
||||
if (opts.pretty) {
|
||||
return pino({ ...options, transport: { target: "pino-pretty", options: { translateTime: "SYS:HH:MM:ss" } } });
|
||||
return redactChildBindings(
|
||||
pino({ ...options, transport: { target: "pino-pretty", options: { translateTime: "SYS:HH:MM:ss" } } }),
|
||||
);
|
||||
}
|
||||
return pino(options);
|
||||
// 同步写 stdout:进程随后 Deno.exit() 时(迁移/自检子命令、优雅退出)不丢尾部日志
|
||||
return redactChildBindings(pino(options, pino.destination({ dest: 1, sync: true })));
|
||||
}
|
||||
|
||||
/**
|
||||
* pino 的 child() 会把 formatters.bindings 重置为恒等函数,绑定字段(如 `child({ password })`)
|
||||
* 原样写进每一行。这里在根实例上覆盖 child:子孙 logger 经原型链继承,绑定先脱敏再交给 pino。
|
||||
*/
|
||||
function redactChildBindings(logger: Logger): Logger {
|
||||
type ChildFn = (this: Logger, bindings: pino.Bindings, options?: pino.ChildLoggerOptions) => Logger;
|
||||
const original = logger.child as unknown as ChildFn;
|
||||
const child: ChildFn = function (bindings, options) {
|
||||
return original.call(this, redact(bindings) as pino.Bindings, options);
|
||||
};
|
||||
logger.child = child as unknown as Logger["child"];
|
||||
return logger;
|
||||
}
|
||||
Reference in new issue
Block a user