关于 monorepo
的概念本文不再赘述,至于为什么选用 pnpm
作为包管理器而不用 npm
或者 yarn
,参考Pnpm: 最先进的包管理器
下面我们就用 pnpm
来搭建 menorepo
,看看 pnpm
能否胜任。
项目初始化
安装 pnpm
并创建项目
npx pnpm add -g pnpm // 全局安装 pnpm
mkdir menorepo
cd menorepo
npm init -y
创建 workspace
在项目根目录下新建一个 pnpm-workspace.yaml
文件,用来建立
工作空间
这也是建立管理 menorepo
的核心步骤,文件内容如下
packages:
# 所有在 packages 子目录下的 package
- 'packages/**'
# 不包括在 test 文件夹下的 package
- '!**/test/**'
后续我们的项目都将建立在 packages
目录下。
创建子项目
在 package
目录下新建两个子项目 core
和 utils
,
npm init -y // 创建 package.json 文件
tsc --init // 创建 tscconfig.json
touch index.ts // 创建 index.ts 文件
此时目录结构如下:
.
├── package.json
├── packages
│ ├── core
│ │ ├── index.ts
│ │ ├── package.json
│ └── tsconfig.json
│ └── utils
│ ├── index.ts
│ ├── package.json
│ └── tsconfig.json
└── pnpm-workspace.yaml
两个 package
的 name
分别修改为 core
和 utils
// packages/core
{
"name": "core",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"author": "",
"license": "MIT",
"dependencies": {}
}
// packages/utils
{
"name": "utils",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"author": "",
"license": "MIT",
"dependencies": {}
}
后续所有操作都是通过
name
这个字段来区分不同的子项目, 所以该字段的值必须保持唯一不可重复。
安装依赖
在 utils
里面声明一个打印日志的函数,并在 core
项目中进行引用
// packages/uilts/index.ts
import log from 'npmlog'
log.level = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : 'info'
log.heading = 'js-cli'
log.addLevel('success', 2000, {fg: 'green', bold: true})
export { log }
// packages/core/index.ts
import { log } from 'utils'
const core = () => {
console.log('core change')
log.info('test', 'Hello world!')
}
export default core
子项目依赖安装
pnpm add npmlog --filter utils // --filter 表示要作用到哪个子项目
后续给子项目单独安装依赖的时候,都可以通过 --filter
来指定具体的子项目
--filter 可以接多个项目名,--filter A --filter B
全局依赖安装
// 安装 node 声明文件,-W 表示安装在全局的 workspace 里, 这样所有 package 都可以共用该文件
pnpm add @types/node -WD
-WD 是 -W -D的缩写, -D 表示安装在
devDependencies
链接本地库文件
pnpm add utils --filter core // 在 core 里引用 utils
此时在 core
的 package.json
文件里会看到新增的依赖
// packages/core/package.json
"dependencies": {
"utils": "workspace:^1.0.0"
}
在 core
里引用 utils
// core/index.ts
import { log } from 'utils'
const core = () => {
log.info('test', 'Hello world!')
}
core()
export default core
在 core
目录下执行 ts-node index.ts
,即可看到打印结果
配置启动命令
子项目添加启动命令
修改 core/package.json
, 添加 start
命令
// packages/core/package.json
"scripts": {
"start": "ts-node index.ts",
},
笔者电脑已经全局安装了 ts-node
和 typescript
,所以可以直接用ts-node
命令。如果本地没有安装,或者想把依赖安装在当前项目里,可以这样操作
// 本地项目安装依赖
pnpm add ts-node typescript -WD
// 修改启动命令
"scripts": {
"start": "node_modules/ts-node/dist/bin.js index.ts",
},
切换到packages/core
目录执行npm run start
,即可看到打印结果
根目录添加启动命令
如果想在根目录也能运行 core
项目,修改根目录下的 package.json
// package.json
"scripts": {
"dev:core": "pnpm start --filter \"core\"",
},
切换到根目录执行dev:core
,即可看到打印结果
添加 bin 快捷命令
如果我们还想在其他子项目目录通过自己自定义的命令运行 core
项目,需要添加 bin
命令, 修改 core/package.json
文件,添加 demo-cli
自定义命令
// packages/core/package.json
"name": "core",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"bin": {
"demo-cli": "bin/index.ts"
},
"scripts": {
"start": "node_modules/ts-node/dist/bin.js index.ts",
},
在 core
目录下新增 bin
文件夹, 并在文件夹下创建 index.ts
文件,文件内容如下
#! /usr/bin/env ts-node
import core from '../index'
core()
由于我们当前是直接执行的 ts 文件,所以需要把 bin 的可执行文件环境设置成 ts-node 而不是 node
core
项目目录结构如下
.
├── bin
│ └── index.ts
├── index.ts
├── node_modules
├── package.json
└── tsconfig.json
链接库文件
切换到 core
目录下,执行
pnpm link -g // 把 core 这个库链接到全局
这时候,我们在其他目录,比如 utils
目录,执行
demo-cli
也能正确的打印出结果
取消链接
如果想取消链接,通过 which demo-cli
查看链接命令的文件路径
which demo-cli
\
到对应的 bin
目录下把 demo-cli
文件给删掉就行。
到这里,我们其实就能过愉快地用 ts
来写开发自己的库了,并且本地多个库也可以相互引用,方便联调测试;不同的库也可以共享相同的依赖,避免了依赖重复安装的麻烦。
打包
如果我们最终想把自己的库发布到 npm
供其他人下载使用,那么还需要把 ts
文件打包成 js
文件,在这里我们借助 rollup
来实现,我们以改造 core
这个子项目为例。
安装依赖
pnpm add tslib @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-clear -WD
笔者电脑已经全局安装了 rollup,如果没有安装的话请自行安装
配置 rollup
在 core
目录下新建 rollup.config.js
文件,内容如下
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import clear from 'rollup-plugin-clear'
import pkg from "./package.json";
export default [
// browser-friendly UMD build
{
input: "index.ts",
output: {
name: "@js/core",
file: pkg.browser,
format: "umd",
},
plugins: [
clear({
targets: ['dist'],
watch: true,
}),
resolve(),
commonjs(),
typescript({ tsconfig: "./tsconfig.json" }),
],
},
// CommonJS (for Node) and ES module (for bundlers) build.
// (We could have three entries in the configuration array
// instead of two, but it's quicker to generate multiple
// builds from a single configuration where possible, using
// an array for the `output` option, where we can specify
// `file` and `format` for each target)
{
input: "index.ts",
output: [
{ file: pkg.main, format: "cjs", exports: "auto" },
{ file: pkg.module, format: "es" },
],
plugins: [typescript({ tsconfig: "./tsconfig.json" })],
},
];
修改 core/tsconfig.json
文件
"outDir": "dist",
"module": "esnext",
修改 core/package.json
文件
"main": "dist/index.js",
"module": "dist/index.esm.js",
"browser": "dist/index.umd.js",
"files": [
"dist"
],
"bin": {
"demo-cli": "bin/index.js"
},
"scripts": {
"start": "node_modules/ts-node/dist/bin.js index.ts",
"build": "rollup -c",
"test": "echo \"Error: no test specified\" && exit 1"
},
修改 core/bin/index.ts
改成 core/bin/index.js
,并修改里面的内容
#! /usr/bin/env node require('../dist')()
用 js-cli 快速改造 utils 项目
到这里 core
项目就算改造完了,后面再改造 utils
项目时,为了减少重复的工作,大家可以通过笔者开发的 js-cli
脚手架,直接下载 rollup-typescript 模版
// 安装脚手架
npx @js-cli/core
// 切换到 utils 目录
js-cli init
// 选择模版
项目 -> rollup-typescript 模版
重新安装依赖
pnpm add @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-typescript rollup-plugin-clear tslib -WD
pnpm add npmlog --filter utils
pnpm add @types/npmlog --filter utils -D
把之前的代码拷贝到utils/src/index.ts
文件里
import log from 'npmlog'
log.level = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : 'info'
log.heading = 'js-cli'
log.addLevel('success', 2000, {fg: 'green', bold: true})
export { log }
运行打包命令
npm run build
切换到 core
目录,运行打包命令
npm run build
重新链接
pnpm link -g
查看结果
这样两个项目就已经打包完了,并且可以成功运行。
发布
由于 core
项目依赖 utils
项目,所以选择优先发布 utils
项目,修改package.json
文件里的 version
(具体版本请自行决定)
切换到 packages/utils
目录,执行发布命令
// packages/utilsnpm loginnpm publish
在发布完 utils
后,由于我们之前是通过 workspace
的方式,在 core
里引用的 utils
,所以在发布 core
项目之前,还需要手动把依赖改成 utils
对应的线上版本:
"dependencies": {
"utils": "workspace:^1.0.0"
},
改成
"dependencies": {
"utils": "^1.0.0"
},
然后再发布 core
总结
到这里,我们发现 pnpm
基本能满足我们日常开发的需求,但是在发布的时候,pnpm
就有点无能为力了,需要我们手动去升级包的版本、更新包的依赖,当管理的库的数量少的时候还好,一旦后面项目大了,需要管理的库变多了,还靠手动去一个一个的发包,显然是一件让人难以接受的事;除此之外,我们在打包的时候,也需要单独切换到对应的目录下进行打包,当然我们也可以在根目录下把所有子项目的打包命令都添加进去,但这仍然不够灵活,比如我们只改了其中一个库,那么我们只需要把依赖这个库的其他项目进行打包就行,而不是所有项目都要打包。
针对以上我们在开发过程中碰到的问题,在笔者的调研下,发现 rush
可以很好的解决上述问题,具体如何解决,请参考 rush + pnpm + ts + menorepo 脚手架开发之环境篇二