幾乎大部分 JavaScript 或是 TypeScript 程式碼都有 package.json
腳本, 像是 build
, test
跟 lint
。 在 Turborepo 把他們稱作 tasks (任務)。
Turborepo 可以快取這些任務的結果跟 logs 使一些太花時間的任務可以大幅提升速度。
沒找到對應的 Cache 時
每個任務都有其 inputs 輸入 跟 outputs 輸出。
像是 build
可能是用 原始檔案 作為其 input, 並用打包後的檔案以及 stdout / stderr (標準輸出) 中 的 log 紀錄 作為 output。
lint
或是 test
也可能是用 原始檔案 作為其 input, 並以 stdout / stderr (標準輸出) 的 log 作為 output。
接下來我們用 build
作為例子來使用 Turborepo:
Turborepo 會評估
build
用到的 input (預設是專案中所有沒有被 gitignored 的檔案。) 並將他們轉換成hash
。我們可以查看本地端檔案系統中的 cache, cache 會被放在用 hash 命名的資料夾底下 (e.g.
./node_modules/.cache/turbo/78awdk123
)。如果 Turborepo 沒有找到對應的 hash 產出, 就會執行該任務。
一旦任務結束,Turborepo 會儲存所有 outputs (包含檔案跟 log) 到 cache 裏。
Turborepo 會使用多種資訊來建立 hash , 像是 原始檔案,環境變數 甚至是 依賴套件的原始檔。
有找到對應的 Cache 時
假設我們重新執行任務但沒有異動任何 input:
因為 input 沒有改變所以 hash 會相同 (e.g. 78awdk123
)。
Turborepo 會在 cache 中找到相同名稱的 hash 資料夾 (e.g.
./node_modules/.cache/turbo/78awdk123
)。Turborepo 會直接重用 cache 的結果,不會執行該任務。 就是輸出儲存的 logs 到標準輸出, 以及重新使用儲存的 output 檔案到我們預期的檔案位置。
從 cache 中重用檔案跟 logs 幾乎不用花時間。 它可以讓你的建構時間從幾分鐘或是幾小時縮減到幾秒或是幾毫秒。
根據程式碼依賴圖的形狀和粒度,具體結果會有所不同, 但大多數團隊發現, 使用 Turborepo 的 Cache 可以使整個月的構建時間縮短約 40-85%。
調整 Cache Outputs 參數
使用 pipeline,你可以配置整個 Turborepo 的緩存設定。
要覆蓋預設的 cache output 行為, 可以配置 glob 列表到 pipeline.<task>.outputs
。
所有匹配 glob 的檔案都會被視作該任務的產出。
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"outputs": ["dist/**", ".next/**"],
"dependsOn": ["^build"]
},
"test": {
"outputs": [], // leave empty to only cache logs
"dependsOn": ["build"]
}
}
}
如果該任務並不會產生任何檔案 (像是 Jest 的單元測試), 可以將 outputs 設置成 空陣列 (就是 []
), Turborepo 只會 cache 輸出的 log。
當執行 turbo run build test
時, Turborepo 會執行你的 build
跟 test
腳本, 並 cache 他們的 output 進 ./node_modules/.cache/turbo
。
Cache ESLint 的進階訣竅: 你可以在 ESLint 的指令前面加上
TIMING=1
參數, 產生漂亮的 terminal output (包含沒有 error 的時候)。 可以到 ESLint 的文件裡面看詳細
調整 Cache Inputs 參數
當專案中任何文件有異動,會被視為該專案已被改變。 然而針對某些任務,我們可能只希望有相關的檔案異動時才去重跑。 透過標註 inputs
讓我們可以定義哪些檔案與哪個任務有相關。
例如說, 下面的 test
任務, 只需要在 src/
或 test/
中的 .tsx
或是 .ts
檔案跟上次執行不同的時候執行。
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
// ... omitted for brevity
"test": {
// A workspace's `test` task depends on that workspace's
// own `build` task being completed first.
"dependsOn": ["build"],
"outputs": [],
// A workspace's `test` task should only be rerun when
// either a `.tsx` or `.ts` file has changed.
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
}
}
}
關閉 Caching
有的時候,你真的不想產生 cache, 像是你在使用 next dev
或是 react-scripts start
開啟 live reloading。
加上 --no-cache
在任意指令後面可以取消 cache:
# Run `dev` npm script in all workspaces in parallel,
# but don't cache the output
turbo run dev --parallel --no-cache
注意,--no-cache
雖然可以取消 cache 產生, 但是並沒有取消 cache 讀取。 如果你想要取消 cache 讀取可以使用 --force
。
你也可以取消特定任務的 cache, 透過將 pipeline.<task>.cache
設定成 false
:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"dev": {
"cache": false
}
}
}
調整檔案異動觸發 Cache 的規則
針對某些任務, 你可能不希望因為不相關的檔案異動導致不能使用到 cache。
例如,更新 README.md
可能不需要重新觸發 test
任務。 你可以使用 inputs 來限制 turbo
需要考慮哪些檔案。
在這個情況下, test
任務會不會使用 cache 就只需要考慮相關的 .ts
或是 .tsx
檔案。
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
// ...other tasks
"test": {
"outputs": [], // leave empty to only cache logs
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
}
}
}
package.json
永遠都會被視作任務的 input。 因為任務本身被寫在package.json
裏,作為腳本的 key。 當你更改package.json
,所有已產生的 cache output 都會失效。
如果你希望所有的任務都依賴於某個特定檔案, 你可以在 globalDependencies
陣列中指定。
{
"$schema": "https://turbo.build/schema.json",
+ "globalDependencies": [".env"],
"pipeline": {
// ...other tasks
"test": {
"outputs": [], // leave empty to only cache logs
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
}
}
}
turbo.json
永遠會被視作全域依賴。 如果你異動了 turbo.json
,所有已產生的 cache 就會失效。
調整根據環境變量產生 cache 的相關規則
如果你會在編譯時將環境變數直接寫進編譯工具(例如 Next.js 或 Create React App),那切記要告知 turbo
這些參數。 否則,可能會導致編譯時使用錯誤的環境變數!
你透過環境變數的數值可以控制 turbo
的 cache:
將環境變數加入到
pipeline
設定中的env
, 就可以影響每個任務的 cache fingerprint。在所有的任務中,任何名字中帶有
THASH
的環境變數都會影響 cache fingerprint。
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
// env vars will impact hashes of all "build" tasks
"env": ["SOME_ENV_VAR"],
"outputs": ["dist/**"]
},
// override settings for the "build" task for the "web" app
"web#build": {
"dependsOn": ["^build"],
"env": [
// env vars that will impact the hash of "build" task for only "web" app
"STRIPE_SECRET_KEY",
"NEXT_PUBLIC_STRIPE_PUBLIC_KEY",
"NEXT_PUBLIC_ANALYTICS_ID"
],
"outputs": [".next/**"]
}
}
}
宣告環境變數在
dependsOn
並配置$
前綴已經被 deprecated 了。
你可以宣告環境變數在 globalEnv
陣列裏, 所有的任務都會關注他來產生 cache:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
// env vars will impact hashes of all "build" tasks
"env": ["SOME_ENV_VAR"],
"outputs": ["dist/**"]
},
// override settings for the "build" task for the "web" app
"web#build": {
"dependsOn": ["^build"],
"env": [
// env vars that will impact the hash of "build" task for only "web" app
"STRIPE_SECRET_KEY",
"NEXT_PUBLIC_STRIPE_PUBLIC_KEY",
"NEXT_PUBLIC_ANALYTICS_ID"
],
"outputs": [".next/**"],
},
},
+ "globalEnv": [
+ "GITHUB_TOKEN" // env var that will impact the hashes of all tasks,
+ ]
}
自動包含環境變量
為了確保跨環境獲得正確的 cache, Turborepo 會自動推斷我們使用的框架, 並自動包含該框架會使用到的公開環境變數 來建立的應用程式的 cache key。
在 turbo.json
, 你可以安心的省略框架特定的的公開環境變數:
{
"pipeline": {
"build": {
"env": [
- "NEXT_PUBLIC_EXAMPLE_ENV_VAR"
]
}
}
}
注意,這個自動偵測跟包含只有在 Turborepo 成功推斷出你的應用程式使用什麼框架時才有用。 Turborepo 可以偵測的框架與環境變數如下:
Astro:
PUBLIC_*
Blitz:
NEXT_PUBLIC_*
Create React App:
REACT_APP_*
Gatsby:
GATSBY_*
Next.js:
NEXT_PUBLIC_*
Nuxt.js:
NUXT_ENV_*
RedwoodJS:
REDWOOD_ENV_*
Sanity Studio:
SANITY_STUDIO_*
Solid:
VITE_*
Vue:
VUE_APP_*
SvelteKit:
VITE_*
Vite:
VITE_*
有些例外沒有標記在上述列表上。 基於諸多原因, 即便我們在編譯過程中沒有用到, 部分 CI 系統 (包含 Vercel) 也會配置這些帶有前綴的環境變數。 這會使異動變的無法預判 - 使得每次編譯都會註銷 Turborepo 的 cache。
針對這點,Turborepo 使用
TURBO_CI_VENDOR_ENV_KEY
變數,讓 Turborepo 不去推斷這些環境變數。 例如,Vercel 設置了NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA
。 這個數值每次編譯都會改變, 所以 Vercel 會設置TURBO_CI_VENDOR_ENV_KEY="NEXT_PUBLIC_VERCEL_"
用於排除那些變數。幸運的, 你在其他建構系統才需要考慮這些問題, 當你在 Vercel 上使用 Turborepo 時不用考慮這些。
關於 monorepos 的註記
只有在某個任務使用到的框架, 有使用到的環境變數才會被包含到 cache key。
換句話說,假設使用 Next.js,其包含環境變數的 cache key, 只會存在在被偵測到使用 Next.js 的工作區。 其他工作區的任務則不受影響。
例如,假設有個 monorepo 有三個工作區: 一個 Next.js 專案, 一個 Create React App 專案, 一個 TypeScript 專案。
每個專案都有 build
腳本, 且應用程式部分都依賴於 TypeScript 專案。
讓我們假設這個 Turborepo 有標準的 turbo.json
pipeline 可以讓他們按照順序編譯:
{
"pipeline": {
"build": {
"dependsOn": ["^build"]
}
}
}
在 1.4 版之後, 當你執行 turbo run build
時, Turborepo 在構建 TypeScript 專案時, 不會考慮任何編譯時期的環境變量。
但是,在構建 Next.js 應用時, Turborepo 將推斷 NEXT_PUBLIC_
開頭的環境變量 可能會改變 .next
資料夾的輸出, 因此在做 hash 運算時會包含這些變量。 同樣,在計算 Create React App 的構建腳本的 hash 時, 將包括以 REACT_APP_PUBLIC_
開頭的所有編譯時期環境變量。
eslint-config-turbo
我們可以使用 eslint-config-turbo 輔助我們偵測沒用到的依賴套件被打包, 且幫助我們確定 Turborepo 的 cache 有被正確的分配在多個環境,
雖然自動包含環境變量應該能處理大多數情況和大多數框架, 但這個 ESLint 配置 將為使用其他構建時間 inline 環境變量的團隊提供即時反饋。 這也幫助支援那些無法使用自動偵測的框架的團隊。
為了啟用, 我們要讓根目錄的 eslintrc
繼承 eslint-config-turbo
:
{
// Automatically flag env vars missing from turbo.json
"extends": ["turbo"]
}
為了進行更多控制, 我們可以下載 eslint-plugin-turbo 並配置在 plugin
裏, 並添加我們需要的規則:
{
"plugins": ["turbo"],
"rules": {
// Automatically flag env vars missing from turbo.json
"turbo/no-undeclared-env-vars": "error"
}
}
當你在程式碼中使用非框架相關的環境變量且沒有被標注在 turbo.json
時,plugin 就會進行警告。
沒辦法捕捉到的環境變量
雖然 Turborepo 都運行在我們想執行的任務之前, 但確實有可能在 turbo
已經計算完 hash 之後, 才建立或是異動環境變數。
例如以下 package.json
:
{
"scripts": {
"build": "NEXT_PUBLIC_GA_ID=UA-00000000-0 next build",
"test": "node -r dotenv/config test.js"
}
}
在這個例子, turbo 在調用 build
之前,已經做完 hash 的運算了, 所以無法發現 NEXT_PUBLIC_GA_ID=UA-00000000-0
環境變量, 因此無法區分 cache 是否有包含該變量, 在 dotenv
配置的環境變量也是會有這個問題。
所以要小心在調用 turbo
之前要配置好所有環境變量!
強制覆寫 cache
如果你想要 turbo
不要去讀 cache 或是強制重新計算 cache, 可以在指令後面加上 --force
:
# Run `build` npm script in all workspaces,
# ignoring cache hits.
turbo run build --force
注意到
--force
關閉了 cache 讀取但沒有關閉 cache 產生, 如果你想要關閉 cache 產生,使用--no-cache
。
Logs
turbo
不只是會 cache 任務的 output, 他同樣會紀錄 terminal 的 output (就是 stdout/stderr), 並將它記錄在 <package>/.turbo/run-<command>.log
。 當 turbo
判斷可以使用 cache 時, 他就會將紀錄的 output 直接再拿出來用。
Hashing
現在,你可能想知道 turbo
是如何決定該任務有 cache 還是 沒 cache。
首先,turbo
會使用當前專案的全域狀態來建立 hash:
- 所有檔名符合標註在
globalDependencies
的 glob pattern, 以及環境變數。
所有名字帶有 THASH
的環境變量。 (像是 STRIPE_PUBLIC_THASH_SECRET_KEY
, 但沒有 STRIPE_PUBLIC_KEY
)
然後,它加入更多跟當前工作區任務有關的變因:
將工作區中所有有進到版本控制的檔案內容進行 hash, 或者如果有提供 input glob 的話,將符合的檔案內容進行 hash。
所有內部依賴關係的 hash
在 pipeline 中指定的 output
在根目錄
package.json
的 lockfile 中標註的, 所有 dependencies、devDependencies 和 optionalDependencies 的版本工作區任務的名稱
標記在
pipeline.<task-or-package-task>.dependsOn
的環境變量名稱與其對應的環境變量數值。
一旦 turbo
在執行某個工作區的任務, 它就會在 cache(本地和遠端)中檢查是否有匹配的 hash。
如果匹配,則會跳過執行該任務, 將 cache 的 output 移動或下載到指定位置, 並重現之前記錄的 log。
如果 cache(本地或遠端)中沒有任何與計算出的 hash 匹配的項目, 則 turbo
將在本地端執行任務, 然後輸出 cache 並使用 hash 作為其索引。
在任務執行中,可以從環境變量 TURBO_HASH
得到當前 hash, 這個值在 output 或 Dockerfile
用來打 label 時很有用。
自 turbo v0.6.10 起, 使用
npm
或pnpm
時,turbo
的 hash 算法會略有不同。 使用這些套件管理,turbo
會將在每個工作區任務的 lockfile 內容包含進 hash 算法。 不會像當前的 yarn 那樣分析出所有依賴關係的集合。