Files
fullstack-starter-SQLite/apps/server/AGENTS.md
imbytecat 5e56383f6f feat: 迁移数据库至SQLite并适配嵌入式环境
- 添加SQLite数据库文件及数据目录的忽略规则
- 将数据库连接地址更新为本地SQLite数据库文件路径
- 将数据库从 PostgreSQL 迁移至 SQLite,更新依赖、Drizzle 方言及数据类型映射,确保嵌入式环境兼容性并保持应用层代码不变。
- 将数据库方言从 PostgreSQL 更改为 SQLite。
- 将数据库依赖从 postgres 替换为 better-sqlite3,并添加 better-sqlite3 的类型定义。
- 修改数据库连接字符串验证规则,从URL格式验证改为非空字符串验证。
- 将数据库连接从 PostgreSQL 切换为 Better-SQLite3,并支持创建目录和内存数据库。
- 将 todo 表的 completed 字段从 PostgreSQL 的 boolean 类型改为 SQLite 的 boolean 模式整数类型。
- 将字段定义从 PostgreSQL 适配改为 SQLite 适配,使用文本类型 ID 和时间戳整数类型,并统一使用 UUIDv7 和当前时间作为默认值。
- 添加 better-sqlite3 及其类型定义,并引入相关依赖以支持其功能
- 添加 better-sqlite3 依赖并移除 postgres 依赖
2026-01-26 14:10:25 +08:00

16 KiB
Raw Blame History

AGENTS.md - AI Coding Agent Guidelines

本文档为 AI 编程助手提供此 TanStack Start 全栈项目的开发规范和指南。

项目概览

  • 框架: TanStack Start (React SSR 框架,文件路由)
  • 运行时: Bun
  • 语言: TypeScript (strict mode, ESNext)
  • 样式: Tailwind CSS v4
  • 数据库: SQLite + Drizzle ORM
  • 状态管理: TanStack Query
  • 路由: TanStack Router (文件路由)
  • RPC: ORPC (类型安全 RPC契约优先)
  • 构建工具: Vite + Turbo
  • 代码质量: Biome (格式化 + Lint)

依赖管理

Bun Catalog 系统

项目使用 Bun Catalog 统一管理依赖版本(定义在根目录 package.jsoncatalog 字段)。

安装依赖的正确方式

# ✅ 正确:使用 catalog: 前缀
bun add <package-name>@catalog:

# ❌ 错误:直接安装会绕过版本统一管理
bun add <package-name>@latest

示例

# 添加 systeminformation 依赖到 packages/utils
cd packages/utils
bun add systeminformation@catalog:

# 添加 react 依赖到 apps/server
cd apps/server
bun add react@catalog:

为什么使用 Catalog

  • 确保 monorepo 中所有包使用相同版本
  • 集中管理依赖版本,避免版本冲突
  • 简化依赖升级(只需修改根 package.json

添加新依赖的步骤

  1. 在根目录 package.jsoncatalog 字段添加依赖及版本
  2. 在目标包中使用 bun add <package>@catalog: 安装

构建、Lint 和测试命令

开发

bun dev                    # 启动 Vite 开发服务器
bun db:studio              # 打开 Drizzle Studio 数据库管理界面

构建

bun build                  # 构建 Vite 应用 (输出到 .output/)
bun compile                # 编译为独立可执行文件 (使用 build.ts)

代码质量

bun typecheck              # 运行 TypeScript 类型检查
bun fix                    # 运行 Biome 自动修复格式和 Lint 问题
biome check .              # 检查但不自动修复
biome format --write .     # 仅格式化代码

数据库

bun db:generate            # 从 schema 生成迁移文件
bun db:migrate             # 执行数据库迁移
bun db:push                # 直接推送 schema 变更 (仅开发环境)

测试

注意: 当前未配置测试框架。添加测试时:

  • 使用 Vitest 或 Bun 内置测试运行器
  • 运行单个测试文件: bun test path/to/test.ts
  • 运行特定测试: bun test -t "测试名称模式"

代码风格指南

格式化 (Biome)

缩进: 2 空格 (不使用 tab) 换行符: LF (Unix 风格) 引号: 单引号 'string' 分号: 按需 (ASI - 自动分号插入) 箭头函数括号: 始终使用 (x) => x

示例:

const myFunc = (value: string) => {
  return value.toUpperCase()
}

导入组织

Biome 自动组织导入。顺序:

  1. 外部依赖
  2. 内部导入 (使用 @/* 别名)
  3. 类型导入 (仅导入类型时使用 type 关键字)

示例:

import { createFileRoute } from '@tanstack/react-router'
import { oc } from '@orpc/contract'
import { z } from 'zod'
import { db } from '@/db'
import { todoTable } from '@/db/schema'
import type { ReactNode } from 'react'

TypeScript

严格模式: 启用了额外的严格检查

  • strict: true
  • noUncheckedIndexedAccess: true - 数组/对象索引返回 T | undefined
  • noImplicitOverride: true
  • noFallthroughCasesInSwitch: true

模块解析: bundler 模式 + verbatimModuleSyntax

  • 导入时始终使用 .ts/.tsx 扩展名
  • 使用 @/* 路径别名指向 src/*

类型注解:

  • 公共 API 的函数参数和返回类型必须注解
  • 优先使用显式类型而非 any
  • 对象形状用 type,可扩展契约用 interface
  • 不可变 props 使用 Readonly<T>

命名规范

  • 文件: 工具函数用 kebab-case组件用 PascalCase
    • utils.ts, todo.tsx, NotFound.tsx
  • 路由: 遵循 TanStack Router 约定
    • routes/index.tsx/
    • routes/__root.tsx → 根布局
  • 组件: PascalCase 箭头函数 (Biome 规则 useArrowFunction 强制)
  • 函数: camelCase
  • 常量: 真常量用 UPPER_SNAKE_CASE配置对象用 camelCase
  • 类型/接口: PascalCase

React 模式

组件: 使用箭头函数

const MyComponent = ({ title }: { title: string }) => {
  return <div>{title}</div>
}

路由: 使用 createFileRoute 定义路由

export const Route = createFileRoute('/')({
  component: Home,
})

数据获取: 使用 TanStack Query hooks

  • useSuspenseQuery - 保证有数据
  • useQuery - 数据可能为空

Props: 禁止直接修改 props (Biome 规则 noReactPropAssignments)

数据库 Schema (Drizzle)

  • src/server/db/schema/*.ts 定义 schema
  • src/server/db/schema/index.ts 导出
  • 使用 drizzle-orm/sqlite-core 的 SQLite 类型
  • 主键使用 uuidv7() (TEXT 存储)
  • 时间戳使用 integer({ mode: 'timestamp_ms' }) (Unix 毫秒时间戳)
  • 始终包含 createdAtupdatedAt 时间戳

示例:

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { v7 as uuidv7 } from 'uuid'

export const myTable = sqliteTable('my_table', {
  id: text('id').primaryKey().$defaultFn(() => uuidv7()),
  name: text('name').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
  updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()).$onUpdateFn(() => new Date()),
})

环境变量

  • 使用 @t3-oss/env-core 进行类型安全的环境变量验证
  • src/env.ts 定义 schema
  • 服务端变量: 无前缀
  • 客户端变量: 必须有 VITE_ 前缀
  • 使用 Zod schema 验证

错误处理

  • 异步操作使用 try-catch
  • 抛出带有描述性消息的错误
  • 用户界面错误优先使用 Result 类型或错误边界
  • 适当记录错误 (避免记录敏感数据)

样式 (Tailwind CSS)

  • 使用 Tailwind v4 工具类
  • 通过 @/styles.css?url 导入样式
  • 优先使用组合而非自定义 CSS
  • 响应式修饰符: sm:, md:, lg:
  • UI 文本适当使用中文

目录结构

src/
├── components/            # 可复用 React 组件
├── db/
│   ├── schema/           # Drizzle schema 定义
│   └── index.ts          # 数据库实例
├── integrations/         # 第三方集成 (TanStack Query/Router)
├── lib/                  # 工具函数
├── orpc/                 # ORPC (RPC 层)
│   ├── contracts/        # 契约定义 (input/output schemas)
│   ├── handlers/         # 服务端过程实现
│   ├── middlewares/      # 中间件 (如 DB provider)
│   ├── contract.ts       # 契约聚合
│   ├── router.ts         # 路由组合
│   ├── server.ts         # 服务端实例
│   └── client.ts         # 同构客户端
├── routes/               # TanStack Router 文件路由
│   ├── __root.tsx        # 根布局
│   ├── index.tsx         # 首页
│   └── api/rpc.$.ts      # ORPC HTTP 端点
├── env.ts                # 环境变量验证
└── router.tsx            # 路由配置

重要提示

  • 禁止 编辑 src/routeTree.gen.ts - 自动生成
  • 禁止 提交 .env 文件 - 使用 .env.example 作为模板
  • 必须 在提交前运行 bun fix
  • 必须 使用 @/* 路径别名而非相对导入
  • 必须 利用 React Compiler (babel-plugin-react-compiler) - 避免手动 memoization

Git 工作流

  1. 按照上述风格指南进行修改
  2. 运行 bun fix 自动格式化和 lint
  3. 运行 bun typecheck 确保类型安全
  4. 使用 bun dev 本地测试变更
  5. 使用清晰的描述性消息提交

常见模式

创建 ORPC 过程

步骤 1: 定义契约 (src/orpc/contracts/my-feature.ts)

import { oc } from '@orpc/contract'
import { z } from 'zod'

export const myContract = {
  get: oc.input(z.object({ id: z.uuid() })).output(mySchema),
  create: oc.input(createSchema).output(mySchema),
}

步骤 2: 实现处理器 (src/orpc/handlers/my-feature.ts)

import { os } from '@/orpc/server'
import { dbProvider } from '@/orpc/middlewares'

export const get = os.myFeature.get
  .use(dbProvider)
  .handler(async ({ context, input }) => {
    return await context.db.query.myTable.findFirst(...)
  })

步骤 3: 注册到契约和路由

// src/orpc/contract.ts
export const contract = { myFeature: myContract }

// src/orpc/router.ts
import * as myFeature from './handlers/my-feature'
export const router = os.router({ myFeature })

步骤 4: 在组件中使用

import { orpc } from '@/orpc'
const query = useSuspenseQuery(orpc.myFeature.get.queryOptions({ id }))
const mutation = useMutation(orpc.myFeature.create.mutationOptions())

已知问题与解决方案

构建问题

已解决: Vite 8.0.0-beta.10 已修复与 Nitro 插件的兼容性问题

  • 当前版本: Vite 8.0.0-beta.10 + nitro-nightly@3.0.1-20260125
  • 状态: 构建稳定,开发和生产环境均正常工作

依赖选择经验

ohash vs crypto.createHash

在实现硬件指纹功能时,曾误判 ohash 不适合用于硬件指纹识别。经深入研究发现:

事实

  • ohash 内部使用完整的 SHA-256 算法256 位)
  • 输出 43 字符 Base64URL 编码(等价于 64 字符 Hex
  • 碰撞概率与 crypto.createHash('sha256') 完全相同2^128
  • 自动处理对象序列化,代码更简洁

对比

// ohash - 推荐用于对象哈希
import { hash } from 'ohash'
const fingerprint = hash(systemInfo) // 一行搞定

// crypto - 需要手动序列化
import { createHash } from 'node:crypto'
const fingerprint = createHash('sha256')
  .update(JSON.stringify(systemInfo))
  .digest('base64url')

结论

  • ohash 完全适合硬件指纹场景(数据来自系统 API非用户输入
  • 两者安全性等价,选择取决于代码风格偏好
  • ⚠️ ohash 文档警告的"序列化安全性"仅针对用户输入场景

经验教训

  • 不要仅凭名称("短哈希")判断库的实现
  • 深入研究文档和源码再做技术决策
  • 区分"用户输入场景"和"系统数据场景"的安全要求

缓存库选择:@isaacs/ttlcache

决策时间: 2026-01-26

背景 硬件指纹功能最初使用手动实现的 TTL 缓存module-level 变量 + 手动过期检查)。为提高代码可维护性,迁移到专业缓存库。

选型

  • 选择: @isaacs/ttlcache v2.1.4
  • 理由:
    • 专为 TTL 场景优化,无需 LRU 追踪开销
    • 零依赖6M+ 周下载量
    • 内置 TypeScript 类型
    • 自动过期管理,无需手动定时器
    • API 简洁: new TTLCache({ ttl, max })

实现细节

  • 保留 inFlight Promise 模式用于并发请求去重TTLCache 不提供此功能)
  • 使用单一缓存键 'fingerprint'单服务器场景opts 不影响输出)
  • 默认 TTL: 10 分钟(可通过 cacheTtlMs 参数覆盖)

对比手动实现

  • 更少自定义代码
  • 更清晰的 TTL 语义
  • 经过充分测试的库
  • ⚠️ 仍需手动处理并发去重

经验教训

  • 专业库不一定解决所有问题(如并发去重)
  • 对于简单场景,手动实现 vs 库的选择主要取决于可维护性而非功能

PostgreSQL → SQLite 迁移

迁移时间: 2026-01-26

原因: 项目需要在嵌入环境中运行,无法安装 PostgreSQL

技术选择:

  • 驱动: better-sqlite3 v11.8.1 (跨平台,成熟稳定,原生模块)
  • 类型定义: @types/better-sqlite3 v7.6.12
  • 主键: TEXT 存储 UUIDv7 (保持全局唯一性36 字符字符串)
  • 时间戳: INTEGER 存储 Unix 毫秒时间戳 (integer({ mode: 'timestamp_ms' }))
  • 布尔值: INTEGER 存储 0/1 (integer({ mode: 'boolean' }))
  • 数据库文件: ./data/app.db

变更内容:

  1. 依赖: postgresbetter-sqlite3 + @types/better-sqlite3
  2. Drizzle 方言: dialect: 'postgresql'dialect: 'sqlite'
  3. Schema 导入: drizzle-orm/pg-coredrizzle-orm/sqlite-core
  4. 表定义: pgTablesqliteTable
  5. 数据库连接: drizzle-orm/postgres-jsdrizzle-orm/better-sqlite3
  6. 数据类型映射:
    • UUID: uuid()text() (使用 uuidv7() 生成)
    • 时间戳: timestamp({ withTimezone: true })integer({ mode: 'timestamp_ms' })
    • 布尔值: boolean()integer({ mode: 'boolean' })
    • 默认值: defaultNow()$defaultFn(() => new Date())
  7. 环境变量: DATABASE_URL 从 URL 格式改为文件路径 (./data/app.db)
  8. 目录自动创建: 添加了数据库文件目录的自动创建逻辑

数据库连接层变更:

// Before (PostgreSQL)
import { drizzle } from 'drizzle-orm/postgres-js'
export const createDB = () =>
  drizzle({
    connection: { url: env.DATABASE_URL, prepare: true },
    schema,
  })

// After (SQLite)
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'

export const createDB = () => {
  const dbPath = env.DATABASE_URL
  if (dbPath !== ':memory:') {
    mkdirSync(dirname(dbPath), { recursive: true })
  }
  const sqlite = new Database(dbPath)
  return drizzle(sqlite, { schema })
}

Schema 变更示例:

// Before (PostgreSQL)
import { pgTable, text, boolean, timestamp, uuid } from 'drizzle-orm/pg-core'
export const todoTable = pgTable('todo', {
  id: uuid('id').primaryKey().$defaultFn(() => uuidv7()),
  title: text('title').notNull(),
  completed: boolean('completed').notNull().default(false),
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})

// After (SQLite)
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const todoTable = sqliteTable('todo', {
  id: text('id').primaryKey().$defaultFn(() => uuidv7()),
  title: text('title').notNull(),
  completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
  createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
})

兼容性保证:

  • 所有业务逻辑保持不变
  • ORPC 契约和类型完全兼容
  • 应用层代码无需修改
  • Drizzle 自动处理类型转换 (Date ↔ number, boolean ↔ 0/1)

构建验证:

  • bun typecheck - 类型检查通过
  • bun fix - 代码质量检查通过
  • bun run build - Vite 构建成功
  • bun db:push - 数据库初始化成功
  • SQLite 文件已创建 (./data/app.db, 12KB)

注意事项:

  • SQLite 是文件数据库,适合嵌入环境和开发环境
  • 不支持多进程并发写入 (适合单应用实例)
  • 性能足够支持中小型应用 (日请求 < 10万)
  • 数据库文件和 WAL 文件 (.db-shm, .db-wal) 已添加到 .gitignore
  • better-sqlite3 是原生模块,需要匹配生产环境的 OS 和架构 (构建输出会提示)

Drizzle 时间戳模式:

  • timestamp_ms: Unix 毫秒时间戳 (推荐,精度更高)
  • timestamp: Unix 秒时间戳
  • Drizzle 自动转换 Date 对象 ↔ 数字

命令对比:

# 开发环境初始化 (推荐)
bun db:push

# 生产环境迁移
bun db:generate  # 生成迁移文件
bun db:migrate   # 执行迁移

# 数据库管理 UI
bun db:studio

Git 工作流要求

重要原则:保持代码仓库与文档同步

当遇到技术问题、做出架构决策、或发现重要经验时:

  1. 立即更新 AGENTS.md:记录问题、原因、解决方案
  2. 持续同步:每次重大变更后更新文档
  3. 版本关联在文档中标注相关的库版本、commit hash

这确保未来的开发者(包括 AI 助手)能快速理解项目历史和技术选择。


最后更新: 2026-01-26 项目版本: 基于 package.json 依赖版本