Unbuild
unbuild 是基于 Rollup 的强大的汇总打包器,支持 typescript 并生成 commonjs 和模块格式 + 类型声明。
特点
众所周知,单纯使用 rollup 作为项目打包器虽然灵活,但是配置起来十分复杂。因为除了要安装 rollup 本身,还得安装一系列的 rollup 插件才能满足一个基本项目的要求,例如 @rollup/plugin-alias、@rollup/plugin-typescript、@rollup/plugin-commonjs、rollup-plugin-dts 等等
而 unbuild 内置了 rollup 的常用的一些插件,让我们可以快速进行配置,unbuild 的示例配置如下:
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
// If entries is not provided, will be automatically inferred from package.json
entries: [
// default
'./src/index',
// mkdist builder transpiles file-to-file keeping original sources structure
{
builder: 'mkdist',
input: './src/package/components/',
outDir: './build/components'
},
],
// Change outDir, default is 'dist'
outDir: 'build',
// Generates .d.ts declaration file
declaration: true,
})配置的部分 TS 定义:
interface BuildConfig extends DeepPartial<Omit<BuildOptions, "entries">> {
entries?: (BuildEntry | string)[];
preset?: string | BuildPreset;
hooks?: Partial<BuildHooks>;
}
interface BuildOptions {
name: string;
rootDir: string;
entries: BuildEntry[];
clean: boolean;
declaration?: boolean;
outDir: string;
stub: boolean;
externals: (string | RegExp)[];
dependencies: string[];
peerDependencies: string[];
devDependencies: string[];
alias: {
[find: string]: string;
};
replace: {
[find: string]: string;
};
failOnWarn?: boolean;
rollup: RollupBuildOptions;
}
interface RollupBuildOptions {
emitCJS?: boolean;
cjsBridge?: boolean;
inlineDependencies?: boolean;
replace: RollupReplaceOptions | false;
alias: RollupAliasOptions | false;
resolve: RollupNodeResolveOptions | false;
json: RollupJsonOptions | false;
esbuild: Options | false;
commonjs: RollupCommonJSOptions | false;
dts: Options$1;
}stub 模式
除了快速上手之外,unbuild 还有一个好玩的点是 stub 模式,可以用于命令行工具的开发。
在此之前,如果我们想要开发一个命令行工具,会通过 npm link 到全局,然后在本地测试这个工具。由于我们想要看到编辑代码后,命令行工具在本地效果实时更新,我们不得不使用打包工具的 watch 模式,对源代码进行监听和更新产物代码。
而现在 unbuild 有了 stub 模式,我们有更加简单和优雅的方式进行命令行工具的开发了。
假设我们的项目结构如下:
// your core code is hereimport { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'./src/index',
]
})然后执行 unbuild --stub
$ npx unbuld --stub
将会得到一个 dist/index.mjs 的文件
#!/usr/bin/env node
import jiti from "file:///path/to/your/project/node_modules/.pnpm/jiti@1.18.2/node_modules/jiti/lib/index.js";
/** @type {import("/path/to/your/project/src/index")} */
const _module = jiti(null, { interopDefault: true, esmResolve: true })("/path/to/your/project/src/index.ts");
export default _module;上面的这个文件,通过 jiti 实现了 TypeScript 的即时编译。
由于这个文件可以被 node 直接执行,可以被 npm link,这意味着我们不再需要 watch 模式监听源代码
mkdist 编译
unbuild 支持 file-to-file 的编译模式,这个功能来源于 mkdist。
所谓 file-to-file,就是打包之后的产物目录结构,保持和源码一致。例如假设我们的配置如下:
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
{
input: './src', // 指定目录而非文件,
builder: 'mkdist' // 使用 mkdist 作为打包器
}
]
})生成的目录可能是如下所示:
dist # 生成的目录保持和源码结构保持一致
page
- home.mjs
- index.mjs
- app.mjs
src
page
- home.ts
- index.ts
- app.ts
- unbuild.config.ts
至于这样做的好处,可以查看官方的解释。对于本人而言,可以解决我一个这样的场景:
在开发命令行工具的时候,假设其中的我的源代码中存在如下代码,获取上一层的某个文件的绝对路径:
const pkgPath = path.resolve("../../package.json")
fse.readJSONFile(pkgPath)以 src/index 为入口打包之后,非 file-to-file 模式下,通常只会生成一个文件,例如 dist/index.mjs:
dist
- index.mjs # 所有文件都被打包成了一个文件
src
utils
- index.ts
- index.ts
- unbuild.config.ts
此时就会导致,开发模式下和生产模式下,path.resolve("../../package.json") 执行的结果是不一样的,进而导致 fse.readJSONFile(pkgPath) 报错。
Hooks
为了最大程度提高灵活性,unbuild 也提供了一系列的钩子,可以对打包过程进行干预。但是官方这方面的文档很少,需要自己查看源码实践
interface BuildHooks {
"build:prepare": (ctx: BuildContext) => void | Promise<void>;
"build:before": (ctx: BuildContext) => void | Promise<void>;
"build:done": (ctx: BuildContext) => void | Promise<void>;
"rollup:options": (ctx: BuildContext, options: RollupOptions) => void | Promise<void>;
"rollup:build": (ctx: BuildContext, build: RollupBuild) => void | Promise<void>;
"rollup:dts:options": (ctx: BuildContext, options: RollupOptions) => void | Promise<void>;
"rollup:dts:build": (ctx: BuildContext, build: RollupBuild) => void | Promise<void>;
"rollup:done": (ctx: BuildContext) => void | Promise<void>;
"mkdist:entries": (ctx: BuildContext, entries: MkdistBuildEntry[]) => void | Promise<void>;
"mkdist:entry:options": (ctx: BuildContext, entry: MkdistBuildEntry, options: MkdistOptions) => void | Promise<void>;
"mkdist:entry:build": (ctx: BuildContext, entry: MkdistBuildEntry, output: {
writtenFiles: string[];
}) => void | Promise<void>;
"mkdist:done": (ctx: BuildContext) => void | Promise<void>;
"untyped:entries": (ctx: BuildContext, entries: UntypedBuildEntry[]) => void | Promise<void>;
"untyped:entry:options": (ctx: BuildContext, entry: UntypedBuildEntry, options: any) => void | Promise<void>;
"untyped:entry:schema": (ctx: BuildContext, entry: UntypedBuildEntry, schema: Schema) => void | Promise<void>;
"untyped:entry:outputs": (ctx: BuildContext, entry: UntypedBuildEntry, outputs: UntypedOutputs) => void | Promise<void>;
"untyped:done": (ctx: BuildContext) => void | Promise<void>;
}cjsBridge
从 ESM 到 CJS 的时候,注入的一些 polyfill,查看具体插件
export default defineBuildConfig({
rollup: {
cjsBridge: true,
}
});