目录

内部包

编译策略

即时编译包 

编译包 

可发布包

创建内部包;

创建一个空目录

添加 package.json 

添加 tsconfig.json

添加带有源代码的 src 目录

将包添加到应用程序

编辑 turbo.json

运行 turbo build

内部包的最佳实践


内部包

内部包 是你工作区的构建块,为你提供了一种强大的方式来跨仓库共享代码和功能。Turborepo 使用 package.json 中的依赖关系自动理解内部包之间的关系,在底层创建一个 包图,以优化你仓库的工作流程。

内部包是源代码位于你的工作区内的库。你可以快速创建内部包,以便在你的 monorepo 中共享代码,并且如果以后需要,可以选择将它们发布到 npm 注册中心。

内部包在你的仓库中使用,方法类似于安装来自 npm 注册中心的外部包,在 package.json 中安装它们。然而,你可以使用你的包管理器的工作区安装语法来引用包,而不是标记要安装的版本

{
  "dependencies": {
    "@repo/ui": "workspace:*"
  }
}

然后你可以像使用外部包一样将包导入到你的代码中

import { Button } from '@repo/ui'; 
 
export default function Page() {
  return <Button>Submit</Button>;
}

编译策略


根据你对库的需求,你可以选择三种编译策略之一

  • 即时编译包:通过允许应用程序 bundler 在使用包时编译它,为你的包创建最少的配置。
  • 编译包:通过适量的配置,使用构建工具(如 tsc)或 bundler 编译你的包。
  • 可发布包:编译并准备一个包以发布到 npm 注册中心。这种方法需要最多的配置

即时编译包 

即时编译包由使用它的应用程序编译。这意味着你可以直接使用你的 TypeScript(或未编译的 JavaScript)文件,与本页上的其他策略相比,需要的配置要少得多

此策略在以下情况下最有用

  • 你的应用程序是使用现代 bundler(如 Turbopack、webpack 或 Vite)构建的。
  • 你希望避免配置和设置步骤。
  • 即使在无法命中包的缓存时,你也对应用程序的构建时间感到满意。

即时编译包的 package.json 可能如下所示

{
  "name": "@repo/ui",
  "exports": {
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx"
  },
  "scripts": {
    "lint": "eslint . --max-warnings 0",
    "check-types": "tsc --noEmit"
  }
}

在此 package.json 中,有几点重要事项需要注意

  • 直接导出 TypeScriptexports 字段标记包的入口点,在这种情况下,你直接引用 TypeScript 文件。这是可能的,因为应用程序的 bundler 将在构建过程中使用代码时编译代码。
  • 没有 build 脚本:由于此包正在导出 TypeScript,因此它不需要用于转译包的构建步骤。这意味着你无需在此包中配置构建工具即可使其在你的工作区中工作。

局限性

  • 仅当消费者进行转译时适用:仅当包将用于使用 bundler 或原生理解 TypeScript 的工具时,才能使用此策略。消费者的 bundler 负责将 TypeScript 包转译为 JavaScript。如果你的构建或包的其他用法无法使用 TypeScript,你将需要转移到编译包策略。
  • 没有 TypeScript paths:由其消费者转译的库不能使用 compilerOptions.paths 配置,因为 TypeScript 假定源代码正在编写它的包中进行转译。如果你使用的是 TypeScript 5.4 或更高版本,我们建议使用 Node.js 子路径导入。要了解如何操作,请访问我们的 TypeScript 页面
  • Turborepo 无法缓存即时编译包的构建:由于包没有自己的 build 步骤,因此 Turborepo 无法缓存它。如果你想将配置保持在最低限度,并且可以接受应用程序的构建时间,那么这种权衡可能对你来说是有意义的。
  • 内部依赖项中的错误将被报告:当直接导出 TypeScript 时,如果内部依赖项中的代码存在 TypeScript 错误,则依赖包中的类型检查将失败。在某些情况下,你可能会发现这令人困惑或有问题。

编译包 

编译包是使用构建工具(如 tsc (TypeScript 编译器))处理自身编译的包。

{
  "name": "@repo/ui",
  "exports": {
    "./button": {
      "types": "./src/button.tsx",
      "default": "./dist/button.js"
    },
    "./card": {
      "types": "./src/card.tsx",
      "default": "./dist/card.js"
    }
  },
  "scripts": {
    "build": "tsc"
  }
}

编译你的库会将编译后的 JavaScript 输出生成到一个目录(dist、build 等)中,你将使用该目录作为包的入口点。构建输出将在添加到任务的 outputs 键后被 Turborepo 缓存,从而使你能够获得更快的构建时间。

局限性和权衡

  • 使用 TypeScript 编译器:大多数编译包应使用 tsc。由于该包很可能被使用 bundler 的应用程序使用,因此应用程序的 bundler 将准备库包以在应用程序的最终 bundles 中分发,处理 polyfilling、降级和其他问题。仅当你具有需要它的特定用例时,才应使用 bundler,例如将静态资源捆绑到包的输出中。
  • 更多配置:编译包需要更深入的知识和配置才能创建构建输出。TypeScript 编译器有许多配置,可能难以管理和理解,并且需要进一步配置以优化 bundler,例如 package.json 中的 sideEffects 键。你可以在我们专门的 TypeScript 指南中找到我们的一些建议。

可发布包

将包发布到 npm 注册中心对本页的打包策略提出了最严格的要求。因为你对从注册中心下载包的消费者将如何使用你的包一无所知,所以由于健壮包所需的众多配置,你可能会发现它很困难。

此外,将包发布到 npm 注册中心的过程需要专门的知识和工具。我们建议使用 changesets 来管理版本控制、更新日志和发布过程。

创建内部包;

让我们按照 包的解剖结构 部分和 编译包 模式中的指导,创建你的第一个内部包,以在你的仓库中共享数学实用程序。在下面的步骤中,我们假设你已经 使用 create-turbo 创建了一个新仓库,或者正在使用结构类似的仓库。

创建一个空目录

你需要一个目录来放置包。让我们在 ./packages/math 创建一个。

添加 package.json 

接下来,为包创建 package.json。通过添加此文件,你将满足 内部包的两个要求,使其可被 Turborepo 和你工作区的其余部分发现 

  • 声明包目录
  • 在包的目录中,必须有一个 package.json
{
  "name": "@repo/math",
  "type": "module",
  "scripts": {
    "dev": "tsc --watch",
    "build": "tsc"
  },
  "exports": {
    "./add": {
      "types": "./src/add.ts",
      "default": "./dist/add.js"
    },
    "./subtract": {
      "types": "./src/subtract.ts",
      "default": "./dist/subtract.js"
    }
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*",
    "typescript": "latest"
  }

让我们逐段分解这个 package.json

  • scripts: dev 和 build 脚本使用 TypeScript 编译器 编译包。dev 脚本将监视源代码的更改并自动重新编译包。
  • devDependencies: typescript 和 @repo/typescript-config 是 devDependencies,因此你可以在 @repo/math 包中使用这些包。在一个实际的包中,你可能会有更多的 devDependencies 和 dependencies - 但现在我们可以保持简单。
  • exports: 为包定义多个入口点,以便它可以在其他包中使用 (import { add } from '@repo/math')。

值得注意的是,此 package.json 声明了一个内部包 @repo/typescript-config 作为依赖项。Turborepo 将识别 @repo/math 作为 @repo/typescript-config 的依赖项,以便对你的任务进行排序。

添加 tsconfig.json

通过将 tsconfig.json 文件添加到包的根目录,为此包指定 TypeScript 配置。TypeScript 具有 extends 键,允许你在整个仓库中使用基本配置,并根据需要使用不同的选项覆盖它。

{
  "extends": "@repo/typescript-config/base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

你在这里完成了四件重要的事情

  • 位于 ./packages/typescript-config 中的 @repo/typescript-config/base.json 配置具有你所需的所有配置,因此你可以从中扩展。
  • compilerOptions 中的 outDir 键 告诉 TypeScript 将编译后的输出放在哪里。它与你的 package.json 中 exports 中指定的目录匹配。
  • compilerOptions 中的 rootDir 键 确保 outDir 中的输出使用与 src 目录相同的结构。
  • include 和 exclude 键不会从基本配置继承,根据 TypeScript 规范,因此你在此处包含了它们。

添加带有源代码的 src 目录

你现在可以为你的包编写一些代码了。在 src 目录中创建两个文件

export const add = (a: number, b: number) => a + b;

这些文件映射到当你稍后运行 turbo build 时 tsc 将创建的输出。

将包添加到应用程序

你已准备好在应用程序中使用你的新包。让我们将其添加到 web 应用程序。

  "dependencies": {
+   "@repo/math": "workspace:*",
    "next": "latest",
    "react": "latest",
    "react-dom": "latest"
  },

你刚刚更改了仓库中的依赖项。请务必运行你的包管理器的安装命令以更新你的 lockfile。

 @repo/math 现在在 web 应用程序中可用,你可以在你的代码中使用

import { add } from '@repo/math/add';
 
function Page() {
  return <div>{add(1, 2)}</div>;
}
 
export default Page;

编辑 turbo.json

将新 @repo/math 库的 artifacts 添加到 turbo.json 中 build 任务的 outputs。这确保了其构建输出将被 Turborepo 缓存,以便它们可以在你开始运行构建时立即恢复。

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    }
  }
}

运行 turbo build

如果你已经 全局安装了 turbo,请在你的工作区根目录的终端中运行 turbo build。你也可以使用你的包管理器从 package.json 运行 build 脚本,这将使用 turbo run build。

@repo/math 包在 web 应用程序构建之前构建,以便 ./packages/math/dist 中的运行时代码在 web 应用程序捆绑时可用。 

内部包的最佳实践

每个包一个“目的”

当你创建内部包时,建议创建具有单一“目的”的包。这不是严格的科学或规则,而是一个最佳实践,取决于你的仓库、你的规模、你的组织、你的团队的需求等等。这种策略有几个优点

  • 更易于理解:随着仓库规模的扩大,在仓库中工作的开发人员将更容易找到他们需要的代码。
  • 减少每个包的依赖项:每个包使用更少的依赖项使得 Turborepo 可以更有效地 修剪你的包图的依赖项。

一些例子包括

  • @repo/ui:一个包含所有共享 UI 组件的包
  • @repo/tool-specific-config:一个用于管理特定工具配置的包
  • @repo/graphs:一个用于创建和操作图形数据的特定领域库

应用程序包不包含共享代码

当你创建 应用程序包 时,最好避免将共享代码放在这些包中。相反,你应该为共享代码创建一个单独的包,并让应用程序包依赖于该包。

此外,应用程序包不应安装到其他包中。相反,它们应该被视为你的 包图 的入口点。

更多推荐