HUAN 讙 · 開發者
Monorepo
HUAN 是一個 pnpm workspace。
結構
apps/
admin/ Vite + React 的後台與官網(CSR)
server/ Fastify API、WebSocket 與靜態服務
worker/ FFmpeg 轉檔 worker
device/ Electron 播放器
device-sim/ 無 GUI 的模擬裝置
docs/ Astro 靜態文件站
e2e/ Playwright 端對端測試與文件截圖
packages/
protocol/ @huan/protocol — 共用的 zod 結構與型別
db/ @huan/db — Drizzle schema、連線、migration
shared/ @huan/shared — 排程判定、退避、格式化、TOTP
layout-engine/ @huan/layout-engine — 分割樹幾何與樹狀操作
config/ @huan/config — 環境變數結構
device-core/ @huan/device-core — 裝置同步引擎
docker/ compose.yaml 與 Dockerfile
scripts/ 開發與發布腳本
為什麼是這幾個套件
每一個共用套件都有兩個以上的消費者。這是拆套件的唯一標準。
| 套件 | 消費者 |
|---|---|
@huan/protocol | Admin、Server、Worker、Device |
@huan/db | Server、Worker |
@huan/shared | Server、Admin、Device |
@huan/layout-engine | Admin、Device |
@huan/config | Server、Worker |
@huan/device-core | Device、模擬裝置 |
@huan/protocol 的存在讓「Admin 以為欄位叫 assetId、Server 以為叫 asset_id」這種問題在編譯期就會被抓到。@huan/layout-engine 的存在讓後台預覽與裝置播放不可能算出不同的版面。
建置順序
apps/* 依賴 packages/* 的建置產物(dist/),不是原始碼。
pnpm build:packages # 先建置共用套件
pnpm build # 再建置應用程式
pnpm dev、pnpm test 與 pnpm typecheck 都會自動先跑 build:packages。直接執行 pnpm --filter @huan/server dev 則不會,這是「找不到 @huan/protocol」最常見的原因。
Import 規範
絕對不要跨層級相對匯入:
import { foo } from "../../../shared/foo"; // ✗
正確做法:
import { thing } from "@/lib/thing"; // ✓ 同一個 app 內部
import { DeviceSchema } from "@huan/protocol"; // ✓ 跨套件
@/ 只用在 apps/*
別名由各自的 bundler 與 tsconfig.json 的 paths 同時設定:
| App | 解析者 |
|---|---|
admin | Vite resolve.alias |
device | electron-vite |
server、worker、device-sim | 開發時 tsx,建置時 tsup |
因此開發、型別檢查與 production build 三邊都解析得到。不會只有 IDE 看得懂。
packages/* 用相對匯入
共用套件以 tsc 直接輸出 ESM,沒有 bundler 可以改寫別名,硬加 @/ 會在 Node 執行時炸掉。
這些套件維持扁平結構,用同層的相對匯入,而且副檔名要寫 .js(NodeNext 模組解析的要求):
import { clampRatio } from "./geometry.js"; // ✓
建置方式
| 目標 | 工具 | 原因 |
|---|---|---|
packages/* | tsc | 產生真正的 .d.ts,Node 可以直接執行 |
server、worker、device-sim | tsup | 只打包自己的原始碼,npm 相依維持 external |
admin | Vite | |
device | electron-vite | main / preload / renderer 三段各自建置 |
docs | Astro | SSG,另跑 Pagefind 產生搜尋索引 |
tsup 刻意只打包應用程式自己的程式碼:@/ 別名在打包時就被解析掉,而第三方套件保持原樣,避免把有動態 require 的函式庫硬塞進單一檔案而在執行期爆炸。
TypeScript
全 workspace 開啟 strict。
- 不使用
any。需要未知型別時用unknown並在使用點收斂。 - 不用
as unknown as T繞過型別系統。 - 編譯錯誤要修,不要關掉檢查換取「能跑」。
加入新的相依套件
pnpm --filter @huan/server add some-package
加之前先想一下 ADR-0007 的精神:每多一個相依就多一個要維運、要更新、會壞掉的東西。
Device 的相依套件要額外考慮六個平台架構組合(linux-arm64、linux-x64、win-x64、win-arm64、darwin-x64、darwin-arm64)。原生 Node 模組會讓 ARM 交叉打包變得很困難,優先找純 JS、Web API 或 Electron 內建的解法。