发布日期

第 02 讲|Monorepo 结构与发布包关系

解析 vercel/next.js monorepo 结构、各包发布关系与依赖图

阶段一:框架认知与源码导航 · 第 2 / 40 讲 难度:⭐⭐ · 预计耗时:1.5 小时

学习目标

  • 5 分钟内定位"我要改的功能在哪个包里"。
  • 区分发布包(用户安装的)、内部包(仅本仓库使用)、Rust crates(编译进 SWC/Turbopack)的边界。
  • 能说清 packages/nextpackages/next-swcturbopack/ 三者的关系。

1. 整体俯瞰

仓库根有 7 个核心目录:

next.js/
├── packages/JS / TS 包(pnpm workspaces)
├── crates/Next.js 自己的 Rust crates(SWC plugin、turbopack 绑定)
├── turbopack/Turbopack 子树(git subtree from vercel/turborepo)
├── apps/             ← 内部 demo / dogfooding 应用
├── bench/            ← 性能基准
├── test/             ← e2e / 集成 / 单元测试
├── docs/             ← 公开文档(next.js.org 的源)
└── examples/         ← create-next-app 用得到的示例

pnpm workspaces 的范围定义在根目录:

10:pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'bench/*'
  - 'crates/*/js'
  - 'turbopack/crates/*/js'
  - 'turbopack/crates/turbopack-tests/tests/execution'
  - 'turbopack/packages/*'
updateNotifier: false
publicHoistPattern:
  - '*eslint*'

注意一个细节:Rust crates 的 JS 部分也被纳入 workspacescrates/*/js)。这是因为 SWC / Turbopack 都需要"Rust 编出 .node binary + JS 包薄壳"的发布方式。

2. packages/ —— 18 个 JS 包

这是日常开发最常碰的。挑出最关键的几个:

2.1 packages/next/ —— 主包

用户 npm install next 装的就是这个。结构:

packages/next/
├── src/TypeScript 源码(开发时改这里)
├── dist/             ← 编译产物(git 不追,CI / 本地 build 时生成)
├── package.json      ← 发布元数据 + 大量 exports 字段
├── taskfile.js       ← 自定义构建脚本(基于 taskr)
├── bin/next.tsCLI 入口(编译到 dist/bin/next.js)
└── compiled/         ← 预编译的第三方依赖(react / webpack / babel 等)

源码进一步分层(在第 03 讲深入):

子目录内容
src/cli/CLI 入口:next-dev / next-build / next-start / next-export ...
src/build/构建管线:webpack 配置、entries、manifests、turbopack-build 等
src/server/服务运行时:base-server、next-server、app-render、route-modules ...
src/client/浏览器运行时:app-router、segment-cache、Link、Image ...
src/shared/server/client 共享代码(注意:必须无 Node API)
src/lib/顶层工具函数
src/compiled/预打包好的第三方依赖(详见 2.5)
src/experimental/实验性能力(testmode / testing-utilities)

2.2 packages/next-swc/ —— Rust binding 的 JS 包壳

这个包不是 Rust 源码,而是把 crates/ 里编出来的 .node binary 与少量 JS 胶水代码打成 npm 包。当你跑 pnpm install 时 npm 会拉取与你平台匹配的 @next/swc-darwin-arm64 等 platform-specific binary,由这里 require 出来。

重点:当 SWC 行为异常时,要怀疑两件事——@next/swc-* binary 是不是与本地 Rust 源码不一致(详见 AGENTS.md "Stale Native Binary");以及 crates/ 里的 transform 逻辑。

2.3 packages/create-next-app/ —— 脚手架

npx create-next-app 命令的实现。源码独立,与主包没有强耦合。当用户报"create-next-app 模板有问题"时改这里。

2.4 其他用户可见的包

包名作用用户感知度
fontnext/font 的实现(Google Fonts / Local Fonts)
eslint-plugin-next / eslint-config-nextNext.js 专属 ESLint 规则
next-mdx@next/mdx MDX 集成
third-partiesnext/third-parties(Google Analytics、YouTube 等)
next-env@next/env.env* 文件加载低(被内部 require)
next-codemodnpx @next/codemod:版本升级 codemod升级时高
next-bundle-analyzer@next/bundle-analyzer:包体分析调优时高
next-routing实验性路由能力极低
next-rspack切换到 Rspack(webpack 5 兼容 Rust 替代)的适配层实验
next-plugin-storybookStorybook 集成
next-playwrightPlaywright 配置预设
next-polyfill-*老浏览器 polyfill自动注入
react-refresh-utilsFast Refresh 工具内部使用
eslint-plugin-internal仅本仓库内部使用的 ESLint 规则极低

2.5 packages/next/src/compiled/ —— 为什么有这么多预编译依赖

打开这个目录你会看到 reactreact-domreact-server-dom-webpackwebpackbabelacornamphtmldebug……上百个常见库。

这不是 vendoring,是**"打包后再发布"**:所有这些库都已经 bundle 成单文件(CJS)并固化在 npm 包里。原因有 4 个:

  1. 避免 peerDeps 地狱:用户的 react 版本不会影响 Next.js 内部用的 react
  2. 冻结行为:Next.js 自己依赖的 webpack / babel 行为不会因 npm 更新而漂移。
  3. 缩小安装体积:用户的 node_modules/next/ 是一个相对完整的"自给自足"产物。
  4. 支持 React vendoring(关键):Next.js 同时打了 3 个 React channel(experimental、rc、stable),可在 server 与 client 之间切换。详见第 17 讲与 .claude/skills/react-vendoring/SKILL.md

构建逻辑在主包的 taskfile.js 里,搜索 copy_vendor_react / compile_* 任务。

3. crates/ —— Next.js 自己的 Rust 代码

crates/
├── next-core/                       ← 核心:page loader、entry 生成、module rules
├── next-api/                        ← 给 Node JS 调用的 API├── next-build/Turbopack-based next build
├── next-build-test/                 ← next-build 的内部测试
├── next-custom-transforms/Next 专属 SWC transforms(next/dynamic 处理等)
├── next-code-frame/                 ← 错误码框(生成精美 error frame)
├── next-error-code-swc-plugin/      ← 标注内部错误码的 SWC 插件
├── next-napi-bindings/N-API:Rust ↔ Node.js 桥接
├── next-taskless/                   ← 构建任务调度
└── wasm/WebAssembly 编译目标

关键关系

  • crates/next-core/ 是 Turbopack 在 Next.js 这一侧的"使用层"——它告诉 Turbopack 怎么把 app/page.tsx 编译成可运行的代码。
  • crates/next-custom-transforms/ 是给 SWC 用的(适用于 webpack 也适用于 Turbopack):每个 transform 是一个 SWC visitor,比如把 import dynamic from 'next/dynamic' 处理成正确的 chunk 信息。

普通业务开发不会改 Rust,但当你看到 'use client' / 'use server' 行为异常、next/dynamic 编译不正确时,要会查 crates/next-custom-transforms/

4. turbopack/ —— git subtree

turbopack/
├── crates/Turbopack 主体(50+ crates)
├── packages/TurbopackJS 工具
└── README.md

历史上 Turbopack 来自 vercel/turborepo 仓库,2024 年被 git subtree 合并到 next.js。它不是 npm dependency,而是源码内嵌——所以你修改 turbopack/ 里的 Rust 代码会影响 Next.js 构建。

注意几个关键 crate:

Crate作用
turbopack-core核心抽象:Asset / Module / Chunk
turbopack-ecmascriptJS / TS 处理(依赖 SWC)
turbopack-ecmascript-runtime运行时 JS 模板(在 dev/HMR 时注入到浏览器)
turbopack-cssCSS 处理
turbopack-dev-serverdev mode 的 HTTP server
turbopack-nodeNode.js 端 SSR 容器
turbo-tasks增量计算框架(Turbopack 的"灵魂")

诊断提示:HMR 行为异常、'use client' 边界不对时,先看 turbopack/crates/turbopack-ecmascript/;运行时报"模块未定义"时看 turbopack-ecmascript-runtime/js/src/

5. apps/bench/test/examples/docs/

目录你会改这里吗何时改
apps/偶尔给内部 dogfooding 加个示例
bench/偶尔写性能基准(如 bench:render-pipeline
test/频繁任何源码改动都要加测试
examples/偶尔给社区贡献新示例
docs/偶尔改了用户 API 后必须同步

6. 三层依赖图

把上面所有信息浓缩成一张图:

       ┌──────────────────────────┐
       │     packages/next        │ ← 用户安装的 npm 包
       └────────┬─────────────────┘
                │ runtime require()
       ┌──────────────────────────┐
       │ packages/next/src/       │ ← TypeScript 源码(你改这里)
       │   compiled/  (vendored)       └────────┬─────────────────┘
                │ build-time bindings
       ┌────────┴───────────┬─────────────┐
       ▼                    ▼             ▼
 ┌───────────┐      ┌────────────┐  ┌────────────────┐
 │ next-swc  │      │  crates/   │  │  turbopack/  (.node)   │◀────  (Rust)     │◀─│  (Rust subtree) └───────────┘      └────────────┘  └────────────────┘

记住这层关系:

  • 改 TS 行为packages/next/src/,然后 pnpm --filter=next build
  • 改 SWC transform 行为(如 RSC 边界处理)→ crates/next-custom-transforms/,然后 pnpm build-all
  • 改 Turbopack 行为(如新文件类型支持)→ turbopack/crates/,然后 pnpm build-all
  • 改 vendored 第三方(如自定义 webpack 行为)→ 修改 packages/next/taskfile.js,重新跑 compile_* 任务。

7. 实操:5 道"在哪里改"的小测验

请在脑中(或翻代码)回答以下问题,每题 2 分钟内:

  1. 我想给 next/image 加一个新的 loader 配置项。改哪里?

    答案packages/next/src/client/image-component.tsx + packages/next/src/server/image-optimizer.ts + 类型定义 packages/next/src/server/config-shared.ts

  2. 我发现 'use client' 在 React 19 下边界处理有 bug。改哪里?

    答案主要在 crates/next-custom-transforms/src/transforms/react_server_components/,可能还要联动 packages/next/src/build/webpack/loaders/next-flight-loader/

  3. 我想给 pnpm new-test 加一个新的模板类型。改哪里?

    答案turbo/generators/(turborepo gen 配置)+ 模板文件。

  4. 我想新增一个 experimental flag。改哪里?

    答案3 个最少:packages/next/src/server/config-shared.ts(类型)+ config-schema.ts(zod)+ packages/next/src/build/define-env.ts(用户 bundle 注入)。详见 .claude/skills/flags/SKILL.md 与第 23 讲。

  5. 我想加一行 ESLint 规则禁止用户 import 'next/legacy/image'。改哪里?

    答案packages/eslint-plugin-next/lib/rules/

8. 检验问题

  1. packages/next/src/compiled/ 为什么存在?至少说出 3 个原因。
  2. packages/next-swccrates/next-custom-transforms 是同一个东西吗?关系是什么?
  3. turbopack/ 为什么不是 npm 依赖,而是源码内嵌?
  4. packages/next/src/server/app-render/app-render.tsx 后,需要执行哪条 build 命令最快?为什么不一定要 pnpm build-all?(提示:阅读 AGENTS.md 的「Rebuilding Before Running Tests」)
  5. packages/nextpackage.json 里有那么多 *.js / *.d.ts 顶层文件,它们和 dist/ 是什么关系?

9. 延伸阅读

  • 仓库根 AGENTS.md 的「Codebase structure」→「Monorepo Overview」。
  • packages/next/README.md(这是真正发布到 npm 的 README)。
  • turbopack/README.md(Turbopack 自己的设计说明)。
  • .claude/skills/react-vendoring/SKILL.md(深入理解 compiled/react*)。

下一讲预告

第 03 讲|源码主入口与执行流:从 bin/next 一路追到 app-render.tsx,画一张完整的"请求-渲染"调用栈。