Unbuild
Usage
Install package:
# pnpm
pnpm iImport:
// ESM
import { TEMPLATE_ENUM } from "./constant";
// CommonJS
const { TEMPLATE_ENUM } = require("./constant");unbuild 简介
无官网,只有 github 上一点介绍,配置也只有一个 类型声明 文件,需要查源码。
unbuild 是一个 js 打包工具,相比于 rollup 有更好的开发体验,如 即时编译 jit 。
安装
pnpm i unbuild -D使用
新建 index.ts 、 constant.ts , 分别写入
// index.ts
import { TEMPLATE_ENUM } from './constant'
export default function test() {
console.log(888, TEMPLATE_ENUM)
}
test()// constant.ts
export enum TEMPLATE_ENUM {
WEB = 'WEB',
H5 = 'H5',
}更新 package.json
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
]
}build
npx unbuild这样就打包完成了。 dist 目录下会默认生成 index.cjs 和 index.mjs 文件,以及类型声明文件 index.d.ts
stub
值得注意的是,开发时可以使用 jiti 的插桩,此时运行
npx unbuild --stub这时 dist 目录下生成的是带 jiti 的 bundle ,可以尝试 node 直接执行 这个 bundle 查看输出,然后更改 src 源代码,再次使用 node 直接执行这个 bundle ,你会发现输出的代码是更改过后的。
也就是说我们只需执行一次插桩命令,无需监听源文件的修改就能做到实时更新打包内容,这极大的节省了监听编译源代码所消耗的时间。
build.config.ts
也支持配置文件,新建 build.config.ts
import { defineBuildConfig } from 'unbuild'
// unbuild 配置
export default defineBuildConfig({
// 类型声明
declaration: true,
// 入口
entries: ['./src/index'],
// 默认是输出的cjs和mjs文件的,但是声明了配置文件,则需要设置 emitCJS 为 true 显式输出 cjs,否则不会输出 cjs
rollup: {
emitCJS: true,
inlineDependencies: true,
esbuild: {
target: 'ES2020',
// 压缩
minify: true
}
}
})注意 配置文件 需要 在 rollup 下显式声明 emitCJS 才会导出 cjs 类型文件, declaration 也需要设置为 true 以输出类型声明文件
功能
- 📦 Optimized bundler
- unbuild 基于 rollup ,集成了 rollup 中的插件,开箱即用的支持 ts ,并允许生成 cjs 和 es 格式和类型声明(如果需要生成 iife 类型代码,可以通过 hooks 自己生成)。
- 使用 esbuild 来转换 js 代码,比原生 rollup 更快
- 🪄 Automated config
- 从 package.json 文件中自动推断 打包配置 和 入口文件
- 📁 Bundleless build
- 集成 mkdist ,一个轻量的文件转换器,有点像 webpack 的 loader ,用于文件预处理,而且保持原有的目录文件结构
- ✨ Passive watcher
- stub 插桩,使用 jiti 进行插桩。
- 以往开发过程都需要监听文件改动,以实时打包,每更新一次都得重新打包一次,这在 monorepo 开发过程中,触发多个包一起打包就会较耗时,也不太合理。
- 在 unbuild 中,可以使用插桩
npx unbuild --stub - 此时并没有对文件进行监听,只是生成了 带 jiti 的 bundle , 这时已经可以直接使用打包完的代码了,因为 jiti 可以动态的执行 js 或 ts 源码,在开发 monorepo 时还需执行一次 stub 即可,非常好用。
- ✍ Untype Generator
- 集成 untyped , 可以通过 markdown 和 配置对象生成类型。
- ✔️ Secure builds
- 自动检查各种打包问题,例如潜在的丢失和未使用的依赖等。
源码
大致看一下源码
package.json
// package.json
{
"bin": {
"unbuild": "./dist/cli.mjs"
}
}由 bin 字段可知,运行 npx unbuild 的时候,其实是 node 在运行 dist 目录下的 cli.mjs 文件,对应 src 下即 cli.ts 。
cli.ts
看下 src/cli.ts 代码
#!/usr/bin/env node
import { resolve } from "pathe";
import mri from "mri";
import { build } from "./build";
async function main() {
const args = mri(process.argv.splice(2));
const rootDir = resolve(process.cwd(), args._[0] || ".");
await build(rootDir, args.stub).catch((error) => {
console.error(`Error building ${rootDir}: ${error}`);
throw error;
});
}
main().catch((error) => {
console.error(error);
process.exit(1);
});非常简洁,定义 main 函数,然后执行,里面主要调用了 src/build.ts 导出的 build 构建函数。 build 的时候传入 打包根目录 和 命令行 stub 参数,根目录默认 '.' , 即当前目录。此外 mri 是一个轻量的命令行参数解析器(相比于 minimist 来说)。
build.ts
看下 build.ts
// Read build.config and package.json
const buildConfig = tryRequire("./build.config", rootDir) || {};
const pkg = tryRequire("./package.json", rootDir);
// Resolve preset
const preset = resolvePreset(
buildConfig.preset ||
pkg.unbuild?.preset ||
pkg.build?.preset ||
inputConfig.preset ||
"auto",
rootDir
);源码是 ts ,此处做了适当删减 ts 类型声明
首先通过 tryRequire 函数尝试读取根目录下的 build.config[.js, .ts] 文件 和 package.json 文件,然后 通过 resolvePreset 获取 预配置 preset , preset 是字符串的话则当做文件,通过 tryRequire 去读取,是函数则执行函数拿到返回值。
tryRequire()- 内部使用
jiti进行读取文件内容
- 内部使用
resolvePreset()- 优先获取
build.config文件下的preset字段 - 取不到则依次降级为
package.json里的unbuild.preset或者build.preset inputConfig这里并没有传入,这里猜测为 命令行参数'auto'就比较有意思,直接赋值了autoPreset,autoPreset由auto.ts中导出,具体看下tsexport const autoPreset = definePreset(() => { return { hooks: { "build:prepare"(ctx) { // Disable auto if entries already provided of pkg not available if (!ctx.pkg || ctx.options.entries.length > 0) { return; } const sourceFiles = listRecursively(join(ctx.options.rootDir, "src")); const res = inferEntries(ctx.pkg, sourceFiles); }, }, }; });- 就是 定义了
'build:prepare'的hooks,在打包前处理完配置项 - 注意这里的
return, 当读取不到package.json文件或者entries入口存在情况下,直接返回,否则就根据src目录去推断具体的 入口文件 listRecursively()- 递归
src目录下的所有文件,返回一个各个文件的路径的数组
- 递归
inferEntries()- 接着会处理这个文件路径的数组,先从短到长排序
- 从
package.json中的exports中提取导出的文件名,存放到outputs - 接着会处理
package.json中的bin、main、module、types,一样存放到outputs - 遍历
outputs,过滤掉文件夹,留下文件,逐一使用正则匹配每个output可能的input入口
- 就是 定义了
- 优先获取
接着看回 build.ts , 拿到 buildConfig 和 pkg 和 preset 之后,通过 defu 库将他们与默认参数合并, defu 是一个深度合并参数的 node 包, defu 传入多个合并对象会以 左边 参数为准
// 合并优先级逐渐降低
const options = defu(
buildConfig,
pkg.unbuild || pkg.build,
inputConfig,
preset,
{ ...defaultBuildOptions }
) as BuildOptions组装构建时上下文参数 ctx ,这在生成 type 类型声明 、 复制文件到输出目录、 rollup 打包时都用到
const ctx: BuildContext = {
options,
warnings: new Set(),
pkg,
buildEntries: [],
usedImports: new Set(),
hooks: createHooks(),
};注册 hooks 到 ctx.hooks 上, 包括 preset.hooks 、 inputConfig.hooks 、 buildConfig.hooks ,并调用 'build:prepare' 这个 hooks
// Allow prepare and extending context
await ctx.hooks.callHook("build:prepare", ctx);接着遍历 opitons.entries , builder 字段不存在在自动判断使用 mkdist 还是 rollup
entry.builder = entry.input.endsWith("/") ? "mkdist" : "rollup";然后依次调用打包
// untyped
await typesBuild(ctx);
// mkdist
await mkdistBuild(ctx);
// rollup
await rollupBuild(ctx);如果是 stub ,则 return
// Skip rest for stub
if (options.stub) {
await ctx.hooks.callHook("build:done", ctx);
return;
}