前言
最近想把之前练习过的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,
};
构建效果图
踩坑记录
不完善的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方法,当共享包发生改变时,也无法立即生效。