使用 esbuild 编写开发构建脚本
2025-09-14 13:17:28
上一篇已经把 monorepo 的目录结构搭起来了。
这一篇会使用 esbuild 开发一个构建脚本
node scripts/dev.js reactivity --format esm
因为在 monorepo 中,每个模块都是一个独立的包: 当执行这条命令时,可以打包 packages/reactivity,并输出对应格式的产物。
所以我们约定一套规则:
- 命令的第一个位置参数表示要打包的包名
--format表示输出格式- 每个包的入口都是
packages/<target>/src/index.ts - 每个包的输出目录都是
packages/<target>/dist
解析命令行参数
Node 内置的 parseArgs 可以用来解析命令行参数。
这里需要同时支持两类参数:
reactivity:位置参数,用来表示要打包的包--format esm:选项参数,用来表示打包格式
import { parseArgs } from 'node:util'
const {
values: { format },
positionals
} = parseArgs({
allowPositionals: true,
options: {
format: {
type: 'string',
short: 'f',
default: 'esm'
}
}
})
关键是 allowPositionals: true。 如果不启用,parseArgs 默认不会接收 reactivity 这种位置参数。
执行:
node scripts/dev.js reactivity --format esm
解析结果:
format // 'esm'
positionals // ['reactivity']
这样就能知道当前要打包哪个包。
获取目标包入口
有了 positionals 之后,就可以拿到目标包名:
const target = positionals.length ? positionals[0] : 'vue'
接下来按照约定拼出入口文件:
const entry = resolve(__dirname, `../packages/${target}/src/index.ts`)
不过还有一个问题:如果 scripts/dev.js 使用的是 ESM,文件里不能直接使用 CommonJS 里的 __dirname。
需要自己通过 import.meta.url 创建:
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
这样就能在 ESM 脚本里继续使用 __dirname 来处理路径。
配置 esbuild
输出文件按照约定生成:
const outfile = resolve(__dirname, `../packages/${target}/dist/${target}.${format}.js`)
完整的构建配置如下:
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const pkg = require(`../packages/${target}/package.json`)
esbuild
.context({
// 入口文件
entryPoints: [entry],
// 输出文件
outfile,
// 输出格式:esm、cjs、iife
format,
// cjs 通常给 Node 使用,其他格式先按浏览器环境处理
platform: format === 'cjs' ? 'node' : 'browser',
// 开启 sourcemap,方便调试源码
sourcemap: true,
// 把依赖一起打进产物中
bundle: true,
// iife 格式下挂到全局对象上的名字
globalName: pkg.buildOptions?.name
})
.then(ctx => ctx.watch())
这里使用的是 context().watch(),这样启动脚本后,修改源码会自动重新构建。
配置子包 package.json
为了让构建脚本可以读取包信息,每个子包都需要补充自己的 package.json。
以 shared 为例:
{
"name": "@vue/shared",
"version": "1.0.0",
"description": "工具函数",
"sideEffects": false,
"main": "dist/shared.cjs.js",
"module": "dist/shared.esm.js",
"files": [
"dist",
"index.js"
],
"buildOptions": {
"name": "VueShared",
"formats": [
"esm",
"cjs",
"iife"
]
}
}
完整代码
import { createRequire } from 'node:module'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseArgs } from 'node:util'
import esbuild from 'esbuild'
const {
values: { format },
positionals
} = parseArgs({
allowPositionals: true,
options: {
format: {
type: 'string',
short: 'f',
default: 'esm'
}
}
})
const target = positionals.length ? positionals[0] : 'vue'
const require = createRequire(import.meta.url)
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const entry = resolve(__dirname, `../packages/${target}/src/index.ts`)
const outfile = resolve(
__dirname,
`../packages/${target}/dist/${target}.${format}.js`
)
const pkg = require(`../packages/${target}/package.json`)
esbuild
.context({
// 入口文件
entryPoints: [entry],
// 输出文件
outfile,
// 打包格式 cjs esm iife
format,
// 打包平台 node borwser
platform: format === 'cjs' ? 'node' : 'browser',
// 开启sourcemap 方便调试
sourcemap: true,
// 把所有依赖打包到一个文件
bundle: true,
globalName: pkg.buildOptions.name
})
.then(ctx => ctx.watch())