npm workspaces monorepo:构建顺序、TypeScript配置和CI实践
npm workspaces 是 npm 7+ 内置的 monorepo 方案——不需要安装额外工具,在 package.json 里声明 workspaces 就能用。但"能用"和"好用"之间有不少坑:workspace 间的依赖引用、构建顺序、TypeScript 配置、CI 下的缓存策略,这些才是实际项目里反复踩的。这篇文章把从搭建到上线的完整流程走一遍。
基本配置
目录结构
shellmy-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 并行构建
bashnpm 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 可以自动分析依赖图,只构建有变化的包:
bashnpm 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 影响的包:
bashnpx turbo run build --filter=...[HEAD^1]
--filter=...[HEAD^1] 表示"从上一个 commit 到现在有变化的包,以及依赖它们的包"。
npm workspaces 的局限
- 没有内置构建调度:不分析依赖图,不缓存构建产物
- 没有依赖图可视化:不知道哪个包依赖哪个
-w不够灵活:不能按"依赖了 X 的所有包"过滤,只能指定包名- 发布流程手动:不会自动按依赖顺序发版、不会自动 bump 版本号
如果你遇到这些痛点,说明项目规模已经超出了 npm workspaces 的舒适区——考虑引入 turborepo 做构建调度,或者迁移到 pnpm workspaces(过滤能力更强)。