返回博客

Turborepo 0.4.0

Jared Palmer
姓名
Jared Palmer
X
@jaredpalmer

我很高兴地宣布 Turborepo v0.4.0 版本的发布!

用 Go 重写

虽然我最初用 TypeScript 原型化了 turbo,但很明显,路线图上的某些项目需要更好的性能。经过大约一个月左右的工作,我很高兴终于发布了 turbo CLI 的 Go 版本。它不仅在毫秒内启动,而且新的 Go 实现的哈希速度比 Node.js 实现快 10 到 100 倍。有了这个新的基础(以及您即将读到的一些功能),Turborepo 现在可以扩展到星系大小的项目,同时保持闪电般的速度,这都要归功于 Go 出色的并发控制。

更好的哈希

不仅 v0.4.0 中的哈希速度更快,而且还更智能

主要变化是 turbo 不再在其哈希器中包含根 lockfile 内容的哈希值(该算法负责确定给定任务是否存在于缓存中或需要执行)。相反,turbo 现在根据根 lockfile 哈希包的 dependenciesdevDependencies 的已解析版本集。

旧的行为会在根 lockfile 以任何方式更改时炸毁缓存。有了这种新行为,更改 lockfile 只会破坏受添加/更改/删除依赖项影响的包的缓存。虽然这听起来很复杂,但它的意思仍然是,当您从 npm 安装/删除/更新依赖项时,只有那些实际受更改影响的包才需要重建。

实验性功能:修剪后的工作区

我们最大的客户痛点/请求之一是提高使用大型 Yarn 工作区(或任何工作区实现)时的 Docker 构建时间。核心问题是,工作区最好的功能——将您的 monorepo 减少到单个 lockfile——在 Docker 层缓存方面也是最糟糕的。

为了帮助阐明问题以及 turbo 现在如何解决它,让我们看一个例子。

假设我们有一个带有 Yarn 工作区的 monorepo,其中包括一组名为 frontendadminuibackend 的包。我们还假设 frontendadmin 都是 Next.js 应用程序,它们都依赖于同一个内部 React 组件库包 ui。现在我们还假设 backend 包含一个 Express TypeScript REST API,它实际上不与我们 monorepo 的任何其他部分共享太多代码。

以下是 frontend Next.js 应用程序的 Dockerfile 可能的样子

Dockerfile
FROM node:alpine AS base
RUN apk update
WORKDIR /app
 
# Add lockfile and package.jsons
FROM base AS builder
COPY *.json yarn.lock ./
COPY packages/ui/*.json ./packages/ui/
COPY packages/frontend/*.json ./packages/frontend/
RUN yarn install
 
# Copy source files
COPY packages/ui/ ./packages/ui/
COPY packages/frontend/ ./packages/frontend/
 
# Build
RUN yarn --cwd=packages/ui/ build
RUN yarn --cwd=packages/frontend/ build
 
# Start the Frontend Next.js application
EXPOSE 3000
RUN ['yarn', '--cwd', 'packages/frontend', 'start']

虽然这可以工作,但有些事情可以做得更好

当您的 monorepo 变得越来越大时,最后一个问题尤其痛苦,因为对该 lockfile 的任何更改都会触发几乎完全重建,而不管应用程序是否实际上受到新的/更改的依赖项的影响。

……直到现在。

有了全新的 turbo prune 命令,您现在可以通过确定性地为目标包生成具有修剪后的 lockfile 的稀疏/部分 monorepo 来解决此问题——而无需安装您的 node_modules

让我们看看如何在 Docker 中使用 turbo prune

Dockerfile
FROM node:alpine AS base
RUN apk update && apk add git
 
## Globally install `turbo`
RUN npm i -g turbo
 
# Prune the workspace for the `frontend` app
FROM base as pruner
WORKDIR /app
COPY . .
RUN turbo prune frontend --docker
 
# Add pruned lockfile and package.json's of the pruned subworkspace
FROM base AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/yarn.lock ./yarn.lock
# Install only the deps needed to build the target
RUN yarn install
 
# Copy source code of pruned subworkspace and build
FROM base AS builder
WORKDIR /app
COPY --from=pruner /app/.git ./.git
COPY --from=pruner /app/out/full/ .
COPY --from=installer /app/ .
RUN turbo run build frontend
 
# Start the app
FROM builder as runner
EXPOSE 3000
RUN ['yarn', '--cwd', 'packages/frontend', 'start']

那么 turbo prune 的输出到底是什么?一个名为 out 的文件夹,其中包含以下内容

得益于以上内容,现在可以将 Docker 设置为仅在有真正理由时重建每个应用程序。因此,当 frontend 的源代码或依赖项(无论是内部的还是来自 npm 的)实际发生更改时,它才会重建。adminbackend 也是如此。对 ui 的更改,无论是其源代码还是依赖项,都将触发 frontendadmin 的重建,但触发 backend 的重建。

虽然此示例看起来微不足道,但想象一下,如果每个应用程序都需要长达 20 分钟的时间来构建和部署。这些节省确实开始迅速积累起来,尤其是在大型团队中。

管道

为了让您更好地控制 Turborepo,我们在 turbo 的配置中添加了 pipeline。此新字段允许您指定 monorepo 中的 npm 脚本如何相互关联,以及一些其他按任务选项。然后,turbo 使用此信息来优化调度 monorepo 中的任务,从而消除原本会存在的水瀑。

以下是它的工作原理

./package.json
{
  "turbo": {
    "pipeline": {
      "build": {
        // This `^` tells `turbo` that this pipeline target relies on a topological target being completed.
        // In english, this reads as: "this package's `build` command depends on its dependencies' or
        // devDependencies' `build` command being completed"
        "dependsOn": ["^build"]
      },
      "test": {
        //  `dependsOn` without `^` can be used to express the relationships between tasks at the package level.
        // In English, this reads as: "this package's `test` command depends on its `lint` and `build` command first being completed"
        "dependsOn": ["lint", "build"]
      },
      "lint": {},
      "dev": {}
    }
  }
}

然后,turbo 将解释上述配置以优化调度执行。

这实际上意味着什么?在过去(如 Lerna 和 Nx),turbo 只能按拓扑顺序运行任务。通过添加管道,turbo 现在除了实际的依赖关系图之外,还构建一个拓扑“动作”图,它使用该图来确定任务应以最大并发性执行的顺序。最终结果是,您不再浪费空闲的 CPU 时间等待完成(即,不再有水瀑)。

Turborepo scheduler

改进的缓存控制

得益于 pipeline,我们现在可以很好地按任务打开 turbo 的缓存行为。

基于上面的示例,您现在可以像这样在整个 monorepo 中设置缓存输出约定

./package.json
{
  "turbo": {
    "pipeline": {
      "build": {
        // Cache anything in dist or .next directories emitted by a `build` command
        "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
        "dependsOn": ["^build"]
      },
      "test": {
        // Cache the test coverage report
        "outputs": ["coverage/**"],
        "dependsOn": ["lint", "build"]
      },
      "dev": {
        // Never cache the `dev` command
        "cache": false
      },
      "lint": {},
    }
  }
}

注意:目前,pipeline 存在于项目级别,但在以后的版本中,这些可以在每个包的基础上被覆盖。

接下来是什么?

我知道这很多,但还有更多内容即将推出。以下是 Turborepo 路线图上的下一步内容。

鸣谢