Skip to content
0

Unbuild

unbuild 是基于 Rollup 的强大的汇总打包器,支持 typescript 并生成 commonjs 和模块格式 + 类型声明。

特点

众所周知,单纯使用 rollup 作为项目打包器虽然灵活,但是配置起来十分复杂。因为除了要安装 rollup 本身,还得安装一系列的 rollup 插件才能满足一个基本项目的要求,例如 @rollup/plugin-alias@rollup/plugin-typescript@rollup/plugin-commonjsrollup-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 here
import { 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,
    }
});

Released under the MIT License.