news 2026/9/18 4:51:20

Civitai 单仓库改造全指南:基于 pnpm Workspaces 的基础设施包抽取实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Civitai 单仓库改造全指南:基于 pnpm Workspaces 的基础设施包抽取实践

Civitai 单仓库改造全指南:基于 pnpm Workspaces 的基础设施包抽取实践

【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai

本文以仓库中的 monorepo-conversion-plan.md 为核心骨架,结合当前仓库实际落地状态(pnpm-workspace.yamlturbo.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 缓存。


一、这是一份“历史计划”:先读懂它,再看它如何被修正

文档开篇的说明非常关键——它是一份已被后续实施修正过的历史计划。有两处与原计划不同:

  1. 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
  2. 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)

原计划的工具选型理由,在当时的约束下是完全成立的:

  1. 包管理器已是 pnpm:根 package.json 声明"packageManager": "pnpm@10.28.1",且有"preinstall": "npx only-allow pnpm"强制全团队统一使用 pnpm;
  2. 工作区是一行配置:在根package.jsonpnpm-workspace.yaml声明即可,无需引入新的编排层;
  3. 初期无需构建编排:Next.js 通过transpilePackages透明地编译 workspace 包,不需要每个包先tsc --build产出.js
  4. Turborepo 可以后置:如果 CI 构建时间成为问题,再叠加 Turborepo 做缓存也不迟——它不需要在第一天就上。

从当前仓库看,第 4 点的“后置叠加”确实发生了:turbo.json 已存在,且package.json中出现了大量pnpm --filter @civitai/xxx dev脚本(如dev:authdev:moderatordev:creator-studiodev:training-studiodev: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/下已有authmoderatorcreator-studiotraining-studionotificationsorchestrator-gatewaystorageevent-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.jsonname: "@civitai/db"version: "0.0.0"main: "src/index.ts")。

5. 验证pnpm install成功、pnpm run typecheck通过。

Checkpoint:workspace 存活,但还没有任何代码导入它。

七、包抽取顺序:按依赖序逐步推进

包必须按依赖顺序移动,每个阶段结束时pnpm installpnpm run typecheckpnpm 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.prismapackages/civitai-db/prisma/schema.full.prisma
  • prisma/migrations/packages/civitai-db/prisma/migrations/
  • prisma/programmability/packages/civitai-db/prisma/programmability/
  • prisma/seed.tspackages/civitai-db/prisma/seed.ts
  • src/server/db/client.tspackages/civitai-db/src/client.ts(Prisma client 包装)
  • src/server/db/db-helpers.tspackages/civitai-db/src/db-helpers.ts(553 行——pg 连接池、cancellableQuery、prom-client histogram 注册)
  • src/server/db/pgDb.tsnotifDb.tsdatapacketDb.tsdb-lag-helpers.tspackages/civitai-db/src/
  • src/shared/utils/prisma/enums.tspackages/civitai-db/src/enums.ts(Prisma 生成)
  • src/shared/utils/prisma/models.tspackages/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.jsonexports字段):

{ "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:generatenode scripts/generate-slim-schema.js && prisma generate --no-hintsprisma字段的 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):

  1. 包内循环依赖db-helpers.ts:7~/server/db/client导入dbWrite——文件一搬进包,这就变成包内循环依赖。解法:把dbWrite以参数传给需要的 helper,或重构client.tsdb-helpers.ts使二者互不导入;
  2. prom-client Histogram 注册的 HMR 兜底db-helpers.ts:30-35的 try/catch 回退模式要保留——它同时处理了测试期间多个应用同进程共存的情况;
  3. 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.tsentity-metric.redis.tsentity-metric-populate.tsresource-data.redis.tsfail-open-log.ts
  • src/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 的依赖只有redislodash-esmsgpackrslugifyzodlru-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/clientdayjszod

Phase 4:@civitai/axiom

迁移:src/server/logging/client.ts(58 行——axiomsafeErrorlogToAxiom)。

最小、最简单的包,几乎可以做成单文件包,但它作为清晰的依赖边界仍有价值:

export function createAxiomLogger(config: { token?: string; orgId?: string; datastream?: string; podName?: string; echoToStderr?: boolean; }): { logToAxiom, safeError };

Phase 5:@civitai/telemetry

OTEL 是必须被拆开的两个东西

  1. 自动插桩注册——src/instrumentation.node.ts调用sdk.start(),在进程加载时 patch Prisma/Redis/HTTP。这部分必须留在每个应用内(每个应用有自己的instrumentation.node.ts和 service name)。搬进包里会丢失自动加载行为;
  2. 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.tsclient.tsotel-helpers.tsotel-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.jsondb:migrate使用该 workaround 脚本、db:deploy追加-p标志的写法。

3. Prisma 客户端版本钉扎

@prisma/clientprisma(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.prismaprisma/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 包,二选一:

  1. transpilePackages: ['@civitai/*']——Next 把包源码内联进构建输出。最简单,已在 Phase 0 计划中;
  2. 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 --followgit blame在移动后继续工作,每个文件遵循以下模式:

  1. 纯移动git mv src/server/db/db-helpers.ts packages/civitai-db/src/db-helpers.ts——不做任何内容改动,Git 无歧义识别重命名;
  2. 重构:此时再改 import、把env换成工厂配置、解开循环依赖。diff 小,blame 归属正确;
  3. 加 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)

原文档记录了五条已拍板的决策,值得完整保留:

  1. 包命名:@civitai/scope。所有包使用@civitai/前缀。保留 npm org 名(即使永不公开发布,也能让未来发布变得平凡,并避免命名冲突)。当前仓库所有packages/*/package.json的 name 均为@civitai/*,此决策已执行;
  2. 单一@civitai/datavs 四个窄包(db/redis/clickhouse/axiom)?拍板:四个窄包。未来的纯文本工具应用可以只依赖@civitai/db,不必拉进 ClickHouse 或 Axiom;
  3. schema-common 范围。是否包含 moderator 页面使用的~/server/schema/*.schema.tszod 文件?拍板:推迟。Phase 1–5 的基础设施包不导入任何 zod schema——它们只需要 Prisma 类型。任何~/server/schema/*.schema.ts抽取推迟到 moderator 应用阶段,届时才知道卫星到底需要哪些 schema(从 moderator 依赖分析看,候选是report.schemastrike.schemaimage.schemascanner-review.schema,见 moderator-app-shared-modules.md);
  4. 再导出 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.tsdb-helpers.tspgDb.tsnotifDb.tsdatapacketDb.tsdb-lag-helpers.ts及 redis/clickhouse/logging 对应文件都保留了 shim,monorepo-directory-snapshot.md 的结构树明确标注了这些 SHIM;
  5. 主应用最终是否移入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.jsonpnpm-workspace.yamlpnpm-lock.yamltsconfig.base.json),同时确认该步骤继续推迟,直到卫星应用存在并证明 workspace 布局稳定。

十一、阶段总结表

PhasePackageEffortRisk
0Workspace bootstrapLowLow — just config
1@civitai/dbHighMedium — Prisma client path change is the trickiest single step; circular dep indb-helpers.tsto untangle
2@civitai/redisMediumLow — lots of files, mostly mechanical
3@civitai/clickhouseLowLow
4@civitai/axiomLowLow
5@civitai/telemetryMediumMedium — splitting auto-instrumentation from helpers requires care
6Move 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-schemacivitai-dbcivitai-rediscivitai-clickhousecivitai-axiomcivitai-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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 4:49:26

JS逆向入门:用Chrome DevTools看懂混淆代码

1. 别被“逆向”吓住&#xff1a;它其实只是“看懂别人写的JS代码”很多人一看到“JS逆向”四个字&#xff0c;脑子里立刻浮现出黑客电影里飞速滚动的绿色代码、密不透风的混淆字符串、层层嵌套的eval和Function构造器——然后默默关掉网页&#xff0c;觉得这玩意儿离自己十万八…

作者头像 李华
网站建设 2026/9/18 4:47:56

oh-my-hermes:开源AI Agent框架的部署与实战指南

1. oh-my-hermes 是什么&#xff1a;给AI装上一双"信使之足"先解释一下这个项目的来头。oh-my-hermes 的命名很明显是在致敬 oh-my-zsh 这套装机率极高的终端框架&#xff0c;而 Hermes 则是希腊神话里的信使之神&#xff0c;负责在众神之间传递消息。把这两个词拼在…

作者头像 李华
网站建设 2026/9/18 4:47:14

智能分类垃圾桶传动系统设计:从机构选型到运动仿真全解析

简介&#xff1a;面向机械设计、机械电子及智能装备相关专业的智能分类垃圾桶传动设计与仿真文档&#xff0c;围绕城市生活垃圾智能分拣场景&#xff0c;完整覆盖了从前期网络调研、方案对比到机构设计的全部流程。资源为1个doc文档&#xff0c;压缩包大小3.36MB&#xff0c;内…

作者头像 李华
网站建设 2026/9/18 4:46:42

Oracle 19c升级实战:从版本盘点到高频问题全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 4:45:36

JavaScript高频面试题解析:从类型转换到事件循环与手写实现

最近两周陆续帮几个朋友做了模拟面试&#xff0c;有个现象挺扎心的&#xff1a;简历上写着“熟练掌握 JavaScript”的候选人&#xff0c;基础题答起来反而最容易翻车。问事件循环&#xff0c;能背出宏任务微任务的定义&#xff0c;换一道带 async/await 的输出排序题就乱&#…

作者头像 李华
网站建设 2026/9/18 4:44:14

胶粘剂行业研究报告:口径、Python指标与交叉验证实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华