blog.dopana

Back

Monorepo là chiến lược đặt nhiều package/project trong cùng một repository. Tưởng phức tạp — nhưng với công cụ hiện đại, nó đơn giản hơn bạn nghĩ.

Monorepo vs Polyrepo#

MonorepoPolyrepo
Code chung 1 repoMỗi project 1 repo riêng
Dễ share codePhải publish package
atomic commit xuyên packagePhải release nhiều repo
Tooling phức tạp hơnTooling đơn giản hơn
CI/CD 1 pipelineCI/CD nhiều pipeline

pnpm Workspaces — Nền Tảng#

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
yaml

Cấu trúc project:

my-monorepo/
├── apps/
│   ├── web/          # Next.js app
│   ├── api/          # Express API
│   └── admin/        # Admin dashboard
├── packages/
│   ├── ui/           # Shared UI components
│   ├── utils/        # Shared utilities
│   ├── config/       # ESLint, tsconfig
│   └── db/           # Prisma schema + client
├── pnpm-workspace.yaml
├── package.json      # Root package.json
└── turbo.json
plaintext

Root package.json#

{
  "private": true,
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "test": "turbo test"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.5.0"
  }
}
json

Turborepo — Cache & Task Runner#

Turborepo caches output — nếu code không đổi, build lại trong mili giây:

# Lần 1 — build thật
turbo build
# Tasks: 5 successful, 0 cached

# Lần 2 — dùng cache
turbo build
# Tasks: 0 successful, 5 cached → < 1s
bash

Cấu Hình Package#

packages/ui/package.json#

{
  "name": "@myorg/ui",
  "private": true,
  "exports": {
    "./button": "./src/Button.tsx",
    "./card": "./src/Card.tsx"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@myorg/config": "workspace:*",
    "typescript": "^5.5.0"
  }
}
json

apps/web/package.json#

{
  "name": "web",
  "dependencies": {
    "@myorg/ui": "workspace:*",
    "@myorg/utils": "workspace:*",
    "@myorg/db": "workspace:*",
    "next": "^14.0.0"
  }
}
json

Local Development#

# Chạy tất cả apps
pnpm dev

# Chạy specific app
pnpm --filter web dev

# Thêm dependency
pnpm --filter web add zod

# Thêm shared package vào app
pnpm --filter web add @myorg/ui

# Build specific package và dependent
pnpm --filter @myorg/ui build
bash

Dependency Graph#

                    web
                   / | \
                  /  |  \
                 /   |   \
              ui   utils  db
               |           |
               +-------+---+
                       |
                    config
plaintext

Turborepo tự động xác định thứ tự build dựa trên dependency graph.

Shared Config#

packages/config/tsconfig.json#

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true
  }
}
json

apps/web/tsconfig.json#

{
  "extends": "@myorg/config/tsconfig.json",
  "compilerOptions": {
    "jsx": "preserve",
    "plugins": [{ "name": "next" }]
  }
}
json

packages/config/eslint.js#

module.exports = {
  extends: ['turbo', 'prettier'],
  rules: {
    '@typescript-eslint/no-unused-vars': 'error',
  },
};
javascript

CI/CD với Monorepo#

Nx — Alternative Cho Turborepo#

Nx mạnh hơn Turborepo ở một số điểm:

Nx có generator (code scaffolding), dependency graph visualization, và affected commands:

# Chỉ test package bị ảnh hưởng bởi thay đổi
nx affected:test

# Visualize dependency graph
nx graph
bash

Docker Build#

Kết Luận#

Monorepo không dành cho mọi project — nếu team < 5 người, project nhỏ, polyrepo vẫn ổn. Nhưng khi bạn có:

  • Nhiều app share code
  • Nhiều package cần atomic change
  • Team muốn reuse config, types, utils

Thì monorepo với pnpm + Turborepo là lựa chọn tuyệt vời. Bắt đầu từ đơn giản, thêm package khi cần.

Tài liệu tham khảo#