127 Commits

Author SHA1 Message Date
e59e085217 chore(vscode): remove unused extension recommendations 2026-02-15 21:51:36 +08:00
cd9826ded3 chore(desktop): tweak electron-vite dev watch and remove redundant --config flags 2026-02-09 04:19:53 +08:00
2efc57d9ee feat(desktop): show native error dialogs on startup failures
Replace silent console.error + app.quit() with dialog.showErrorBox()
so users actually see why the app failed to start instead of it just
disappearing. Covers server spawn errors, timeout, port allocation
failure, mid-session server crashes, and window creation failures.
2026-02-09 03:35:24 +08:00
1f5940438a fix(desktop): use array format for win target in electron-builder config 2026-02-09 03:16:48 +08:00
0bab6372ac chore(desktop): reorganize electron-builder config and refine packaging targets 2026-02-09 03:15:01 +08:00
5f0c9d33cb chore 2026-02-09 02:58:43 +08:00
73982939a8 chore(desktop): add app icon and track resources directory 2026-02-09 02:51:56 +08:00
10c2d61523 fix(desktop): use CJS for preload script to fix sandbox loading error 2026-02-09 02:17:57 +08:00
18ce05854a feat(server): add NODE_ENV to shared env schema 2026-02-09 01:59:45 +08:00
7eccef5d8f chore(desktop): remove redundant config fields for KISS 2026-02-09 01:41:34 +08:00
41667cb33b refactor(desktop): simplify main process logic and improve naming
- Remove logLifecycle wrapper, inline the conditional logging
- Remove redundant shouldAbortWindowLoad check before final loadURL
- Rename getServerUrl to resolveServerUrl to reflect side effects
- Add .catch on createWindow to prevent silent async failures
2026-02-09 01:27:29 +08:00
00c944e1b5 refactor(desktop): 精简主进程启动与退出逻辑并减少打包态日志噪音 2026-02-09 01:13:08 +08:00
f9edfd0058 fix(desktop): guard shutdown race and kill sidecar process tree 2026-02-09 00:57:30 +08:00
9aea89e16d fix(desktop): force app exit on windows window close 2026-02-09 00:40:01 +08:00
26b74b25f2 fix(desktop): use stdio ignore for sidecar to prevent process hang on quit
Piped stdio handles kept the event loop alive on Windows after killing
the sidecar process, preventing the Electron app from exiting.
2026-02-09 00:23:05 +08:00
ccf220fc29 fix(desktop): ensure sidecar process stops on app shutdown 2026-02-08 23:59:54 +08:00
a585069cdc refactor: rename compile:mac/win to compile:darwin/windows to match Bun target names 2026-02-08 23:39:30 +08:00
b149cc5dc0 refactor: decentralize turbo task config — move compile/dist to workspace turbo.json
Sink package-specific tasks from root turbo.json into workspace configs:
- compile/compile:* → apps/server/turbo.json (only server compiles binaries)
- dist/dist:* → apps/desktop/turbo.json (only desktop distributes)
- Cross-package deps (desktop→server#compile) owned by desktop config
- Desktop dist scripts no longer bypass Turbo by calling bun run build

Root turbo.json reduced from 16 to 4 generic lifecycle tasks.
2026-02-08 23:26:24 +08:00
9f38636d76 refactor(server): use Bun.Build.CompileTarget and derive host target instead of manual map 2026-02-08 22:47:39 +08:00
63906ec09b refactor(server): use util.parseArgs for declarative CLI arg parsing in compile.ts 2026-02-08 22:38:49 +08:00
8c4e4ad150 refactor(server): use type guard to eliminate as Target casts in compile.ts 2026-02-08 22:33:02 +08:00
e171db8196 refactor: simplify compile.ts to single-target and add per-platform compile scripts
- Rewrite compile.ts from 112 to 66 lines: single target with auto-detect host, remove multi-target batch logic
- Add compile:linux/mac/win scripts to server, root, and turbo configs
- Wire desktop dist:* to depend on matching server compile:* (avoid unnecessary cross-platform compilation)
- Update AGENTS.md docs across root, server, and desktop
2026-02-08 22:25:30 +08:00
dac6bb1643 refactor: 统一打包命令为 dist 体系,build 仅编译不打包
- build:linux/mac/win → dist/dist:linux/dist:mac/dist:win
- Turbo 任务依赖:desktop#dist:* → server#compile → server#build
- 根目录 bun dist 一条命令完成完整打包流水线
- 更新 AGENTS.md 文档同步命令变更
2026-02-08 20:48:58 +08:00
8c0ea632d7 style(desktop): 规范 package.json 字段排序 2026-02-08 20:31:17 +08:00
db23ee42fc chore: 更新 lockfile 依赖版本 2026-02-08 20:26:16 +08:00
0784546e50 fix(desktop): 修复 artifactName 路径错误并移除 deb 构建目标
scoped package name 中的 / 导致 ${name} 作为路径出错,改用 ${productName};
移除 deb target(fpm 依赖不可用);补充 package.json 元信息字段
2026-02-08 20:03:47 +08:00
2fe3e15659 refactor: 重命名 build.ts → compile.ts,统一脚本与文件命名
- compile 脚本调用 compile.ts,消除与 build 的歧义
- desktop turbo.json 添加 build outputs 缓存配置
2026-02-08 19:46:04 +08:00
ed02993350 style(desktop): loading 页面改为白色轻盈主题 2026-02-08 19:40:20 +08:00
e4e5ff2211 chore: 移除 useSortedClasses 规则,等待 nursery 毕业后再启用 2026-02-08 19:19:18 +08:00
d69a573a33 feat: 为 desktop/server 添加 Tailwind CSS 支持和 Biome 集成
- desktop renderer 接入 @tailwindcss/vite,loading 页面改用 Tailwind 类
- 两个 app 添加 biome.json 继承配置:tailwindDirectives + useSortedClasses
2026-02-08 19:17:53 +08:00
6cc1bc6834 refactor(desktop): 用类型收窄替代 as AddressInfo 断言 2026-02-08 19:03:13 +08:00
894fd17d1a fix(desktop): 动态分配 sidecar 端口替代硬编码,避免端口冲突
使用 net.createServer().listen(0) 探测可用端口,通过 PORT 环境变量
传递给 sidecar binary(VS Code language server 同款模式)
2026-02-08 18:38:45 +08:00
888f20fdab fix(desktop): 加载 loading 页面作为启动屏,配置平台级 extraResources 对接交叉编译
- main process 启动时先加载 renderer/index.html 显示 spinner
- electron-builder 按平台引用 server/out/ 下对应架构的 sidecar binary
- 移除 mise.toml 中无关的 rust 工具配置
2026-02-08 18:21:40 +08:00
7318600e20 refactor(desktop): 替换 WebUI 为 Electron + electron-vite 桌面壳方案
- 使用 electron-vite 构建 main/preload,electron-builder 打包分发
- main process: dev 模式直连 localhost:3000,生产模式 spawn sidecar binary
- 添加 loading 页面,server 就绪前显示加载动画
- 更新 catalog 依赖: electron, electron-vite, electron-builder
- 移除 @webui-dev/bun-webui 依赖
2026-02-08 18:16:13 +08:00
e8e473b357 refactor(desktop): 替换 Electrobun 为 WebUI 作为桌面窗口方案
Electrobun 太不稳定,改用 webui-dev/webui(轻量 C 库,~300KB)通过
系统浏览器或 WebView 提供桌面窗口。已验证 bun:ffi 加载和
bun build --compile 均正常工作。

- 移除 electrobun 依赖和配置
- 添加 @webui-dev/bun-webui 依赖
- 重写桌面入口为 WebUI 窗口方案
- 移除 Conveyor 打包工具(mise.toml)
2026-02-08 04:15:34 +08:00
41d97ca312 refactor(deps): 将 @hydraulic/conveyor 从 bun 依赖迁移到 mise 全局工具 2026-02-08 03:21:38 +08:00
cfe7de2a70 chore(deps): 添加 @hydraulic/conveyor 依赖到 desktop 应用 2026-02-08 03:11:43 +08:00
b87de26e17 chore(deps): 升级 TanStack devtools 和 vite-tsconfig-paths 依赖版本 2026-02-08 02:41:22 +08:00
b8d38872ad refactor(desktop): 优化 Electrobun 构建配置
- 从 package.json 动态读取版本号替代硬编码
- 启用所有平台的 CEF 捆绑
- 将构建目标从配置文件移至 CLI 参数
- 启用 asar 打包并为 dev 脚本添加 --env=dev 标志
2026-02-08 02:38:37 +08:00
7450c685d3 chore: 移除已完成的 electrobun 生产模式计划文档 2026-02-08 00:40:43 +08:00
2048f73155 refactor(server): 按照官方推荐顺序重排 Vite 插件并清理冗余配置 2026-02-07 22:13:16 +08:00
70b5d27493 chore(desktop): 添加 win-x64 构建目标 2026-02-07 21:04:39 +08:00
5d5d3a51f6 chore(desktop): 禁用 mac 和 win 平台的 CEF 捆绑 2026-02-07 20:57:58 +08:00
3306e18395 refactor(desktop): 使用预分配端口替代 stdout 解析获取服务器端口 2026-02-07 19:32:56 +08:00
14bcdb33af chore(deps): 升级 TanStack 路由和 Start 依赖版本 2026-02-07 19:11:28 +08:00
cc81d95178 chore(desktop): 升级 electrobun 至 1.12.0-beta.1 2026-02-07 19:10:33 +08:00
55d45e6a49 docs(desktop): 更新 AGENTS.md 文档与开发计划以反映最新实现 2026-02-07 18:49:50 +08:00
b7a6a793a3 feat(desktop): 实现生产模式下的内嵌服务器子进程支持 2026-02-07 18:49:41 +08:00
6b12745e50 chore(desktop): 更新应用名称、标识符和版本号 2026-02-07 17:46:00 +08:00
989d8973f5 chore(desktop): 简化构建和开发脚本 2026-02-07 17:16:32 +08:00
41e79449ce docs: 更新 AGENTS.md 适配 Electrobun 替代 Tauri 2026-02-07 17:00:52 +08:00
4bbb0c4a16 refactor(server): simplify build script, remove Effect dependency 2026-02-07 16:44:56 +08:00
2b3026cf69 chore(turbo): simplify and optimize monorepo configuration 2026-02-07 16:35:30 +08:00
adb14cff77 chore: 重构 Turbo 构建配置并强化 Bun 专用说明
将应用特定的构建输出配置下沉至各自 turbo.json,根级 build 任务添加拓扑依赖;AGENTS.md 统一添加 Bun 专用运行时警告;桌面端启用 Linux CEF 渲染器。
2026-02-07 16:14:55 +08:00
44ca7a0f5e chore: 扩展 Turbo build 任务的输出目录配置 2026-02-07 07:06:55 +08:00
59b4edc2d2 chore(desktop): 降级 electrobun 至 0.8.0 稳定版 2026-02-07 06:59:19 +08:00
9d0e9a6aac chore(desktop): 添加 .gitignore 和多平台构建脚本 2026-02-07 06:29:08 +08:00
f758fd5947 chore: 更新 bun.lock 锁文件 2026-02-07 06:15:52 +08:00
26f9421130 chore: 统一 Node/TypeScript 配置并修复桌面端类型环境 2026-02-07 05:53:38 +08:00
29969550ed refactor(desktop): 从 Tauri 迁移到 Electrobun
- 移除 Tauri v2 代码 (src-tauri/, copy.ts)
- 添加 Electrobun 配置和入口 (electrobun.config.ts, src/bun/index.ts)
- 更新 package.json 使用 catalog 管理 electrobun 依赖
- 移除 server 中的 @tauri-apps/api 依赖
- 更新 AGENTS.md 文档
2026-02-07 05:04:53 +08:00
9aa3b46ee5 chore(desktop): 更新 Cargo 依赖 2026-02-07 03:45:08 +08:00
f3ea0f0789 chore: 更新依赖版本 2026-02-07 03:32:42 +08:00
bde325d9ae chore: 更新 biome 和 turbo 依赖版本 2026-02-07 03:31:01 +08:00
e41c4e4515 docs: 更新 AGENTS.md 文档结构和内容
- 新增根目录 AGENTS.md 作为 monorepo 总览
- 移动 desktop AGENTS.md 从 src-tauri/ 到 apps/desktop/
- 修正 server AGENTS.md 目录结构 (src/server/api/ 而非 src/orpc/)
- 明确 desktop 为纯 Tauri 壳子,无前端代码,通过 sidecar 加载 server
2026-02-07 03:29:51 +08:00
49d1f706e7 feat: 添加本地工具包依赖支持项目功能
- 添加本地工具包依赖 @furtherverse/utils 以支持项目功能。
- 添加对 workspace 内 utils 包的依赖引用
2026-01-23 16:18:20 +08:00
d13c3602c9 feat: 添加按路径导入支持及指纹工具导出
- 添加模块导出配置以支持按路径导入源码文件
- 导出指纹工具模块的全部功能。
2026-01-23 16:17:08 +08:00
3755b0f873 feat: 添加硬件指纹生成功能
- 生成硬件指纹,通过收集系统唯一标识、主板、BIOS、系统信息、磁盘布局和网络接口数据并哈希处理
2026-01-23 16:14:58 +08:00
be4e8212ec feat: 添加 ohash 和 systeminformation 依赖支持指纹与系统信息
- 添加 ohash 依赖并更新其版本至 ^2.0.11
- 添加 ohash 依赖以支持哈希功能
- 添加 ohask 和 systeminformation 依赖项并指定其来源为 catalog。
- 引入 ohash 和 systeminformation 库以支持指纹生成和系统信息获取。
2026-01-23 15:59:07 +08:00
0cab61af91 feat: 添加 systeminformation 依赖以支持系统信息获取
- 添加 systeminformation 依赖并更新其版本至 5.30.5,同时配置其运行时环境和二进制文件路径。
- 添加 systeminformation 依赖以获取系统信息
- 添加 systeminformation 依赖项以支持系统信息获取功能。
2026-01-23 15:54:30 +08:00
104b04064d refactor: 重命名文件以更准确反映其功能
- 重命名文件以更准确地反映其功能,将索引文件重命名为指纹处理文件。
2026-01-23 15:52:50 +08:00
b6c413aad9 feat: 添加类型检查与模块别名配置
- 添加开发依赖 @furtherverse/tsconfig 和 typescript 到 utils 包中
- 配置模块导入别名并添加类型检查依赖项
- 添加工具函数库的初始化文件
2026-01-23 15:50:59 +08:00
5a0a899e93 feat: 升级 lightningcss 至 1.30.2 并更新相关依赖
- 升级 lightningcss 及其所有平台相关依赖至 1.30.2 版本,并更新相关依赖项的版本号和哈希值,同时移除已废弃的 vitefu/vite 依赖项。
- 将版本号从 0.0.0 更新为 1.0.0
- 添加新的工具包模块并配置其名称、版本和模块类型。
2026-01-23 15:43:44 +08:00
254bef6162 chore: 升级依赖包至最新版本以兼容新功能和修复问题
- 升级依赖包版本以兼容最新功能和修复已知问题,包括 @effect/platform、@tanstack/react-router 系列、effect、nitro、vite、zod 及相关构建工具的版本更新。
- 升级依赖包版本以获取最新功能和修复,包括 @effect/platform、@tanstack/react-router 系列、effect、nitro、vite 和 zod。
2026-01-23 15:33:57 +08:00
610b81c32d feat: 更新页面标题为“Furtherverse”
- 将页面标题从“Fullstack Starter”更改为“Furtherverse”
2026-01-22 17:00:00 +08:00
afb0880d8e refactor: 统一数据库相关命名规范并优化单例实现
- 统一使用DB类型别名替换Database类型定义
- 将 db 中间件中获取数据库实例的函数名从 getDb 改为 getDB 以保持命名一致性。
- 重命名数据库相关函数和类型以使用一致的命名规范,并确保单例模式正确返回数据库实例。
2026-01-22 16:52:16 +08:00
3b50528435 chore: 更新数据库模式文件路径
- 更新数据库模式文件路径为新的位置。
2026-01-22 16:46:54 +08:00
c4b179464b refactor: 更新导入路径为相对路径别名
- 更新导入路径以使用相对路径别名指向 package.json 文件
2026-01-22 16:44:59 +08:00
6ea358bab5 feat: 移除API文档认证方案配置
- 移除API文档配置中的认证方案设置
2026-01-22 16:43:38 +08:00
dede23ead9 feat: 添加 OpenAPI 支持与集成
- 添加 @orpc/openapi 依赖以支持 OpenAPI 生成和集成。
- 配置 OpenAPI 文档生成与请求拦截,集成 Zod 验证错误处理并支持 Bearer 认证。
- 添加 @orpc/openapi 依赖并更新版本号至 1.13.4
- 添加 @orpc/openapi 依赖以支持 OpenAPI 生成功能。
2026-01-22 16:43:11 +08:00
2b3e91167e refactor: 更好的orpc结构 2026-01-22 16:37:10 +08:00
660ee0a545 refactor: 优化合约类型导出与导入方式
- 导出合约类型以支持类型安全的接口定义
- 更新类型导入以使用 Contract 而非直接导出的 contract 变量
2026-01-22 16:21:08 +08:00
7c8452c731 refactor: 移除废弃的 Context 类型并优化类型定义
- 移除 Context 类型导入并添加空对象类型定义以兼容当前上下文需求
- 移除已废弃的 Context 类型定义并清理注释代码
2026-01-22 16:19:33 +08:00
7beb911efb refactor: 优化数据库实例获取逻辑,移除无用变量
- 移除未使用的 IS_SERVERLESS 变量并直接调用 getDb 不传参数
- 修改数据库实例获取逻辑,将 serverless 参数改为 singleton 标志,控制是否返回单例实例。
2026-01-22 16:13:34 +08:00
a8db6212a1 refactor: 重构数据库实例创建逻辑并优化类型声明
- 重构数据库实例创建逻辑,将 `createDb` 函数改为箭头函数并优化 `getDb` 返回类型声明。
2026-01-22 16:00:39 +08:00
af807eeb53 refactor: 优化数据库连接管理与统一接入方式
- 优化数据库连接管理,直接使用获取数据库实例的函数并传入是否为无服务器环境的标识。
- 统一使用db中间件替代dbProvider,简化数据库连接处理并保持代码一致性。
- 添加数据库实例的单例获取机制并定义类型接口
2026-01-22 15:56:00 +08:00
70252fbd94 chore: 更新依赖至最新稳定版本以优化构建性能
- 更新项目依赖版本以升级 Rollup、LightningCSS、Vite 及其相关构建工具至最新稳定版本,并同步更新多个平台的二进制包以确保兼容性和性能优化。
2026-01-22 15:29:38 +08:00
c364b6d27f feat: 升级 TanStack、Turbo 等依赖至最新版本
- 升级多个 TanStack 相关依赖至 1.154.7 版本,同步更新 h3-v2 和 nitro 的依赖版本以保持兼容性。
- 升级 turbo 和 tanstack 相关依赖至最新版本以获取最新功能和修复。
2026-01-22 15:25:44 +08:00
7632b9a4ef refactor: 移除构建与开发任务中复制操作的依赖
- 移除构建和开发任务中对复制操作的依赖,仅保留对服务器编译任务的依赖。
2026-01-22 00:26:26 +08:00
efd29a9d63 feat: 添加构建脚本中的复制步骤以正确处理二进制文件
- 在构建和开发脚本中添加复制步骤以确保二进制文件正确处理
2026-01-22 00:26:11 +08:00
48bc50e221 feat: 添加构建与开发任务配置
- 添加构建任务的依赖项并指定输出路径,同时在开发任务中启用服务器开发模式并设置输出路径。
2026-01-22 00:24:33 +08:00
da2a7391da refactor: 简化构建脚本并确保产物正确复制
- 移除构建和开发脚本中的冗余bun run命令,直接调用tauri命令
- 在开发配置中添加对复制任务的依赖并指定输出路径,确保构建产物正确复制。
2026-01-22 00:21:27 +08:00
9aec9d2829 refactor: 将 sidecar 名称从 "app" 更改为 "server"
- 将 sidecar 的名称从 "app" 更改为 "server" 以正确启动服务进程。
2026-01-22 00:09:49 +08:00
16181e2e9d fix: 恢复 server-desktop 包依赖配置
- 恢复 server-desktop 包的依赖配置并确保其正确声明
2026-01-22 00:06:36 +08:00
7d3df0ec49 refactor: 重命名桌面应用为 server-desktop
- 更新项目结构中的目录名称为 server-desktop
- 将应用名称从 `app-desktop` 更改为 `server-desktop`
- 将桌面应用的包名和库名从 app-desktop 和 app_desktop_lib 更改为 server-desktop 和 server_desktop_lib。
- 将主函数中的运行入口从 app_desktop_lib 更改为 server_desktop_lib。
- 更新产品名称和标识符以反映新的应用名称为 server-desktop。
2026-01-21 23:59:39 +08:00
fc846fa24d chore: 调整构建任务依赖,添加开发服务器模式支持
- 调整构建任务依赖,为开发环境添加服务器开发模式依赖。
2026-01-21 23:56:15 +08:00
4710166942 chore: 更新构建脚本,清理旧二进制文件并复制资源
- 更新构建和开发脚本,确保在构建和开发前先清理旧的二进制文件并执行复制操作。
2026-01-21 23:50:46 +08:00
96705e965d feat: 添加目录存在性检查并优化复制逻辑
- 添加目录存在性检查功能并更新复制逻辑以使用该功能
2026-01-21 23:44:31 +08:00
fc916c7c1d refactor: 重命名复制文件并更新构建脚本
- 重命名文件以更准确地反映其功能
- 更新构建脚本,将复制二进制文件的命令改为使用新的复制脚本文件。
2026-01-21 23:40:07 +08:00
ece366c4d7 refactor: 移除未使用的类型定义,清理冗余代码
- 移除未使用的类型定义,清理冗余代码。
2026-01-21 23:38:31 +08:00
46984c2687 refactor: 重构桌面端构建配置与依赖管理
- 删除桌面端构建配置文件
- 添加二进制文件复制工具,支持多平台目标映射并自动处理文件路径与扩展名,通过类型安全配置和错误处理确保构建流程稳定可靠。
- 添加依赖项以支持类型安全的模式定义和项目配置,并新增脚本用于复制二进制文件。
- 添加桌面应用的TypeScript配置文件并继承统一的tsconfig设置,排除node_modules和src-tauri目录。
- 添加必要的开发依赖项以支持类型检查和构建工具链
2026-01-21 23:38:16 +08:00
712ed1919f refactor: 重构桌面端构建配置与二进制路径
- 添加桌面端构建配置文件
- 将外部二进制文件路径从 `binaries/app` 更改为 `binaries/server`。
2026-01-21 23:29:31 +08:00
553e055a96 feat: 添加 TypeScript 支持和类型定义
- 添加类型定义和 TypeScript 依赖以支持类型检查和开发环境。
2026-01-21 23:21:02 +08:00
985662cb22 refactor: 重命名 outfilePrefix 为 outfile 并统一更新代码
- 将构建配置中的 outfilePrefix 更名为 outfile,并统一更新相关代码以使用新的字段名。
2026-01-21 22:09:11 +08:00
924bcd6aa2 refactor: 重构构建流程,提升输出灵活性与配置性
- 移除目标映射表并根据配置前缀生成输出文件名,使构建输出更灵活可配置
- 重构构建任务配置,将原有分步构建逻辑合并为统一的编译任务,明确依赖关系和输出路径
2026-01-21 22:02:01 +08:00
896bb0ca7d refactor: 重命名 AGENTS.md 文件至 server 目录
- 重命名 AGENTS.md 文件至 apps/server/ 目录下
2026-01-21 21:46:20 +08:00
3d3765cdbf feat: 添加桌面应用忽略文件及构建依赖配置
- 添加桌面应用的忽略文件配置,包括日志、依赖目录、构建输出及编辑器临时文件。
- 添加桌面端开发任务依赖于服务器编译任务的配置。
2026-01-21 21:46:00 +08:00
13a4a333ff refactor: 清理冗余配置与依赖项
- 移除重复的版本号字段并保持配置文件一致性
- 移除 `@types/bun` 开发依赖项
- 移除 tsconfig 包中多余的 devDependencies 项和无效的 catalog: 依赖配置
2026-01-21 20:14:53 +08:00
e6293ce52f refactor: 移除本地数据库依赖并清理废弃模块
- 移除对本地数据库包的依赖并更新客户端、合约和服务器包的引用为目录源。
- 移除对本地数据库包的依赖引用并清理相关配置
- 删除数据库包的配置文件及依赖项
- 移除对 todo 模式的导出,清理已废弃的模块引用。
- 删除待办事项表的定义及相关字段配置
- 删除已废弃的字段生成工具函数及对应配置,移除对 uuidv7 和 PostgreSQL 特定生成策略的依赖。
- 移除字段工具导出,不再从字段工具模块导出内容
- 删除 SQLite 数据库模块的初始化文件
- 删除数据库包中的 TypeScript 配置文件以统一项目配置
2026-01-21 16:34:33 +08:00
219d6cfb4f refactor: 统一数据库模块入口命名并更新导入路径
- 更新数据库工具导入路径,将创建数据库实例的模块从 '@/db/utils' 改为 '@/db'。
- 将 utils.ts 重命名为 index.ts 以统一模块入口文件命名规范
2026-01-21 16:33:38 +08:00
da5f08f8c1 feat: 添加 UUID 支持及待办事项数据表定义
- 添加 uuid 依赖以支持唯一标识符生成
- 移除对 todoTable 的导出,不再从数据库模式文件中暴露该表定义。
- 导出 todo 模式定义文件中的所有内容
- 添加待办事项数据表定义,包含标题和完成状态字段,并集成自动生成字段。
- 添加用于定义主键、创建时间和更新时间字段的实用工具函数,并支持不同 PostgreSQL 版本的 UUID 生成策略,同时提供生成字段的键名映射。
- 添加 uuid 依赖到项目中
2026-01-21 16:33:03 +08:00
f1608c3546 refactor: 重构ORPC客户端并统一导出路径
- 删除API入口文件中的导出内容
- 更新上下文类型为导入的Context类型并移除注释掉的旧类型定义。
- 添加空的 Context 类型定义以支持上下文类型,暂时忽略复杂度检查警告
- 创建支持服务端和客户端的统一ORPC客户端,基于请求头上下文和Fetch链接实现前后端一致的RPC调用。
- 重构客户端代码,将ORPC客户端初始化逻辑移至独立文件并统一导出,提升代码可维护性和模块化程度。
- 更新导入路径,将 orpc 从 '@/api' 改为 '@/lib/orpc/query-client'。
2026-01-21 16:26:59 +08:00
e49e8606da refactor: 移除旧版 devtools 集成,改用组件化方式
- 移除 TanStack Query 开发工具的集成配置
- 移除对 tanstack-query devtools 的导出
- 移除 TanStack Router 开发工具插件的集成配置
- 移除 tanstack-router 开发工具集成的导出
- 移除旧的 devtools 集成方式,改用新的组件化方式引入 TanStack Router 和 Query 的开发工具面板。
2026-01-21 16:11:46 +08:00
d922c2c242 refactor: 统一路由模块命名规范并更新导入路径
- 更新导入路径,将 router 从 './router' 改为 './routers'。
- 删除API路由配置,移除对todo处理函数的引用及服务器路由实例的定义。
- 添加路由配置,集成待办事项模块到主路由中。
- 重命名文件以统一项目中路由模块的命名规范
- 将导入路径从 '@/api/router' 更新为 '@/api/routers' 以正确引用路由配置。
2026-01-21 16:08:00 +08:00
3dd1beb567 refactor: 重构合约API模块结构并修复路径错误
- 删除已废弃的合约API模块及其导出配置
- 添加合约接口导出,包含待办事项相关接口。
- 修复文件路径拼写错误,将文件名从 `contracts` 重命名为 `contract`。
2026-01-21 16:05:32 +08:00
da661d4495 fix: 移除 biome 配置中对 useHookAtTopLevel 的禁用设置
- 移除 biome 配置中对 useHookAtTopLevel 规则的禁用设置
2026-01-21 16:01:27 +08:00
f5fd28621e rename 2026-01-21 16:00:28 +08:00
76796613b4 feat: 添加动态参数路径API路由支持
- 添加API路由文件以支持动态参数路径的请求处理
- 添加对 `/api/$` 路由的支持,包括路由配置、类型定义和路由树的完整集成。
2026-01-21 15:57:53 +08:00
168c160bd4 refactor: 升级核心依赖并清理无用依赖
- 移除未使用的开发依赖项 @biomejs/biome 和 turbo
- 升级 TanStack 生态相关依赖至最新版本,包括 react-query、react-router、react-start 等核心包,并同步更新其依赖项和构建工具 esbuild 的多平台支持版本。
- 升级 React Query、React Router 及其相关依赖至最新版本,并移除未使用的 TypeScript 依赖。
2026-01-21 15:54:34 +08:00
e1ab34b7ec fix: 修复环境变量文件路径问题
- 将 .env.example 文件重命名为 apps/server/.env.example
2026-01-21 15:50:39 +08:00
484ecd85a3 refactor: 重构数据库相关代码并更新依赖
- 添加本地数据库包依赖以支持项目数据库功能
- 导出 todoTable 以供数据库 schema 使用
- 移除对 todo 模式的导出
- 删除待办事项数据表的定义配置
- 重命名文件以更准确地反映其用途,将数据库相关工具函数集中到新的工具文件中。
- 将数据库创建函数的导入路径从 '@/db' 更新为 '@/db/utils'。
- 将数据库包版本更新为1.0.0并添加对工作区中数据库包的依赖。
2026-01-21 15:47:24 +08:00
28d0c9ad3d feat: 添加数据库包及 UUID 支持,初始化类型检查与字段工具
- 将 typecheck 脚本改为使用 tsc --noEmit,并添加 @furtherverse/tsconfig 工作区依赖。
- 添加新的本地包 @furtherverse/database 并更新 uuid 依赖至 13.0.0 版本,同时修复相关依赖引用。
- 添加 uuid 依赖以支持生成唯一标识符功能
- 初始化数据库包的配置,设置模块导入导出路径并配置类型检查与格式化脚本。
- 导出 todo 模式定义文件中的所有内容
- 添加待办事项表结构,包含自动生成字段、标题和完成状态字段。
- 添加用于生成主键、创建和更新时间戳字段的实用工具函数,并提供自动生成字段及其键的映射。
- 导出字段工具模块中的所有内容
- 添加 SQLite 数据库连接初始化功能
- 添加数据库包的 TypeScript 配置并继承基础配置文件
2026-01-21 15:42:49 +08:00
653a144736 refactor: 重命名项目应用模块为 desktop 和 server
- 重命名项目应用模块,将 demo-desktop 和 demo-server 重命名为 desktop 和 server,并同步更新相关依赖引用。
2026-01-21 15:05:17 +08:00
011c9211f5 rename 2026-01-21 15:00:11 +08:00
fd20ca2c52 feat: 添加桌面应用和服务器应用项目并更新依赖
- 添加新的桌面应用和服务器应用项目,并更新依赖和版本信息。
2026-01-21 14:53:58 +08:00
38db2ae6f2 mv tauri 2026-01-21 14:53:22 +08:00
babd0f5615 chore 2026-01-21 14:41:56 +08:00
a6125718f5 refactor: 重构演示应用目录结构与配置文件路径
- 重命名构建配置文件至指定应用目录
- 重命名 drizzle.config.ts 文件至 apps/demo-app/ 目录下
- 添加演示应用的完整包配置,包含构建、开发、数据库管理及依赖项,支持 Tauri 桌面应用开发。
- 重命名 robots.txt 文件至 demo-app 应用目录下
- 重命名 src-tauri/.gitignore 文件为 apps/demo-app/src-tauri/.gitignore
- 重命名 AGENTS.md 文件至指定目录路径
- 重命名构建脚本文件以匹配新项目路径结构
- 重命名默认能力配置文件以匹配新项目路径
- 重命名 Cargo.lock 文件以匹配新的项目路径结构
- 重命名 Cargo.toml 文件以正确反映其在项目中的路径位置
- 重命名图标文件以正确匹配新项目路径
- 重命名128x128.png图标文件至demo-app应用目录下
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以匹配新项目路径
- 重命名图标文件以正确反映其在项目中的路径位置
- 重命名图标文件以匹配新项目路径结构
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以匹配新项目路径结构
- 重命名图标文件以正确放置在演示应用的资源目录中
- 重命名图标文件以正确匹配新项目路径
- 重命名图标文件以正确匹配新项目路径
- 重命名命令模块文件路径以匹配项目结构
- 重命名文件以正确反映其在项目中的位置
- 重命名主程序文件路径以匹配项目结构
- 重命名 sidecar.rs 文件至 demo-app 项目路径下
- 重命名 tauri.conf.json 文件至指定目录路径
- 重命名错误组件文件以正确反映其在项目中的位置
- 重命名文件 NotFount.tsx 到指定目录路径
- 重命名数据库入口文件路径以适应项目结构调整
- 重命名数据 schema 文件路径以匹配项目结构
- 重命名文件以正确反映其在项目中的位置路径
- 重命名环境配置文件以正确反映其在项目中的位置。
- 重命名文件以正确反映其在项目中的路径位置
- 重命名文件以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的路径位置
- 重命名文件以正确反映其在项目中的路径位置
- 重命名工具函数文件以正确反映其在项目中的位置
- 重命名客户端文件以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的位置
- 重命名文件路径以正确组织项目结构中的契约文件
- 重命名文件路径以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的位置
- 重命名数据库中间件文件以正确反映其在项目中的位置
- 重命名中间件文件路径以匹配项目结构
- 重命名路由文件以正确反映其在项目中的位置
- 重命名文件以正确反映其在项目中的位置。
- 重命名类型文件以正确反映其在项目中的路径位置
- 重命名路由配置文件以匹配项目目录结构
- 重命名根路由文件以正确反映其在项目中的位置
- 重命名RPC路由文件至demo-app应用目录下
- 重命名路由文件路径以匹配项目结构调整
- 重命名路由树生成文件至 demo-app 应用目录下
- 重命名样式文件以正确反映其在项目中的位置
- 添加 TypeScript 配置以扩展 React 项目模板并设置路径别名。
- 重命名 vite.config.ts 文件至 apps/demo-app/ 目录下
- 移除 biome.json 中对 routeTree.gen.ts 文件的排除规则
- 更新依赖版本以统一使用 catalog 依赖管理,提升项目依赖一致性并升级关键包至最新稳定版本。
- 配置安装时的公共提升模式,包含类型包和特定命名空间的包。
- 删除空的 drizzle 目录占位文件
- 将 node 版本更新为最新版本,同时将 bun 和 rust 版本设置为最新。
- 将项目名称更新为 monorepo 并重构脚本与依赖配置以支持工作区结构和统一的 turbo 管理。
- 添加基础 TypeScript 配置文件,启用严格模式并配置模块解析与编译选项以支持现代 JavaScript 特性。
- 添加 Bun 专用的 TypeScript 配置,继承基础配置并引入 Bun 类型定义。
- 添加 tsconfig 包的配置文件并定义基础、Bun 和 React 的配置导出,同时引入 Bun 类型定义作为开发依赖。
- 添加React项目专用的TypeScript配置,指定JSX处理方式并扩展基础配置。
- 删除旧的 TypeScript 配置文件以移除过时的编译选项和路径别名设置。
2026-01-21 14:31:12 +08:00
108 changed files with 2449 additions and 6682 deletions

View File

@@ -5,8 +5,6 @@
"mikestead.dotenv", "mikestead.dotenv",
"oven.bun-vscode", "oven.bun-vscode",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"rust-lang.rust-analyzer", "tamasfe.even-better-toml"
"tamasfe.even-better-toml",
"tauri-apps.tauri-vscode"
] ]
} }

View File

@@ -37,7 +37,8 @@
"files.associations": { "files.associations": {
".env": "dotenv", ".env": "dotenv",
".env.*": "dotenv", ".env.*": "dotenv",
"**/tsconfig*.json": "jsonc", "**/tsconfig.json": "jsonc",
"**/tsconfig.*.json": "jsonc",
"**/biome.json": "jsonc", "**/biome.json": "jsonc",
"**/opencode.json": "jsonc" "**/opencode.json": "jsonc"
}, },

368
AGENTS.md
View File

@@ -1,159 +1,119 @@
# AGENTS.md - AI Coding Agent Guidelines # AGENTS.md - AI Coding Agent Guidelines
本文档为 AI 编程助手提供此 TanStack Start 全栈项目的开发规范和指南。 Guidelines for AI agents working in this Bun monorepo.
## 项目概览 ## Project Overview
- **框架**: TanStack Start (React SSR 框架,文件路由) > **This project uses [Bun](https://bun.sh) exclusively as both the JavaScript runtime and package manager. Do NOT use Node.js / npm / yarn / pnpm. All commands start with `bun` — use `bun install` for dependencies and `bun run` / `bun <script>` for scripts. Never use `npm`, `npx`, or `node`.**
- **运行时**: Bun
- **语言**: TypeScript (strict mode, ESNext)
- **样式**: Tailwind CSS v4
- **数据库**: PostgreSQL + Drizzle ORM
- **状态管理**: TanStack Query
- **路由**: TanStack Router (文件路由)
- **RPC**: ORPC (类型安全 RPC契约优先)
- **构建工具**: Vite + Turbo
- **代码质量**: Biome (格式化 + Lint)
- **桌面壳** (可选): Tauri v2 (详见 `src-tauri/AGENTS.md`)
## 构建、Lint 和测试命令 - **Monorepo**: Bun workspaces + Turborepo orchestration
- **Runtime**: Bun (see `mise.toml` for version) — **NOT Node.js**
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
- **Apps**:
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
- `apps/desktop` - Electron desktop shell, sidecar server pattern (see `apps/desktop/AGENTS.md`)
- **Packages**: `packages/utils`, `packages/tsconfig` (shared configs)
### 开发 ## Build / Lint / Test Commands
### Root Commands (via Turbo)
```bash ```bash
bun dev # 使用 Turbo 并行启动 Tauri + Vite 开发服务器 bun dev # Start all apps in dev mode
bun dev:vite # 仅启动 Vite 开发服务器 (localhost:3000) bun build # Build all apps
bun dev:tauri # 启动 Tauri 桌面应用 bun compile # Compile server to standalone binary (current platform, depends on build)
bun db:studio # 打开 Drizzle Studio 数据库管理界面 bun compile:linux # Compile server for Linux x64
bun compile:mac # Compile server for macOS arm64
bun compile:win # Compile server for Windows x64
bun dist # Full packaging pipeline: server build → compile → desktop distributable (current platform)
bun dist:linux # Full pipeline → Linux distributable
bun dist:mac # Full pipeline → macOS distributable
bun dist:win # Full pipeline → Windows distributable
bun fix # Lint + format (Biome auto-fix)
bun typecheck # TypeScript check across monorepo
``` ```
### 构建 ### Server App (`apps/server`)
```bash ```bash
bun build # 完整构建 (Vite → 编译 → Tauri 打包) bun dev # Vite dev server (localhost:3000)
bun build:vite # 仅构建 Vite (输出到 .output/) bun build # Production build -> .output/
bun build:compile # 编译为独立可执行文件 (使用 build.ts) bun compile # Compile to standalone binary (current platform)
bun build:tauri # 构建 Tauri 桌面安装包 bun compile:linux # Compile for Linux x64
bun compile:mac # Compile for macOS arm64
bun compile:win # Compile for Windows x64
bun fix # Biome auto-fix
bun typecheck # TypeScript check
# Database (Drizzle)
bun db:generate # Generate migrations from schema
bun db:migrate # Run migrations
bun db:push # Push schema (dev only)
bun db:studio # Open Drizzle Studio
``` ```
### 代码质量 ### Desktop App (`apps/desktop`)
```bash ```bash
bun typecheck # 运行 TypeScript 编译器检查 (tsc -b) bun dev # electron-vite dev mode (requires server dev running)
bun fix # 运行 Biome 自动修复格式和 Lint 问题 bun build # electron-vite build (main + preload)
biome check . # 检查但不自动修复 bun dist # Build + package for current platform
biome format --write . # 仅格式化代码 bun dist:linux # Build + package for Linux
bun dist:mac # Build + package for macOS
bun dist:win # Build + package for Windows
bun fix # Biome auto-fix
bun typecheck # TypeScript check
``` ```
### 数据库 ### Testing
No test framework configured yet. When adding tests:
```bash ```bash
bun db:generate # 从 schema 生成迁移文件 bun test path/to/test.ts # Run single test file
bun db:migrate # 执行数据库迁移 bun test -t "pattern" # Run tests matching pattern
bun db:push # 直接推送 schema 变更 (仅开发环境)
``` ```
### 测试 ## Code Style (TypeScript)
**注意**: 当前未配置测试框架。添加测试时:
- 使用 Vitest 或 Bun 内置测试运行器
- 运行单个测试文件: `bun test path/to/test.ts`
- 运行特定测试: `bun test -t "测试名称模式"`
## 代码风格指南 ### Formatting (Biome)
- **Indent**: 2 spaces | **Line endings**: LF
- **Quotes**: Single `'` | **Semicolons**: Omit (ASI)
- **Arrow parentheses**: Always `(x) => x`
### 格式化 (Biome) ### Imports
Biome auto-organizes. Order: 1) External packages → 2) Internal `@/*` aliases → 3) Type imports (`import type { ... }`)
**缩进**: 2 空格 (不使用 tab)
**换行符**: LF (Unix 风格)
**引号**: 单引号 `'string'`
**分号**: 按需 (ASI - 自动分号插入)
**箭头函数括号**: 始终使用 `(x) => x`
示例:
```typescript
const myFunc = (value: string) => {
return value.toUpperCase()
}
```
### 导入组织
Biome 自动组织导入。顺序:
1. 外部依赖
2. 内部导入 (使用 `@/*` 别名)
3. 类型导入 (仅导入类型时使用 `type` 关键字)
示例:
```typescript ```typescript
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { oc } from '@orpc/contract'
import { z } from 'zod' import { z } from 'zod'
import { db } from '@/db' import { db } from '@/server/db'
import { todoTable } from '@/db/schema'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
``` ```
### TypeScript ### TypeScript Strictness
- `strict: true`, `noUncheckedIndexedAccess: true`, `noImplicitOverride: true`, `verbatimModuleSyntax: true`
- Use `@/*` path aliases (maps to `src/*`)
**严格模式**: 启用了额外的严格检查 ### Naming Conventions
- `strict: true` | Type | Convention | Example |
- `noUncheckedIndexedAccess: true` - 数组/对象索引返回 `T | undefined` |------|------------|---------|
- `noImplicitOverride: true` | Files (utils) | kebab-case | `auth-utils.ts` |
- `noFallthroughCasesInSwitch: true` | Files (components) | PascalCase | `UserProfile.tsx` |
| Components | PascalCase arrow | `const Button = () => {}` |
| Functions | camelCase | `getUserById` |
| Constants | UPPER_SNAKE | `MAX_RETRIES` |
| Types/Interfaces | PascalCase | `UserProfile` |
**模块解析**: `bundler` 模式 + `verbatimModuleSyntax` ### React Patterns
- 导入时始终使用 `.ts`/`.tsx` 扩展名 - Components: arrow functions (enforced by Biome)
- 使用 `@/*` 路径别名指向 `src/*` - Routes: TanStack Router file conventions (`export const Route = createFileRoute(...)`)
- Data fetching: `useSuspenseQuery(orpc.feature.list.queryOptions())`
- Let React Compiler handle memoization (no manual `useMemo`/`useCallback`)
**类型注解**: ### Error Handling
- 公共 API 的函数参数和返回类型必须注解 - Use `try-catch` for async operations; throw descriptive errors
- 优先使用显式类型而非 `any` - ORPC: Use `ORPCError` with proper codes (`NOT_FOUND`, `INPUT_VALIDATION_FAILED`)
- 对象形状用 `type`,可扩展契约用 `interface` - Never use empty catch blocks
- 不可变 props 使用 `Readonly<T>`
### 命名规范 ## Database (Drizzle ORM)
- **文件**: 工具函数用 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 模式
**组件**: 使用箭头函数
```typescript ```typescript
const MyComponent = ({ title }: { title: string }) => {
return <div>{title}</div>
}
```
**路由**: 使用 `createFileRoute` 定义路由
```typescript
export const Route = createFileRoute('/')({
component: Home,
})
```
**数据获取**: 使用 TanStack Query hooks
- `useSuspenseQuery` - 保证有数据
- `useQuery` - 数据可能为空
**Props**: 禁止直接修改 props (Biome 规则 `noReactPropAssignments`)
### 数据库 Schema (Drizzle)
-`src/db/schema/*.ts` 定义 schema
-`src/db/schema/index.ts` 导出
- 使用 `drizzle-orm/pg-core` 的 PostgreSQL 类型
- 主键使用 `uuidv7()` (需要 PostgreSQL 扩展)
- 始终包含 `createdAt``updatedAt` 时间戳
示例:
```typescript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
export const myTable = pgTable('my_table', { export const myTable = pgTable('my_table', {
id: uuid().primaryKey().default(sql`uuidv7()`), id: uuid().primaryKey().default(sql`uuidv7()`),
name: text().notNull(), name: text().notNull(),
@@ -162,116 +122,74 @@ export const myTable = pgTable('my_table', {
}) })
``` ```
### 环境变量 ## Environment Variables
- 使用 `@t3-oss/env-core` 进行类型安全的环境变量验证 - Use `@t3-oss/env-core` with Zod validation in `src/env.ts`
- `src/env.ts` 定义 schema - Server vars: no prefix | Client vars: `VITE_` prefix required
- 服务端变量: 无前缀 - Never commit `.env` files
- 客户端变量: 必须有 `VITE_` 前缀
- 使用 Zod schema 验证
### 错误处理 ## Dependency Management
- 异步操作使用 try-catch - All versions centralized in root `package.json` `catalog` field
- 抛出带有描述性消息的错误 - Workspace packages use `"catalog:"` — never hardcode versions
- 用户界面错误优先使用 Result 类型或错误边界 - Internal packages use `"workspace:*"` references
- 适当记录错误 (避免记录敏感数据)
### 样式 (Tailwind CSS) ## Critical Rules
- 使用 Tailwind v4 工具类 **DO:**
- 通过 `@/styles.css?url` 导入样式 - Run `bun fix` before committing
- 优先使用组合而非自定义 CSS - Use `@/*` path aliases (not relative imports)
- 响应式修饰符: `sm:`, `md:`, `lg:` - Include `createdAt`/`updatedAt` on all tables
- UI 文本适当使用中文 - Use `catalog:` for dependency versions
## 目录结构 **DON'T:**
- Use `npm`, `npx`, `node`, `yarn`, `pnpm` — always use `bun` / `bunx`
- Edit `src/routeTree.gen.ts` (auto-generated)
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
- Commit `.env` files
- Use empty catch blocks `catch(e) {}`
- Hardcode dependency versions in workspace packages
## Git Workflow
1. Make changes following style guide
2. `bun fix` - auto-format and lint
3. `bun typecheck` - verify types
4. `bun dev` - test locally
5. Commit with descriptive message
## Directory Structure
``` ```
src/ .
├── components/ # 可复用 React 组件 ├── apps/
├── db/ │ ├── server/ # TanStack Start fullstack app
│ ├── schema/ # Drizzle schema 定义 │ ├── src/
└── index.ts # 数据库实例 │ │ ├── client/ # ORPC client, Query client
├── integrations/ # 第三方集成 (TanStack Query/Router) ├── components/
├── lib/ # 工具函数 ├── routes/ # File-based routing
├── orpc/ # ORPC (RPC 层) └── server/ # API layer + database
├── contracts/ # 契约定义 (input/output schemas) │ │ ├── api/ # ORPC contracts, routers, middlewares
├── handlers/ # 服务端过程实现 │ │ └── db/ # Drizzle schema
├── middlewares/ # 中间件 (如 DB provider) │ └── AGENTS.md
── contract.ts # 契约聚合 ── desktop/ # Electron desktop shell
├── router.ts # 路由组合 ├── src/
├── server.ts # 服务端实例 ├── main/
│ └── client.ts # 同构客户端 │ │ │ └── index.ts # Main process entry
├── routes/ # TanStack Router 文件路由 └── preload/
├── __root.tsx # 根布局 │ └── index.ts # Preload script
├── index.tsx # 首页 ├── electron.vite.config.ts
└── api/rpc.$.ts # ORPC HTTP 端点 ├── electron-builder.yml # Packaging config
├── env.ts # 环境变量验证 │ └── AGENTS.md
── router.tsx # 路由配置 ── packages/
│ ├── tsconfig/ # Shared TS configs
│ └── utils/ # Shared utilities
├── biome.json # Linting/formatting config
├── turbo.json # Turbo task orchestration
└── package.json # Workspace root + dependency catalog
``` ```
## 重要提示 ## See Also
- **禁止** 编辑 `src/routeTree.gen.ts` - 自动生成 - `apps/server/AGENTS.md` - Detailed TanStack Start / ORPC patterns
- **禁止** 提交 `.env` 文件 - 使用 `.env.example` 作为模板 - `apps/desktop/AGENTS.md` - Electron desktop development guide
- **必须** 在提交前运行 `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`)
```typescript
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`)
```typescript
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: 注册到契约和路由**
```typescript
// 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: 在组件中使用**
```typescript
import { orpc } from '@/orpc'
const query = useSuspenseQuery(orpc.myFeature.get.queryOptions({ id }))
const mutation = useMutation(orpc.myFeature.create.mutationOptions())
```
---
**最后更新**: 2026-01-18
**项目版本**: 基于 package.json 依赖版本

3
apps/desktop/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# electron-vite build output
out/
dist/

81
apps/desktop/AGENTS.md Normal file
View File

@@ -0,0 +1,81 @@
# AGENTS.md - Desktop App Guidelines
Thin Electron shell hosting the fullstack server app.
## Tech Stack
> **⚠️ This project uses Bun as the package manager. Runtime is Electron (Node.js). Never use `npm`, `npx`, `yarn`, or `pnpm`.**
- **Type**: Electron desktop shell
- **Design**: Server-driven desktop (thin native window hosting web app)
- **Runtime**: Electron (Main/Renderer) + Sidecar server binary (Bun-compiled)
- **Build Tool**: electron-vite (Vite-based, handles main + preload builds)
- **Packager**: electron-builder (installers, signing, auto-update)
- **Orchestration**: Turborepo
## Architecture
- **Server-driven design**: The desktop app is a "thin" native shell. It does not contain UI or business logic; it opens a BrowserWindow pointing to the `apps/server` TanStack Start application.
- **Dev mode**: Opens a BrowserWindow pointing to `localhost:3000`. Requires `apps/server` to be running separately (Turbo handles this).
- **Production mode**: Spawns a compiled server binary (from `resources/`) as a sidecar process, waits for readiness, then loads its URL.
## Commands
```bash
bun dev # electron-vite dev (requires server dev running)
bun build # electron-vite build (main + preload)
bun dist # Build + package for current platform
bun dist:linux # Build + package for Linux
bun dist:mac # Build + package for macOS
bun dist:win # Build + package for Windows
bun fix # Biome auto-fix
bun typecheck # TypeScript check
```
## Directory Structure
```
.
├── src/
│ ├── main/
│ │ └── index.ts # Main process (server lifecycle + BrowserWindow)
│ └── preload/
│ └── index.ts # Preload script (security isolation)
├── resources/ # Sidecar binaries (gitignored, copied from server build)
├── out/ # electron-vite build output (gitignored)
├── electron.vite.config.ts
├── electron-builder.yml # Packaging configuration
├── package.json
├── turbo.json
└── AGENTS.md
```
## Development Workflow
1. **Start server**: `bun dev` in `apps/server` (or use root `bun dev` via Turbo).
2. **Start desktop**: `bun dev` in `apps/desktop`.
3. **Connection**: Main process polls `localhost:3000` until responsive, then opens BrowserWindow.
## Production Build Workflow
From monorepo root, run `bun dist` to execute the full pipeline automatically (via Turbo task dependencies):
1. **Build server**: `apps/server``vite build``.output/`
2. **Compile server**: `apps/server``bun compile.ts``out/server-{os}-{arch}`
3. **Package desktop**: `apps/desktop``electron-vite build` + `electron-builder` → distributable
The `electron-builder.yml` `extraResources` config reads binaries directly from `../server/out/`, no manual copy needed.
To build for a specific platform explicitly, use `bun dist:linux` / `bun dist:mac` / `bun dist:win` in `apps/desktop`.
## Critical Rules
**DO:**
- Use arrow functions for all utility functions.
- Keep the desktop app as a thin shell — no UI or business logic.
- Use `catalog:` for all dependency versions in `package.json`.
**DON'T:**
- Use `npm`, `npx`, `yarn`, or `pnpm`. Use `bun` for package management.
- Include UI components or business logic in the desktop app.
- Use `as any` or `@ts-ignore`.

9
apps/desktop/biome.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": "//",
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

BIN
apps/desktop/build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -0,0 +1,48 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/electron-userland/electron-builder/refs/heads/master/packages/app-builder-lib/scheme.json
appId: com.furtherverse.desktop
productName: Furtherverse
executableName: furtherverse
npmRebuild: false
asarUnpack:
- resources/**
files:
- "!**/.vscode/*"
- "!src/*"
- "!electron.vite.config.{js,ts,mjs,cjs}"
- "!{.env,.env.*,bun.lock}"
- "!{tsconfig.json,tsconfig.node.json}"
- "!{AGENTS.md,README.md,CHANGELOG.md}"
# macOS
mac:
target:
- dmg
category: public.app-category.productivity
extraResources:
- from: ../server/out/server-darwin-arm64
to: server
dmg:
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
# Windows
win:
target:
- portable
extraResources:
- from: ../server/out/server-windows-x64.exe
to: server.exe
portable:
artifactName: ${productName}-${version}-${os}-${arch}-Portable.${ext}
# Linux
linux:
target:
- AppImage
category: Utility
extraResources:
- from: ../server/out/server-linux-x64
to: server
appImage:
artifactName: ${productName}-${version}-${os}-${arch}.${ext}

View File

@@ -0,0 +1,10 @@
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'electron-vite'
export default defineConfig({
main: {},
preload: {},
renderer: {
plugins: [tailwindcss()],
},
})

29
apps/desktop/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "@furtherverse/desktop",
"version": "1.0.0",
"private": true,
"main": "out/main/index.js",
"scripts": {
"build": "electron-vite build",
"dev": "electron-vite dev --watch",
"dist": "electron-builder",
"dist:linux": "electron-builder --linux",
"dist:mac": "electron-builder --mac",
"dist:win": "electron-builder --win",
"fix": "biome check --write",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"tree-kill": "catalog:"
},
"devDependencies": {
"@furtherverse/tsconfig": "workspace:*",
"@tailwindcss/vite": "catalog:",
"@types/node": "catalog:",
"electron": "catalog:",
"electron-builder": "catalog:",
"electron-vite": "catalog:",
"tailwindcss": "catalog:"
}
}

View File

@@ -0,0 +1,233 @@
import { spawn } from 'node:child_process'
import { createServer } from 'node:net'
import { join } from 'node:path'
import { app, BrowserWindow, dialog, shell } from 'electron'
import killProcessTree from 'tree-kill'
const DEV_SERVER_URL = 'http://localhost:3000'
let mainWindow: BrowserWindow | null = null
let serverProcess: ReturnType<typeof spawn> | null = null
let isQuitting = false
let serverReady = false
const shouldAbortWindowLoad = (): boolean =>
isQuitting || !mainWindow || mainWindow.isDestroyed()
const showErrorAndQuit = (title: string, detail: string) => {
if (isQuitting) return
dialog.showErrorBox(title, detail)
app.quit()
}
const getAvailablePort = (): Promise<number> =>
new Promise((resolve, reject) => {
const server = createServer()
server.listen(0, () => {
const addr = server.address()
if (!addr || typeof addr === 'string') {
server.close()
reject(new Error('Failed to resolve port'))
return
}
server.close(() => resolve(addr.port))
})
server.on('error', reject)
})
const isServerReady = async (url: string): Promise<boolean> => {
try {
const response = await fetch(url, { method: 'HEAD' })
return response.ok
} catch {
return false
}
}
const waitForServer = async (
url: string,
timeoutMs = 15_000,
): Promise<boolean> => {
const start = Date.now()
while (Date.now() - start < timeoutMs && !isQuitting) {
if (await isServerReady(url)) return true
await new Promise<void>((resolve) => setTimeout(resolve, 200))
}
return false
}
const stopServerProcess = () => {
if (!serverProcess) {
return
}
const runningServer = serverProcess
serverProcess = null
if (!runningServer.pid || runningServer.exitCode !== null) {
return
}
killProcessTree(runningServer.pid, (error?: Error) => {
if (error) {
console.error('Failed to stop server process:', error)
}
})
}
const spawnServer = (port: number): string => {
const binaryName = process.platform === 'win32' ? 'server.exe' : 'server'
const binaryPath = join(process.resourcesPath, binaryName)
serverProcess = spawn(binaryPath, [], {
env: {
...process.env,
PORT: String(port),
HOST: '127.0.0.1',
},
stdio: 'ignore',
})
serverProcess.unref()
serverProcess.on('error', (err) => {
console.error('Failed to start server:', err)
showErrorAndQuit(
'Startup Failed',
'A required component failed to start. Please reinstall the app.',
)
})
serverProcess.on('exit', (code) => {
serverProcess = null
if (!isQuitting && serverReady) {
showErrorAndQuit(
'Service Stopped',
app.isPackaged
? 'The background service stopped unexpectedly. Please restart the app.'
: `Server process exited unexpectedly (code ${code}). Check the server logs for details.`,
)
}
})
return `http://127.0.0.1:${port}`
}
const resolveServerUrl = async (): Promise<string | null> => {
if (!app.isPackaged) {
return DEV_SERVER_URL
}
let port: number
try {
port = await getAvailablePort()
} catch {
showErrorAndQuit(
'Startup Failed',
"The app couldn't allocate a local port. Please close other apps and try again.",
)
return null
}
if (isQuitting) {
return null
}
return spawnServer(port)
}
const createWindow = async () => {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
show: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: true,
},
})
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
mainWindow.on('closed', () => {
mainWindow = null
})
if (process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
mainWindow.show()
const serverUrl = await resolveServerUrl()
if (!serverUrl || shouldAbortWindowLoad()) {
stopServerProcess()
return
}
if (!app.isPackaged) console.log(`Waiting for server at ${serverUrl}...`)
const ready = await waitForServer(serverUrl)
if (shouldAbortWindowLoad()) {
stopServerProcess()
return
}
if (!ready) {
showErrorAndQuit(
'Startup Failed',
app.isPackaged
? 'The app is taking too long to start. Please try again.'
: 'Dev server not responding. Run `bun dev` in apps/server first.',
)
return
}
serverReady = true
mainWindow.loadURL(serverUrl)
}
const beginQuit = () => {
isQuitting = true
stopServerProcess()
}
app
.whenReady()
.then(createWindow)
.catch((e) => {
console.error('Failed to create window:', e)
showErrorAndQuit(
"App Couldn't Start",
app.isPackaged
? "We couldn't open the application window. Please restart your computer and try again."
: `Failed to create window: ${e instanceof Error ? e.message : String(e)}`,
)
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
beginQuit()
app.quit()
}
})
app.on('activate', () => {
if (!isQuitting && BrowserWindow.getAllWindows().length === 0) {
createWindow().catch((e) => {
console.error('Failed to re-create window:', e)
showErrorAndQuit(
"App Couldn't Start",
app.isPackaged
? "We couldn't open the application window. Please restart the app."
: `Failed to re-create window: ${e instanceof Error ? e.message : String(e)}`,
)
})
}
})
app.on('before-quit', () => {
beginQuit()
})

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Furtherverse</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body class="bg-white h-screen w-screen flex flex-col items-center justify-center overflow-hidden select-none cursor-default font-sans antialiased m-0">
<div class="flex flex-col items-center gap-8 animate-fade-in">
<h1 class="text-3xl font-medium tracking-tight text-zinc-900">Furtherverse</h1>
<div class="w-24 h-[2px] bg-zinc-100 rounded-full overflow-hidden relative">
<div class="h-full w-full bg-zinc-800 origin-left animate-loading-bar"></div>
</div>
<div class="text-[10px] uppercase tracking-widest text-zinc-400 font-medium animate-pulse-slow">Starting</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
@import "tailwindcss";
@theme {
--animate-fade-in: fade-in 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
--animate-pulse-slow: pulse-slow 3s ease-in-out infinite;
--animate-loading-bar: loading-bar 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-slow {
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 0.8;
}
}
@keyframes loading-bar {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(0);
}
100% {
transform: translateX(100%);
}
}

View File

@@ -0,0 +1,7 @@
{
"extends": "@furtherverse/tsconfig/base.json",
"compilerOptions": {
"types": ["node"]
},
"include": ["src/main/**/*", "src/preload/**/*", "electron.vite.config.ts"]
}

25
apps/desktop/turbo.json Normal file
View File

@@ -0,0 +1,25 @@
{
"$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"outputs": ["out/**"]
},
"dist": {
"dependsOn": ["build", "@furtherverse/server#compile"],
"outputs": ["dist/**"]
},
"dist:linux": {
"dependsOn": ["build", "@furtherverse/server#compile:linux"],
"outputs": ["dist/**"]
},
"dist:mac": {
"dependsOn": ["build", "@furtherverse/server#compile:darwin"],
"outputs": ["dist/**"]
},
"dist:win": {
"dependsOn": ["build", "@furtherverse/server#compile:windows"],
"outputs": ["dist/**"]
}
}
}

208
apps/server/AGENTS.md Normal file
View File

@@ -0,0 +1,208 @@
# AGENTS.md - Server App Guidelines
TanStack Start fullstack web app with ORPC (contract-first RPC).
## Tech Stack
> **⚠️ This project uses Bun — NOT Node.js / npm. All commands use `bun`. Never use `npm`, `npx`, or `node`.**
- **Framework**: TanStack Start (React 19 SSR, file-based routing)
- **Runtime**: Bun — **NOT Node.js**
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
- **Language**: TypeScript (strict mode)
- **Styling**: Tailwind CSS v4
- **Database**: PostgreSQL + Drizzle ORM
- **State**: TanStack Query v5
- **RPC**: ORPC (contract-first, type-safe)
- **Build**: Vite + Nitro
## Commands
```bash
# Development
bun dev # Vite dev server (localhost:3000)
bun db:studio # Drizzle Studio GUI
# Build
bun build # Production build → .output/
bun compile # Compile to standalone binary (current platform, depends on build)
bun compile:linux # Compile for Linux x64
bun compile:mac # Compile for macOS arm64
bun compile:win # Compile for Windows x64
# Code Quality
bun fix # Biome auto-fix
bun typecheck # TypeScript check
# Database
bun db:generate # Generate migrations from schema
bun db:migrate # Run migrations
bun db:push # Push schema directly (dev only)
# Testing (not yet configured)
bun test path/to/test.ts # Run single test
bun test -t "pattern" # Run tests matching pattern
```
## Directory Structure
```
src/
├── client/ # Client-side code
│ ├── orpc.client.ts # ORPC isomorphic client
│ └── query-client.ts # TanStack Query client
├── components/ # React components
├── routes/ # TanStack Router file routes
│ ├── __root.tsx # Root layout
│ ├── index.tsx # Home page
│ └── api/
│ └── rpc.$.ts # ORPC HTTP endpoint
├── server/ # Server-side code
│ ├── api/ # ORPC layer
│ │ ├── contracts/ # Input/output schemas (Zod)
│ │ ├── middlewares/ # Middleware (db provider, auth)
│ │ ├── routers/ # Handler implementations
│ │ ├── context.ts # Request context
│ │ ├── server.ts # ORPC server instance
│ │ └── types.ts # Type exports
│ └── db/
│ ├── schema/ # Drizzle table definitions
│ └── index.ts # Database instance
├── env.ts # Environment variable validation
├── router.tsx # Router configuration
├── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
└── styles.css # Tailwind entry
```
## ORPC Pattern
### 1. Define Contract (`src/server/api/contracts/feature.contract.ts`)
```typescript
import { oc } from '@orpc/contract'
import { createSelectSchema } from 'drizzle-zod'
import { z } from 'zod'
import { featureTable } from '@/server/db/schema'
const selectSchema = createSelectSchema(featureTable)
export const list = oc.input(z.void()).output(z.array(selectSchema))
export const create = oc.input(insertSchema).output(selectSchema)
```
### 2. Implement Router (`src/server/api/routers/feature.router.ts`)
```typescript
import { ORPCError } from '@orpc/server'
import { db } from '../middlewares'
import { os } from '../server'
export const list = os.feature.list.use(db).handler(async ({ context }) => {
return await context.db.query.featureTable.findMany()
})
```
### 3. Register in Index Files
```typescript
// src/server/api/contracts/index.ts
import * as feature from './feature.contract'
export const contract = { feature }
// src/server/api/routers/index.ts
import * as feature from './feature.router'
export const router = os.router({ feature })
```
### 4. Use in Components
```typescript
import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
import { orpc } from '@/client/orpc.client'
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
const mutation = useMutation(orpc.feature.create.mutationOptions())
```
## Database Schema (Drizzle)
```typescript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm'
export const myTable = pgTable('my_table', {
id: uuid().primaryKey().default(sql`uuidv7()`),
name: text().notNull(),
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
})
```
## Code Style
### Formatting (Biome)
- **Indent**: 2 spaces
- **Quotes**: Single `'`
- **Semicolons**: Omit (ASI)
- **Arrow parens**: Always `(x) => x`
### Imports
Biome auto-organizes:
1. External packages
2. Internal `@/*` aliases
3. Type imports (`import type { ... }`)
```typescript
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { db } from '@/server/db'
import type { ReactNode } from 'react'
```
### TypeScript
- `strict: true`
- `noUncheckedIndexedAccess: true` - array access returns `T | undefined`
- Use `@/*` path aliases (maps to `src/*`)
### Naming
| Type | Convention | Example |
|------|------------|---------|
| Files (utils) | kebab-case | `auth-utils.ts` |
| Files (components) | PascalCase | `UserProfile.tsx` |
| Components | PascalCase arrow | `const Button = () => {}` |
| Functions | camelCase | `getUserById` |
| Types | PascalCase | `UserProfile` |
### React
- Use arrow functions for components (Biome enforced)
- Use `useSuspenseQuery` for guaranteed data
- Let React Compiler handle memoization (no manual `useMemo`/`useCallback`)
## Environment Variables
```typescript
// src/env.ts - using @t3-oss/env-core
import { createEnv } from '@t3-oss/env-core'
import { z } from 'zod'
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
},
clientPrefix: 'VITE_',
client: {
VITE_API_URL: z.string().optional(),
},
})
```
## Critical Rules
**DO:**
- Run `bun fix` before committing
- Use `@/*` path aliases
- Include `createdAt`/`updatedAt` on all tables
- Use `ORPCError` with proper codes
**DON'T:**
- Use `npm`, `npx`, `node`, `yarn`, `pnpm` — always use `bun` / `bunx`
- Edit `src/routeTree.gen.ts` (auto-generated)
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
- Commit `.env` files
- Use empty catch blocks

12
apps/server/biome.json Normal file
View File

@@ -0,0 +1,12 @@
{
"$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": "//",
"files": {
"includes": ["**", "!**/routeTree.gen.ts"]
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

64
apps/server/compile.ts Normal file
View File

@@ -0,0 +1,64 @@
import { rm } from 'node:fs/promises'
import { parseArgs } from 'node:util'
const ENTRYPOINT = '.output/server/index.mjs'
const OUTDIR = 'out'
const SUPPORTED_TARGETS: readonly Bun.Build.CompileTarget[] = [
'bun-windows-x64',
'bun-darwin-arm64',
'bun-linux-x64',
'bun-linux-arm64',
]
const isSupportedTarget = (value: string): value is Bun.Build.CompileTarget =>
(SUPPORTED_TARGETS as readonly string[]).includes(value)
const { values } = parseArgs({
options: { target: { type: 'string' } },
strict: true,
allowPositionals: false,
})
const resolveTarget = (): Bun.Build.CompileTarget => {
if (values.target !== undefined) {
if (!isSupportedTarget(values.target)) {
throw new Error(
`Invalid target: ${values.target}\nAllowed: ${SUPPORTED_TARGETS.join(', ')}`,
)
}
return values.target
}
const os = process.platform === 'win32' ? 'windows' : process.platform
const candidate = `bun-${os}-${process.arch}`
if (!isSupportedTarget(candidate)) {
throw new Error(`Unsupported host: ${process.platform}-${process.arch}`)
}
return candidate
}
const main = async () => {
const target = resolveTarget()
const suffix = target.replace('bun-', '')
const outfile = `server-${suffix}`
await rm(OUTDIR, { recursive: true, force: true })
const result = await Bun.build({
entrypoints: [ENTRYPOINT],
outdir: OUTDIR,
compile: { outfile, target },
})
if (!result.success) {
throw new Error(result.logs.map(String).join('\n'))
}
console.log(`${target}${OUTDIR}/${outfile}`)
}
main().catch((err) => {
console.error('❌', err instanceof Error ? err.message : err)
process.exit(1)
})

View File

@@ -3,7 +3,7 @@ import { env } from '@/env'
export default defineConfig({ export default defineConfig({
out: './drizzle', out: './drizzle',
schema: './src/db/schema/index.ts', schema: './src/server/db/schema/index.ts',
dialect: 'postgresql', dialect: 'postgresql',
dbCredentials: { dbCredentials: {
url: env.DATABASE_URL, url: env.DATABASE_URL,

57
apps/server/package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "@furtherverse/server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"compile": "bun compile.ts",
"compile:darwin": "bun compile.ts --target bun-darwin-arm64",
"compile:linux": "bun compile.ts --target bun-linux-x64",
"compile:windows": "bun compile.ts --target bun-windows-x64",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"dev": "vite dev",
"fix": "biome check --write",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@furtherverse/utils": "workspace:*",
"@orpc/client": "catalog:",
"@orpc/contract": "catalog:",
"@orpc/openapi": "catalog:",
"@orpc/server": "catalog:",
"@orpc/tanstack-query": "catalog:",
"@orpc/zod": "catalog:",
"@t3-oss/env-core": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
"@tanstack/react-router-ssr-query": "catalog:",
"@tanstack/react-start": "catalog:",
"drizzle-orm": "catalog:",
"drizzle-zod": "catalog:",
"postgres": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"uuid": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@furtherverse/tsconfig": "workspace:*",
"@tailwindcss/vite": "catalog:",
"@tanstack/devtools-vite": "catalog:",
"@tanstack/react-devtools": "catalog:",
"@tanstack/react-query-devtools": "catalog:",
"@tanstack/react-router-devtools": "catalog:",
"@types/bun": "catalog:",
"@vitejs/plugin-react": "catalog:",
"babel-plugin-react-compiler": "catalog:",
"drizzle-kit": "catalog:",
"nitro": "catalog:",
"tailwindcss": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "catalog:"
}
}

View File

@@ -0,0 +1,24 @@
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { createRouterClient } from '@orpc/server'
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { router } from '@/server/api/routers'
import type { RouterClient } from '@/server/api/types'
const getORPCClient = createIsomorphicFn()
.server(() =>
createRouterClient(router, {
context: () => ({
headers: getRequestHeaders(),
}),
}),
)
.client(() => {
const link = new RPCLink({
url: `${window.location.origin}/api/rpc`,
})
return createORPCClient<RouterClient>(link)
})
export const orpc: RouterClient = getORPCClient()

View File

@@ -0,0 +1,30 @@
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
import { orpc as orpcClient } from './orpc.client'
export const orpc = createTanstackQueryUtils(orpcClient, {
experimental_defaults: {
todo: {
create: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
update: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
remove: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
},
},
})

View File

@@ -9,6 +9,9 @@ export const env = createEnv({
client: { client: {
VITE_APP_TITLE: z.string().min(1).optional(), VITE_APP_TITLE: z.string().min(1).optional(),
}, },
shared: {
NODE_ENV: z.enum(['development', 'production', 'test']),
},
runtimeEnv: process.env, runtimeEnv: process.env,
emptyStringAsUndefined: true, emptyStringAsUndefined: true,
}) })

View File

@@ -10,6 +10,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiSplatRouteImport } from './routes/api/$'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$' import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
@@ -17,6 +18,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ApiSplatRoute = ApiSplatRouteImport.update({
id: '/api/$',
path: '/api/$',
getParentRoute: () => rootRouteImport,
} as any)
const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
id: '/api/rpc/$', id: '/api/rpc/$',
path: '/api/rpc/$', path: '/api/rpc/$',
@@ -25,27 +31,31 @@ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/api/$': typeof ApiSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/api/$': typeof ApiSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/api/$': typeof ApiSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/api/rpc/$' fullPaths: '/' | '/api/$' | '/api/rpc/$'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/api/rpc/$' to: '/' | '/api/$' | '/api/rpc/$'
id: '__root__' | '/' | '/api/rpc/$' id: '__root__' | '/' | '/api/$' | '/api/rpc/$'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
ApiSplatRoute: typeof ApiSplatRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute ApiRpcSplatRoute: typeof ApiRpcSplatRoute
} }
@@ -58,6 +68,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/api/$': {
id: '/api/$'
path: '/api/$'
fullPath: '/api/$'
preLoaderRoute: typeof ApiSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/api/rpc/$': { '/api/rpc/$': {
id: '/api/rpc/$' id: '/api/rpc/$'
path: '/api/rpc/$' path: '/api/rpc/$'
@@ -70,6 +87,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
ApiSplatRoute: ApiSplatRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute, ApiRpcSplatRoute: ApiRpcSplatRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -1,15 +1,15 @@
import { TanStackDevtools } from '@tanstack/react-devtools' import { TanStackDevtools } from '@tanstack/react-devtools'
import type { QueryClient } from '@tanstack/react-query' import type { QueryClient } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { import {
createRootRouteWithContext, createRootRouteWithContext,
HeadContent, HeadContent,
Scripts, Scripts,
} from '@tanstack/react-router' } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { ErrorComponent } from '@/components/Error' import { ErrorComponent } from '@/components/Error'
import { NotFoundComponent } from '@/components/NotFount' import { NotFoundComponent } from '@/components/NotFount'
import { devtools as queryDevtools } from '@/integrations/tanstack-query'
import { devtools as routerDevtools } from '@/integrations/tanstack-router'
import appCss from '@/styles.css?url' import appCss from '@/styles.css?url'
export interface RouterContext { export interface RouterContext {
@@ -27,7 +27,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
content: 'width=device-width, initial-scale=1', content: 'width=device-width, initial-scale=1',
}, },
{ {
title: 'Fullstack Starter', title: 'Furtherverse',
}, },
], ],
links: [ links: [
@@ -54,7 +54,16 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
config={{ config={{
position: 'bottom-right', position: 'bottom-right',
}} }}
plugins={[routerDevtools, queryDevtools]} plugins={[
{
name: 'TanStack Router',
render: <TanStackRouterDevtoolsPanel />,
},
{
name: 'TanStack Query',
render: <ReactQueryDevtoolsPanel />,
},
]}
/> />
<Scripts /> <Scripts />
</body> </body>

View File

@@ -0,0 +1,86 @@
import { OpenAPIHandler } from '@orpc/openapi/fetch'
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
import { ORPCError, onError, ValidationError } from '@orpc/server'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { name, version } from '@/../package.json'
import { router } from '@/server/api/routers'
const handler = new OpenAPIHandler(router, {
plugins: [
new OpenAPIReferencePlugin({
docsProvider: 'scalar',
schemaConverters: [new ZodToJsonSchemaConverter()],
specGenerateOptions: {
info: {
title: name,
version,
},
// components: {
// securitySchemes: {
// bearerAuth: {
// type: 'http',
// scheme: 'bearer',
// },
// },
// },
},
docsPath: '/docs',
specPath: '/spec.json',
}),
],
interceptors: [
onError((error) => {
console.error(error)
}),
],
clientInterceptors: [
onError((error) => {
if (
error instanceof ORPCError &&
error.code === 'BAD_REQUEST' &&
error.cause instanceof ValidationError
) {
// If you only use Zod you can safely cast to ZodIssue[]
const zodError = new z.ZodError(
error.cause.issues as z.core.$ZodIssue[],
)
throw new ORPCError('INPUT_VALIDATION_FAILED', {
status: 422,
message: z.prettifyError(zodError),
data: z.flattenError(zodError),
cause: error.cause,
})
}
if (
error instanceof ORPCError &&
error.code === 'INTERNAL_SERVER_ERROR' &&
error.cause instanceof ValidationError
) {
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
cause: error.cause,
})
}
}),
],
})
export const Route = createFileRoute('/api/$')({
server: {
handlers: {
ANY: async ({ request }) => {
const { response } = await handler.handle(request, {
prefix: '/api',
context: {
headers: request.headers,
},
})
return response ?? new Response('Not Found', { status: 404 })
},
},
},
})

View File

@@ -2,7 +2,7 @@ import { ORPCError, onError, ValidationError } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch' import { RPCHandler } from '@orpc/server/fetch'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod' import { z } from 'zod'
import { router } from '@/orpc/router' import { router } from '@/server/api/routers'
const handler = new RPCHandler(router, { const handler = new RPCHandler(router, {
interceptors: [ interceptors: [
@@ -49,7 +49,9 @@ export const Route = createFileRoute('/api/rpc/$')({
ANY: async ({ request }) => { ANY: async ({ request }) => {
const { response } = await handler.handle(request, { const { response } = await handler.handle(request, {
prefix: '/api/rpc', prefix: '/api/rpc',
context: {}, context: {
headers: request.headers,
},
}) })
return response ?? new Response('Not Found', { status: 404 }) return response ?? new Response('Not Found', { status: 404 })

View File

@@ -1,10 +1,8 @@
import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { isTauri } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
import type { ChangeEventHandler, FormEventHandler } from 'react' import type { ChangeEventHandler, FormEventHandler } from 'react'
import { useEffect, useState } from 'react' import { useState } from 'react'
import { orpc } from '@/orpc' import { orpc } from '@/client/query-client'
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
component: Todos, component: Todos,
@@ -21,11 +19,6 @@ function Todos() {
const updateMutation = useMutation(orpc.todo.update.mutationOptions()) const updateMutation = useMutation(orpc.todo.update.mutationOptions())
const deleteMutation = useMutation(orpc.todo.remove.mutationOptions()) const deleteMutation = useMutation(orpc.todo.remove.mutationOptions())
useEffect(() => {
if (!isTauri()) return
getCurrentWindow().setTitle('待办事项')
}, [])
const handleCreateTodo: FormEventHandler<HTMLFormElement> = (e) => { const handleCreateTodo: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault() e.preventDefault()
if (newTodoTitle.trim()) { if (newTodoTitle.trim()) {

View File

@@ -0,0 +1,25 @@
import type { DB } from '@/server/db'
/**
* 基础 Context - 所有请求都包含的上下文
*/
export interface BaseContext {
headers: Headers
}
/**
* 数据库 Context - 通过 db middleware 扩展
*/
export interface DBContext extends BaseContext {
db: DB
}
/**
* 认证 Context - 通过 auth middleware 扩展(未来使用)
*
* @example
* export interface AuthContext extends DBContext {
* userId: string
* user: User
* }
*/

View File

@@ -0,0 +1,7 @@
import * as todo from './todo.contract'
export const contract = {
todo,
}
export type Contract = typeof contract

View File

@@ -5,7 +5,7 @@ import {
createUpdateSchema, createUpdateSchema,
} from 'drizzle-zod' } from 'drizzle-zod'
import { z } from 'zod' import { z } from 'zod'
import { todoTable } from '@/db/schema' import { todoTable } from '@/server/db/schema'
const selectSchema = createSelectSchema(todoTable) const selectSchema = createSelectSchema(todoTable)

View File

@@ -0,0 +1,11 @@
import { os } from '@orpc/server'
import { getDB } from '@/server/db'
export const db = os.middleware(async ({ context, next }) => {
return next({
context: {
...context,
db: getDB(),
},
})
})

View File

@@ -0,0 +1 @@
export * from './db.middleware'

View File

@@ -0,0 +1,6 @@
import { os } from '../server'
import * as todo from './todo.router'
export const router = os.router({
todo,
})

View File

@@ -1,20 +1,18 @@
import { ORPCError } from '@orpc/server' import { ORPCError } from '@orpc/server'
import { eq } from 'drizzle-orm' import { eq } from 'drizzle-orm'
import { todoTable } from '@/db/schema' import { todoTable } from '@/server/db/schema'
import { dbProvider } from '@/orpc/middlewares' import { db } from '../middlewares'
import { os } from '@/orpc/server' import { os } from '../server'
export const list = os.todo.list export const list = os.todo.list.use(db).handler(async ({ context }) => {
.use(dbProvider)
.handler(async ({ context }) => {
const todos = await context.db.query.todoTable.findMany({ const todos = await context.db.query.todoTable.findMany({
orderBy: (todos, { desc }) => [desc(todos.createdAt)], orderBy: (todos, { desc }) => [desc(todos.createdAt)],
}) })
return todos return todos
}) })
export const create = os.todo.create export const create = os.todo.create
.use(dbProvider) .use(db)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [newTodo] = await context.db const [newTodo] = await context.db
.insert(todoTable) .insert(todoTable)
@@ -29,7 +27,7 @@ export const create = os.todo.create
}) })
export const update = os.todo.update export const update = os.todo.update
.use(dbProvider) .use(db)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const [updatedTodo] = await context.db const [updatedTodo] = await context.db
.update(todoTable) .update(todoTable)
@@ -45,7 +43,7 @@ export const update = os.todo.update
}) })
export const remove = os.todo.remove export const remove = os.todo.remove
.use(dbProvider) .use(db)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
await context.db.delete(todoTable).where(eq(todoTable.id, input.id)) await context.db.delete(todoTable).where(eq(todoTable.id, input.id))
}) })

View File

@@ -0,0 +1,5 @@
import { implement } from '@orpc/server'
import type { BaseContext } from './context'
import { contract } from './contracts'
export const os = implement(contract).$context<BaseContext>()

View File

@@ -3,9 +3,8 @@ import type {
InferContractRouterInputs, InferContractRouterInputs,
InferContractRouterOutputs, InferContractRouterOutputs,
} from '@orpc/contract' } from '@orpc/contract'
import type { contract } from './contract' import type { Contract } from './contracts'
export type Contract = typeof contract
export type RouterClient = ContractRouterClient<Contract> export type RouterClient = ContractRouterClient<Contract>
export type RouterInputs = InferContractRouterInputs<Contract> export type RouterInputs = InferContractRouterInputs<Contract>
export type RouterOutputs = InferContractRouterOutputs<Contract> export type RouterOutputs = InferContractRouterOutputs<Contract>

View File

@@ -0,0 +1,27 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import { env } from '@/env'
import * as schema from '@/server/db/schema'
export const createDB = () =>
drizzle({
connection: {
url: env.DATABASE_URL,
prepare: true,
},
schema,
})
export type DB = ReturnType<typeof createDB>
export const getDB = (() => {
let db: DB | null = null
return (singleton = true): DB => {
if (!singleton) {
return createDB()
}
db ??= createDB()
return db
}
})()

View File

@@ -0,0 +1,8 @@
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
import { generatedFields } from './utils/field'
export const todoTable = pgTable('todo', {
...generatedFields,
title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
})

View File

@@ -0,0 +1,58 @@
import { sql } from 'drizzle-orm'
import { timestamp, uuid } from 'drizzle-orm/pg-core'
import { v7 as uuidv7 } from 'uuid'
// id
export const id = (name: string) => uuid(name)
export const pk = (name: string, strategy?: 'native' | 'extension') => {
switch (strategy) {
// PG 18+
case 'native':
return id(name).primaryKey().default(sql`uuidv7()`)
// PG 13+ with extension
case 'extension':
return id(name).primaryKey().default(sql`uuid_generate_v7()`)
// Any PG version
default:
return id(name)
.primaryKey()
.$defaultFn(() => uuidv7())
}
}
// timestamp
export const createdAt = (name = 'created_at') =>
timestamp(name, { withTimezone: true }).notNull().defaultNow()
export const updatedAt = (name = 'updated_at') =>
timestamp(name, { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date())
// generated fields
export const generatedFields = {
id: pk('id'),
createdAt: createdAt('created_at'),
updatedAt: updatedAt('updated_at'),
}
// Helper to create omit keys from generatedFields
const createGeneratedFieldKeys = <T extends Record<string, unknown>>(
fields: T,
): Record<keyof T, true> => {
return Object.keys(fields).reduce(
(acc, key) => {
acc[key as keyof T] = true
return acc
},
{} as Record<keyof T, true>,
)
}
export const generatedFieldKeys = createGeneratedFieldKeys(generatedFields)

View File

@@ -0,0 +1,9 @@
{
"extends": "@furtherverse/tsconfig/react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

26
apps/server/turbo.json Normal file
View File

@@ -0,0 +1,26 @@
{
"$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"env": ["NODE_ENV", "VITE_*"],
"outputs": [".output/**"]
},
"compile": {
"dependsOn": ["build"],
"outputs": ["out/**"]
},
"compile:darwin": {
"dependsOn": ["build"],
"outputs": ["out/**"]
},
"compile:linux": {
"dependsOn": ["build"],
"outputs": ["out/**"]
},
"compile:windows": {
"dependsOn": ["build"],
"outputs": ["out/**"]
}
}
}

View File

@@ -10,24 +10,21 @@ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [ plugins: [
tanstackDevtools(), tanstackDevtools(),
nitro({
preset: 'bun',
serveStatic: 'inline',
}),
tsconfigPaths(),
tailwindcss(), tailwindcss(),
tsconfigPaths(),
tanstackStart(), tanstackStart(),
react({ react({
babel: { babel: {
plugins: ['babel-plugin-react-compiler'], plugins: ['babel-plugin-react-compiler'],
}, },
}), }),
nitro({
preset: 'bun',
serveStatic: 'inline',
}),
], ],
server: { server: {
port: 3000, port: 3000,
strictPort: true, strictPort: true,
watch: {
ignored: ['**/src-tauri/**'],
},
}, },
}) })

View File

@@ -6,7 +6,6 @@
"useIgnoreFile": true "useIgnoreFile": true
}, },
"files": { "files": {
"includes": ["**", "!**/routeTree.gen.ts"],
"ignoreUnknown": false "ignoreUnknown": false
}, },
"formatter": { "formatter": {

289
build.ts
View File

@@ -1,289 +0,0 @@
import { Schema } from '@effect/schema'
import { $ } from 'bun'
import { Console, Context, Data, Effect, Layer } from 'effect'
// ============================================================================
// Domain Models & Schema
// ============================================================================
const targetMap = {
'bun-windows-x64': 'x86_64-pc-windows-msvc',
'bun-darwin-arm64': 'aarch64-apple-darwin',
'bun-darwin-x64': 'x86_64-apple-darwin',
'bun-linux-x64': 'x86_64-unknown-linux-gnu',
'bun-linux-arm64': 'aarch64-unknown-linux-gnu',
} as const
const BunTargetSchema = Schema.Literal(
'bun-windows-x64',
'bun-darwin-arm64',
'bun-darwin-x64',
'bun-linux-x64',
'bun-linux-arm64',
)
type BunTarget = Schema.Schema.Type<typeof BunTargetSchema>
const BuildConfigSchema = Schema.Struct({
entrypoint: Schema.String.pipe(Schema.nonEmptyString()),
outputDir: Schema.String.pipe(Schema.nonEmptyString()),
targets: Schema.Array(BunTargetSchema).pipe(Schema.minItems(1)),
})
type BuildConfig = Schema.Schema.Type<typeof BuildConfigSchema>
const BuildResultSchema = Schema.Struct({
target: BunTargetSchema,
outputs: Schema.Array(Schema.String),
})
type BuildResult = Schema.Schema.Type<typeof BuildResultSchema>
// ============================================================================
// Error Models (使用 Data.TaggedError)
// ============================================================================
class CleanError extends Data.TaggedError('CleanError')<{
readonly dir: string
readonly cause: unknown
}> {}
class BuildError extends Data.TaggedError('BuildError')<{
readonly target: BunTarget
readonly cause: unknown
}> {}
class ConfigError extends Data.TaggedError('ConfigError')<{
readonly message: string
readonly cause: unknown
}> {}
// ============================================================================
// Services
// ============================================================================
/**
* 配置服务
*/
class BuildConfigService extends Context.Tag('BuildConfigService')<
BuildConfigService,
BuildConfig
>() {
/**
* 从原始数据创建并验证配置
*/
static fromRaw = (raw: unknown) =>
Effect.gen(function* () {
const decoded = yield* Schema.decodeUnknown(BuildConfigSchema)(raw)
return decoded
}).pipe(
Effect.catchAll((error) =>
Effect.fail(
new ConfigError({
message: '配置验证失败',
cause: error,
}),
),
),
)
/**
* 默认配置 Layer
*/
static readonly Live = Layer.effect(
BuildConfigService,
BuildConfigService.fromRaw({
entrypoint: '.output/server/index.mjs',
// outputDir: 'out',
outputDir: 'src-tauri/binaries',
targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'],
}),
)
}
/**
* 文件系统服务
*/
class FileSystemService extends Context.Tag('FileSystemService')<
FileSystemService,
{
readonly cleanDir: (dir: string) => Effect.Effect<void, CleanError>
}
>() {
static readonly Live = Layer.succeed(FileSystemService, {
cleanDir: (dir: string) =>
Effect.tryPromise({
try: async () => {
await $`rm -rf ${dir}`
},
catch: (cause: unknown) =>
new CleanError({
dir,
cause,
}),
}),
})
}
/**
* 构建服务
*/
class BuildService extends Context.Tag('BuildService')<
BuildService,
{
readonly buildForTarget: (
config: BuildConfig,
target: BunTarget,
) => Effect.Effect<BuildResult, BuildError>
readonly buildAll: (
config: BuildConfig,
) => Effect.Effect<ReadonlyArray<BuildResult>, BuildError>
}
>() {
static readonly Live = Layer.succeed(BuildService, {
buildForTarget: (config: BuildConfig, target: BunTarget) =>
Effect.gen(function* () {
yield* Console.log(`🔨 开始构建: ${target}`)
const output = yield* Effect.tryPromise({
try: () =>
Bun.build({
entrypoints: [config.entrypoint],
compile: {
outfile: `app-${targetMap[target]}`,
target: target,
},
outdir: config.outputDir,
}),
catch: (cause: unknown) =>
new BuildError({
target,
cause,
}),
})
const paths = output.outputs.map((item: { path: string }) => item.path)
return {
target,
outputs: paths,
} satisfies BuildResult
}),
buildAll: (config: BuildConfig) =>
Effect.gen(function* () {
const effects = config.targets.map((target) =>
Effect.gen(function* () {
yield* Console.log(`🔨 开始构建: ${target}`)
const output = yield* Effect.tryPromise({
try: () =>
Bun.build({
entrypoints: [config.entrypoint],
compile: {
outfile: `app-${targetMap[target]}`,
target: target,
},
outdir: config.outputDir,
}),
catch: (cause: unknown) =>
new BuildError({
target,
cause,
}),
})
const paths = output.outputs.map(
(item: { path: string }) => item.path,
)
return {
target,
outputs: paths,
} satisfies BuildResult
}),
)
return yield* Effect.all(effects, { concurrency: 'unbounded' })
}),
})
}
/**
* 报告服务
*/
class ReporterService extends Context.Tag('ReporterService')<
ReporterService,
{
readonly printSummary: (
results: ReadonlyArray<BuildResult>,
) => Effect.Effect<void>
}
>() {
static readonly Live = Layer.succeed(ReporterService, {
printSummary: (results: ReadonlyArray<BuildResult>) =>
Effect.gen(function* () {
yield* Console.log('\n📦 构建完成:')
for (const result of results) {
yield* Console.log(` ${result.target}:`)
for (const path of result.outputs) {
yield* Console.log(` - ${path}`)
}
}
}),
})
}
// ============================================================================
// Main Program
// ============================================================================
const program = Effect.gen(function* () {
const config = yield* BuildConfigService
const fs = yield* FileSystemService
const builder = yield* BuildService
const reporter = yield* ReporterService
// 1. 清理输出目录
yield* fs.cleanDir(config.outputDir)
yield* Console.log(`✓ 已清理输出目录: ${config.outputDir}`)
// 2. 并行构建所有目标
const results = yield* builder.buildAll(config)
// 3. 输出构建摘要
yield* reporter.printSummary(results)
return results
})
// ============================================================================
// Layer Composition
// ============================================================================
const MainLayer = Layer.mergeAll(
BuildConfigService.Live,
FileSystemService.Live,
BuildService.Live,
ReporterService.Live,
)
// ============================================================================
// Runner
// ============================================================================
const runnable = program.pipe(
Effect.provide(MainLayer),
Effect.catchTags({
CleanError: (error) =>
Console.error(`❌ 清理目录失败: ${error.dir}`, error.cause),
BuildError: (error) =>
Console.error(`❌ 构建失败 [${error.target}]:`, error.cause),
ConfigError: (error) =>
Console.error(`❌ 配置错误: ${error.message}`, error.cause),
}),
Effect.tapErrorCause((cause) => Console.error('❌ 未预期的错误:', cause)),
)
Effect.runPromise(runnable).catch(() => {
process.exit(1)
})

1326
bun.lock

File diff suppressed because it is too large Load Diff

2
bunfig.toml Normal file
View File

@@ -0,0 +1,2 @@
[install]
publicHoistPattern = ["@types/*", "bun-types", "nitro*"]

View File

@@ -1,4 +1,3 @@
[tools] [tools]
node = "24"
bun = "1" bun = "1"
rust = 'latest' node = "24"

View File

@@ -1,61 +1,75 @@
{ {
"name": "fullstack-starter", "name": "@furtherverse/monorepo",
"version": "1.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": { "scripts": {
"build": "turbo build:tauri", "build": "turbo run build",
"build:compile": "bun build.ts", "compile": "turbo run compile",
"build:tauri": "tauri build", "compile:linux": "turbo run compile:linux",
"build:vite": "vite build", "compile:mac": "turbo run compile:mac",
"db:generate": "drizzle-kit generate", "compile:win": "turbo run compile:win",
"db:migrate": "drizzle-kit migrate", "dev": "turbo run dev",
"db:push": "drizzle-kit push", "dist": "turbo run dist",
"db:studio": "drizzle-kit studio", "dist:linux": "turbo run dist:linux",
"dev": "turbo dev:tauri", "dist:mac": "turbo run dist:mac",
"dev:tauri": "tauri dev", "dist:win": "turbo run dist:win",
"dev:vite": "vite dev", "fix": "turbo run fix",
"fix": "biome check --write", "typecheck": "turbo run typecheck"
"typecheck": "tsc -b"
}, },
"dependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.14",
"turbo": "^2.8.3",
"typescript": "^5.9.3"
},
"catalog": {
"@biomejs/biome": "^2.3.11",
"@orpc/client": "^1.13.4", "@orpc/client": "^1.13.4",
"@orpc/contract": "^1.13.4", "@orpc/contract": "^1.13.4",
"@orpc/openapi": "^1.13.4",
"@orpc/server": "^1.13.4", "@orpc/server": "^1.13.4",
"@orpc/tanstack-query": "^1.13.4", "@orpc/tanstack-query": "^1.13.4",
"@orpc/zod": "^1.13.4", "@orpc/zod": "^1.13.4",
"@t3-oss/env-core": "^0.13.10", "@t3-oss/env-core": "^0.13.10",
"@tanstack/react-query": "^5.90.18",
"@tanstack/react-router": "^1.151.0",
"@tanstack/react-router-ssr-query": "^1.151.0",
"@tanstack/react-start": "^1.151.0",
"@tauri-apps/api": "^2.9.1",
"drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3",
"postgres": "^3.4.8",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"zod": "^4.3.5"
},
"devDependencies": {
"@biomejs/biome": "^2.3.11",
"@effect/platform": "^0.94.1",
"@effect/schema": "^0.75.5",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/devtools-vite": "^0.4.1", "@tanstack/devtools-vite": "^0.5.1",
"@tanstack/react-devtools": "^0.9.2", "@tanstack/react-devtools": "^0.9.5",
"@tanstack/react-query-devtools": "^5.91.2", "@tanstack/react-query": "^5.90.20",
"@tanstack/react-router-devtools": "^1.151.0", "@tanstack/react-query-devtools": "^5.91.3",
"@tauri-apps/cli": "^2.9.6", "@tanstack/react-router": "^1.158.4",
"@types/bun": "^1.3.6", "@tanstack/react-router-devtools": "^1.158.4",
"@vitejs/plugin-react": "^5.1.2", "@tanstack/react-router-ssr-query": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@types/bun": "^1.3.8",
"@types/node": "^24.3.0",
"@vitejs/plugin-react": "^5.1.3",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"drizzle-kit": "^0.31.8", "drizzle-kit": "^0.31.8",
"effect": "^3.19.14", "drizzle-orm": "^0.45.1",
"nitro": "npm:nitro-nightly@latest", "drizzle-zod": "^0.8.3",
"electron": "^34.0.0",
"electron-builder": "^26.0.0",
"electron-vite": "^5.0.0",
"nitro": "npm:nitro-nightly@3.0.1-20260206-171553-bc737c0c",
"ohash": "^2.0.11",
"postgres": "^3.4.8",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"systeminformation": "^5.30.7",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tree-kill": "^1.2.2",
"turbo": "^2.7.5", "turbo": "^2.7.5",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.0-beta.8", "uuid": "^13.0.0",
"vite-tsconfig-paths": "^6.0.4" "vite": "^8.0.0-beta.13",
"vite-tsconfig-paths": "^6.1.0",
"zod": "^4.3.6"
},
"overrides": {
"@types/node": "catalog:"
} }
} }

View File

@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Base",
"compilerOptions": {
"target": "esnext",
"lib": ["ESNext"],
"module": "preserve",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
},
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Bun",
"extends": "./base.json",
"compilerOptions": {
"types": ["bun-types"]
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@furtherverse/tsconfig",
"version": "1.0.0",
"private": true,
"type": "module",
"exports": {
"./base.json": "./base.json",
"./bun.json": "./bun.json",
"./react.json": "./react.json"
}
}

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React",
"extends": "./base.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "react-jsx"
}
}

View File

@@ -0,0 +1,20 @@
{
"name": "@furtherverse/utils",
"version": "1.0.0",
"private": true,
"type": "module",
"imports": {
"#*": "./src/*"
},
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
},
"dependencies": {
"ohash": "catalog:",
"systeminformation": "catalog:"
},
"devDependencies": {
"@furtherverse/tsconfig": "workspace:*"
}
}

View File

@@ -0,0 +1,29 @@
import { hash } from 'ohash'
import si from 'systeminformation'
async function getSystemInfo() {
const [uuid, baseboard, bios, system, diskLayout, networkInterfaces] =
await Promise.all([
si.uuid(),
si.baseboard(),
si.bios(),
si.system(),
si.diskLayout(),
si.networkInterfaces(),
])
return {
uuid,
baseboard,
bios,
system,
diskLayout,
networkInterfaces,
}
}
export async function getHardwareFingerprint() {
const systemInfo = await getSystemInfo()
return hash(systemInfo)
}

View File

@@ -0,0 +1 @@
export * from './fingerprint'

10
src-tauri/.gitignore vendored
View File

@@ -1,10 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
# Tauri Sidecar
binaries/

View File

@@ -1,357 +0,0 @@
# AGENTS.md - Tauri Shell 项目开发指南
本文档为 AI 编程助手和开发者提供项目规范、构建命令和代码风格指南。
## 项目概览
- **项目类型**: Tauri v2 桌面应用(轻量级壳子)
- **后端**: Rust (Edition 2021)
- **架构**: Sidecar 模式 - Sidecar App 承载主要业务逻辑
- **设计理念**: Tauri 仅提供原生桌面能力文件对话框、系统通知等Web 逻辑全部由 Sidecar App 处理
- **开发模式**: 使用 localhost:3000需手动启动开发服务器
- **生产模式**: 自动启动 Sidecar 二进制
- **异步运行时**: Tokio
- **Rust 版本**: 1.92.0+
- **工具管理**: 使用 mise 管理 Rust 和 Tauri CLI 版本(见 `mise.toml`
## 构建、测试、运行命令
### 开发运行
```bash
# 开发模式运行 (需要先启动开发服务器)
# 终端 1: 启动前端开发服务器
bun run dev
# 终端 2: 启动 Tauri 应用
tauri dev
# 或者使用单命令并行启动(需要配置 package.json
bun run dev:tauri
```
**开发模式说明**
- 开发模式下Tauri 直接连接到 `localhost:3000`(不启动 sidecar 二进制)
- 需要手动运行 `bun run dev` 来启动开发服务器
- 支持热重载HMR无需重启 Tauri 应用
### 构建
```bash
# 开发构建 (debug mode)
cargo build
# 生产构建
cargo build --release
# Tauri 应用打包 (生成安装程序)
tauri build
```
### 代码检查
```bash
# 编译检查 (不生成二进制)
cargo check
# Clippy 代码质量检查
cargo clippy
# Clippy 严格模式 (所有警告视为错误)
cargo clippy -- -D warnings
# 代码格式化检查
cargo fmt -- --check
# 自动格式化代码
cargo fmt
```
### 测试
```bash
# 运行所有测试
cargo test
# 运行单个测试 (按名称过滤)
cargo test test_function_name
# 运行特定模块的测试
cargo test module_name::
# 显示测试输出 (包括 println!)
cargo test -- --nocapture
# 运行单个测试并显示输出
cargo test test_name -- --nocapture
```
### 清理
```bash
# 清理构建产物
cargo clean
```
## 项目结构
```
app-desktop/
├── src/
│ ├── main.rs # 入口文件 (仅调用 lib::run)
│ ├── lib.rs # 核心应用逻辑 (注册插件、命令、状态)
│ ├── commands/
│ │ └── mod.rs # 原生桌面功能命令 (文件对话框、通知等)
│ └── sidecar.rs # Sidecar 进程管理 (启动、端口扫描、清理)
├── binaries/ # Sidecar 二进制文件
│ └── app-* # Sidecar App 可执行文件 (示例: app)
├── capabilities/ # Tauri v2 权限配置
│ └── default.json
├── icons/ # 应用图标资源
├── gen/schemas/ # 自动生成的 Schema (不要手动编辑)
├── Cargo.toml # Rust 项目配置
├── tauri.conf.json # Tauri 应用配置
├── build.rs # Rust 构建脚本
└── mise.toml # 开发工具版本管理
```
## Rust 代码风格指南
### 导入 (Imports)
- 使用标准库、外部 crate、当前 crate 的顺序,用空行分隔
- 按字母顺序排列
- 优先使用具体导入而非通配符 `*`
```rust
// ✅ 推荐
use std::sync::Mutex;
use std::time::Duration;
use tauri::Manager;
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::{CommandEvent, CommandChild};
// ❌ 避免
use tauri::*;
```
### 命名规范
- **函数和变量**: `snake_case`
- **类型、结构体、枚举、Trait**: `PascalCase`
- **常量和静态变量**: `SCREAMING_SNAKE_CASE`
- **生命周期参数**: 简短小写字母,如 `'a`, `'b`
```rust
// ✅ 推荐
struct SidecarProcess(Mutex<Option<CommandChild>>);
const DEFAULT_PORT: u16 = 3000;
async fn find_available_port(start: u16) -> u16 { }
// ❌ 避免
struct sidecar_process { }
const defaultPort: u16 = 3000;
```
### 类型注解
- 函数参数必须有类型注解
- 函数返回值必须明确声明 (除非返回 `()`)
- 优先使用具体类型而非 `impl Trait` (除非必要)
- 使用 `&str` 而非 `String` 作为只读字符串参数
```rust
// ✅ 推荐
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
async fn is_port_available(port: u16) -> bool {
tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.is_ok()
}
```
### 错误处理
- 使用 `Result<T, E>` 返回可能失败的操作
- 使用 `expect()` 时提供有意义的错误消息 (中文)
- 避免 `unwrap()` 在生产代码中,除非逻辑上保证不会 panic
- 使用 `?` 操作符传播错误
- 记录关键错误信息到控制台
```rust
// ✅ 推荐
let sidecar = app_handle
.shell()
.sidecar("app")
.expect("无法找到 app sidecar");
let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败");
// 日志记录
eprintln!("✗ Sidecar App 启动失败");
println!("✓ Sidecar App 启动成功!");
// ❌ 避免
let data = read_file().unwrap(); // 无上下文信息
```
### 异步代码
- 使用 `async/await` 而非手动创建 Future
- Tauri 内部使用 `tauri::async_runtime::spawn` 启动异步任务
- 使用 Tokio 的异步 API (如 `tokio::net::TcpListener`)
- 避免阻塞异步运行时 (使用 `tokio::task::spawn_blocking`)
```rust
// ✅ 推荐
tauri::async_runtime::spawn(async move {
let port = find_available_port(3000).await;
// ...
});
```
### 格式化
- 使用 `cargo fmt` 自动格式化
- 缩进: 4 空格
- 行宽: 100 字符 (rustfmt 默认)
- 结构体和枚举的字段每行一个 (如果超过一定长度)
- 链式调用适当换行提高可读性
### 注释
- 使用中文注释说明复杂逻辑
- 代码块前添加简短说明注释
- 避免显而易见的注释
```rust
// ✅ 推荐
// 全局状态:存储 Sidecar App 进程句柄
struct SidecarProcess(Mutex<Option<CommandChild>>);
// 检查端口是否可用
async fn is_port_available(port: u16) -> bool { }
```
## Tauri 特定规范
### 模块组织
- **`lib.rs`**: 主入口,负责注册插件、命令、状态管理
- **`commands/mod.rs`**: 所有 Tauri 命令集中定义,命令必须是 `pub fn`
- **`sidecar.rs`**: Sidecar 进程管理逻辑,导出公共 API`spawn_sidecar`, `cleanup_sidecar_process`
```rust
// lib.rs - 模块声明
mod commands;
mod sidecar;
use sidecar::SidecarProcess;
// 注册命令时使用模块路径
.invoke_handler(tauri::generate_handler![commands::greet])
```
### 命令定义
- 使用 `#[tauri::command]` 宏标记命令
- 命令函数必须是公开的或在 `invoke_handler` 中注册
- 参数类型必须实现 `serde::Deserialize`
- 返回类型必须实现 `serde::Serialize`
```rust
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// 在 Builder 中注册
.invoke_handler(tauri::generate_handler![greet])
```
### 状态管理
- 使用 `app.manage()` 注册全局状态
- 状态必须实现 `Send + Sync`
- 使用 `Mutex``RwLock` 保证线程安全
```rust
struct SidecarProcess(Mutex<Option<CommandChild>>);
// 注册状态
app.manage(SidecarProcess(Mutex::new(None)));
// 访问状态
if let Some(state) = app_handle.try_state::<SidecarProcess>() {
*state.0.lock().unwrap() = Some(child);
}
```
### Sidecar 进程管理
- Sidecar 二进制必须在 `tauri.conf.json``bundle.externalBin` 中声明
- 使用 `app.shell().sidecar()` 启动 sidecar
- 在应用退出时清理子进程 (监听 `RunEvent::ExitRequested`)
```rust
// 启动 sidecar
let sidecar = app_handle
.shell()
.sidecar("app")
.expect("无法找到 app sidecar")
.env("PORT", port.to_string());
// 清理进程
match event {
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
if let Some(child) = process.take() {
let _ = child.kill();
}
}
_ => {}
}
```
## 依赖管理
-`Cargo.toml` 中明确声明依赖版本
- 使用语义化版本 (如 `"2"` 表示兼容 2.x.x)
- 仅启用需要的 feature 以减少编译时间和二进制大小
```toml
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["net"] }
```
## 开发工具
推荐安装以下 VSCode 扩展:
- `tauri-apps.tauri-vscode` - Tauri 官方支持
- `rust-lang.rust-analyzer` - Rust 语言服务器
## 最佳实践
1. **开发环境配置**:
- 开发模式下需先启动前端开发服务器(`bun run dev`),再启动 Tauri`tauri dev`
- 生产构建自动打包 sidecar 二进制,无需额外配置
2. **进程生命周期**: 始终在应用退出时清理子进程和资源
3. **端口管理**:
- 开发模式固定使用 3000 端口(与开发服务器匹配)
- 生产模式使用端口扫描避免硬编码端口冲突
4. **超时处理**: 异步操作设置合理的超时时间 (如 5 秒)
5. **日志**: 使用表情符号 (✓/✗/🔧/🚀) 和中文消息提供清晰的状态反馈
6. **错误退出**: 关键错误时调用 `std::process::exit(1)`
7. **窗口配置**: 使用 `WebviewWindowBuilder` 动态创建窗口
## 提交代码前检查清单
- [ ] `cargo fmt` 格式化通过
- [ ] `cargo clippy` 无警告
- [ ] `cargo check` 编译通过
- [ ] `cargo test` 测试通过
- [ ] 更新相关注释和文档
- [ ] 检查是否有 `unwrap()` 需要替换为 `expect()`
- [ ] 验证 Tauri 应用正常启动和退出

4773
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
[package]
name = "app-desktop"
version = "0.1.0"
description = "A Tauri App"
authors = ["imbytecat"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "app_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["net"] }

View File

@@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@@ -1,27 +0,0 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"local": true,
"remote": {
"urls": [
"http://localhost:*",
"http://127.0.0.1:*",
"http{s}?://localhost(:\\d+)?/*"
]
},
"permissions": [
"core:default",
"core:window:allow-set-title",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "binaries/app",
"sidecar": true
}
]
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,8 +0,0 @@
// 原生桌面功能命令
// 未来可能包含: 文件对话框、系统通知、剪贴板等
// 示例命令 (可根据需要删除或替换)
#[tauri::command]
pub fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

View File

@@ -1,33 +0,0 @@
use tauri::Manager;
// 模块声明
mod commands;
mod sidecar;
use sidecar::SidecarProcess;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.setup(|app| {
// 注册全局状态
app.manage(SidecarProcess(std::sync::Mutex::new(None)));
// 启动 Sidecar 进程
let app_handle = app.handle().clone();
sidecar::spawn_sidecar(app_handle);
Ok(())
})
.invoke_handler(tauri::generate_handler![commands::greet])
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| {
// 监听应用退出事件,清理 Sidecar 进程
if let tauri::RunEvent::Exit = event {
// 只在 Exit 事件时清理,避免重复执行
sidecar::cleanup_sidecar_process(app_handle);
}
});
}

View File

@@ -1,6 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_desktop_lib::run()
}

View File

@@ -1,166 +0,0 @@
use std::sync::Mutex;
use std::time::Duration;
use tauri::Manager;
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
// ===== 配置常量 =====
/// Sidecar App 启动超时时间(秒)
const STARTUP_TIMEOUT_SECS: u64 = 5;
/// 默认起始端口
const DEFAULT_PORT: u16 = 3000;
/// 端口扫描范围(从起始端口开始扫描的端口数量)
const PORT_SCAN_RANGE: u16 = 100;
/// 窗口默认宽度
const DEFAULT_WINDOW_WIDTH: f64 = 1200.0;
/// 窗口默认高度
const DEFAULT_WINDOW_HEIGHT: f64 = 800.0;
/// 窗口标题
const WINDOW_TITLE: &str = "Tauri App";
// ===== 数据结构 =====
/// 全局状态:存储 Sidecar 进程句柄
pub struct SidecarProcess(pub Mutex<Option<CommandChild>>);
// 检查端口是否可用(未被占用)
async fn is_port_available(port: u16) -> bool {
tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.is_ok()
}
// 查找可用端口
async fn find_available_port(start: u16) -> u16 {
for port in start..start + PORT_SCAN_RANGE {
if is_port_available(port).await {
return port;
}
}
start // 回退到起始端口
}
/// 启动 Sidecar 进程并创建主窗口
pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
// 检测是否为开发模式
let is_dev = cfg!(debug_assertions);
if is_dev {
// 开发模式:直接创建窗口连接到 Vite 开发服务器
println!("🔧 开发模式");
match tauri::WebviewWindowBuilder::new(
&app_handle,
"main",
tauri::WebviewUrl::External("http://localhost:3000".parse().unwrap()),
)
.title(WINDOW_TITLE)
.inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
.center()
.build()
{
Ok(_) => println!("✓ 开发窗口创建成功"),
Err(e) => {
eprintln!("✗ 窗口创建失败: {}", e);
}
}
return;
}
// 生产模式:启动 sidecar 二进制
tauri::async_runtime::spawn(async move {
println!("🚀 生产模式");
// 查找可用端口
let port = find_available_port(DEFAULT_PORT).await;
println!("使用端口: {}", port);
// 启动 sidecar
let sidecar = app_handle
.shell()
.sidecar("app")
.expect("无法找到 app")
.env("PORT", port.to_string());
let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败");
// 保存进程句柄到全局状态
if let Some(state) = app_handle.try_state::<SidecarProcess>() {
*state.0.lock().unwrap() = Some(child);
}
// 监听 stdout等待服务器就绪信号
let start_time = std::time::Instant::now();
let timeout = Duration::from_secs(STARTUP_TIMEOUT_SECS);
let mut app_ready = false;
while let Some(event) = rx.recv().await {
if let CommandEvent::Stdout(line) = event {
let output = String::from_utf8_lossy(&line);
println!("App: {}", output);
// 检测 App 启动成功的标志
if output.contains("Listening on:") || output.contains("localhost") {
app_ready = true;
println!("✓ App 启动成功!");
// 创建主窗口
let url = format!("http://localhost:{}", port);
tauri::WebviewWindowBuilder::new(
&app_handle,
"main",
tauri::WebviewUrl::External(url.parse().unwrap()),
)
.title(WINDOW_TITLE)
.inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
.center()
.build()
.expect("创建窗口失败");
break;
}
}
// 超时检查
if start_time.elapsed() > timeout {
eprintln!("✗ 启动超时: App 未能在 {} 秒内启动", STARTUP_TIMEOUT_SECS);
break;
}
}
if !app_ready {
eprintln!("✗ App 启动失败");
std::process::exit(1);
}
});
}
/// 清理 Sidecar 进程 (在应用退出时调用)
pub fn cleanup_sidecar_process(app_handle: &tauri::AppHandle) {
let is_dev = cfg!(debug_assertions);
if is_dev {
// 开发模式退出时发送异常信号exit 1让 Turbo 停止 Vite 服务器
println!("🔧 开发模式退出,终止所有依赖任务...");
std::process::exit(1);
}
// 生产模式:正常清理 sidecar 进程
println!("应用退出,正在清理 Sidecar 进程...");
if let Some(state) = app_handle.try_state::<SidecarProcess>() {
if let Ok(mut process) = state.0.lock() {
if let Some(child) = process.take() {
let _ = child.kill();
println!("✓ Sidecar 进程已终止");
}
}
}
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "app-desktop",
"version": "0.1.0",
"identifier": "com.imbytecat.app-desktop",
"app": {
"withGlobalTauri": true,
"windows": [],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["binaries/app"]
}
}

View File

@@ -1,13 +0,0 @@
import { drizzle } from 'drizzle-orm/postgres-js'
import * as schema from '@/db/schema'
import { env } from '@/env'
export function createDb() {
return drizzle({
connection: {
url: env.DATABASE_URL,
prepare: true,
},
schema,
})
}

View File

@@ -1,15 +0,0 @@
import { sql } from 'drizzle-orm'
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
export const todoTable = pgTable('todo', {
id: uuid('id').primaryKey().default(sql`uuidv7()`),
title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date()),
})

View File

@@ -1,7 +0,0 @@
import type { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
export const devtools = {
name: 'TanStack Query',
render: <ReactQueryDevtoolsPanel />,
} satisfies TanStackDevtoolsReactPlugin

View File

@@ -1 +0,0 @@
export * from './devtools'

View File

@@ -1,7 +0,0 @@
import type { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
export const devtools = {
name: 'TanStack Router',
render: <TanStackRouterDevtoolsPanel />,
} satisfies TanStackDevtoolsReactPlugin

View File

@@ -1 +0,0 @@
export * from './devtools'

View File

View File

@@ -1,53 +0,0 @@
import { createORPCClient } from '@orpc/client'
import { RPCLink } from '@orpc/client/fetch'
import { createRouterClient } from '@orpc/server'
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { router } from './router'
import type { RouterClient } from './types'
const getORPCClient = createIsomorphicFn()
.server(() =>
createRouterClient(router, {
context: () => ({
headers: getRequestHeaders(),
}),
}),
)
.client(() => {
const link = new RPCLink({
url: `${window.location.origin}/api/rpc`,
})
return createORPCClient<RouterClient>(link)
})
const client: RouterClient = getORPCClient()
export const orpc = createTanstackQueryUtils(client, {
experimental_defaults: {
todo: {
create: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
update: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
remove: {
mutationOptions: {
onSuccess: (_, __, ___, ctx) => {
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
},
},
},
},
},
})

Some files were not shown because too many files have changed in this diff Show More