Command Palette

Search for a command to run...

Command Palette

Search for a command to run...

Tech
PreviousNext

Nx Monorepo Architecture Evolution: From Categories to Composition

From the deprecated Package-Based vs Integrated classification to the modern Workspaces + Project References approach

「這個 Nx repo 到底是 package-based 還是 integrated?」——想回答這個問題,卻發現官方已經不這樣分了

起因

在建構一個 Nx monorepo 的過程中,想搞清楚自己的 repo 屬於哪種架構。它同時用了 pnpm workspace:^ 協定、TypeScript project references、還有 nx sync 自動維護 tsconfig

兩邊都沾到:各 package 有自己的 package.json(像 package-based),但又沒有用 paths mapping(不像傳統的 integrated)

去翻了 Nx 官方文件才發現——這個分類已經被廢棄了


Nx 的舊分類

Nx 過去把 monorepo 分成三種類型:

類型特徵依賴方式版本管理
Package-Based各 project 有自己的 package.jsonworkspace:* 協定互相依賴各自管理版本
Integrated共用根層級設定TypeScript imports + paths mapping統一版本管理
Standalone單一應用不適用不適用

Package-Based

每個 library 或 app 都是一個獨立的 npm package,有自己的 package.jsontsconfig.json,透過 workspace protocol 互相引用:

{
  "dependencies": {
    "@myorg/shared-utils": "workspace:*",
    "@myorg/ui-components": "workspace:^"
  }
}

好處是每個 package 邊界清楚,可以獨立發布

壞處是設定檔散落各處,維護成本高

Integrated

沒有各自的 package.json,所有 project 共享根層級的 node_modules,透過 tsconfig.base.jsonpaths 做模組解析:

{
  "compilerOptions": {
    "paths": {
      "@myorg/shared-utils": ["libs/shared-utils/src/index.ts"],
      "@myorg/ui-components": ["libs/ui-components/src/index.ts"]
    }
  }
}

好處是設定集中,開發體驗好

壞處是 package 邊界模糊,不容易獨立發布

為什麼會搞混

現實中,大部分 repo 都是混合使用的——有些 package 需要獨立發布所以有 package.json,有些內部 library 只用 paths mapping。兩種做法並存才是常態


為什麼廢棄

從 Nx 20 開始,官方正式不再區分 package-based 和 integrated。

原因很直觀:

  1. 實務上都是混合體 — 你可能有些 package 需要 package.json 發布到 registry,有些純內部 library 只需要 paths mapping
  2. 功能是可組合的 — workspace protocol、paths mapping、project references 這些都是獨立的功能,不是互斥的選擇
  3. 工具已經成熟 — pnpm workspaces、TypeScript project references、Nx sync 可以協同運作,不需要二選一

與其問「我的 repo 是哪一類」,不如問「我用了哪些功能、為什麼用」。


現代做法:Workspaces + Project References

Nx 20+ 推薦的做法是把兩個正交的關注點拆開處理:

關注點工具作用
Package linkingpnpm/npm/yarn workspaces讓 packages 之間能互相 import
Type checkingTypeScript project referencestsc 能正確做增量型別檢查
Task orchestrationNx project graph決定 build/test 的執行順序與快取
Reference 同步nx sync自動維護 tsconfig references

Workspaces 處理 Package Linking

pnpm-workspace.yaml 定義 workspace 範圍:

packages:
  - "packages/*"
  - "apps/*"

然後在根層級 package.jsonworkspace:^ 協定引用內部 packages:

{
  "dependencies": {
    "@myorg/shared-utils": "workspace:^",
    "@myorg/ui-components": "workspace:^"
  }
}

workspace:^ 表示「用 workspace 裡的版本,發布時轉換成 ^x.y.z」。這樣開發時走 symlink,發布時是正常的 semver range

TypeScript Project References 處理型別檢查

每個 package 的 tsconfig.json 設定 composite: true

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist"
  }
}

然後在根層級的 tsconfig.jsonreferences 宣告依賴關係:

{
  "references": [
    { "path": "./packages/shared-utils" },
    { "path": "./packages/ui-components" },
    { "path": "./apps/my-app" }
  ]
}

這讓 TypeScript 能做增量編譯——只重新檢查有改動的 package 和它的下游。

nx sync 自動維護 References

手動維護 tsconfig.jsonreferences 很容易忘記或搞錯。Nx 提供 @nx/js:typescript-sync generator,根據 project graph 自動同步:

$ pnpm dlx nx sync

它會:

  1. 分析 Nx 的 project graph,找出 packages 之間的依賴關係
  2. 自動更新各個 tsconfig.jsonreferences 欄位
  3. 確保 compositedeclaration 等必要設定都在

一個完整範例

根層級 tsconfig.base.json(注意:沒有 paths):

{
  "compilerOptions": {
    "composite": true,
    "target": "ES2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "declaration": true
  }
}

根層級 package.json

{
  "name": "@myorg/root",
  "private": true,
  "devDependencies": {
    "@myorg/shared-utils": "workspace:^",
    "@myorg/ui-components": "workspace:^"
  }
}

各 package 的 package.json 用 source publishing(main 直接指向 source):

{
  "name": "@myorg/shared-utils",
  "main": "./src/index.ts",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts",
      "default": "./src/index.ts"
    }
  }
}

型別解析靠 pnpm symlink + package.json exports,不需要 paths

詳細機制在下一篇說明


跟其他工具的比較

面向Nxpnpm workspaces (單獨)Turborepo
Package linking用 workspace protocol用 workspace protocol用 workspace protocol
TypeScript 解析nx sync 自動維護 project references手動維護手動維護
Task orchestrationProject graph + 細粒度快取無(需搭配其他工具)Pipeline + hash-based 快取
tsconfig 維護@nx/js:typescript-sync 自動同步手動手動
增量建置基於 project graph 的 affected 命令基於 hash 比對
程式碼產生內建 generators
模組邊界ESLint plugin 強制執行

結論

不要再問「這是 package-based 還是 integrated」了。現代 monorepo 的正確思維是:

  1. 用 workspaces 處理 package 之間的 linking
  2. 用 project references 處理 TypeScript 的增量型別檢查
  3. 用 Nx sync 自動維護兩者之間的一致性
  4. 用 project graph 做 task orchestration 和快取

這些功能是正交的,可以按需組合,不需要硬塞進某個分類裡。


系列文章

  1. Nx Monorepo 架構演進 ← 目前位置
  2. 型別解析:paths 與 Project References
  3. moduleResolution:bundler vs nodenext

參考資源