rush + pnpm + ts + monorepo 脚手架开发之环境篇一

7,017 阅读7分钟

关于 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,即可看到打印结果

image-20211124122258725.png

根目录添加启动命令

如果想在根目录也能运行 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 脚手架开发之环境篇二

相关链接

js-cli 脚手架源码