6月4日 23:09

npm workspaces monorepo:构建顺序、TypeScript配置和CI实践

npm workspaces 是 npm 7+ 内置的 monorepo 方案——不需要安装额外工具,在 package.json 里声明 workspaces 就能用。但"能用"和"好用"之间有不少坑:workspace 间的依赖引用、构建顺序、TypeScript 配置、CI 下的缓存策略,这些才是实际项目里反复踩的。这篇文章把从搭建到上线的完整流程走一遍。

基本配置

目录结构

shell
my-monorepo/ ├── package.json # 根配置,声明 workspaces ├── package-lock.json # 统一的锁文件 ├── packages/ │ ├── utils/ # 工具库 │ │ └── package.json │ ├── core/ # 核心业务 │ │ └── package.json │ └── app/ # 应用 │ └── package.json └── tsconfig.base.json # 共享的 TypeScript 配置

根 package.json

json
{ "name": "my-monorepo", "private": true, "workspaces": ["packages/*"] }

private: true 必须设——根目录不是可发布的包,npm publish 时会跳过。

子包 package.json

json
// packages/utils/package.json { "name": "@myorg/utils", "version": "1.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "dev": "tsc --watch" } }
json
// packages/app/package.json { "name": "@myorg/app", "version": "1.0.0", "dependencies": { "@myorg/utils": "workspace:*" // 引用 workspace 内的包 } }

workspace:* 是 workspace 协议——npm 会在 node_modules 里创建符号链接指向 packages/utils,而不是从 registry 下载。本地开发改了 utils 的代码,app 里直接生效,不需要 npm link

安装依赖

bash
# 在根目录安装所有 workspace 的依赖 npm install # 给特定 workspace 加依赖 npm install lodash --workspace=@myorg/app # 给根目录加依赖(构建工具等) npm install -D typescript -w .

一个 npm install 解决所有 workspace 的依赖,只生成一个 package-lock.json。这比每个子目录单独 install 高效得多。

构建顺序

monorepo 最大的痛点之一:@myorg/app 依赖 @myorg/utils,utils 没编译,app 就引用不到类型定义。npm workspaces 本身不管理构建顺序

手动按顺序构建

json
// 根 package.json { "scripts": { "build": "npm run build -w @myorg/utils -w @myorg/core -w @myorg/app" } }

-w 按声明顺序执行。缺点:每次加新包要手动改这个列表。

用 npm-run-all 并行构建

bash
npm install -D npm-run-all
json
{ "scripts": { "build:utils": "npm run build -w @myorg/utils", "build:core": "npm run build -w @myorg/core", "build:app": "npm run build -w @myorg/app", "build": "run-s build:utils build:core build:app" } }

run-s 串行执行,run-p 并行执行。没有依赖关系的包可以并行,有依赖的串行。

更好的方案:用 turborepo 或 nx

大型 monorepo 用 npm workspaces 管构建顺序太痛苦。turborepo 和 nx 可以自动分析依赖图,只构建有变化的包:

bash
npm install -D turbo
json
// 根 package.json { "scripts": { "build": "turbo run build" } }

turborepo 自动分析 @myorg/app 依赖 @myorg/utils,先构建 utils 再构建 app,且只构建有改动的包。和 npm workspaces 不冲突——turborepo 只是调度层,底层还是 npm。

TypeScript 配置

monorepo 里 TypeScript 项目引用(Project References)是关键——让 tsc 知道包之间的依赖关系,支持增量编译。

共享基础配置

json
// tsconfig.base.json { "compilerOptions": { "target": "ES2020", "module": "commonjs", "strict": true, "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "dist", "rootDir": "src" } }

子包配置

json
// packages/utils/tsconfig.json { "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true // 必须设,支持 project references }, "include": ["src"] }
json
// packages/app/tsconfig.json { "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true }, "references": [ { "path": "../utils" } // 声明对 utils 的引用 ], "include": ["src"] }

composite: true + references 让 tsc 先编译依赖包,再编译当前包。增量编译只重新编译有变化的包。

发布

workspace 协议转真实版本

workspace:* 是本地开发用的占位符,发布时 npm 会自动替换成实际版本号:

json
// 开发时 "dependencies": { "@myorg/utils": "workspace:*" } // npm publish 时自动替换为 "dependencies": { "@myorg/utils": "^1.0.0" }

逐个发布

bash
# 先构建所有包 npm run build # 发布特定包 npm publish --workspace=@myorg/utils # 发布所有包(按依赖顺序) npm publish --workspaces

CI/CD 配置

GitHub Actions 缓存

yaml
- uses: actions/setup-node@v4 with: node-version: 20 cache: npm # 自动缓存 ~/.npm - run: npm ci # 严格按 lock 文件安装 - run: npm run build - run: npm test

cache: npm 缓存 npm 的全局缓存目录,npm ci 从缓存安装比从网络快 5-10 倍。

只构建变更的包

配合 turborepo,CI 里只构建受 PR 影响的包:

bash
npx turbo run build --filter=...[HEAD^1]

--filter=...[HEAD^1] 表示"从上一个 commit 到现在有变化的包,以及依赖它们的包"。

npm workspaces 的局限

  • 没有内置构建调度:不分析依赖图,不缓存构建产物
  • 没有依赖图可视化:不知道哪个包依赖哪个
  • -w 不够灵活:不能按"依赖了 X 的所有包"过滤,只能指定包名
  • 发布流程手动:不会自动按依赖顺序发版、不会自动 bump 版本号

如果你遇到这些痛点,说明项目规模已经超出了 npm workspaces 的舒适区——考虑引入 turborepo 做构建调度,或者迁移到 pnpm workspaces(过滤能力更强)。

标签:NPM