什么是monorepo

A monorepo is single repository containing multiple distinct projects, with well-defined relationships, which means projects inside depend on each other, they share code.

Monorepo and Monolith

A good monorepo is the opposite of monolithic, and monorepos are not a silver bullet. Nothing is.

银弹(silver bullet)出自The Mythical Man-Month 中文意义好比万金油

使用pnpm管理monorepo

pnpm的node_module结构跟npm构建的不太一样, 具体见这里Flat node_modules is not the only way, 以及explain the circular symlink

优点

  • install using global store
  • Dependencies are symlinked to reduce complexity
  • Hard linking from global store(prevent many copies of same package)

概念解释

  1. symlink and hardlink
  • linux系统中有一个重要概念inode(the abbreviation for "index node"). 它是文件和目录的唯一标志符, inode存储了元数据(metadata). 元数据包含(fileType、fileSize、ownerId、read write and execute permission 、last change time···) 并且有一个唯一的inode number, 使用命令ls -i (file name|directory name)可查看.

    一般情况, 一个文件名“唯一”对应一个 inode. 但是linux允许多个文件名都硬连接到同一个inode. 这表示我们可以使用不同的文件名访问同样的内容, 对文件内容进行修改将“反映”到所有文件, 删除目标文件不影响其它拥有相同inode number但文件名不同的hardlink访问,只有最后一个hardlink被删除, 这个inode number才会被释放, 这种机制就被称为硬连接hardlink.

  • 两张图总结: 图1 /usr/sbin/mail/var/qmail/bin/sendmail都是hardlink, 图2 /usr/sbin/mail是一个symlink指向一个hardlink/var/qmail/bin/sendmail
   Figure 1-hardlink
hardlink
   Figure 2-symlink
symlinklink
Hard LinksSoft Links
It is a copy of the original file that serves as a pointer to the same file, allowing it to be accessed even if the original file is deleted or relocated.It is a short pointer file that links a filename to a pathname. It's nothing more than a shortcut to the original file, much like the Windows OS's shortcut option.
It has a similar inode number to the target file.It has a different inode number.
It is not allowed the relative path.It allows both relative and absolute paths.
It cannot be established outside the file system.It may be established in the file system.
It has an additional name for the original file that references to the target file through inode.It is different from the original file and is an alternative for it, but it does not use inode.
It may only link to a file.It may link both to a directory or a file.
It remains valid even if the target file is deleted.It becomes invalid when the originating file is deleted.
  • 上文说到pnpm insall后的node_modules目录不太一样, 实际上项目的依赖都经由软连接指向同级别目录中的.pnpm目录里.
>$ tree -L 2
├── node_modules
│   ├── .pnpm
│   ├── @changesets
│   ├── @eslint
│   ├── @types
│   ├── @typescript-eslint
│   ├── eslint -> .pnpm/[email protected]/node_modules/eslint
│   ├── prop-types -> .pnpm/[email protected]/node_modules/prop-types
│   ├── tsup -> .pnpm/[email protected][email protected]/node_modules/tsup
│   └── typescript -> .pnpm/[email protected]/node_modules/typescript
  1. global store
    指的是Mac/linux中/Users/<你的用户名>/.pnpm-store路径下的公共文件. pnpm安装项目依赖的时候, 如果依赖包存在在该路径下, 直接使用
pnpm-link.webp

详细分析文章, 配合上图Symlink node_modules structure

workspace

workspace必须有一个pnpm-workspace.yaml在其根目录. 以nextra的提交版本bc319706为例.

pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'examples/*'

工程目录结构

├── examples
│   ├── blog
│   ├── docs
│   └── swr-site
├── node_modules
├── package.json
├── packages
│   ├── nextra
│   ├── nextra-theme-blog
│   └── nextra-theme-docs
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── prettier.config.js
├── renovate.json
└── turbo.json

执行pnpm run dev --recursive有如下输出

Scope: all 7 workspace projects
. dev$ turbo run dev
└─ Running...
│ • Packages in scope: blog, docs, nextra, nextra-theme-blog, nextra-theme-docs, swr-site
└─ Running...

正好对应examplespackages目录下的子工程, 接着看examples/docs目录下的package.json.

{
  "name": "docs",
  "dependencies": {
    "react": "*",
    "react-dom": "*",
    "next": ">=13",
    "nextra": "workspace:*",
    "nextra-theme-docs": "workspace:*"
  },
  "dependenciesMeta": {
    "nextra": {
      "injected": true
    },
    "nextra-theme-docs": {
      "injected": true
    }
  }
}

dependenciesMeta.*.injected的作用官网讲的很清楚了。

在文中的例子nextra-theme-docsdocs都有相同的依赖项react react-dom等等, 而且reactnextra-theme-docs中声明在peerDependencies里面, 那么被injected的依赖包nextra将会安装宿主host package的react版本.
另外, 关于peerDependencies由来参见Domenic's peerDependencies blog. 简单来说该选项的作用是当NPM解析peerDependencies里面依赖时, 首先判断依赖是不是安装了, 安装了且版本兼容则忽略, 否则, 如果根目录存在该依赖的不同版本, 则在自身的node_modules下面安装, 根目录不存在则会安装到根目录.

{
  "name": "nextra-theme-docs",
  "version": "2.0.2",
  "peerDependencies": {
    "next": ">=9.5.3",
    "react": ">=16.13.1",
    "react-dom": ">=16.13.1"
  },
}

exports field in package.json

公用包编译完成打包, 推荐仅导出需要暴露出的模块, 参考写法如下
exports definition
webpack package-exports
proposal-pkg-exports

扩展文章

Everything you need to know about monorepo
Monorepo生态
介绍 Google 如何将数十亿代码通过 monorepo 方式组织的
掘金-pnpm
monorepo下模块包设计实践