Turborepo 0.4.0

Jared Palmer
名稱
Jared Palmer
X (原 Twitter)
@jaredpalmer

我很興奮地宣布 Turborepo v0.4.0 的發布!

以 Go 語言重寫

雖然我最初在 TypeScript 中建立了 turbo 的原型,但很明顯,路線圖上的某些項目需要更好的效能。經過大約一個月左右的工作,我很高興終於發布 Go 版本的 turbo CLI。它不僅在毫秒內啟動,而且新的 Go 實作在雜湊處理速度上比 Node.js 實作快 10 到 100 倍。憑藉這個新的基礎(以及您即將讀到的一些功能),Turborepo 現在可以擴展到星系級大小的專案,同時保持極快的速度,這一切都歸功於 Go 出色的並發控制。

更佳的雜湊處理

v0.4.0 中的雜湊處理不僅更快,而且聰明。

主要的變更是 turbo 不再在其雜湊器(負責判斷給定任務是否存在於快取中或是否需要執行的演算法)中包含根鎖定檔內容的雜湊值。相反,turbo 現在根據根鎖定檔雜湊套件的 dependenciesdevDependencies 的已解析版本集合。

舊的行為會在根鎖定檔以任何方式變更時,使快取爆炸。透過這種新行為,變更鎖定檔只會使受新增/變更/移除依賴項影響的那些套件的快取失效。雖然這聽起來很複雜,但再次強調,這一切都表示,當您從 npm 安裝/移除/更新依賴項時,只有那些實際上受到變更影響的套件才需要重建。

實驗性功能:精簡工作區

我們最大的客戶痛點/請求之一是改進在使用大型 Yarn 工作區(或任何工作區實作)時的 Docker 建置時間。核心問題是工作區的最佳功能——將您的 monorepo 縮減為單一鎖定檔——在 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 變得越來越大時,最後一個問題尤其令人痛苦,因為對此鎖定檔的任何變更都會觸發幾乎完整的重建,無論應用程式是否實際上受到新的/變更的依賴項的影響。

...直到現在。

透過全新的 turbo prune 命令,您現在可以透過確定性地為目標套件生成具有精簡鎖定檔的稀疏/部分 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 分鐘才能建置和部署。這些節省真的開始快速累積,尤其是在大型團隊中。

Pipeline

為了讓您更進一步控制您的 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 只能以拓樸順序執行任務。透過新增 pipeline,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 路線圖上的下一步。

致謝