Cách thiết lập monorepo pnpm + Turborepo từ đầu
Dựng đúng monorepo pnpm + Turborepo từ đầu: task caching thực sự với cú pháp v2 "tasks", không phải "pipeline" deprecated trong hầu hết tutorial.
Bởi Ethan
1.515 từ · 8 phút đọc
Phần lớn các hướng dẫn về setup này đều sai. Không phải sai theo kiểu tinh tế — chúng dùng một key trong turbo.json đã bị loại bỏ từ Turborepo v2, khiến build của bạn hoặc im lặng fallback hoặc ném ra warning mà bạn phải biết mà tìm. Hướng dẫn này dùng cú pháp mặc định hiện tại: pnpm 10+, Turborepo 2.x, Node 20+.
Bài viết này dành cho ai
Một lập trình viên mid-level đã làm việc trong monorepo nhưng chưa bao giờ tự setup từ đầu. Bạn đã có Node và pnpm. Bạn muốn cache task thực sự giữa các package, không chỉ là dùng chung node_modules.
Nếu bạn vẫn đang cân nhắc giữa pnpm và npm, hãy xem pnpm vs npm — điều thực sự thay đổi khi bạn chuyển đổi trước.
Nếu bạn đã có repo và chỉ muốn thêm Turborepo vào, hướng dẫn adding to an existing monorepo có phần incremental path. Hướng dẫn này bắt đầu từ con số không.
Bước 1: Cài đặt trước
Bạn cần Node 20+ và pnpm 10+. Kiểm tra phiên bản đang có:
node --version # should be v20 or higher
pnpm --version # should be 10.x or higher
Để cài hoặc nâng cấp lên pnpm 10:
npm install -g pnpm@10
Nếu bạn đang dùng pnpm 11, lưu ý rằng pnpm 11 standalone yêu cầu Node 22+. Hướng dẫn này lấy pnpm 10 làm phiên bản tối thiểu — mọi thứ ở đây cũng chạy được trên 11, nhưng đừng dùng pnpm 11 với Node 20.
Lỗi thường gặp: Nếu pnpm --version hiển thị 8.x hay 9.x, hãy nâng cấp. Các phiên bản pnpm cũ có cài đặt mặc định khác cho linkWorkspacePackages, có thể gây ra hoisting không mong đợi khi kết hợp với Turborepo.
Bước 2: Dựng cấu trúc workspace
mkdir my-monorepo && cd my-monorepo
Tạo pnpm-workspace.yaml ở thư mục gốc:
packages:
- "apps/*"
- "packages/*"
File này không thể thiếu. Không có nó, pnpm sẽ coi mỗi thư mục là một package độc lập và các workspace protocol link (workspace:*) sẽ báo lỗi resolution khi install.
Tạo package.json ở thư mục gốc:
{
"name": "my-monorepo",
"private": true,
"packageManager": "[email protected]",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
},
"devDependencies": {
"turbo": "latest"
}
}
Trường packageManager là bắt buộc, không phải tùy chọn. Corepack dùng nó để ghim phiên bản, và tài liệu của Turborepo cũng đặt nó là yêu cầu. Thiếu trường này, bạn sẽ gặp version drift giữa các môi trường dev ngay khi có người thứ hai clone repo.
Bây giờ install:
pnpm install
Lỗi thường gặp: Nếu bạn gặp ERR_PNPM_WORKSPACE_NOT_FOUND hay tương tự, kiểm tra xem pnpm-workspace.yaml có nằm ở thư mục gốc của repo không, không phải trong thư mục con.
Bước 3: Thêm Turborepo
Tạo turbo.json ở thư mục gốc:
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
}
}
}
Key là "tasks", không phải "pipeline". Mọi hướng dẫn từ 2022–2023 đều dùng pipeline — đó là key của v1. Turborepo v2 đổi tên thành tasks. Nếu bạn paste config từ trước 2024, ít nhất bạn sẽ nhận được deprecation warning; ở một số phiên bản, build im lặng bỏ qua hoàn toàn việc cache.
Thêm .turbo vào .gitignore:
.turbo
Lỗi thường gặp: Nếu bạn thấy ERROR: turbo.json references a key "pipeline" hoặc không có gì được cache, bạn đang dùng config v1. Thay "pipeline" bằng "tasks" và chạy lại.
Bước 4: Tạo hai package và kết nối chúng
Tạo một thư viện utility và một app phụ thuộc vào nó:
mkdir -p packages/utils/src apps/web/src
packages/utils/package.json:
{
"name": "@acme/utils",
"version": "0.0.1",
"private": true,
"main": "./dist/index.js",
"exports": {
".": {
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -b",
"dev": "tsc -b --watch"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
packages/utils/src/index.ts:
export function greet(name: string): string {
return `Hello, ${name}!`;
}
packages/utils/tsconfig.json:
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"module": "commonjs",
"target": "es2020",
"strict": true
},
"include": ["src"]
}
apps/web/package.json:
{
"name": "@acme/web",
"version": "0.0.1",
"private": true,
"dependencies": {
"@acme/utils": "workspace:*"
},
"scripts": {
"build": "tsc -b",
"dev": "tsc -b --watch"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
apps/web/src/index.ts:
import { greet } from "@acme/utils";
console.log(greet("world"));
apps/web/tsconfig.json:
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"module": "commonjs",
"target": "es2020",
"strict": true
},
"references": [{ "path": "../../packages/utils" }],
"include": ["src"]
}
Bây giờ install để tạo workspace:* link:
pnpm install
pnpm tạo symlink từ node_modules/@acme/utils về packages/utils. Protocol workspace:* nghĩa là “dùng phiên bản local hiện có”. Khi pnpm publish, nó sẽ tự động chuyển thành semver range thực.
Lỗi thường gặp: Nếu apps/web không resolve được @acme/utils lúc build, kiểm tra xem pnpm install đã chạy sau khi bạn thêm dependency chưa. Symlink được tạo ra lúc install, không phải lúc khởi tạo repo.
Lỗi thường gặp (phantom dependencies): pnpm không hoist theo mặc định. Nếu app của bạn cố import một package nằm trong node_modules của @acme/utils nhưng không được khai báo trong apps/web/package.json, nó sẽ thất bại. Đây là chủ ý — pnpm bắt buộc khai báo dependency rõ ràng. Sửa bằng cách thêm package còn thiếu vào đúng package.json.
Bước 5: Chạy turbo build lần đầu và kiểm tra cache
pnpm turbo build
Lần chạy đầu tiên sẽ build tất cả. Bạn sẽ thấy Turborepo xác định dependency graph, chạy build của @acme/utils trước (vì apps/web phụ thuộc vào nó qua dependsOn: ["^build"]), rồi build @acme/web.
Chạy lại ngay lập tức:
pnpm turbo build
Kết quả:
• Packages in scope: @acme/utils, @acme/web
• Running build in 2 packages
• Remote caching disabled
@acme/utils:build: cache hit, replaying logs dc3f1a2...
@acme/web:build: cache hit, replaying logs 9b4e8c1...
Tasks: 2 successful, 2 total
Cached: 2 cached, 2 total
Time: 87ms >>> FULL TURBO
“FULL TURBO” nghĩa là mọi task đều được khôi phục từ local cache mà không cần thực thi lại. Trên một project thực với 10–30 package, sự khác biệt này tính bằng phút, không phải mili giây.
Để debug khi cache miss:
pnpm turbo build --summarize
Lệnh này ghi một file JSON vào .turbo/runs/ với toàn bộ thông tin hash — những file nào ảnh hưởng đến cache key, biến môi trường nào được đưa vào, và hash khác với lần chạy trước ở điểm nào. Khá dài dòng, nhưng đây là câu trả lời chính xác khi cache hoạt động không như mong đợi.
Bước 6: Những lỗi hay gặp
pipeline vs tasks: Đã đề cập nhưng đáng nhắc lại. Nếu bạn đang sửa một turbo.json cũ, search-and-replace "pipeline" thành "tasks". Configuration reference là nguồn chính thống cho cú pháp v2 hiện tại — URL migration guide v1→v2 trả về 404 từ năm 2026.
Thiếu pnpm-workspace.yaml: Lỗi setup phổ biến nhất. File này phải có ở thư mục gốc trước khi bạn chạy pnpm install. Nếu bạn thêm vào sau, hãy chạy lại pnpm install để tạo lại symlink.
Thiếu outputs: Không có "outputs": ["dist/**"] trên một build task, Turborepo vẫn chạy task nhưng không lưu file nào vào cache. Lần chạy tiếp theo sẽ khớp cache hash nhưng không khôi phục gì. Các package phía sau sẽ thất bại vì dist/ trống. Luôn khai báo outputs cho những task tạo ra file.
"^build" vs "build" trong dependsOn: "^build" nghĩa là “chạy build ở tất cả package mà package này phụ thuộc vào”. "build" (không có ^) nghĩa là “chạy build trong cùng package trước”. Với phần lớn build task, bạn cần "^build". Dùng nhầm "build" khiến các package build theo thứ tự sai khi có cross-package dependency.
cache: false cho dev task: Config của task dev ở trên dùng "cache": false và "persistent": true. Không có cache: false, Turborepo có thể cố cache output của một process chạy dài — điều này không có ý nghĩa. Không có persistent: true, Turborepo không biết task được thiết kế để chạy vô thời hạn và có thể không xử lý đúng vòng đời process trong turbo dev.
Tính nghiêm ngặt của pnpm hoisting: Strict hoisting của pnpm là tính năng, không phải lỗi — nó buộc bạn khai báo mọi dependency mà code thực sự dùng. Nhưng nó phá vỡ những package được viết với giả định flat hoisting kiểu npm. Nếu bạn thấy Cannot find module 'X' cho một package bạn không cài rõ ràng, hãy thêm nó vào package.json của package đang dùng nó.
Tài liệu tham khảo
- pnpm workspaces
- pnpm-workspace.yaml reference
- Turborepo: getting started
- Turborepo: configuring tasks
- Turborepo: caching
- turbo.json v2 configuration reference
- Creating internal packages
Sau khi monorepo chạy được, thiết lập Claude Code cho monorepo hướng dẫn cách scope AI theo từng package riêng biệt bằng CLAUDE.md hierarchy và worktrees.