Skip to content

单体仓架构

回答的问题:为什么四个站在同一个仓、同一份依赖里;边界靠什么维持;改一处会波及什么。

1. 形状

natlan-web/
├── package.json          唯一的包清单:一套依赖、一份 node_modules
├── ui/                   共享组件库(普通目录,非依赖包)
├── apps/
│   ├── control/          控制台      console.natlan.io
│   ├── peering/          对等门户    peering.natlan.io
│   ├── auth/             OAuth 授权界面  auth.natlan.io
│   └── home/             品牌首页 + fleet 岛  natlan.io
├── scripts/              仓级工具(i18n 完整性检查、Cap 重新 vendor)
└── docs/                 本文档体系

apps/* 目录下没有 package.json。SvelteKit 不需要它,配置内联在各自的 vite.config.ts 里;构建脚本因此是 cd apps/X && vite build 的形式。

2. 依赖方向

只有一个方向是允许的:

apps/control ─┐
apps/peering ─┼─→ ui/  →  node_modules
apps/auth    ─┤
apps/home    ─┘

ui/ 不得反向依赖任何 app:库里不能出现 $lib/...$app/...。组件需要宿主 能力时走可选 props 或 setUiHost() 注入,而不是 import。

单包结构没有任何机制强制这条边界(多包结构可以靠 exports 挡住),因此它是约定, 靠 review 与 npm run check 时的解析失败兜底。约定的完整表述见 reference/ui/contracts.md

3. 为什么合仓

合仓前是四个独立仓,共享代码靠手工同步:

  • 首页的 fleet 地图是控制台 FleetMap.svelte同级目录相对引用,另配四个 shim 桩掉 $lib/apitoasturlstate$app/environment,再写 CSS 把用不到的按钮藏起来;
  • 授权界面与控制台各存一份逐字节相同的 vendored Cap 组件,改一处要同步两处;
  • 门户维护着控制台设计令牌的一份 trimmed 拷贝,缺了整套 --c-data-* 与状态色阶, 库组件搬过去会渲染成无色。

合仓把这三笔账一次结清:地图、验证码、设计令牌各只剩一份。

4. 为什么不用 npm workspaces

试过并拆掉了。workspaces 的核心价值是独立版本化与独立发布,这里一个都用不上—— 四个站从同一个仓、同一次构建出去,ui/ 永远不会发到 registry。剩下的全是代价:

  • 四份 package.json 的 svelte / vite / bits-ui 版本要手动对齐,否则装出两份实例;
  • 要维护 exports 子路径映射;
  • bits-ui 的 context(如 Tooltip.Provider)跨副本失效,得靠 dedupe: ['svelte', 'bits-ui'] 补洞——首页的 vite 配置里原本就有这一行, 单包结构下这个洞不存在,那行随之删除。

换来的是没有强制边界(见第 2 节)。这是明知的取舍。

5. 四站的技术形态并不统一,这是有意的

框架路由产物托管
controlSvelteKit + adapter-static客户端路由,ssr=falsebuild/Cloudflare Worker(薄壳,盖 CSP nonce)
peeringSvelteKit + adapter-static客户端路由,ssr=falsebuild/Cloudflare Worker(纯静态资产)
auth朴素 Vite + Svelte单入口 /authorize,无路由dist/源站 nginx 同源静态
home手写 HTML + Worker 脚本仓内文件直接打包Cloudflare Worker(自带 fetch 处理)

差异的理由逐条落在各自的参考文档里:

  • 授权页不用 SvelteKit:只有一个入口、没有路由也没有 SSR,套 SvelteKit 只会多一层 adapter 和 .svelte-kit 产物。$ui 别名因此在它的 vite.config.ts 里自行声明 (另外两站由 sveltekit({ alias }) 注入)。
  • 授权页不上 Worker:见 auth-hosting.md
  • 控制台需要 Worker:见 reference/csp.md

6. 改一处会波及什么

改动自动重建的站需要手工介入的
apps/control/*control
apps/peering/*peering
apps/home/*home若首页快照过期,另需 node bake.js
apps/auth/*全部手工:构建 + 投放到源站
ui/*control、peering、home授权页要手工重建投放

自动重建由 Cloudflare Workers Builds 的 Build watch paths 驱动(每个项目配 apps/<自己>/* + ui/*)。授权页不在 CF 上,因此不受 watch paths 覆盖——这是它 最容易被遗忘的一点,参见 guides/deploy-auth.md

7. 共享的边界画在哪里

ui/ 收的是展示层:由 data prop 驱动、自己不取数、不认识 API、不做轮询。 因此同一个看板既能挂在控制台,也能挂在只读的门户上。

写操作一律做成可选 prop,不传即不渲染。例如 FleetMaponRequestSnapshot: 控制台传真函数,门户和首页不传,按钮根本不进产物——不是渲染出来再拒绝。这是同一个 组件能在一处可写、在另外两处只读的全部机制,也是首页产物体积下降 47% 的来源。

取数、轮询、鉴权、URL 状态、toast 全部留在 app 层。各 app 的这层实现约定见 interaction-contracts.md