pnpm monorepo开发环境构建cjs和esm共享包

439 阅读4分钟

前言

最近想把之前练习过的pnpm+turbo组合的monorepo项目实现一个完整的全栈项目,实现过程中发现共享的package会由于nestjs默认导出模块为commonjs,而现在大部分前端项目都是vite构建的esm模块,导致共享的package包就很难提供给不同模块的app共用。

查阅资料后找到一个不那么完美的解决方案,开发过程中始终别扭,然后在继续查阅资料的过程中,找到了今天的主角tsup、rollup,它们可以打包一个package为cjs、esm两种类型提供给app加载。

补充说明

实际开发体验中,如果是package和app一起开发,还是最简单的方法最有效,用tsup或rollup打包始终觉得很别扭,而且修改完package里面的内容后,nestjs的app在vscode内总会出现类型提示不存在或包不存在的问题,再一个原因是tsup或rollup,每次修改都需要编译cjs和esm,在开发上效率实在太低,所以不推荐这个方法开发

更好的开发方案

使用下面 不完善的cjs和esm处理方法 进行开发,package都打包为commonjs、这样nestjs app开发体验最好,没发现各种问题。

然后vue + vite的管理台app采用vite预编译esm的办法,修改package以后,由于是预编译了esm模块会发现修改不会生效,下面采用间接方法解决这个问题,经过大量踩坑最合适的还是方法2

1. 用nodemon监视package内src的文件,当文件发生改变以后就执行vite命令重新加载项目

// nodemon.json
{
  "restartable": "rs",
  "watch": ["../../packages/**/src/**/*"],
  "ignore": ["dist"],
  "delay": "1000",
  "exec": "vite --force",
  "ext": "ts js json"
}

2. 用vite的插件 vite-plugin-restart监视文件变化然后restart

import ViteRestart from "vite-plugin-restart";

// vite.config.ts
plugins: [
  ViteRestart({
    restart: ["../../packages/**/src/**/*"]
  })
]

3. packages内的包暴露src路径,但是如果包内还有其它包的依赖,比如@repo/contract依赖于@repo/drizzle,但是@repo/drizzle是编译成commonjs提供给nestjs使用,那么vue+vite引用@repo/contract/src内的模块时,还是会因为cjs和esm的问题产生报错。

// @repo/contract package.json
{
  "name": "@repo/contract",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "compile:pkg": "tsc",
    "dev": "tsc --watch",
    "build": "tsc"
  },
  "********** 注释 ************": "默认导出cjs的模块给nest使用,暴露出来的src提供给vite使用",
  "types": "./dist/index.d.ts",
  "main": "./dist/index.js",
  "exports": {
    ".": "./dist/index.js",
    "./src": "./src/index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@ts-rest/core": "^3.45.2",
    "zod": "^3.23.8",
    "@repo/drizzle": "workspace:^"
  },
  "devDependencies": {
    "typescript": "^5.4.5"
  }
}

项目目录结构介绍

├── apps  // apps(主要应用,admin为管理台ui、api为nestjs+fastify提供接口)
│   ├── admin
│   │   ├── README.md
│   │   ├── index.html
│   │   ├── node_modules
│   │   ├── package.json
│   │   ├── public
│   │   ├── src
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── vite.config.ts
│   └── api
│       ├── README.md
│       ├── dist
│       ├── nest-cli.json
│       ├── node_modules
│       ├── package.json
│       ├── src
│       ├── test
│       ├── tsconfig.build.json
│       └── tsconfig.json
├── node_modules
├── package.json
├── packages  // packages(公共包,drizzle为api提供数据库orm支持、rest-contract提供api合约)
│   ├── drizzle
│   │   ├── drizzle.config.ts
│   │   ├── kit
│   │   ├── node_modules
│   │   ├── package.json
│   │   ├── src
│   │   ├── tsconfig.json
│   │   └── tsup.config.ts
│   └── rest-contract
│       └── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

package打包cjs和esm版本

tsup、rollup都可以打包cjs和esm模块,这里我暂时是用tsup实现的,以drizzle共享包为例,tsup提供打包功能,package.json暴露不同模块提供外部使用。

├── drizzle
│   ├── drizzle.config.ts // drizzle-orm配置
│   ├── kit               // drizzle-kit相关操作如seed播种数据操作
│   │   │   ├── db.ts
│   │   │   ├── mock
│   │   │   │   └── data.ts
│   │   │   └── seed.ts
│   ├── node_modules
│   ├── package.json      // package配置
│   ├── src               // drizzle-orm相关,如schema
│   │   │   ├── enum.ts
│   │   │   ├── index.ts
│   │   │   ├── schema
│   │   │   │   ├── base.schema.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── relation.schema.ts
│   │   │   │   └── system.schema.ts
│   │   │   └── utils
│   │   │       └── password.ts
│   ├── tsconfig.json     // ts配置
│   └── tsup.config.ts    // tsup配置

package.json

看注释位置的主要配置,tsx提供nodejs的ts直接运行环境,下面是配置。

// @repo/drizzle package.json
{
  "name": "@repo/drizzle",
  "version": "0.0.1",
  "description": "共享drizzle包",
  
  // 主要配置:
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "exports": {
    ".": {
      "types": {
        "require": "./dist/index.d.cts",
        "import": "./dist/index.d.ts"
      },
      "default": {
        "require": "./dist/index.cjs",
        "import": "./dist/index.js"
      }
    }
  },
  
  "scripts": {
    "build:@repo/drizzle": "tsup src/*",
    "dev": "tsup src/* --watch",
    "db:migrate:deploy": "drizzle-kit migrate deploy",
    "db:migrate:dev": "drizzle-kit migrate dev",
    "db:push": "drizzle-kit push",
    "db:generate": "drizzle-kit generate",
    "studio": "drizzle-kit studio --verbose",
    "seed": "tsx kit/seed.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/bcrypt": "^5.0.2",
    "@types/node": "^20.3.1",
    "@types/pg": "^8.11.6",
    "drizzle-kit": "^0.22.7",
    "tsup": "^8.1.0",
    "tsx": "^4.15.2"
  },
  "dependencies": {
    "bcrypt": "^5.1.1",
    "drizzle-orm": "^0.31.2",
    "drizzle-zod": "^0.5.1",
    "pg": "^8.12.0",
    "zod": "^3.23.8"
  }
}

tsup.json

tsup提供打包cjs和esm的打包功能,下面是配置。

// tsup.json
import type { Options } from "tsup";

export const tsup: Options = {
  entry: ["src/*.ts"],
  format: ["cjs", "esm"],
  dts: true,
  // splitting: true,
  splitting: false,
  clean: true,
  outDir: "dist",
  sourcemap: "inline",
  cjsInterop: true,
};

构建效果图

image.png

踩坑记录

不完善的cjs和esm处理方法

monorepo项目中,pnpm vite commonjs和esm不兼容处理方法

# 最简单的方法:
# 以shared-api为例,vite optimizeDeps配置,作用是:dev的时候vite预先把commonjs转换为esm包缓存在.vite目录
# 缺点是:如果shared-api包有修改,只能通过强制重新加载再次让vite编译esm缓存(暂时通过手动保存一下vite.config.ts实现再次强制编译)
# package.json内配置dev的时候加上--force参数

// package.json
{
  "dev": "vite --force"
}

// vite.config.ts
{
  optimizeDeps:{
    include: [
      // TODO: 由于加载的是commonjs,这里用vite预编译为esm模块
      "@repo/shared-api"
    ]
  },
  build: {
    // TODO: 由于加载的是commonjs,这里用vite预编译为esm模块
    commonjsOptions: {
      include: ["@repo/shared-api", "node_modules"],
    },
  }
}

另一种不完善方案:

packages打包为esm模块,nestjs利用webpack编译加载esm模块,类似于上面的vite方法,当共享包发生改变时,也无法立即生效。

webpack加载esm

相关链接

tsup.egoist.dev/#using-cust…

rollupjs.org/introductio…

参考链接

js.work/posts/2b6dd…

pengzhanbo.cn/article/exp…

juejin.cn/post/703738…

github.com/axios/axios…