Civitai 单仓库改造全指南:基于 pnpm Workspaces 的基础设施包抽取实践
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
本文以仓库中的 monorepo-conversion-plan.md 为核心骨架,结合当前仓库实际落地状态(
pnpm-workspace.yaml、turbo.json、next.config.mjs、各packages/*/package.json与 monorepo-directory-snapshot.md)编写。它记录了一次真实的、“先计划、后落地、再修正”的 monorepo 迁移全流程:如何在不搬动 2,500+ 文件的前提下,把全局基础设施(Prisma、Postgres 连接池、Redis、ClickHouse、Axiom、OTEL)抽成一组可被多个应用复用的 pnpm workspace 包。读完本文,你将掌握:主应用留在仓库根目录的增量式迁移策略、基础包互不依赖的规则、以工厂函数替代模块级单例的改造模式,以及如何在迁移中保住 Git 历史与 Docker/CI 缓存。
一、这是一份“历史计划”:先读懂它,再看它如何被修正
文档开篇的说明非常关键——它是一份已被后续实施修正过的历史计划。有两处与原计划不同:
- Prisma 契约被单独拆分:原计划把 Prisma schema、生成的 client、
enums.ts/models.ts全部放进@civitai/db;实际落地时,它们落入了独立的@civitai/db-schema包(并引入了prisma-kysely生成器),而@civitai/db只保留运行时客户端工厂并依赖 db-schema。这一“契约与运行时分离”的设计在 monorepo-directory-snapshot.md 中被明确为:类型消费者只依赖@civitai/db-schema,需要活连接的消费者才依赖@civitai/db。 - Turborepo 后来被采用:计划里写了“不用 Turborepo”,但当前仓库根目录已存在 turbo.json,定义了
build/typecheck/lint/test/dev任务图。这与package.json中的turbo: ^2.9.17devDependency 相互印证。
阅读本文时,请始终带着“计划 vs 现实”的双重视角:原计划的价值在于决策逻辑(为什么、怎么做、避开什么坑),当前仓库则是验证这些决策的活样本。
二、改造目标:把“全局基础设施”抽成共享包
本仓库原本是一个单体 Next.js 应用(主应用位于仓库根目录,源码在src/下,通过~/server/...这类路径别名互相引用)。随着未来卫星应用(如 moderator 应用)的出现,需要一种方式让多个应用共享底层基础设施,而不是各自复制一份。
改造目标非常聚焦:
- 把全局使用的基础设施——Prisma schema/client、Postgres 连接池、Redis、ClickHouse、Axiom 日志——抽取为共享 workspace 包;
- 未来的新应用(moderator 应用,见 moderator-app-shared-modules.md)改为消费这些包,不再从
~/server/...导入; - 该方案取代了早先的 git submodule 提案:原本打算做成 submodule 的东西现在变成 workspace 包,且包集合收窄到纯基础设施(db、redis、clickhouse、axiom、telemetry)。
关键点是“只抽基础设施,不抽领域代码”。Civitai 特有的领域常量——如browsingLevel.constants.ts(DB 编码的 NSFW 位标志解释)、air.ts(AIR 标识符格式)、flags.ts(位运算辅助)——留在主应用。它们是领域约定,不是基础设施;抽取它们要推迟到卫星应用真正需要时再说。
三、为什么选 pnpm Workspaces(而不是 Turborepo、Nx)
原计划的工具选型理由,在当时的约束下是完全成立的:
- 包管理器已是 pnpm:根 package.json 声明
"packageManager": "pnpm@10.28.1",且有"preinstall": "npx only-allow pnpm"强制全团队统一使用 pnpm; - 工作区是一行配置:在根
package.json或pnpm-workspace.yaml声明即可,无需引入新的编排层; - 初期无需构建编排:Next.js 通过
transpilePackages透明地编译 workspace 包,不需要每个包先tsc --build产出.js; - Turborepo 可以后置:如果 CI 构建时间成为问题,再叠加 Turborepo 做缓存也不迟——它不需要在第一天就上。
从当前仓库看,第 4 点的“后置叠加”确实发生了:turbo.json 已存在,且package.json中出现了大量pnpm --filter @civitai/xxx dev脚本(如dev:auth、dev:moderator、dev:creator-studio、dev:training-studio、dev:storage),说明 workspace 已从“主应用 + 若干包”扩展为“多应用 + 多包”的真实格局,此时任务编排工具的价值才显现出来。
四、基础包规则(Base-package Rule):互不依赖、纯基础设施
这是整个架构最重要的两条纪律,原文档明确写出,落地后依然有效:
规则一:基础包之间不允许互相导入。每个基础包(db、redis、clickhouse、axiom、telemetry)都是自包含的——只允许外部依赖,不允许依赖某个@civitai/*兄弟基础包。更高层包(如未来的civitai-moderator-common)可以组合多个基础包,但基础包本身保持独立。如果两个基础包需要共享常量,各自维护一份拷贝,或通过子路径导出(如@civitai/redis/keys)。
这条规则的收益:
- 每个基础包可以独立使用(一个纯文本工具应用可以只依赖
@civitai/db,不必拉进 ClickHouse 或 Axiom); - 避免低层包之间形成脆弱的依赖图。
规则二:基础包只装基础设施。Civitai 特有的领域常量留在主应用。当前仓库已用 ESLint 规则强制这条边界——monorepo-directory-snapshot.md 明确提到“package dependency-boundary enforcement(一条 ESLint 规则阻止基础包互相导入或导入主应用)”。
五、布局决策:主应用留在根目录,不对称是特性
原计划给出了目标布局:
/ ├── package.json # root: pnpm workspace config + main app deps ├── pnpm-workspace.yaml # new ├── next.config.mjs # main app config (unchanged) ├── src/ # main app source (unchanged) ├── prisma/ # MOVES to packages/civitai-db ├── packages/ │ ├── civitai-db/ │ ├── civitai-redis/ │ ├── civitai-clickhouse/ │ ├── civitai-axiom/ │ └── civitai-telemetry/ # OTEL helpers (withSpan, etc.) — auto-instrumentation stays per-app └── apps/ └── moderator/ # added later为什么主应用留在根目录,而不是搬进apps/main/?把 2,500+ 个文件挪进apps/main/会触发 monorepo-split-overview.md 早就否掉的“灾难”。主应用留在根目录意味着:
~/路径导入永不改变;- 新应用进
apps/,共享代码进packages/; - 这种不对称本身就是特性——它让改造可以增量进行。
如果未来对称性变得重要(比如第三个应用出现,让“根目录即应用”显得奇怪),git mv src/ apps/main/src/可以变成工作区就绪后的一次性清理(即下文 Phase 6)。
当前仓库正是这一决策的活证据:pnpm-workspace.yaml 将根目录本身列为 workspace 成员(packages: ['.', 'packages/*', 'apps/*']),主应用源码仍在根src/,而apps/下已有auth、moderator、creator-studio、training-studio、notifications、orchestrator-gateway、storage、event-engine等多个卫星应用。
六、工作区引导(Phase 0):让 workspace 先“活”起来
Phase 0 的目标只有一个:workspace 建立起来,但还没有任何包被引用。四个步骤:
1. 新增pnpm-workspace.yaml:
packages: - '.' - 'packages/*' - 'apps/*'注意第一行'.'——根目录本身是 workspace 成员,这正是“主应用留在根目录”的技术前提。pnpm 官方文档支持根成员,且没有任何功能代价。
2. 根package.json增加"workspaces": ["packages/*", "apps/*"](仅作信息提示;pnpm 实际以 yaml 为准)。
3.next.config.mjs增加transpilePackages: ['@civitai/*'],让 Next.js 按需编译 workspace 包——这样不需要为每个包单独配置构建步骤。当前仓库的 next.config.mjs 已把transpilePackages扩展为一个长列表:@civitai/db-schema、@civitai/db、@civitai/db-queries、@civitai/shared、@civitai/buzz、@civitai/redis、@civitai/clickhouse、@civitai/axiom、@civitai/flipt、@civitai/telemetry、@civitai/auth、@civitai/notifications、@civitai/moderation等,与“包集合已扩大”的现状一致。
4. 创建空包目录与最小package.json(name: "@civitai/db"、version: "0.0.0"、main: "src/index.ts")。
5. 验证pnpm install成功、pnpm run typecheck通过。
Checkpoint:workspace 存活,但还没有任何代码导入它。
七、包抽取顺序:按依赖序逐步推进
包必须按依赖顺序移动,每个阶段结束时pnpm install、pnpm run typecheck、pnpm run build三项全部绿灯。
Phase 1:@civitai/db(Postgres —— schema、client、连接池)
把 Postgres 相关的一切收进一个包:Prisma schema、生成的 client、migrations、programmability 脚本、pg 连接池与辅助函数。未来若有第二个数据库 schema(如 analytics),应新建独立包(如myapp-db)——这个包是 Civitai 的权威 schema。
迁移清单:
prisma/schema.full.prisma→packages/civitai-db/prisma/schema.full.prismaprisma/migrations/→packages/civitai-db/prisma/migrations/prisma/programmability/→packages/civitai-db/prisma/programmability/prisma/seed.ts→packages/civitai-db/prisma/seed.tssrc/server/db/client.ts→packages/civitai-db/src/client.ts(Prisma client 包装)src/server/db/db-helpers.ts→packages/civitai-db/src/db-helpers.ts(553 行——pg 连接池、cancellableQuery、prom-client histogram 注册)src/server/db/pgDb.ts、notifDb.ts、datapacketDb.ts、db-lag-helpers.ts→packages/civitai-db/src/src/shared/utils/prisma/enums.ts→packages/civitai-db/src/enums.ts(Prisma 生成)src/shared/utils/prisma/models.ts→packages/civitai-db/src/models.ts(Prisma 生成)
Prisma client 输出目录:在 schema 中添加:
generator client { provider = "prisma-client-js" output = "../generated/client" }client 现在活在包内部,不再位于根node_modules/.prisma/client。所有应用都从@civitai/db/client导入。
包导出(package.json的exports字段):
{ "exports": { ".": "./src/index.ts", "./client": "./generated/client/index.js", "./enums": "./src/enums.ts" } }对照当前仓库,packages/civitai-db/package.json 的实际 exports 是".": "./src/index.ts"与"./kysely": "./src/kysely.ts"——./client、./enums被收敛进 db-schema 的导出面(packages/civitai-db-schema/package.json 的 exports 含./enums、./models、./kysely),这正是文档开头“实际落地不同”的第二处体现。
更新根脚本:db:generate改为在包内运行;db:migrate更新迁移脚本中的路径。当前仓库的 package.json 中db:generate是node scripts/generate-slim-schema.js && prisma generate --no-hints,prisma字段的 schema 路径已指向packages/civitai-db-schema/prisma/schema.prisma,印证了落地后的路径变更。
为 monorepo 重构:工厂函数替代模块级单例。现有代码用模块级单例直接读env:
// today import { env } from '~/env/server'; const instanceUrlMap = { primary: env.DATABASE_URL, ... }; export const dbWrite = createClient('primary');对要被 N 个应用消费的包,需要暴露工厂:
// packages/civitai-db/src/index.ts export function createDbClients(config: { databaseUrl: string; databaseReplicaUrl?: string; notificationDbUrl: string; notificationDbReplicaUrl?: string; datapacketReplicaUrl?: string; serviceLabel: string; // for prom-client labels — distinguishes apps }): { dbWrite: PrismaClient; dbRead: PrismaClient; pgDb: AugmentedPool; notifDb: AugmentedPool; // ... }主应用保留一个薄包装在src/server/db/client.ts:
import { createDbClients } from '@civitai/db'; import { env } from '~/env/server'; const clients = createDbClients({ databaseUrl: env.DATABASE_URL, // ... serviceLabel: 'civitai-app', }); export const { dbWrite, dbRead, pgDb, notifDb } = clients;主应用的调用点不用改——它们仍然import { dbWrite } from '~/server/db/client',只是实现搬走了。这就是“shim 薄包装”策略:迁移对外部代码是零 diff的。
三个已知坑(Gotchas):
- 包内循环依赖:
db-helpers.ts:7从~/server/db/client导入dbWrite——文件一搬进包,这就变成包内循环依赖。解法:把dbWrite以参数传给需要的 helper,或重构client.ts与db-helpers.ts使二者互不导入; - prom-client Histogram 注册的 HMR 兜底:
db-helpers.ts:30-35的 try/catch 回退模式要保留——它同时处理了测试期间多个应用同进程共存的情况; Prisma.Sql类型来源:来自@prisma/client,而它生活在这个包里。运行时与类型相同,只是模块路径变了。
Phase 2:@civitai/redis
迁移清单:
src/server/redis/client.ts(1,105 行——clients + helpers)src/server/redis/caches.ts(1,571 行——缓存 key 常量、TTL、createCachedObject基础设施)src/server/redis/queues.ts、entity-metric.redis.ts、entity-metric-populate.ts、resource-data.redis.ts、fail-open-log.tssrc/utils/cache-helpers.ts(若确为纯 helpers,需先验证)
工厂模式:
export function createRedisClients(config: { redisUrl: string; sysRedisUrl: string; failOpenLogger?: (event: object) => void; }): { redis: RedisClient; sysRedis: RedisClient; // ... }一个重要的解耦细节:fail-open-log.ts原本直接用logToAxiom,现在改为通过 config 注入 logger 函数——这样 redis 包就不会硬依赖@civitai/axiom,符合“基础包互不依赖”的规则。
缓存 key 常量走子路径导出:key 字符串与 TTL 从caches.ts抽出到keys.ts,并通过子路径导出@civitai/redis/keys。只想拿 key 的消费者(比如一个只想失效缓存、不想实例化 redis client 的脚本)直接import '@civitai/redis/keys',tree-shaking 会把 redis client 从它的 bundle 中剔除。当前仓库中 packages/civitai-redis/package.json 的依赖只有redis、lodash-es、msgpackr、slugify、zod、lru-cache,没有兄弟@civitai/*包——规则一在落地中得到了遵守。
Phase 3:@civitai/clickhouse
迁移:src/server/clickhouse/client.ts(733 行)。
与@civitai/db相同的工厂模式。ClickHouse client 更简单——单个 URL,无副本路由:
export function createClickhouseClient(config: { url: string; database: string; username: string; password: string; }): ClickHouseClient;如果 ClickHouse 查询 helper 存在于 services 层(如事件追踪 helper),留在应用内——只迁移连接层。packages/civitai-clickhouse/package.json 证实依赖仅为@clickhouse/client、dayjs、zod。
Phase 4:@civitai/axiom
迁移:src/server/logging/client.ts(58 行——axiom、safeError、logToAxiom)。
最小、最简单的包,几乎可以做成单文件包,但它作为清晰的依赖边界仍有价值:
export function createAxiomLogger(config: { token?: string; orgId?: string; datastream?: string; podName?: string; echoToStderr?: boolean; }): { logToAxiom, safeError };Phase 5:@civitai/telemetry
OTEL 是必须被拆开的两个东西:
- 自动插桩注册——
src/instrumentation.node.ts调用sdk.start(),在进程加载时 patch Prisma/Redis/HTTP。这部分必须留在每个应用内(每个应用有自己的instrumentation.node.ts和 service name)。搬进包里会丢失自动加载行为; - Helper 函数——
withSpan、span attribute helpers、src/utils/otel-helpers.ts的工具。这些搬进@civitai/telemetry。
包还可以导出一个bootstrapOtel(config)函数,做今天instrumentation.node.ts做的事,让每个应用的该文件变成三行:
// apps/moderator/src/instrumentation.node.ts import { bootstrapOtel } from '@civitai/telemetry/node'; bootstrapOtel({ serviceName: 'civitai-moderator' });prom-client指标注册(src/server/prom/client.ts)遵循同样模式——helper 进包,注册调用留在应用。
当前仓库 packages/civitai-telemetry/src 下有index.ts、client.ts、otel-helpers.ts、otel-logs.ts及测试目录,依赖为@opentelemetry/*与prom-client;monorepo-directory-snapshot.md 也确认了index.ts(导出withSpan+ helpers,浏览器安全子集)、node.ts(导出bootstrapOtel(config))、prom.ts的结构。主应用侧src/instrumentation.node.ts收缩为约 3 行调用bootstrapOtel({ serviceName: 'civitai-app' })。
八、横切关注点(Cross-cutting Concerns)
1. env 校验
当前在src/env/server.ts(T3 风格 zod 校验)。两个选项:
- 每应用自校验 env:每个应用校验自己的 env,包通过工厂接收解析好的配置——推荐,且与工厂模式天然对齐;
- 共享 env 包:
@civitai/env导出 zod schema——应用需要不同子集时会很脆。
结论:采用 per-app env。包绝不直接 importenv。
2. 迁移工具链
prisma/migrations/移到packages/civitai-db/prisma/migrations/。scripts/下的迁移脚本(如 prisma-migrate-with-views-workaround.mjs)更新路径。按 CLAUDE.md 约定的手动应用迁移惯例不变——这解释了根package.json中db:migrate使用该 workaround 脚本、db:deploy追加-p标志的写法。
3. Prisma 客户端版本钉扎
@prisma/client与prisma(CLI)移入packages/civitai-db/package.json。主应用不再直接声明它们,而是依赖@civitai/db,由它依赖@prisma/client——整个 workspace 只有一个 Prisma 版本。
4. CI
- 工作区根一次
pnpm install安装全部; pnpm -r run typecheck类型检查所有包与应用;pnpm run build(主应用内)仍然产出 Next standalone bundle;- 现有 CI workflow 大体存活——主入口点未变;
- 为
pr-check.yml增加paths:过滤器:apps/moderator/内的变更不应触发主应用构建(反之亦然);否则每个 PR 都会全量重建。
5. Docker
对现有Dockerfile有两处具体改动:
(1)工作区感知的安装层。在pnpm install之前,除了根package.json,还要复制pnpm-workspace.yaml与 workspace 内每一个package.json(根 +packages/*+apps/*)。这保住了安装层缓存的技巧——lockfile + package.json 很少变化,安装层保持温热:
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY packages/*/package.json ./packages/ COPY apps/*/package.json ./apps/ # only after satellites exist RUN pnpm install --frozen-lockfile(2)更新 Prisma schema 路径。当前 Dockerfile 有两行 COPY 分别钉住prisma/schema.full.prisma和prisma/schema.prisma以做层缓存——都更新为packages/civitai-db/prisma/...。
对于卫星应用未来的apps/moderator/Dockerfile,使用pnpm deploy --filter <app> --prod /tmp/out产出一个只含该应用传递依赖的精简部署包,该 bundle 成为 runner 阶段的 COPY 源。这是 pnpm 官方认可的 monorepo Docker 镜像模式。
6. Next.jsoutput: 'standalone'与 workspace 包
主应用的生产运行时依赖.next/standalone(Dockerfile 第 61 行;next.config.mjs 中output: 'standalone'仍然存在)。standalone 模式用nft(Node File Trace)决定打包内容。面对 workspace 包,二选一:
transpilePackages: ['@civitai/*']——Next 把包源码内联进构建输出。最简单,已在 Phase 0 计划中;outputFileTracingRoot: path.join(__dirname, '../..')——告诉nft跟随 symlink 到 workspace 根。仅当包自己有tsc --build产出预编译.js时才需要。
选择方案 1。方案 2 只在将来希望每个包有构建产物时才相关(发布、用缓存加速冷构建)。
7. 明确不需要的工具
- Turborepo / Nx:在“很多包 × 很多应用 × 慢 CI”时才值得。对 6 个包 + 2 个应用,裸 pnpm 更简单。CI 构建时间成为真问题时再加。(注:如前所述,当前仓库后来确实加了 Turborepo——计划的判断是“可以后置”,而不是“永远不要”。)
- Changesets / lerna:只服务于对外发布的包。
workspace:*已处理内部版本管理。 - TypeScript project references(tsconfig 的
references):Next +transpilePackages透明处理跨包编译。 - 独立 publish/registry 配置:包保持内部使用。
8. 现有分支的迁移
最好在**冻结周(freeze week)**做,此时活跃分支最少。每个未合分支都需要一次 rebase 以吸收包边界。Phase 1 的 shim 再导出技巧能显著缓解冲击:没被改动过的分支依然能编译,因为旧导入路径继续有效。
9. Git 历史保留:每个文件三提交模式
Git隐式跟踪重命名——读取时通过逐提交比较“删除 vs 新增”的文件内容推断,默认 50% 相似度阈值。为了让git log --follow和git blame在移动后继续工作,每个文件遵循以下模式:
- 纯移动:
git mv src/server/db/db-helpers.ts packages/civitai-db/src/db-helpers.ts——不做任何内容改动,Git 无歧义识别重命名; - 重构:此时再改 import、把
env换成工厂配置、解开循环依赖。diff 小,blame 归属正确; - 加 shim(如适用):在旧路径创建一行再导出。它是真正的新文件——不需要历史,因为原内容的历史在包路径那边。
团队应掌握的工具:
git log --follow <path>——跨重命名显示完整历史;裸git log <path>不会;git blame——自动跟随简单重命名;git blame -C还能检测从其他文件复制的内容(对文件拆分有用);- GitHub Web UI——"View blame" 与文件历史无需 flag 即可跟随重命名。
要避免的坑:
- 移动 + 大幅编辑合并进同一提交:内容相似度跌破 50% 时,Git 会漏掉重命名,历史变成“可找到但不可跟随”;
- squash-merge 迁移 PR:把三提交模式压成一个大 diff,提高重命名检测失败的概率。迁移 PR 用rebase-merge 或 merge commit;
- 文件拆分:如果
db-helpers.ts在移动中被拆成多个文件,Git 只会对内容重叠最高的文件自动检测重命名。优先先移动后拆分(先 move,再在后续提交 split),而不是先拆分后移动。
九、决策记录(Decisions)
原文档记录了五条已拍板的决策,值得完整保留:
- 包命名:
@civitai/scope。所有包使用@civitai/前缀。保留 npm org 名(即使永不公开发布,也能让未来发布变得平凡,并避免命名冲突)。当前仓库所有packages/*/package.json的 name 均为@civitai/*,此决策已执行; - 单一
@civitai/datavs 四个窄包(db/redis/clickhouse/axiom)?拍板:四个窄包。未来的纯文本工具应用可以只依赖@civitai/db,不必拉进 ClickHouse 或 Axiom; - schema-common 范围。是否包含 moderator 页面使用的
~/server/schema/*.schema.tszod 文件?拍板:推迟。Phase 1–5 的基础设施包不导入任何 zod schema——它们只需要 Prisma 类型。任何~/server/schema/*.schema.ts抽取推迟到 moderator 应用阶段,届时才知道卫星到底需要哪些 schema(从 moderator 依赖分析看,候选是report.schema、strike.schema、image.schema、scanner-review.schema,见 moderator-app-shared-modules.md); - 再导出 shim(Phase 1 过渡):永久保留(drift-tolerant)。意思是旧文件(如
src/server/db/client.ts)保留为一行再导出文件:export * from '@civitai/db'(或用主应用 env 调用createDbClients的薄包装)。现有~/server/db/client导入继续编译。Drift-tolerant = 永久保留 shim,绝不批量重写主应用约 2,500 个调用点。代价是同一代码有两个合法导入路径(评审时有些约定噪音);收益是零强制 churn。当前仓库中src/server/db/client.ts、db-helpers.ts、pgDb.ts、notifDb.ts、datapacketDb.ts、db-lag-helpers.ts及 redis/clickhouse/logging 对应文件都保留了 shim,monorepo-directory-snapshot.md 的结构树明确标注了这些 SHIM; - 主应用最终是否移入
apps/main/?拍板:无限期跳过,避免冻结周成本。新应用进apps/,主应用永久留在根目录。仅当第三个应用或强理由出现时再重议。packages: ['.', 'packages/*', 'apps/*']完全支持根成员;不对称布局无功能代价,唯一摩擦是将来第三个应用加入时的轻微审美不一致。
十、Phase 6(可选,不计划):主应用移入apps/main/
当前无限期跳过,理由同上(冻结周成本)。不对称布局(主应用在根 + 卫星在apps/)是规划的永久状态。
如果未来触发点出现(第三个应用、强烈的一致性偏好、会因不对称而崩溃的工具链),迁移步骤为:
git mv src/ apps/main/src/git mv next.config.mjs apps/main/git mvprisma 相关根脚本 →apps/main/(只搬应用专属的,不搬 workspace 级的)- 根
package.json改为纯 workspace(不再含应用依赖) pnpm-workspace.yaml从 packages 列表中去掉根- 更新以根为目标的 CI workflow
- 需要冻结周,无活跃功能分支
monorepo-directory-snapshot.md 将其称为“Phase 7(可选最后一步)”,并补充了纯 workspace shell 的未来形态(根目录仅保留package.json、pnpm-workspace.yaml、pnpm-lock.yaml、tsconfig.base.json),同时确认该步骤继续推迟,直到卫星应用存在并证明 workspace 布局稳定。
十一、阶段总结表
| Phase | Package | Effort | Risk |
|---|---|---|---|
| 0 | Workspace bootstrap | Low | Low — just config |
| 1 | @civitai/db | High | Medium — Prisma client path change is the trickiest single step; circular dep indb-helpers.tsto untangle |
| 2 | @civitai/redis | Medium | Low — lots of files, mostly mechanical |
| 3 | @civitai/clickhouse | Low | Low |
| 4 | @civitai/axiom | Low | Low |
| 5 | @civitai/telemetry | Medium | Medium — splitting auto-instrumentation from helpers requires care |
| 6 | Move main app toapps/main/ | — | Not planned— kept at root indefinitely to avoid freeze-week cost |
Phase 5 完成后,apps/moderator作为 workspace 成员消费这些包的基础就绪了——那是另一份独立计划(见 moderator-app-shared-modules.md)。
十二、落地验证:计划 vs 当前仓库对照
把计划与仓库现状逐条核对,可以确认这套方法论的可执行性:
- workspace 配置:pnpm-workspace.yaml 内容与 Phase 0 完全一致(
'.'+packages/*+apps/*); - 包命名与数量:
packages/下确有civitai-db-schema、civitai-db、civitai-redis、civitai-clickhouse、civitai-axiom、civitai-telemetry等窄包,且基础包之间无@civitai/*互依赖(对照各自 package.json 可验证); - 契约/运行时分离:
@civitai/db-schema持有 Prisma schema、migrations、生成的 enums/models/kysely 类型;@civitai/db只提供运行时工厂并依赖@civitai/db-schema: workspace:*; - transpilePackages:next.config.mjs 的
transpilePackages列表覆盖全部 workspace 包,且serverExternalPackages中保留了@prisma/client、@prisma/instrumentation、redis、OTEL 等原生/插桩依赖,避免运行时出现“双份 Prisma runtime”导致的instanceof Sql失败(配置注释里记录了operator does not exist: integer = jsonb这个真实事故); - Turborepo 后置:turbo.json 已按“计划允许后置”的方式加入,
package.json提供pnpm --filter @civitai/xxx系列脚本编排多应用; - 主应用留在根:根
src/、根next.config.mjs、根package.json(含全部主应用依赖)原样保留,apps/已容纳多个卫星应用。
结语
这份 monorepo 改造计划的真正价值,不在于“要不要用 Turborepo”这类单一答案,而在于它提供了一套可以增量执行、且每一步都可验证的迁移方法论:主应用留在根目录保护 2,500+ 文件的导入路径不动;工厂函数替代模块级单例让包可被任意数量的应用消费;shim 再导出实现零强制 churn 的过渡;三提交模式保住 Git 历史;Docker/CI 的缓存技巧让成本可控。当前仓库 monorepo-directory-snapshot.md 证明这套计划已经走完核心阶段并落地成型——对任何打算把大型单体 Next.js 应用改造成 pnpm workspace monorepo 的团队,这是一份罕见的、有完整“事后对照”的实战蓝本。
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考