skip to content
logo

Search

Monorepo 的内部包是否需要构建?

8 min read

Monorepo 中的内部包有两种构建策略:传统内置包(打包)和内部包(不打包)方法。通过对比两者的优缺点,建议采用打包策略,并提供了优化源码跳转和实现实时更新的具体方案。

内部包构建的选择

选择 monorepo 的最常见原因是想要分离共享代码,让它能在多个应用程序和服务器部署中重复使用。这样可以让依赖关系更清晰,也能更好地实现关注点分离。

比如说,共享代码可能是多个 Web 应用都在用的同一套 UI 组件,或者是服务端和客户端代码共用的一些类型定义、工具函数和业务逻辑。

一般来说,我们不会把这些内部包发布到 NPM,因为这些源代码都是公司内部的,而且只会在这个特定的代码仓库里用到。

在使用 Typescript 时,有两种不同的方式来处理这些共享的内部包。

1. 传统的内置包方法(打包)

这种方式下,我们会把 TS 代码编译成 JS,也可以选择把它打包。包的配置文件会指向编译后的 JS 输出。这和开发单个开源包时的做法差不多,尤其是当你想把包发布到 NPM 或 JSR 的时候。

虽然打包这步是可选的,但我还是建议打包,因为它能解决很多问题,而且现在的打包工具又快又好用。

这种方法的好处是:

  • 每个包都是独立构建的,像 Turborepo 这样的工具能高效地缓存内容
  • 用打包工具可以轻松处理 Typescript 的路径别名,因为打包后的输出不会包含相对路径
  • 可以生成 ESM 模块,不用操心那些格式要求的 .js 和 /index.js 后缀
  • 可以同时支持不同的输出格式,比如 ESM 和 CommonJS

这种方法的缺点是:

  • 需要更多配置和工具
  • 如果用打包工具,可能要额外配置来优化源码映射和 IDE 跳转功能

2. “内部包”方法(不打包)

用这种方法的话,就不用构建(和打包)了,直接在包的配置里指向 Typescript 源文件就行。

这种方法的好处是:

  • 简单易用,基本不用配置
  • 配合使用 Vite、Next.js 这类开发服务器时,代码改动能立即生效,实时更新
  • IDE 能直接找到你的源文件和类型定义

这种方法的缺点是:

  • 效率不太高,因为类型检查、编译和打包的工作都交给了使用方
  • 要用 Typescript 路径别名的话,得给每个包都配置一个独特的别名
  • 没法发布到 NPM,因为配置文件默认你会用 Typescript 和打包工具

个人建议:打包

我觉得打包带来的好处比不打包构建要多,我更喜欢把每个包都当作独立单元来处理。

我认为打包工具是开发工具链中不可或缺的一部分,打包工具这块发展也很快,特别是那些基于 Rust 的实现,速度非常快。作为应用开发者,我们不用太操心这个问题。

另外,虽然构建传统的内置包方法需要更多配置,但通过一些优化可以获得和”内部包”方法一样好的开发体验。

实现源码跳转

在 monorepo 项目中,特别是在处理跨包依赖时,源码跳转功能对于理解和调试代码,提升开发体验至关重要。

  • 当开发者点击从共享包中导入的代码(比如函数或类)时,我们希望能直接跳转到原始的 TypeScript 源文件,而不是构建或打包后的输出文件。
  • 同样,当点击来自共享包的导入类型时,编辑器应该能跳转到原始类型定义,而不是由 tsc 或打包器生成的 d.ts 输出文件。

要解决传统内置包方法中 IDE 跳转的问题,有两种方案:

  1. 在 tsconfig.json 中配置 TS 引用,让编译器了解包之间的关系。这样不仅能实现源码跳转,还能启用增量构建。

  2. 生成类型定义映射文件(.d.ts.map)。具体做法是:

    • 让打包工具(如 tsup)只输出源文件和源映射文件
    • 用额外的 tsc 构建步骤生成类型定义及其映射文件
    • 运行命令:tsup && tsc --emitDeclarationOnly
    • 在 tsconfig 中设置 declaration 和 declarationMap 为 true

这样 IDE 就能通过类型定义映射找到原始源码,实现精确跳转。

关于为什么需要两个工具才能实现源码跳转?

主要原因是 tsup 生成的类型定义是基于打包后的输出,这不是我们想要的,因为我们需要让 IDE 能跳转到原始源码。解决方案是让 tsup 只负责输出源文件和源映射文件,然后用 tsc 在额外的构建步骤中生成类型定义及其映射文件。这样虽然类型文件和打包后的文件结构不一样,但会保持原始源码的结构,IDE 就能通过类型定义映射找到原始源码,实现精确跳转。

实现实时更新

要实现类似”内部包”方法的实时更新效果,我们可以使用 Turborepo 的监视模式:

Turbo v2 引入了监视模式,可以监控文件系统变化并自动重新构建。它会在每个包的根目录创建 .turbo 目录,保存构建元数据,当文件变化时,它会智能地只重新构建受影响的包。

通过这些优化,传统内置包方法不仅能保持构建的优势,还能获得接近”内部包”方法的开发体验。结合前面提到的源码跳转功能,我们可以在不牺牲开发体验的情况下享受到打包带来的所有好处。