小Cの已经记不起来的博客

用 pnpm workspace 管理 TypeScript 多包项目依赖

之前项目里包一多,每个包单独 npm install,改个公共包的版本,得挨个改 package.json,然后挨个 install,烦得很。后来换成 pnpm workspace,一开始没搞明白 workspace:*,改完 shared 的代码,web 里死活不更新,查了半天才发现依赖版本号写死了。下面是我现在用的方式,不算什么最佳实践,但跑起来挺省心。

先看清楚目录结构

我一般把项目放成这样:

my-monorepo/
  package.json
  pnpm-workspace.yaml
  tsconfig.base.json
  packages/
    shared/
      package.json
      tsconfig.json
      src/
    web/
      package.json
      tsconfig.json
      src/
    server/
      package.json
      tsconfig.json
      src/

packages 下每个目录就是一个独立的包,有自己的 package.json 和 tsconfig.json。根目录只放公共配置和脚本,不放业务代码。

根目录就两个关键文件

第一个是 pnpm-workspace.yaml,内容很简单:

packages:
  - 'packages/*'

这行就是告诉 pnpm,packages 下面每个文件夹都算 workspace 成员。你要是还有 apps 目录,就再加一行 - 'apps/*'。

第二个是根目录的 package.json:

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "pnpm -r build",
    "dev": "pnpm --parallel -r dev",
    "typecheck": "pnpm -r typecheck"
  },
  "devDependencies": {
    "typescript": "^5.4.5"
  }
}

private 一定要设成 true,别手贱去掉,不然 pnpm 可能会把根包当发布包处理。公共的开发依赖,比如 typescript、eslint、prettier,统一装在根上,用 pnpm add -w -D typescript,-w 就是 workspace root 的意思。别在根上装业务依赖,不然子包引用起来会乱。

包之间依赖用 workspace:*

比如 web 要用 shared,就在 packages/web/package.json 里这么写:

{
  "name": "@my/web",
  "dependencies": {
    "@my/shared": "workspace:*"
  }
}

workspace:* 会直接链接到本地包,你改 shared 的代码,web 里立刻就能看到。如果写具体版本号,比如 "^1.0.0",pnpm 可能去 registry 拉,本地改动不生效。这个坑我踩过,改了半天 shared,web 里一点反应没有,后来把版本号换成 workspace:* 才正常。

给某个包加依赖,别 cd 到子包再 pnpm add,统一在根目录用 --filter:

pnpm add axios --filter @my/web
pnpm add -D vitest --filter @my/shared

--filter 后面写包名全称,别只写 web,不然匹配不到。装完看一眼 pnpm-lock.yaml,只有一个,提交它就够了。

TypeScript 配置别各写各的

多包项目类型检查容易出问题,我一般抽一个根 tsconfig.base.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "declaration": true,
    "composite": true,
    "skipLibCheck": true
  }
}

每个子包的 tsconfig.json 继承它:

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

如果不想每个包单独 build,可以在根目录加一个 tsconfig.json,用 project references:

{
  "files": [],
  "references": [
    { "path": "packages/shared" },
    { "path": "packages/web" },
    { "path": "packages/server" }
  ]
}

然后 tsc -b 一次性构建,shared 会先于 web 构建。注意 composite 和 declaration 得开着,不然 references 会报错。

常用命令和几个坑

pnpm -r build 会按拓扑顺序递归执行所有包的 build,shared 先跑,web 后跑。想并行跑开发服务,用 pnpm --parallel -r dev。只想跑某个包,就 pnpm --filter @my/web dev。

几个实际遇到的坑:pnpm 的 node_modules 是符号链接结构,有些老工具不认,比如旧版 webpack 要配 resolve.symlinks,Vite 一般没事。子包里的 .bin 实际在根 node_modules/.bin,脚本里直接写命令就行。还有,如果某个包要独立部署,记得用 pnpm deploy 把 workspace 链接打包成实际依赖,别直接把带符号链接的 node_modules 扔上去。

大概就这些。先把根目录、workspace:* 和 --filter 弄对,后面基本就顺了。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放