refactor: 主动审计修复多处可观测性、依赖、代码质量缺口
通过并行 explore + librarian + 自查发现并修复: 代码缺陷 - shutdown.ts: db.$client.end().finally(...) 静默吞错——关闭失败会 谎报 "DB pool closed" 后照常 exit 0。改用 await + try/catch 分别记录成功/失败,setTimeout 也换成 Bun.sleep。 - interceptors.ts: 两条 instanceof ORPCError && instanceof ValidationError 重复检查,改用 early return + 单 if 分支区分 code。 - types.ts: 移除从未被引用的 RouterInputs 死代码(仅 RouterOutputs 被 TodoItem 用到)。 Bun 原生 API(删/换 Node 兼容层) - fields.ts: uuid v7 → Bun.randomUUIDv7(),删除 uuid 依赖 - migrate.ts: node:crypto.createHash → Bun.CryptoHasher.hash, 少一个 Promise.all 项 + 一个 import - shutdown.ts: setTimeout → Bun.sleep(顺带) Biome 2.4 规则补强 - domains.types: "all"——开启类型感知规则集(noFloatingPromises / noMisusedPromises / useAwaitThenable / noUnnecessaryConditions 等 Promise/异步陷阱) - domains.drizzle: "recommended"、domains.react: "recommended" - 显式开启 suspicious.noImportCycles(2.4 已 promote) 文档 - AGENTS.md 在 Stack & runtime 段加 "Prefer Bun-native APIs" 原则,列出 UUIDv7/SHA-256/sleep/Bun.file 的优先路径 - AGENTS.md 在 Code style (Biome) 段记录本次启用的 lint domain 与 noImportCycles 规则 验证:fix / typecheck / test 3/3 / build 568ms / compile 117M / docker compose 全套(migrate JSON 日志 ✓、UUIDv7 写入 ✓、SIGTERM shutdown 正确序列化 ✓)
This commit is contained in:
@@ -5,6 +5,7 @@ Compact, repo-specific notes for AI agents. Generic language/framework knowledge
|
|||||||
## Stack & runtime
|
## Stack & runtime
|
||||||
|
|
||||||
- **Bun-only** (`mise.toml` pins `bun = 1.3.13`). Never invoke `npm`/`npx`/`node`/`yarn`/`pnpm`. Use `bun run <script>` (bare `bun <script>` can collide with Bun built-in subcommands).
|
- **Bun-only** (`mise.toml` pins `bun = 1.3.13`). Never invoke `npm`/`npx`/`node`/`yarn`/`pnpm`. Use `bun run <script>` (bare `bun <script>` can collide with Bun built-in subcommands).
|
||||||
|
- **Prefer Bun-native APIs over external packages and `node:*` polyfills.** UUIDv7 → `Bun.randomUUIDv7()` (not the `uuid` package); SHA-256 → `Bun.CryptoHasher.hash('sha256', s, 'hex')` (not `node:crypto.createHash`); short sleeps → `Bun.sleep(ms)` (not raw `setTimeout` with promise wrapping); file I/O in build scripts → `Bun.file` / `Bun.write` are fine. The runtime is Bun, the deployment target is Bun, the test runner is Bun — there is no "portability" concern that would justify dragging in npm packages or Node compat shims for things Bun ships natively.
|
||||||
- TanStack Start (React 19 SSR, file-routed) + Vite 8 + Nitro (nightly, preset `bun`). Vite dev port is **strict 3000**.
|
- TanStack Start (React 19 SSR, file-routed) + Vite 8 + Nitro (nightly, preset `bun`). Vite dev port is **strict 3000**.
|
||||||
- PostgreSQL + **Drizzle ORM `0.45.2` (0.x, NOT 1.0 beta)** — see "Drizzle" section, this matters a lot.
|
- PostgreSQL + **Drizzle ORM `0.45.2` (0.x, NOT 1.0 beta)** — see "Drizzle" section, this matters a lot.
|
||||||
- ORPC (contract-first), TanStack Query v5, Tailwind v4.
|
- ORPC (contract-first), TanStack Query v5, Tailwind v4.
|
||||||
@@ -97,6 +98,7 @@ Contract → Router → Handler → Client, all type-safe from a single contract
|
|||||||
## Code style (Biome)
|
## Code style (Biome)
|
||||||
|
|
||||||
- 2-space, LF, single quotes, **semicolons as-needed** (omitted unless required), 120-col, arrow parens always, `useArrowFunction: "error"` (covers function *expressions* only). Also `noReactPropAssignments: "error"`.
|
- 2-space, LF, single quotes, **semicolons as-needed** (omitted unless required), 120-col, arrow parens always, `useArrowFunction: "error"` (covers function *expressions* only). Also `noReactPropAssignments: "error"`.
|
||||||
|
- Lint domains enabled (Biome 2.4): `types: "all"` (catches `noFloatingPromises`, `noMisusedPromises`, `useAwaitThenable`, `noUnnecessaryConditions` — TS-aware async/promise traps), `drizzle: "recommended"`, `react: "recommended"`. `noImportCycles` is on under `suspicious`.
|
||||||
- **Route components use `function Foo()` declarations**, placed below the `Route` config so the file reads top-down (route on top, component below). This is the official TanStack Router/Start pattern and relies on hoisting — `const Foo = () => {}` would TDZ-error when referenced from `createFileRoute({ component: Foo })` above it. Inline arrows are fine for trivial leaf components (e.g. plain redirect routes). Non-route components (UI primitives in `src/components/`) use `const Foo = () => {}`.
|
- **Route components use `function Foo()` declarations**, placed below the `Route` config so the file reads top-down (route on top, component below). This is the official TanStack Router/Start pattern and relies on hoisting — `const Foo = () => {}` would TDZ-error when referenced from `createFileRoute({ component: Foo })` above it. Inline arrows are fine for trivial leaf components (e.g. plain redirect routes). Non-route components (UI primitives in `src/components/`) use `const Foo = () => {}`.
|
||||||
- Imports are auto-organized into two groups (external, then `@/*`), each alphabetical, with `import type` interleaved (NOT a separate group). `bun run fix` handles this; don't hand-sort.
|
- Imports are auto-organized into two groups (external, then `@/*`), each alphabetical, with `import type` interleaved (NOT a separate group). `bun run fix` handles this; don't hand-sort.
|
||||||
- Files: utils `kebab-case.ts`, components `PascalCase.tsx`.
|
- Files: utils `kebab-case.ts`, components `PascalCase.tsx`.
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"domains": {
|
||||||
|
"drizzle": "recommended",
|
||||||
|
"react": "recommended",
|
||||||
|
"types": "all"
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"complexity": {
|
"complexity": {
|
||||||
@@ -30,6 +35,7 @@
|
|||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noExplicitAny": "error",
|
"noExplicitAny": "error",
|
||||||
|
"noImportCycles": "error",
|
||||||
"noTsIgnore": "error"
|
"noTsIgnore": "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.9",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"uuid": "^14.0.0",
|
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -666,8 +665,6 @@
|
|||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
"uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="],
|
|
||||||
|
|
||||||
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
|
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
"postgres": "^3.4.9",
|
"postgres": "^3.4.9",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"uuid": "^14.0.0",
|
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+2
-3
@@ -6,12 +6,11 @@ export default defineCommand({
|
|||||||
description: 'Apply pending database migrations',
|
description: 'Apply pending database migrations',
|
||||||
},
|
},
|
||||||
async run() {
|
async run() {
|
||||||
const [{ env }, { drizzle }, { sql }, { embeddedMigrations }, { createHash }, { getLogger }] = await Promise.all([
|
const [{ env }, { drizzle }, { sql }, { embeddedMigrations }, { getLogger }] = await Promise.all([
|
||||||
import('@/env'),
|
import('@/env'),
|
||||||
import('drizzle-orm/postgres-js'),
|
import('drizzle-orm/postgres-js'),
|
||||||
import('drizzle-orm'),
|
import('drizzle-orm'),
|
||||||
import('@/server/db/migrations.gen'),
|
import('@/server/db/migrations.gen'),
|
||||||
import('node:crypto'),
|
|
||||||
import('@/server/logger'),
|
import('@/server/logger'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ export default defineCommand({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sha256 = (s: string) => createHash('sha256').update(s).digest('hex')
|
const sha256 = (s: string) => Bun.CryptoHasher.hash('sha256', s, 'hex')
|
||||||
|
|
||||||
const db = drizzle({ connection: { url: env.DATABASE_URL, max: 1, onnotice: () => {} } })
|
const db = drizzle({ connection: { url: env.DATABASE_URL, max: 1, onnotice: () => {} } })
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ export const logError = (error: unknown) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const handleValidationError = (error: unknown) => {
|
export const handleValidationError = (error: unknown) => {
|
||||||
if (error instanceof ORPCError && error.code === 'BAD_REQUEST' && error.cause instanceof ValidationError) {
|
if (!(error instanceof ORPCError) || !(error.cause instanceof ValidationError)) return
|
||||||
|
|
||||||
|
if (error.code === 'BAD_REQUEST') {
|
||||||
// ORPC widens issues to the Standard Schema shape; every contract here is built from Zod/drizzle-zod,
|
// ORPC widens issues to the Standard Schema shape; every contract here is built from Zod/drizzle-zod,
|
||||||
// so the runtime objects are Zod issues. Rehydrate to reuse z.prettifyError / z.flattenError.
|
// so the runtime objects are Zod issues. Rehydrate to reuse z.prettifyError / z.flattenError.
|
||||||
const zodError = new z.ZodError(error.cause.issues as z.core.$ZodIssue[])
|
const zodError = new z.ZodError(error.cause.issues as z.core.$ZodIssue[])
|
||||||
@@ -22,7 +24,7 @@ export const handleValidationError = (error: unknown) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof ORPCError && error.code === 'INTERNAL_SERVER_ERROR' && error.cause instanceof ValidationError) {
|
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
||||||
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
|
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
|
||||||
cause: error.cause,
|
cause: error.cause,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ContractRouterClient, InferContractRouterInputs, InferContractRouterOutputs } from '@orpc/contract'
|
import type { ContractRouterClient, InferContractRouterOutputs } from '@orpc/contract'
|
||||||
import type { Contract } from './contracts'
|
import type { Contract } from './contracts'
|
||||||
|
|
||||||
export type RouterClient = ContractRouterClient<Contract>
|
export type RouterClient = ContractRouterClient<Contract>
|
||||||
export type RouterInputs = InferContractRouterInputs<Contract>
|
|
||||||
export type RouterOutputs = InferContractRouterOutputs<Contract>
|
export type RouterOutputs = InferContractRouterOutputs<Contract>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { timestamp, uuid } from 'drizzle-orm/pg-core'
|
import { timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||||
import { v7 as uuidv7 } from 'uuid'
|
|
||||||
|
|
||||||
export const generatedFields = {
|
export const generatedFields = {
|
||||||
id: uuid('id')
|
id: uuid('id')
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => uuidv7()),
|
.$defaultFn(() => Bun.randomUUIDv7()),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default () => {
|
|||||||
const logger = getLogger(['shutdown'])
|
const logger = getLogger(['shutdown'])
|
||||||
let exiting = false
|
let exiting = false
|
||||||
|
|
||||||
const shutdown = (signal: NodeJS.Signals) => {
|
const shutdown = async (signal: NodeJS.Signals) => {
|
||||||
if (exiting) {
|
if (exiting) {
|
||||||
logger.warn('Forcing exit on repeated signal', { signal })
|
logger.warn('Forcing exit on repeated signal', { signal })
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
@@ -15,14 +15,16 @@ export default () => {
|
|||||||
exiting = true
|
exiting = true
|
||||||
logger.info('Draining for shutdown', { signal, graceMs: 500 })
|
logger.info('Draining for shutdown', { signal, graceMs: 500 })
|
||||||
|
|
||||||
setTimeout(() => {
|
await Bun.sleep(500)
|
||||||
db.$client.end().finally(() => {
|
try {
|
||||||
logger.info('DB pool closed, exiting')
|
await db.$client.end()
|
||||||
process.exit(0)
|
logger.info('DB pool closed, exiting')
|
||||||
})
|
} catch (error) {
|
||||||
}, 500)
|
logger.error('DB pool close failed during shutdown', { error })
|
||||||
|
}
|
||||||
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('SIGINT', () => shutdown('SIGINT'))
|
process.on('SIGINT', () => void shutdown('SIGINT'))
|
||||||
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
process.on('SIGTERM', () => void shutdown('SIGTERM'))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user