Turborepo 0.4.0

Jared Palmer
姓名
Jared Palmer
X
@jaredpalmer

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

使用 Go 重写

虽然我最初在 TypeScript 中原型化了 turbo,但很明显,路线图上的某些项目需要更好的性能。经过大约一个多月的工作,我很高兴最终发布 Go 版本的 turbo CLI。它不仅可以在毫秒内启动,而且新的 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 分钟才能构建和部署。这些节省确实开始迅速累积,尤其是在大型团队中。

管道 (Pipelines)

为了让您更好地控制 Turborepo,我们在 turbo 的配置中添加了 pipelinepipeline 中的这个新字段允许您指定 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 路线图上的下一步。

鸣谢