阅读视图

发现新文章,点击刷新页面。

⚡️ vite-plugin-oxc:从 Babel 到 Oxc,我为 Vite 写了一个高性能编译插件

写在前面

关注前端工具链的人应该都注意到了,Rust 正在「入侵」这个领域。SWC 被 Next.js 采用,Biome 在蚕食 ESLint 和 Prettier 的地盘,而 Oxc 作为新一代 Rust 工具链,性能数据更是夸张——比 SWC 还快好几倍。

更值得关注的是 Vite 的动向。Evan You 团队正在开发 Rolldown,一个用 Rust 重写的 Rollup,底层就是基于 Oxc。按照 roadmap,Rolldown 会逐步集成到 Vite 中,届时整个构建流程都将是 Rust 实现。

既然大势所趋,为什么不提前体验一下?Oxc 的各个模块(oxc-transformoxc-resolveroxc-minify)都已经通过 npm 包发布了,完全可以在当前的 Vite 项目中直接使用。于是我动手写了 vite-plugin-oxc,把 Oxc 的能力接入 Vite 的插件体系,算是在 Rolldown 正式落地前的一次提前尝鲜。

这就是这个插件的由来。

项目源码:github.com/Sunny-117/v…


一、JavaScript 编译工具的前世今生

在聊具体实现之前,有必要回顾一下 JavaScript 编译工具这些年的演进。这不是为了炒冷饭,而是理解这个演进脉络,才能明白为什么 Oxc 的出现是一种必然。

1.1 Babel:开创性的存在

2014 年,当 ES6 规范还在草案阶段,6to5(后来更名为 Babel)横空出世。那时候浏览器对新语法的支持参差不齐,Babel 让开发者可以放心使用箭头函数、解构赋值、类等新特性,然后转译成 ES5 代码跑在老旧浏览器上。

Babel 的架构设计很经典:

源代码 → Parser(解析成 AST)→ Transformer(插件转换)→ Generator(生成代码)

这套架构的优势在于插件系统的灵活性。任何人都可以写一个 Babel 插件,操作 AST 实现自定义的代码转换。十年过去了,Babel 生态里积累了数以万计的插件,覆盖了几乎所有你能想到的代码转换需求。

但问题也很明显:

Babel 是纯 JavaScript 实现的。JavaScript 是单线程、解释执行、带 GC 的语言,天生就不是性能敏感型任务的最佳选择。当项目规模膨胀到几十万行代码时,Babel 的编译时间会变得令人抓狂。我见过一些大型 monorepo 项目,光 Babel 编译就要好几分钟。

另一个痛点是配置复杂度。@babel/preset-env@babel/plugin-transform-runtimecore-jsbrowserslist……这些概念交织在一起,新手很容易迷失在配置地狱里。我至今还记得当年为了搞清楚 useBuiltIns: 'usage'useBuiltIns: 'entry' 的区别,翻了多少遍文档。

1.2 esbuild:用 Go 重写一切

2020 年,esbuild 的出现彻底改变了游戏规则。

作者 Evan Wallace(Figma 联合创始人)用 Go 语言重写了一个 JavaScript/TypeScript 打包器,性能数据令人瞠目结舌:比 Webpack 快 10-100 倍。这不是什么黑魔法,原因很朴素:

  1. Go 是编译型语言,执行效率远高于 JavaScript
  2. Go 的并发模型让 esbuild 可以充分利用多核 CPU
  3. 从零设计,没有历史包袱,数据结构和算法都针对性能优化过
  4. All-in-one,解析、转换、打包、压缩一条龙,减少中间环节的开销

Vite 选择 esbuild 做预构建(pre-bundling)正是看中了这一点。在开发模式下,esbuild 可以在几百毫秒内把 node_modules 里的依赖打包好,让 Vite 的冷启动时间保持在秒级。

但 esbuild 也有它的局限:

  • 不做类型检查。它只剥离 TypeScript 类型,不验证类型正确性。
  • 插件系统相对简单。不像 Babel 那样可以精细操作 AST,esbuild 的插件主要用于自定义模块解析和加载。
  • 不追求 100% 兼容。一些边缘场景的语法转换可能和 Babel 结果不一致。
  • 作者明确表示不会支持某些特性,比如装饰器的旧版实现。

对于大多数项目来说,这些局限不是问题。但在某些场景下,你可能还是得请出 Babel。

1.3 SWC:Rust 阵营的第一枪

2019 年,韩国开发者 Donny(강동윤)用 Rust 启动了 SWC 项目。名字来源于 "Speedy Web Compiler",目标很直接:做一个更快的 Babel 替代品。

SWC 的策略是 兼容 Babel。它实现了大部分 Babel 的转换能力,配置项也尽量保持一致,让迁移成本降到最低。性能方面,SWC 号称比 Babel 快 20 倍以上。

2021 年,SWC 被 Vercel 收购,成为 Next.js 12 的默认编译器。这是一个标志性事件——Rust 编写的前端工具链开始进入主流视野。

SWC 的优势在于:

  • Rust 的性能。编译型语言、零成本抽象、无 GC 停顿。
  • 良好的 Babel 兼容性。支持大部分 Babel 插件的功能。
  • 持续的投入。有 Vercel 背书,项目维护有保障。

但 SWC 也有一些问题。最常被吐槽的是 编译产物的稳定性。早期版本偶尔会出现一些边缘情况的 bug,导致生产环境翻车。另外,SWC 的架构设计主要服务于 Next.js 的需求,作为通用工具使用时,某些场景的支持不够完善。

1.4 工具演进的本质规律

回顾这段历史,可以看到一个清晰的趋势:

时期 代表工具 实现语言 核心特点
2014-2019 Babel JavaScript 开创性、生态丰富、慢
2020-2021 esbuild Go 极致性能、功能精简
2021-2023 SWC Rust 高性能、Babel 兼容
2023-now Oxc Rust 更快、模块化、工具链

这个演进本质上是在解决同一个问题:如何在保证功能的前提下,榨干硬件的每一分性能

JavaScript 天生不适合这类 CPU 密集型任务,所以社区开始用系统级语言重写。Go 和 Rust 之争,目前看来 Rust 略占上风——主要是因为 Rust 的零成本抽象和更精细的内存控制,在极致性能场景下更有优势。


二、Oxc 凭什么更快

Oxc(Oxidation Compiler)是 2023 年开始崭露头角的新项目,作者是 Boshen Chen。相比前辈们,Oxc 有几个独特的特点。

2.1 不只是编译器,是完整工具链

Oxc 的野心不止于一个编译器。它的目标是提供一整套高性能 JavaScript 工具链:

  • oxc-parser:JavaScript/TypeScript 解析器
  • oxc-transform:代码转换器(JSX、TypeScript 等)
  • oxc-resolver:模块路径解析器
  • oxc-minify:代码压缩器
  • oxc-linter:代码检查器(对标 ESLint)
  • oxc-formatter:代码格式化器(对标 Prettier)

每个模块都可以独立使用,通过 npm 包的形式分发(底层是 Rust 编译成 N-API 原生模块)。这种模块化设计让你可以按需引入,而不是大包大揽。

2.2 性能数据

根据 Oxc 官方的 benchmark,在 parser 层面:

  • 比 SWC 快 3 倍
  • 比 Babel parser 快 40+ 倍

在 transformer 层面,处理 TypeScript 的速度大约是 SWC 的 4 倍

这些数字看起来很夸张,但我自己跑过测试,差距确实存在。Oxc 的作者在性能优化上下了很大功夫,比如:

  • 使用 bumpalo 这种 arena allocator 来减少内存分配开销
  • AST 节点设计更紧凑,减少内存占用
  • 大量使用 SIMD 指令加速字符串处理
  • 零拷贝解析,尽量复用源代码字符串

2.3 兼容性策略

Oxc 的兼容性策略比较务实。它不追求 100% 兼容 Babel 的每一个行为,而是覆盖 实际生产中最常用的转换场景

  • TypeScript 类型剥离
  • JSX 转换(classic 和 automatic 两种模式)
  • ES target 降级(async/await、optional chaining 等)
  • React Fast Refresh 注入

对于大多数项目来说,这些功能已经够用了。


三、vite-plugin-oxc 的设计思路

有了前面的背景铺垫,现在进入正题:如何把 Oxc 接入 Vite?

3.1 需求分析

我给自己定的目标是:

  1. 替代 Vite 内置的 esbuild 做代码转换。包括 TypeScript 类型剥离、JSX 转换。
  2. 提供模块解析能力。用 oxc-resolver 替代 Vite 的默认解析逻辑(可选)。
  3. 提供代码压缩能力。用 oxc-minify 在生产构建时压缩代码。
  4. 支持 React Fast Refresh。开发模式下的 HMR 必须正常工作。
  5. 零配置可用。默认配置就能满足大多数项目的需求。

3.2 Vite 插件机制简介

Vite 的插件系统基于 Rollup,但做了一些扩展。一个 Vite 插件本质上是一个对象,包含若干个 hook 函数。和我们这个插件相关的主要有:

  • config:修改 Vite 配置
  • configResolved:配置解析完成后的回调,可以拿到最终配置
  • resolveId:自定义模块 ID 解析
  • load:自定义模块加载
  • transform:代码转换
  • transformIndexHtml:转换 HTML 入口文件
  • generateBundle:生成产物后的回调,可以修改最终产物

另外还有一个关键配置:enforce。它决定插件的执行顺序:

  • enforce: 'pre':在 Vite 核心插件之前执行
  • 不设置:在 Vite 核心插件之后、用户插件之前执行
  • enforce: 'post':在所有插件之后执行

对于代码转换类插件,通常需要设置 enforce: 'pre',这样才能在 Vite 的默认处理之前介入。

3.3 整体架构

┌─────────────────────────────────────────────────────────────┐
│                     vite-plugin-oxc                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐   │
│  │  Transform   │  │   Resolve    │  │     Minify       │   │
│  │  (oxc-       │  │  (oxc-       │  │   (oxc-minify)   │   │
│  │  transform)  │  │  resolver)   │  │                  │   │
│  └──────────────┘  └──────────────┘  └──────────────────┘   │
│         │                 │                   │             │
│         ▼                 ▼                   ▼             │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                React Fast Refresh                    │   │
│  │               (HMR 边界检测 + 运行时)                 │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

这个插件由三个核心功能模块组成,加上一套 React Fast Refresh 的支持逻辑。下面逐一拆解。


四、Transform 模块:代码转换的核心

代码转换是整个插件最核心的功能。它的职责是把 TypeScript、JSX 这些浏览器不认识的语法,转换成标准的 JavaScript。

4.1 基本实现

先看核心代码:

import { transformSync as oxcTransform } from 'oxc-transform'

// 在 transform hook 中
transform(code, id, transformOptions) {
  if (!filter(id) || options.transform === false) return null

  const isJsxFile = /\.[jt]sx$/.test(id)
  const enableRefresh = isDev && options.reactRefresh && isJsxFile

  const result = oxcTransform(id, code, {
    ...transformOpts,
    sourceType: guessSourceType(id, transformOptions?.format),
    sourcemap: options.sourcemap,
    jsx: {
      ...jsxOpts,
      development: isDev,
      refresh: enableRefresh ? {} : undefined,
    },
  })

  if (result.errors.length) {
    throw new SyntaxError(
      result.errors.map((error) => error.message).join('\n')
    )
  }

  return {
    code: result.code,
    map: result.map,
  }
}

oxcTransformoxc-transform 提供的同步转换函数。它接收三个参数:

  1. 文件路径(用于 sourcemap 和错误信息)
  2. 源代码字符串
  3. 转换选项

返回值包含转换后的代码和 sourcemap。

4.2 sourceType 推断

一个容易被忽略的细节是 sourceType 的处理。JavaScript 有两种模块类型:

  • module:ESM,支持 import/export
  • script:传统脚本,支持 CommonJS

如果 sourceType 判断错误,转换结果可能出问题。比如把 ESM 代码当成 script 处理,import 语句就会报语法错误。

我实现了一个 guessSourceType 函数来推断:

export function guessSourceType(
  id: string,
  format?: string
): 'module' | 'script' | undefined {
  // 优先使用上游传递的 format 信息
  if (format === 'module' || format === 'module-typescript') {
    return 'module'
  } else if (format === 'commonjs' || format === 'commonjs-typescript') {
    return 'script'
  }

  // 根据文件扩展名推断
  const moduleFormat = getModuleFormat(id)
  if (moduleFormat) {
    return moduleFormat === 'module' ? 'module' : 'script'
  }
}

export function getModuleFormat(
  id: string
): 'module' | 'commonjs' | 'json' | undefined {
  const ext = path.extname(id)
  switch (ext) {
    case '.mjs':
    case '.mts':
      return 'module'
    case '.cjs':
    case '.cts':
      return 'commonjs'
    case '.json':
      return 'json'
    case '.jsx':
    case '.tsx':
      return 'module' // JSX/TSX 默认当 ESM 处理
  }
}

这里有个约定:.mjs/.mts 是 ESM,.cjs/.cts 是 CommonJS,.jsx/.tsx 默认当 ESM。对于 .js/.ts,则依赖上游的 format 信息或者返回 undefined 让 Oxc 自己判断。

4.3 与 Vite 内置 esbuild 的协同

Vite 默认会用 esbuild 处理 TypeScript 和 JSX。如果我们的插件也处理这些文件,就会重复转换,结果不可预期。

解决方案是在 config hook 里禁用 esbuild 对 JSX/TSX 的处理:

config(_userConfig, { command }) {
  return {
    esbuild: {
      // 让 esbuild 只处理纯 .ts 文件
      include: /\.ts$/,
      // 排除 JSX/TSX,交给我们的插件处理
      exclude: /\.[jt]sx$/,
    },
    optimizeDeps: {
      esbuildOptions: {
        jsx: 'automatic',
      },
    },
  }
}

这样配置后,.ts 文件继续走 esbuild(速度也很快),而 .jsx.tsx.js 走我们的 Oxc 转换。

不过说实话,这个设计有点 trade-off。理想情况下应该完全接管所有 JS/TS 文件的转换,但考虑到 Vite 生态的兼容性,保持这种混合模式可能更稳妥。

4.4 JSX 转换配置

JSX 转换有两种模式:

  1. Classic:转换成 React.createElement 调用
  2. Automatic:转换成 _jsx/_jsxs 调用,自动引入 react/jsx-runtime

React 17+ 推荐使用 automatic 模式,这也是我们插件的默认行为。配置项支持自定义:

const jsxOpts = transformOpts.jsx && typeof transformOpts.jsx === 'object'
  ? transformOpts.jsx
  : {}

oxcTransform(id, code, {
  jsx: {
    ...jsxOpts,
    development: isDev, // 开发模式启用额外的调试信息
    refresh: enableRefresh ? {} : undefined, // Fast Refresh 注入
  },
})

development: true 会在转换结果中加入 __source__self 等调试信息,方便在 React DevTools 里看到组件的源码位置。


五、Resolve 模块:模块解析

模块解析看起来简单,实际上是个大坑。import './foo' 这行代码,到底应该解析成哪个文件?

  • ./foo.js
  • ./foo.ts
  • ./foo/index.js
  • ./foo/index.ts
  • 还是 ./foo.json

这取决于项目配置、Node.js 版本、模块类型等一系列因素。

5.1 为什么要自己做解析

Vite 内部已经有一套解析逻辑,为什么我们还要用 oxc-resolver 再来一套?

两个原因:

  1. 性能。Oxc resolver 是 Rust 实现的,解析速度比 Vite 的 JavaScript 实现更快。在大型项目中,模块解析的开销不可忽视。
  2. 一致性。既然 transform 用了 Oxc,resolver 也用 Oxc,整个工具链的行为会更一致。

当然,这是可选功能,默认开启但可以关闭。

5.2 基本实现

import { ResolverFactory } from 'oxc-resolver'

let resolver: InstanceType<typeof ResolverFactory> | null = null

// 在 configResolved 中初始化
configResolved(config) {
  if (options.resolve !== false) {
    resolver = new ResolverFactory({
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.node'],
      conditionNames: ['import', 'require', 'browser', 'node', 'default'],
      builtinModules: true,
      moduleType: true,
      ...options.resolve,
    })
  }
}

// 在 resolveId hook 中使用
resolveId(id, importer, _resolveOptions) {
  // 处理 React Refresh 虚拟模块
  if (id === '/@react-refresh') {
    return id
  }

  if (!resolver || options.resolve === false) return null

  // 默认跳过 node_modules,除非显式启用
  if (
    !options.resolveNodeModules &&
    id[0] !== '.' &&
    !path.isAbsolute(id)
  ) {
    return null
  }

  try {
    const directory = importer ? path.dirname(importer) : process.cwd()
    const resolved = resolver.sync(directory, id)

    // 处理 Node.js 内置模块
    if (resolved.error?.startsWith('Builtin module')) {
      return {
        id,
        external: true,
        moduleSideEffects: false,
      }
    }

    if (resolved.path) {
      const format = getModuleFormat(resolved.path) || resolved.moduleType || 'commonjs'
      return {
        id: resolved.path,
        format,
      }
    }
  } catch (error) {
    // 解析失败,交给 Vite 的默认逻辑处理
    return null
  }

  return null
}

5.3 性能优化:跳过 node_modules

一个重要的优化是 默认不解析 node_modules

if (
  !options.resolveNodeModules &&
  id[0] !== '.' &&          // 不是相对路径
  !path.isAbsolute(id)       // 不是绝对路径
) {
  return null  // 交给 Vite 处理
}

为什么?因为 Vite 的预构建机制已经把 node_modules 里的依赖处理好了,我们没必要再去解析一遍。只解析项目内的相对路径和绝对路径,性能开销可控。

如果某些场景确实需要解析 node_modules,可以通过配置项开启:

oxc({
  resolveNodeModules: true
})

5.4 内置模块处理

Node.js 有一些内置模块(fspathhttp 等),在浏览器环境是不存在的。当 oxc-resolver 遇到这些模块时,会返回一个特殊的 error:

if (resolved.error?.startsWith('Builtin module')) {
  return {
    id,
    external: true,
    moduleSideEffects: false,
  }
}

把它标记为 external,告诉 Vite 这个模块不需要处理。


六、Minify 模块:代码压缩

生产构建时,代码压缩是必不可少的一环。oxc-minify 提供了和 Terser 类似的压缩能力,但性能更好。

6.1 在 generateBundle 中压缩

代码压缩放在 generateBundle hook 里做:

async generateBundle(_outputOptions, bundle) {
  if (options.minify === false) return

  const { minifySync } = await import('oxc-minify')

  for (const fileName of Object.keys(bundle)) {
    const chunk = bundle[fileName]
    if (chunk.type !== 'chunk') continue

    try {
      const result = minifySync(fileName, chunk.code, {
        ...(options.minify === true ? {} : options.minify),
        sourcemap: options.sourcemap,
      })
      chunk.code = result.code

      // SourceMap 合并...
    } catch (error) {
      this.error(`Failed to minify ${fileName}: ${error}`)
    }
  }
}

这里有几个设计考量:

  1. 为什么在 generateBundle 而不是 transform 因为压缩应该在所有代码转换完成后、输出文件前进行。此时代码已经是最终形态,压缩效果最好。

  2. 为什么用动态 import? oxc-minify 只在生产构建时需要,开发模式下不需要加载这个依赖,动态 import 可以减少冷启动时间。

  3. 为什么跳过非 chunk 类型? bundle 对象里既有 JS chunk,也有 CSS、图片等 asset。我们只压缩 JS。

6.2 SourceMap 合并

压缩代码会改变代码的行列位置,如果项目需要 sourcemap,必须把压缩前后的 sourcemap 合并,才能正确映射到源码。

这个合并逻辑用 @ampproject/remapping 来做:

import remapping, { type EncodedSourceMap } from '@ampproject/remapping'

if (result.map && chunk.map) {
  const minifyMap: EncodedSourceMap = {
    version: 3,
    file: result.map.file,
    sources: result.map.sources,
    sourcesContent: result.map.sourcesContent,
    names: result.map.names,
    mappings: result.map.mappings,
  }
  const chunkMap: EncodedSourceMap = {
    version: 3,
    file: chunk.map.file,
    sources: chunk.map.sources,
    sourcesContent: chunk.map.sourcesContent,
    names: chunk.map.names,
    mappings: chunk.map.mappings,
  }

  // 合并两个 sourcemap
  const merged = remapping([minifyMap, chunkMap], () => null)

  chunk.map = {
    file: merged.file ?? '',
    mappings: merged.mappings as string,
    names: merged.names,
    sources: merged.sources as string[],
    sourcesContent: merged.sourcesContent as string[],
    version: merged.version,
    toUrl() {
      return `data:application/json;charset=utf-8;base64,${Buffer.from(JSON.stringify(this)).toString('base64')}`
    },
  }
}

remapping 函数接收一个 sourcemap 数组,按顺序合并。第一个是最终代码的 map(压缩后),第二个是上一步的 map(压缩前)。合并后的 map 可以从最终代码直接映射回原始源码。

6.3 压缩选项透传

oxc-minify 支持 Terser 风格的压缩选项:

// 默认压缩
oxc({ minify: true })

// 自定义选项
oxc({
  minify: {
    mangle: true,        // 变量名混淆
    compress: {
      dropConsole: true, // 删除 console.log
    },
  }
})

// 禁用压缩
oxc({ minify: false })

这些选项原封不动传给 minifySync,插件层只加了一个 sourcemap 选项的处理。


七、React Fast Refresh:HMR 的核心

React Fast Refresh 是 React 官方的热更新方案,可以在修改组件代码后保留组件状态,只更新改变的部分。要让它正常工作,需要在编译时注入一些运行时代码。

这部分是整个插件最复杂的地方。

7.1 Fast Refresh 的工作原理

Fast Refresh 的基本原理是:

  1. 编译时:在每个模块末尾注入代码,把模块导出的组件注册到 Fast Refresh runtime。
  2. 运行时:当模块热更新时,runtime 对比新旧导出,判断是否可以安全刷新。
  3. 刷新执行:如果可以安全刷新,runtime 触发 React 重新渲染更新后的组件,同时保留状态。

关键在于「安全刷新」的判断。Fast Refresh 只能处理「纯组件变更」的情况。如果模块导出了非组件内容(比如常量、工具函数),且这些内容发生了变化,就必须做完整刷新。

7.2 运行时模块

我实现了一个虚拟模块 /@react-refresh,提供 Fast Refresh 的运行时代码:

const refreshRuntimeCode = `
import RefreshRuntime from 'react-refresh/runtime';

export function injectIntoGlobalHook(globalObject) {
  RefreshRuntime.injectIntoGlobalHook(globalObject);
}

export function register(type, id) {
  RefreshRuntime.register(type, id);
}

export function createSignatureFunctionForTransform() {
  return RefreshRuntime.createSignatureFunctionForTransform();
}

export function performReactRefresh() {
  return RefreshRuntime.performReactRefresh();
}

// 判断是否是 React 组件
export function isLikelyComponentType(type) {
  if (typeof type !== 'function') return false;
  if (type.prototype != null && type.prototype.isReactComponent) return true;
  if (type.$$typeof) return false;
  const name = type.name || type.displayName;
  return typeof name === 'string' && /^[A-Z]/.test(name);
}

// 注册模块导出的组件
export function registerExportsForReactRefresh(filename, moduleExports) {
  for (const key in moduleExports) {
    if (key === '__esModule') continue;
    const exportValue = moduleExports[key];
    if (isLikelyComponentType(exportValue)) {
      RefreshRuntime.register(exportValue, filename + ' export ' + key);
    }
  }
}

// 防抖更新
let enqueueUpdateTimer = null;
function enqueueUpdate() {
  if (enqueueUpdateTimer === null) {
    enqueueUpdateTimer = setTimeout(() => {
      enqueueUpdateTimer = null;
      RefreshRuntime.performReactRefresh();
    }, 16);
  }
}

// 验证刷新边界并触发更新
export function validateRefreshBoundaryAndEnqueueUpdate(id, prevExports, nextExports) {
  // 检查导出是否发生不兼容的变化
  for (const key in prevExports) {
    if (key === '__esModule') continue;
    if (!(key in nextExports)) {
      return 'Could not Fast Refresh (export removed)';
    }
  }
  for (const key in nextExports) {
    if (key === '__esModule') continue;
    if (!(key in prevExports)) {
      return 'Could not Fast Refresh (new export)';
    }
  }

  let hasExports = false;
  for (const key in nextExports) {
    if (key === '__esModule') continue;
    hasExports = true;
    const value = nextExports[key];
    if (isLikelyComponentType(value)) continue;
    if (prevExports[key] === nextExports[key]) continue;
    return 'Could not Fast Refresh (non-component export changed)';
  }

  if (hasExports) {
    enqueueUpdate();
  }
  return undefined;
}

export const __hmr_import = (module) => import(/* @vite-ignore */ module);
`

这段代码做了几件事:

  1. 组件注册registerExportsForReactRefresh 遍历模块导出,把看起来像组件的函数注册到 runtime。
  2. 边界验证validateRefreshBoundaryAndEnqueueUpdate 检查新旧导出的差异,判断是否可以安全刷新。
  3. 防抖更新enqueueUpdate 用 16ms 的防抖,避免短时间内多次触发刷新。

7.3 HTML Preamble 注入

Fast Refresh 需要在页面加载之前初始化全局钩子。通过 transformIndexHtml hook 注入:

transformIndexHtml() {
  if (!isDev || !options.reactRefresh) return []

  return [
    {
      tag: 'script',
      attrs: { type: 'module' },
      children: `
import { injectIntoGlobalHook } from "/@react-refresh";
injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
`,
    },
  ]
}

这段脚本会被插入到 HTML 的 <head> 中,在任何业务代码执行之前运行。它做两件事:

  1. 调用 injectIntoGlobalHook(window) 初始化 runtime。
  2. window 上挂载两个占位函数 $RefreshReg$$RefreshSig$,防止业务代码报错。

7.4 模块尾部代码注入

最后,需要在每个 JSX/TSX 模块末尾注入 HMR 边界检测代码:

if (enableRefresh && transformedCode.includes('$RefreshReg$')) {
  const refreshFooter = `
import * as RefreshRuntime from "/@react-refresh";
const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
if (import.meta.hot && !inWebWorker) {
  if (!window.$RefreshReg$) {
    throw new Error(
      "vite-plugin-oxc can't detect preamble. Something is wrong."
    );
  }

  RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => {
    RefreshRuntime.registerExportsForReactRefresh(${JSON.stringify(id)}, currentExports);
    import.meta.hot.accept((nextExports) => {
      if (!nextExports) return;
      const invalidateMessage = RefreshRuntime.validateRefreshBoundaryAndEnqueueUpdate(
        ${JSON.stringify(id)},
        currentExports,
        nextExports
      );
      if (invalidateMessage) import.meta.hot.invalidate(invalidateMessage);
    });
  });
}
function $RefreshReg$(type, id) {
  return RefreshRuntime.register(type, ${JSON.stringify(id)} + ' ' + id)
}
function $RefreshSig$() {
  return RefreshRuntime.createSignatureFunctionForTransform();
}
`
  transformedCode = transformedCode + refreshFooter
}

这段代码的逻辑:

  1. 动态导入自身RefreshRuntime.__hmr_import(import.meta.url) 拿到当前模块的导出。
  2. 注册导出:把导出的组件注册到 runtime。
  3. 接受热更新:通过 import.meta.hot.accept 监听更新,拿到新的导出后验证边界。
  4. 判断刷新方式:如果边界验证失败(invalidateMessage 不为空),调用 invalidate 触发完整刷新;否则自动执行 Fast Refresh。

注意这里有个细节:只有当转换后的代码包含 $RefreshReg$ 时才注入。因为 Oxc 只会在检测到组件定义时才插入这些调用,如果模块里没有组件(比如纯工具函数文件),就不需要这套逻辑。

7.5 Web Worker 兼容

代码里有个判断:

const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
if (import.meta.hot && !inWebWorker) {
  // ...
}

Web Worker 环境没有 window 对象,也不支持 HMR,所以需要跳过。


八、文件过滤:控制处理范围

不是所有文件都需要经过 Oxc 处理。CSS、图片、JSON 这些应该跳过。

8.1 Filter 实现

export function createFilter(
  include?: FilterPattern,
  exclude?: FilterPattern
): (id: string) => boolean {
  const includePatterns = normalizePatterns(include)
  const excludePatterns = normalizePatterns(exclude)

  return (id: string) => {
    // 先检查 exclude,命中则跳过
    if (excludePatterns.length > 0) {
      for (const pattern of excludePatterns) {
        if (testPattern(pattern, id)) {
          return false
        }
      }
    }

    // 再检查 include
    if (includePatterns.length === 0) {
      return true // 没有 include 规则则默认处理
    }

    for (const pattern of includePatterns) {
      if (testPattern(pattern, id)) {
        return true
      }
    }

    return false
  }
}

function testPattern(pattern: string | RegExp, id: string): boolean {
  if (typeof pattern === 'string') {
    return id.includes(pattern)
  }
  return pattern.test(id)
}

这个实现遵循一个简单的规则:exclude 优先于 include。如果一个文件同时匹配 include 和 exclude,以 exclude 为准。

8.2 默认配置

include: options.include || [/\.[cm]?[jt]sx?$/],
exclude: options.exclude || [/node_modules/],

默认配置的含义:

  • include: 处理所有 .js.jsx.ts.tsx.mjs.mts.cjs.cts 文件
  • exclude: 跳过 node_modules 目录

[cm]? 这个正则匹配可选的 c(CommonJS)或 m(Module)前缀,覆盖了 Node.js 的各种模块扩展名约定。


九、配置系统设计

一个好的插件应该做到「零配置可用,有需要时可配」。

9.1 类型定义

export interface VitePluginOxcOptions {
  include?: FilterPattern          // 文件包含规则
  exclude?: FilterPattern          // 文件排除规则
  enforce?: 'pre' | 'post'         // 插件执行顺序
  transform?: TransformOptions | false  // 转换选项,false 禁用
  resolve?: NapiResolveOptions | false  // 解析选项,false 禁用
  resolveNodeModules?: boolean     // 是否解析 node_modules
  minify?: MinifyOptions | boolean // 压缩选项
  sourcemap?: boolean              // SourceMap 生成
  reactRefresh?: boolean           // React Fast Refresh
}

每个配置项都可以是具体的选项对象、布尔值、或不设置(使用默认值)。

9.2 选项解析

export function resolveOptions(
  options: VitePluginOxcOptions,
  isDev: boolean
): ResolvedOptions {
  return {
    include: options.include || [/\.[cm]?[jt]sx?$/],
    exclude: options.exclude || [/node_modules/],
    enforce: options.enforce,
    transform: options.transform !== false ? (options.transform || {}) : false,
    resolve: options.resolve !== false ? (options.resolve || {}) : false,
    resolveNodeModules: options.resolveNodeModules || false,
    minify: options.minify !== false ? (options.minify || false) : false,
    sourcemap: options.sourcemap ?? isDev,  // 开发模式默认开启
    reactRefresh: options.reactRefresh ?? true,  // 默认开启
  }
}

几个设计决策:

  1. transformresolve 默认开启,可以传 false 禁用
  2. minify 默认关闭,需要显式传 true 或选项对象开启
  3. sourcemap 根据环境决定,开发模式默认开启,生产模式默认关闭
  4. reactRefresh 默认开启,因为大部分 React 项目都需要

9.3 enforce 处理

enforce 的处理比较特殊:

const plugin: Plugin = {
  name: 'vite-plugin-oxc',
  enforce: 'pre',  // 默认值
  // ...
}

// 如果用户显式设置了 enforce,覆盖默认值
if ('enforce' in rawOptions) {
  plugin.enforce = rawOptions.enforce
}

为什么不直接用 options.enforce || 'pre'?因为用户可能想显式设置 enforce: undefined,表示不要任何 enforce 约束。用 'enforce' in rawOptions 可以区分「没传」和「传了 undefined」两种情况。


十、测试策略

工程化项目离不开测试。这个插件的测试主要覆盖以下场景。

10.1 单元测试结构

import { describe, it, expect, vi, beforeEach } from 'vitest'
import vitePluginOxc from '../src/index'

// Mock oxc-transform
vi.mock('oxc-transform', () => ({
  transformSync: vi.fn((_id: string, code: string, _options?: unknown) => ({
    code: `// Transformed: ${code}`,
    map: null,
    errors: [],
  })),
}))

// Mock oxc-resolver
vi.mock('oxc-resolver', () => ({
  ResolverFactory: class MockResolverFactory {
    sync(_directory: string, id: string) {
      return {
        path: `/resolved/${id}`,
        moduleType: 'module',
      }
    }
  },
}))

// Mock oxc-minify
vi.mock('oxc-minify', () => ({
  minifySync: vi.fn((fileName: string, code: string) => ({
    code: `/* Minified */ ${code.replace(/\s+/g, ' ').trim()}`,
    map: null,
  })),
}))

为什么要 mock Oxc 的依赖?因为:

  1. 隔离测试范围。单元测试关注的是插件的集成逻辑,不是 Oxc 本身的转换行为。
  2. 测试执行速度。原生依赖的加载需要时间,mock 后测试更快。
  3. 确定性。Oxc 的版本更新可能改变输出,mock 可以保证测试稳定。

10.2 核心测试用例

describe('vite-plugin-oxc', () => {
  it('should create plugin with default options', () => {
    const plugin = vitePluginOxc()
    expect(plugin.name).toBe('vite-plugin-oxc')
    expect(plugin.enforce).toBe('pre')
    expect(typeof plugin.transform).toBe('function')
  })

  it('should allow overriding enforce option', () => {
    const pluginPost = vitePluginOxc({ enforce: 'post' })
    expect(pluginPost.enforce).toBe('post')

    const pluginNone = vitePluginOxc({ enforce: undefined })
    expect(pluginNone.enforce).toBeUndefined()
  })
})

describe('generateBundle - oxc-minify integration', () => {
  it('should minify chunk code using oxc-minify', async () => {
    const plugin = vitePluginOxc({ minify: true })
    ;(plugin.configResolved as Function)({ command: 'build' })

    const bundle = {
      'index.js': {
        type: 'chunk',
        code: 'function hello() { console.log("hi"); }',
        map: null,
      },
    }

    await (plugin.generateBundle as Function).call({ error: vi.fn() }, {}, bundle)

    expect(bundle['index.js'].code).toContain('Minified')
  })

  it('should skip minification when minify is false', async () => {
    const plugin = vitePluginOxc({ minify: false })
    ;(plugin.configResolved as Function)({ command: 'build' })

    const originalCode = 'function hello() { console.log("hi"); }'
    const bundle = {
      'index.js': { type: 'chunk', code: originalCode, map: null },
    }

    await (plugin.generateBundle as Function).call({ error: vi.fn() }, {}, bundle)

    expect(bundle['index.js'].code).toBe(originalCode)
  })

  it('should merge sourcemaps when both exist', async () => {
    // 测试 sourcemap 合并逻辑
  })
})

测试覆盖了:

  • 插件创建和默认配置
  • 配置覆盖
  • 压缩功能的开启/关闭
  • SourceMap 合并
  • 错误处理

十一、性能实测

说了这么多理论,实际效果如何?我用一个中型 React 项目做了测试。

11.1 测试环境

  • 项目规模:约 200 个 TypeScript/TSX 文件,5 万行代码
  • 机器配置:MacBook Pro M2,16GB 内存
  • Node.js:v20.10.0

11.2 开发模式冷启动

方案 首次启动时间
Vite 默认(esbuild) 1.2s
vite-plugin-oxc 1.1s

开发模式差距不大,因为 Vite 的预构建已经很快了。

11.3 生产构建

方案 构建时间 产物体积
Vite 默认 18.3s 1.42 MB
vite-plugin-oxc (无压缩) 12.1s 1.58 MB
vite-plugin-oxc (开启压缩) 14.7s 1.39 MB

Transform 阶段提速明显(约 33%),开启 oxc-minify 后总体时间也有优势,且压缩效果略好于默认的 esbuild。

11.4 HMR 响应时间

修改一个组件文件后:

方案 HMR 更新时间
Vite + esbuild 50-80ms
vite-plugin-oxc 40-60ms

HMR 场景下 Oxc 的优势更明显,因为单文件转换时 Oxc 的启动开销比例更低。


十二、踩过的坑

开发过程中遇到了不少问题,记录几个典型的。

12.1 esbuild 的 JSX 处理冲突

最开始没有禁用 esbuild 的 JSX 处理,导致 JSX 被转换了两次,结果代码里出现了奇怪的双重嵌套。

解决方案就是在 config hook 里配置 esbuild 跳过 JSX/TSX:

config() {
  return {
    esbuild: {
      include: /\.ts$/,
      exclude: /\.[jt]sx$/,
    },
  }
}

12.2 SourceMap 合并顺序

第一版 sourcemap 合并写反了顺序:

// 错误写法
const merged = remapping([chunkMap, minifyMap], () => null)

// 正确写法
const merged = remapping([minifyMap, chunkMap], () => null)

remapping 的数组是从「最终代码」到「原始代码」的顺序。压缩后的 map 在前,压缩前的 map 在后。

12.3 React Refresh 的 preamble 时机

Fast Refresh 的 preamble 必须在任何业务代码之前执行。最开始我用 transform hook 在第一个 JSX 文件转换时注入,结果时机不稳定。

后来改用 transformIndexHtml hook,直接往 HTML 里插入 <script>,稳定多了。

12.4 模块格式推断

有些项目混用 ESM 和 CommonJS,如果模块格式判断错误,会导致语法错误或运行时问题。

最后的方案是综合多个信息源:

  1. 上游传递的 format 参数
  2. 文件扩展名(.mjs/.cjs 等)
  3. Oxc resolver 返回的 moduleType
  4. 兜底默认值

12.5 虚拟模块的处理

/@react-refresh 是个虚拟模块,不存在于文件系统。需要在 resolveIdload 两个 hook 里配合处理:

resolveId(id) {
  if (id === '/@react-refresh') {
    return id  // 告诉 Vite 这个 ID 我来处理
  }
}

load(id) {
  if (id === '/@react-refresh') {
    return refreshRuntimeCode  // 返回模块内容
  }
}

十三、未来展望

13.1 Rolldown 的影响

Vite 团队正在开发 Rolldown,一个用 Rust 重写的 Rollup。一旦 Rolldown 成熟,Vite 的整个构建流程都会是 Rust 实现,性能会再上一个台阶。

Rolldown 底层使用的就是 Oxc 的 parser 和 transformer,所以 vite-plugin-oxc 的很多逻辑可能会被 Vite 原生支持。到那时,这个插件的历史使命可能就完成了。

实际上,官方文档里已经提到:

这个包已弃用。请使用 @vitejs/plugin-react,因为 rolldown-vite 已自动启用基于 Oxc 的 Fast Refresh 转换。

这说明方向是对的,只是时机早了一点。

13.2 工具链的 Rust 化趋势

纵观前端工具链的演进,Rust 化是一个明确的趋势:

  • Bundler:Rolldown、Turbopack
  • Compiler:SWC、Oxc
  • Linter:oxlint、Biome
  • Formatter:dprint、Biome
  • Package Manager:pnpm(部分 Rust)、Bun(Zig)

JavaScript 工具用 JavaScript 写的时代正在过去。对于开发者来说,这意味着更快的开发体验,但也意味着参与工具开发的门槛变高了——你得会 Rust。

13.3 这个插件的定位

虽然 Rolldown 出来后这个插件可能就没用了,但它的价值在于:

  1. 作为学习材料。展示了如何把一个 Rust 工具链集成到现有的 JavaScript 生态中。
  2. 作为过渡方案。在 Rolldown 正式发布前,想尝鲜 Oxc 的人可以用这个插件。
  3. 作为参考实现。React Fast Refresh 的集成逻辑,sourcemap 合并的处理,这些代码可以被其他项目借鉴。

十四、总结

回到开头的问题:2026 年了,前端构建为什么还是慢?

答案是:工具链正在追赶硬件的脚步,只是还没追上。

从 Babel 到 esbuild,从 SWC 到 Oxc,每一代工具都在压榨更多性能。vite-plugin-oxc 是我在这条路上的一次尝试——用 Oxc 这套 Rust 工具链,给 Vite 的构建流程提提速。

核心实现其实不复杂:

  • Transform:调用 oxc-transform 做代码转换,处理好 sourceType 推断和 JSX 配置
  • Resolve:用 oxc-resolver 做模块解析,默认跳过 node_modules 保证性能
  • Minify:在 generateBundle 阶段用 oxc-minify 压缩,注意 sourcemap 合并
  • React Fast Refresh:虚拟模块 + HTML preamble + 模块尾部注入,三件套配合

难点在于细节:与 Vite 内置 esbuild 的配合、sourcemap 合并的顺序、模块格式的正确推断、HMR 边界检测的实现……这些东西文档不会告诉你,只能靠踩坑。

这个插件的代码开源在 GitHub 上,欢迎 star 和 PR。虽然它可能很快就会被 Rolldown 取代,但在那之前,希望它能给想了解 Vite 插件开发、Oxc 集成的同学一些参考。

前端工具链的进化永远不会停止。今天是 Oxc,明天可能是更快的东西。作为开发者,保持学习、保持好奇,可能是我们能做的最重要的事。

项目源码:github.com/Sunny-117/v… 欢迎 Star、Issue 和 PR!


参考资料

构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的

本文将从第一人称实战视角,深入探讨前端构建工具的技术演进,以及我在设计 robuild 过程中的架构思考与工程实践。

引言:为什么我们需要又一个构建工具?

在开始正文之前,我想先回答一个无法回避的问题:在 Webpack、Rollup、esbuild、Vite 已经如此成熟的今天,为什么还要设计一个新的构建工具?

答案很简单:库构建与应用构建是两个本质不同的问题域

Webpack 为复杂应用而生,Vite 为开发体验而生,esbuild 为速度而生。但当我们需要构建一个 npm 库时,我们需要的是:

  1. 零配置:库作者不应该花时间在配置上
  2. 多格式输出:ESM、CJS、甚至 UMD 一键生成
  3. 类型声明:TypeScript 项目的 .d.ts 自动生成
  4. Tree-shaking 友好:输出代码必须对消费者友好
  5. 极致性能:构建速度不应该成为开发瓶颈

robuild 就是为解决这些问题而设计的。它基于 Rolldown(Rust 实现的 Rollup 替代品)和 Oxc(Rust 实现的 JavaScript 工具链),专注于库构建场景。

接下来,让我从构建工具的历史演进说起。


第一章:构建工具的三次演进

1.1 第一次革命:Webpack 时代(2012-2017)

2012 年,Webpack 横空出世,彻底改变了前端工程化的格局。

在 Webpack 之前,前端工程师面对的是一个碎片化的世界:RequireJS 处理模块加载,Grunt/Gulp 处理任务流程,各种工具各司其职却又互不兼容。Webpack 的革命性在于它提出了一个统一的心智模型:一切皆模块

// Webpack 的核心思想:统一的依赖图
// JS、CSS、图片、字体,都是图中的节点
module.exports = {
  entry: './src/index.js',
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.png$/, use: ['file-loader'] }
    ]
  }
}

Webpack 的架构基于以下核心概念:

  1. 依赖图(Dependency Graph):从入口点出发,递归解析所有依赖
  2. Loader 机制:将非 JS 资源转换为模块
  3. Plugin 系统:基于 Tapable 的事件驱动架构
  4. Chunk 分割:智能的代码分割策略

但 Webpack 也有其历史局限性:

  • 配置复杂:动辄数百行的配置文件
  • 构建速度:随着项目规模增长,构建时间呈指数级增长
  • 输出冗余:运行时代码占比较高,不利于库构建

1.2 第二次革命:Rollup 时代(2017-2022)

2017 年左右,Rollup 开始崛起,它代表了一种完全不同的设计哲学:面向 ES Module 的静态分析

Rollup 的核心创新是 Tree Shaking——通过静态分析 ES Module 的 import/export 语句,只打包实际使用的代码。这在库构建场景下意义重大:

// input.js
import { add, multiply } from './math.js'
console.log(add(2, 3))
// multiply 未使用

// math.js
export function add(a, b) { return a + b }
export function multiply(a, b) { return a * b }

// output.js (Rollup 输出,multiply 被移除)
function add(a, b) { return a + b }
console.log(add(2, 3))

Rollup 能做到这一点,是因为 ES Module 具有以下静态特性:

  1. 静态导入import 语句必须在模块顶层,不能动态
  2. 静态导出export 的绑定在编译时就能确定
  3. 只读绑定:导入的值不能被重新赋值

这使得编译器可以在构建时进行精确的依赖分析,而不需要运行代码。

作用域提升(Scope Hoisting) 是 Rollup 的另一个重要特性。与 Webpack 将每个模块包裹在函数中不同,Rollup 会将所有模块"展平"到同一个作用域:

// Webpack 风格的输出
var __webpack_modules__ = {
  "./src/a.js": (module) => { module.exports = 1 },
  "./src/b.js": (module, exports, require) => {
    const a = require("./src/a.js")
    module.exports = a + 1
  }
}

// Rollup 风格的输出
const a = 1
const b = a + 1

这种输出更紧凑、运行时开销更低,非常适合库构建。

1.3 第三次革命:Rust Bundler 时代(2022-今)

2022 年开始,我们迎来了构建工具的第三次革命:Rust 重写一切

这场革命的先驱是 esbuild(Go 语言)和 SWC(Rust)。它们用系统级语言重写了 JavaScript 的解析、转换、打包流程,获得了 10-100 倍的性能提升。

为什么 Rust 成为了这场革命的主角?

  1. 零成本抽象:高级语言特性不带来运行时开销
  2. 内存安全:编译器保证没有数据竞争和悬空指针
  3. 真正的并行:无 GC 停顿,能充分利用多核
  4. 可编译到 WASM:可以在浏览器和 Node.js 中运行

Rolldown 和 Oxc 是这场革命的最新成果:

Rolldown:Rollup 的 Rust 实现,由 Vue.js 团队主导,目标是成为 Vite 的默认打包器。它保持了 Rollup 的 API 兼容性,同时获得了 Rust 带来的性能优势。

Oxc:一个完整的 JavaScript 工具链,包括解析器、转换器、代码检查器、格式化器、压缩器。它的设计目标是成为 Babel、ESLint、Prettier、Terser 的统一替代品。

传统工具链                    Oxc 工具链
Babel (转换)                 oxc-transform
ESLint (检查)        →       oxc-linter
Prettier (格式化)            oxc-formatter
Terser (压缩)                oxc-minify

robuild 选择基于 Rolldown + Oxc 构建,正是看中了这两个项目的技术潜力和生态定位。


第二章:理解 Bundler 核心原理

在深入 robuild 的设计之前,我想先从原理层面解释 Bundler 是如何工作的。我会实现一个 Mini Bundler,让你真正理解打包器的核心逻辑。

2.1 从零实现 Mini Bundler

一个最简的 Bundler 需要完成以下步骤:

  1. 解析:将源代码转换为 AST
  2. 依赖收集:从 AST 中提取 import 语句
  3. 依赖图构建:递归处理所有依赖,构建完整的模块图
  4. 打包:将所有模块合并为单个文件

下面是完整的实现代码:

// mini-bundler.js
// 一个完整的 Mini Bundler 实现,约 300 行代码
// 支持 ES Module 解析、依赖图构建、打包输出

import * as fs from 'node:fs'
import * as path from 'node:path'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import { transformFromAstSync } from '@babel/core'

// ============================================
// 第一部分:模块解析器
// ============================================

let moduleId = 0  // 模块计数器,用于生成唯一 ID

/**
 * 解析单个模块
 * @param {string} filePath - 模块文件的绝对路径
 * @returns {Object} 模块信息对象
 */
function parseModule(filePath) {
  const content = fs.readFileSync(filePath, 'utf-8')

  // 1. 使用 Babel 解析为 AST
  // 这里我们支持 TypeScript 和 JSX
  const ast = parse(content, {
    sourceType: 'module',
    plugins: ['typescript', 'jsx']
  })

  // 2. 收集依赖信息
  const dependencies = []
  const imports = []  // 详细的导入信息
  const exports = []  // 详细的导出信息

  traverse.default(ast, {
    // 处理 import 声明
    // import { foo, bar } from './module'
    // import defaultExport from './module'
    // import * as namespace from './module'
    ImportDeclaration({ node }) {
      const specifier = node.source.value
      dependencies.push(specifier)

      // 提取导入的具体内容
      const importedNames = node.specifiers.map(spec => {
        if (spec.type === 'ImportDefaultSpecifier') {
          return { type: 'default', local: spec.local.name }
        }
        if (spec.type === 'ImportNamespaceSpecifier') {
          return { type: 'namespace', local: spec.local.name }
        }
        // ImportSpecifier
        return {
          type: 'named',
          imported: spec.imported.name,
          local: spec.local.name
        }
      })

      imports.push({
        specifier,
        importedNames,
        start: node.start,
        end: node.end
      })
    },

    // 处理动态 import()
    // const mod = await import('./module')
    CallExpression({ node }) {
      if (node.callee.type === 'Import' &&
          node.arguments[0]?.type === 'StringLiteral') {
        dependencies.push(node.arguments[0].value)
        imports.push({
          specifier: node.arguments[0].value,
          isDynamic: true,
          start: node.start,
          end: node.end
        })
      }
    },

    // 处理 export 声明
    // export { foo, bar }
    // export const x = 1
    // export default function() {}
    ExportNamedDeclaration({ node }) {
      if (node.declaration) {
        // export const x = 1
        if (node.declaration.declarations) {
          for (const decl of node.declaration.declarations) {
            exports.push({
              type: 'named',
              name: decl.id.name,
              local: decl.id.name
            })
          }
        }
        // export function foo() {}
        else if (node.declaration.id) {
          exports.push({
            type: 'named',
            name: node.declaration.id.name,
            local: node.declaration.id.name
          })
        }
      }
      // export { foo, bar }
      for (const spec of node.specifiers || []) {
        exports.push({
          type: 'named',
          name: spec.exported.name,
          local: spec.local.name
        })
      }
      // export { foo } from './module'
      if (node.source) {
        dependencies.push(node.source.value)
      }
    },

    ExportDefaultDeclaration({ node }) {
      exports.push({ type: 'default', name: 'default' })
    },

    // export * from './module'
    ExportAllDeclaration({ node }) {
      dependencies.push(node.source.value)
      exports.push({
        type: 'star',
        from: node.source.value,
        as: node.exported?.name  // export * as name
      })
    }
  })

  // 3. 转换代码:移除类型注解,转换为 CommonJS
  // 这样我们可以在运行时执行
  const { code } = transformFromAstSync(ast, content, {
    presets: ['@babel/preset-typescript'],
    plugins: [
      ['@babel/plugin-transform-modules-commonjs', {
        strict: true,
        noInterop: false
      }]
    ]
  })

  return {
    id: moduleId++,
    filePath,
    dependencies,
    imports,
    exports,
    code,
    ast
  }
}

// ============================================
// 第二部分:模块路径解析
// ============================================

/**
 * 解析模块路径
 * 将 import 语句中的相对路径转换为绝对路径
 */
function resolveModule(specifier, fromDir) {
  // 相对路径
  if (specifier.startsWith('.') || specifier.startsWith('/')) {
    let resolved = path.resolve(fromDir, specifier)

    // 尝试添加扩展名
    const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json']

    // 直接匹配文件
    for (const ext of extensions) {
      const withExt = resolved + ext
      if (fs.existsSync(withExt)) {
        return withExt
      }
    }

    // 尝试 index 文件
    for (const ext of extensions) {
      const indexPath = path.join(resolved, `index${ext}`)
      if (fs.existsSync(indexPath)) {
        return indexPath
      }
    }

    // 如果原路径存在(有扩展名的情况)
    if (fs.existsSync(resolved)) {
      return resolved
    }

    throw new Error(`Cannot resolve module: ${specifier} from ${fromDir}`)
  }

  // 外部模块(node_modules)
  // 简化处理,返回原始标识符
  return specifier
}

/**
 * 判断是否为外部模块
 */
function isExternalModule(specifier) {
  return !specifier.startsWith('.') && !specifier.startsWith('/')
}

// ============================================
// 第三部分:依赖图构建
// ============================================

/**
 * 构建依赖图
 * 从入口开始,递归解析所有模块
 * @param {string} entryPath - 入口文件路径
 * @returns {Array} 模块数组(拓扑排序)
 */
function buildDependencyGraph(entryPath) {
  const absoluteEntry = path.resolve(entryPath)
  const entryModule = parseModule(absoluteEntry)

  // 广度优先遍历
  const moduleQueue = [entryModule]
  const moduleMap = new Map()  // filePath -> module
  moduleMap.set(absoluteEntry, entryModule)

  for (const module of moduleQueue) {
    const dirname = path.dirname(module.filePath)

    // 存储依赖映射:相对路径 -> 模块 ID
    module.mapping = {}

    for (const dep of module.dependencies) {
      // 跳过外部模块
      if (isExternalModule(dep)) {
        module.mapping[dep] = null  // null 表示外部依赖
        continue
      }

      // 解析依赖的绝对路径
      const depPath = resolveModule(dep, dirname)

      // 避免重复解析
      if (moduleMap.has(depPath)) {
        module.mapping[dep] = moduleMap.get(depPath).id
        continue
      }

      // 解析新的依赖模块
      const depModule = parseModule(depPath)
      moduleMap.set(depPath, depModule)
      moduleQueue.push(depModule)
      module.mapping[dep] = depModule.id
    }
  }

  // 返回模块数组
  return Array.from(moduleMap.values())
}

// ============================================
// 第四部分:代码生成
// ============================================

/**
 * 生成打包后的代码
 * @param {Array} modules - 模块数组
 * @returns {string} 打包后的代码
 */
function generateBundle(modules) {
  // 生成模块定义
  let modulesCode = ''

  for (const mod of modules) {
    // 每个模块包装为 [factory, mapping] 格式
    // factory 是模块工厂函数
    // mapping 是依赖映射表
    modulesCode += `
    // ${mod.filePath}
    ${mod.id}: [
      function(module, exports, require) {
        ${mod.code}
      },
      ${JSON.stringify(mod.mapping)}
    ],`
  }

  // 生成运行时代码
  const runtime = `
// Mini Bundler 输出
// 生成时间: ${new Date().toISOString()}

(function(modules) {
  // 模块缓存
  const cache = {}

  // 自定义 require 函数
  function require(id) {
    // 如果是外部模块(id 为 null),使用原生 require
    if (id === null) {
      throw new Error('External module should be loaded via native require')
    }

    // 检查缓存
    if (cache[id]) {
      return cache[id].exports
    }

    // 获取模块定义
    const [factory, mapping] = modules[id]

    // 创建模块对象
    const module = {
      exports: {}
    }

    // 缓存模块(处理循环依赖)
    cache[id] = module

    // 创建本地 require 函数
    // 将相对路径映射为模块 ID
    function localRequire(name) {
      const mappedId = mapping[name]

      // 外部模块
      if (mappedId === null) {
        // 在实际环境中,这里应该使用 native require
        // 为了演示,我们抛出错误
        if (typeof window === 'undefined') {
          return require(name)  // Node.js 环境
        }
        throw new Error(\`External module not available: \${name}\`)
      }

      return require(mappedId)
    }

    // 执行模块工厂函数
    factory(module, module.exports, localRequire)

    return module.exports
  }

  // 执行入口模块(ID 为 0)
  require(0)

})({${modulesCode}
})
`

  return runtime
}

// ============================================
// 第五部分:主入口
// ============================================

/**
 * 打包入口
 * @param {string} entryPath - 入口文件路径
 * @param {string} outputPath - 输出文件路径
 */
function bundle(entryPath, outputPath) {
  console.log(`\n📦 Mini Bundler`)
  console.log(`   Entry: ${entryPath}`)
  console.log(`   Output: ${outputPath}\n`)

  // 1. 构建依赖图
  console.log('1. Building dependency graph...')
  const modules = buildDependencyGraph(entryPath)
  console.log(`   Found ${modules.length} modules:`)
  for (const mod of modules) {
    console.log(`   - [${mod.id}] ${path.relative(process.cwd(), mod.filePath)}`)
    console.log(`         Deps: ${mod.dependencies.join(', ') || '(none)'}`)
  }

  // 2. 生成打包代码
  console.log('\n2. Generating bundle...')
  const bundledCode = generateBundle(modules)

  // 3. 写入文件
  const outputDir = path.dirname(outputPath)
  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true })
  }
  fs.writeFileSync(outputPath, bundledCode, 'utf-8')

  // 4. 输出统计
  const stats = fs.statSync(outputPath)
  console.log(`\n3. Bundle stats:`)
  console.log(`   Size: ${(stats.size / 1024).toFixed(2)} KB`)
  console.log(`   Modules: ${modules.length}`)

  console.log(`\n✅ Bundle created successfully!`)
  console.log(`   ${outputPath}\n`)

  return { modules, code: bundledCode }
}

// 导出 API
export {
  bundle,
  parseModule,
  buildDependencyGraph,
  generateBundle,
  resolveModule
}

// CLI 支持
if (process.argv[2]) {
  const entry = process.argv[2]
  const output = process.argv[3] || './dist/bundle.js'
  bundle(entry, output)
}

让我们用一个例子测试这个 Mini Bundler:

// src/index.js - 入口文件
import { add, multiply } from './math.js'
import { formatResult } from './utils/format.js'

const result = add(2, 3)
console.log(formatResult('2 + 3', result))
console.log(formatResult('2 * 3', multiply(2, 3)))
// src/math.js - 数学工具
export function add(a, b) {
  return a + b
}

export function multiply(a, b) {
  return a * b
}

export function subtract(a, b) {
  return a - b
}
// src/utils/format.js - 格式化工具
export function formatResult(expression, result) {
  return `${expression} = ${result}`
}

运行打包:

node mini-bundler.js src/index.js dist/bundle.js

输出:

📦 Mini Bundler
   Entry: src/index.js
   Output: dist/bundle.js

1. Building dependency graph...
   Found 3 modules:
   - [0] src/index.js
         Deps: ./math.js, ./utils/format.js
   - [1] src/math.js
         Deps: (none)
   - [2] src/utils/format.js
         Deps: (none)

2. Generating bundle...

3. Bundle stats:
   Size: 1.84 KB
   Modules: 3

 Bundle created successfully!
   dist/bundle.js

2.2 AST 依赖分析深入

上面的代码使用 Babel 进行解析。让我们更深入地看看 AST 依赖分析的细节。

ES Module 的依赖信息主要来自以下 AST 节点类型:

// 1. ImportDeclaration - 静态导入
import defaultExport from './module.js'
import { named } from './module.js'
import * as namespace from './module.js'

// 2. ExportNamedDeclaration - 具名导出(可能包含 re-export)
export { foo } from './module.js'
export { default as foo } from './module.js'

// 3. ExportAllDeclaration - 星号导出
export * from './module.js'
export * as name from './module.js'

// 4. ImportExpression - 动态导入
const mod = await import('./module.js')

下面是一个更完整的依赖提取实现:

/**
 * 从 AST 中提取所有依赖信息
 * 返回结构化的依赖对象
 */
function extractDependencies(ast, filePath) {
  const result = {
    // 静态导入
    staticImports: [],
    // 动态导入
    dynamicImports: [],
    // Re-export
    reExports: [],
    // CommonJS require(用于分析混合代码)
    requires: [],
    // 导出信息
    exports: {
      named: [],      // export { foo }
      default: false, // export default
      star: []        // export * from
    }
  }

  traverse.default(ast, {
    // ===== 导入 =====

    ImportDeclaration({ node }) {
      const specifier = node.source.value

      // 提取导入的具体绑定
      const bindings = node.specifiers.map(spec => {
        switch (spec.type) {
          case 'ImportDefaultSpecifier':
            return {
              type: 'default',
              local: spec.local.name,
              imported: 'default'
            }
          case 'ImportNamespaceSpecifier':
            return {
              type: 'namespace',
              local: spec.local.name,
              imported: '*'
            }
          case 'ImportSpecifier':
            return {
              type: 'named',
              local: spec.local.name,
              imported: spec.imported.name
            }
        }
      })

      result.staticImports.push({
        specifier,
        bindings,
        // 位置信息用于 source map 和错误报告
        loc: {
          start: node.loc.start,
          end: node.loc.end
        }
      })
    },

    // 动态 import()
    ImportExpression({ node }) {
      const source = node.source

      if (source.type === 'StringLiteral') {
        // 静态字符串
        result.dynamicImports.push({
          specifier: source.value,
          isDynamic: false,
          loc: node.loc
        })
      } else {
        // 动态表达式,无法静态分析
        result.dynamicImports.push({
          specifier: null,
          isDynamic: true,
          expression: source,
          loc: node.loc
        })
      }
    },

    // ===== 导出 =====

    ExportNamedDeclaration({ node }) {
      // export { foo, bar as baz }
      for (const spec of node.specifiers || []) {
        result.exports.named.push({
          exported: spec.exported.name,
          local: spec.local.name
        })
      }

      // export const x = 1 / export function foo() {}
      if (node.declaration) {
        const decl = node.declaration
        if (decl.declarations) {
          // VariableDeclaration
          for (const d of decl.declarations) {
            result.exports.named.push({
              exported: d.id.name,
              local: d.id.name
            })
          }
        } else if (decl.id) {
          // FunctionDeclaration / ClassDeclaration
          result.exports.named.push({
            exported: decl.id.name,
            local: decl.id.name
          })
        }
      }

      // export { foo } from './module'
      if (node.source) {
        result.reExports.push({
          specifier: node.source.value,
          bindings: node.specifiers.map(spec => ({
            exported: spec.exported.name,
            imported: spec.local.name
          })),
          loc: node.loc
        })
      }
    },

    ExportDefaultDeclaration({ node }) {
      result.exports.default = true
    },

    ExportAllDeclaration({ node }) {
      result.exports.star.push({
        specifier: node.source.value,
        as: node.exported?.name || null
      })

      result.reExports.push({
        specifier: node.source.value,
        bindings: '*',
        as: node.exported?.name,
        loc: node.loc
      })
    },

    // ===== CommonJS =====

    CallExpression({ node }) {
      // require('module')
      if (node.callee.type === 'Identifier' &&
          node.callee.name === 'require' &&
          node.arguments[0]?.type === 'StringLiteral') {
        result.requires.push({
          specifier: node.arguments[0].value,
          loc: node.loc
        })
      }
    }
  })

  return result
}

2.3 Tree Shaking 简化实现

Tree Shaking 的核心是标记-清除算法

  1. 标记阶段:从入口点开始,标记所有"活"的导出
  2. 清除阶段:移除所有未标记的代码

下面是一个简化的 Tree Shaking 实现:

/**
 * 简化的 Tree Shaking 实现
 * 核心思想:追踪哪些导出被使用了
 */
class TreeShaker {
  constructor() {
    this.modules = new Map()       // moduleId -> ModuleInfo
    this.usedExports = new Map()   // moduleId -> Set<exportName>
    this.sideEffectModules = new Set()
  }

  /**
   * 添加模块到分析器
   */
  addModule(moduleInfo) {
    this.modules.set(moduleInfo.id, moduleInfo)

    // 分析模块导出
    moduleInfo.exportMap = new Map()

    for (const exp of moduleInfo.exports) {
      if (exp.type === 'named' || exp.type === 'default') {
        moduleInfo.exportMap.set(exp.name, {
          type: exp.type,
          local: exp.local,
          // 追踪导出来源(本地声明 or re-export)
          source: exp.from || null
        })
      }
    }

    // 检测副作用
    moduleInfo.hasSideEffects = this.detectSideEffects(moduleInfo)
  }

  /**
   * 检测模块是否有副作用
   * 副作用包括:顶层函数调用、全局变量修改等
   */
  detectSideEffects(moduleInfo) {
    const { ast } = moduleInfo

    let hasSideEffects = false

    traverse.default(ast, {
      // 顶层表达式语句可能有副作用
      ExpressionStatement(path) {
        // 只检查顶层
        if (path.parent.type === 'Program') {
          const expr = path.node.expression

          // 函数调用
          if (expr.type === 'CallExpression') {
            hasSideEffects = true
          }

          // 赋值表达式
          if (expr.type === 'AssignmentExpression') {
            // 检查是否是全局变量赋值
            const left = expr.left
            if (left.type === 'Identifier') {
              // 简化判断:非 const/let/var 声明的赋值
              hasSideEffects = true
            }
            if (left.type === 'MemberExpression') {
              // window.foo = ... / global.bar = ...
              hasSideEffects = true
            }
          }
        }
      }
    })

    return hasSideEffects
  }

  /**
   * 从入口开始标记使用的导出
   */
  markFromEntry(entryId) {
    const entryModule = this.modules.get(entryId)

    // 入口模块的所有导出都被认为"使用"
    const allExports = Array.from(entryModule.exportMap.keys())
    this.markUsed(entryId, allExports)
  }

  /**
   * 标记模块的导出为已使用
   */
  markUsed(moduleId, exportNames) {
    // 初始化集合
    if (!this.usedExports.has(moduleId)) {
      this.usedExports.set(moduleId, new Set())
    }

    const used = this.usedExports.get(moduleId)
    const module = this.modules.get(moduleId)

    for (const name of exportNames) {
      if (used.has(name)) continue  // 已处理
      used.add(name)

      // 查找导出定义
      const exportInfo = module.exportMap.get(name)
      if (!exportInfo) continue

      // 如果是 re-export,递归标记源模块
      if (exportInfo.source) {
        const sourceModule = this.findModuleBySpecifier(module, exportInfo.source)
        if (sourceModule) {
          this.markUsed(sourceModule.id, [exportInfo.local])
        }
      }

      // 追踪本地导出引用的导入
      this.traceImports(module, exportInfo.local)
    }

    // 如果模块有副作用,标记为必须包含
    if (module.hasSideEffects) {
      this.sideEffectModules.add(moduleId)
    }
  }

  /**
   * 追踪导出绑定使用的导入
   */
  traceImports(module, localName) {
    // 简化实现:标记该模块所有导入的模块
    // 完整实现需要进行作用域分析

    for (const imp of module.imports || []) {
      // 检查导入的绑定是否被使用
      for (const binding of imp.bindings || []) {
        if (binding.local === localName) {
          const sourceModule = this.findModuleBySpecifier(module, imp.specifier)
          if (sourceModule) {
            // 标记使用的具体导出
            const usedExport = binding.imported === 'default'
              ? 'default'
              : binding.imported
            this.markUsed(sourceModule.id, [usedExport])
          }
        }
      }
    }
  }

  /**
   * 根据模块说明符查找模块
   */
  findModuleBySpecifier(fromModule, specifier) {
    const targetId = fromModule.mapping?.[specifier]
    if (targetId !== undefined && targetId !== null) {
      return this.modules.get(targetId)
    }
    return null
  }

  /**
   * 获取 Shake 后的结果
   */
  getShakeResult() {
    const includedModules = []
    const excludedExports = new Map()

    for (const [moduleId, module] of this.modules) {
      const used = this.usedExports.get(moduleId) || new Set()
      const hasSideEffects = this.sideEffectModules.has(moduleId)

      // 包含条件:有使用的导出 OR 有副作用
      if (used.size > 0 || hasSideEffects) {
        includedModules.push({
          id: moduleId,
          path: module.filePath,
          usedExports: Array.from(used),
          hasSideEffects
        })

        // 记录未使用的导出
        const allExports = Array.from(module.exportMap.keys())
        const unused = allExports.filter(e => !used.has(e))
        if (unused.length > 0) {
          excludedExports.set(moduleId, unused)
        }
      }
    }

    return {
      includedModules,
      excludedExports,
      stats: {
        totalModules: this.modules.size,
        includedModules: includedModules.length,
        removedModules: this.modules.size - includedModules.length
      }
    }
  }
}

// 使用示例
function performTreeShaking(modules, entryId) {
  const shaker = new TreeShaker()

  // 添加所有模块
  for (const mod of modules) {
    shaker.addModule(mod)
  }

  // 从入口开始标记
  shaker.markFromEntry(entryId)

  // 获取结果
  return shaker.getShakeResult()
}

2.4 作用域分析核心思路

作用域分析是 Tree Shaking 和变量重命名的基础。核心挑战是正确处理 JavaScript 的作用域规则:

/**
 * 作用域分析器
 * 构建作用域树,追踪变量的声明和引用
 */
class ScopeAnalyzer {
  constructor() {
    this.scopes = []
    this.currentScope = null
  }

  /**
   * 分析 AST,构建作用域树
   */
  analyze(ast) {
    // 创建全局/模块作用域
    this.currentScope = this.createScope('module', null)

    traverse.default(ast, {
      // ===== 作用域边界 =====

      // 函数创建新作用域
      FunctionDeclaration: (path) => {
        this.enterFunctionScope(path)
      },
      'FunctionDeclaration:exit': () => {
        this.exitScope()
      },

      FunctionExpression: (path) => {
        this.enterFunctionScope(path)
      },
      'FunctionExpression:exit': () => {
        this.exitScope()
      },

      ArrowFunctionExpression: (path) => {
        this.enterFunctionScope(path)
      },
      'ArrowFunctionExpression:exit': () => {
        this.exitScope()
      },

      // 块级作用域(if、for、while 等)
      BlockStatement: (path) => {
        // 只有包含 let/const 声明时才创建块级作用域
        if (this.hasBlockScopedDeclarations(path.node)) {
          this.enterBlockScope(path)
        }
      },
      'BlockStatement:exit': (path) => {
        if (this.hasBlockScopedDeclarations(path.node)) {
          this.exitScope()
        }
      },

      // ===== 声明 =====

      VariableDeclaration: (path) => {
        const kind = path.node.kind  // var, let, const

        for (const decl of path.node.declarations) {
          this.declareBinding(decl.id, kind, path)
        }
      },

      FunctionDeclaration: (path) => {
        if (path.node.id) {
          // 函数声明提升到外层作用域
          this.declareBinding(path.node.id, 'function', path)
        }
      },

      ClassDeclaration: (path) => {
        if (path.node.id) {
          this.declareBinding(path.node.id, 'class', path)
        }
      },

      ImportDeclaration: (path) => {
        for (const spec of path.node.specifiers) {
          this.declareBinding(spec.local, 'import', path)
        }
      },

      // ===== 引用 =====

      Identifier: (path) => {
        if (this.isReference(path)) {
          this.recordReference(path.node.name, path)
        }
      }
    })

    return this.scopes
  }

  /**
   * 创建新作用域
   */
  createScope(type, parent) {
    const scope = {
      id: this.scopes.length,
      type,           // 'module', 'function', 'block'
      parent,
      children: [],
      bindings: new Map(),    // name -> BindingInfo
      references: [],         // 引用列表
      // 统计信息
      stats: {
        declarations: 0,
        references: 0
      }
    }

    if (parent) {
      parent.children.push(scope)
    }

    this.scopes.push(scope)
    return scope
  }

  /**
   * 进入函数作用域
   */
  enterFunctionScope(path) {
    const scope = this.createScope('function', this.currentScope)
    this.currentScope = scope

    // 函数参数作为绑定
    for (const param of path.node.params || []) {
      this.declarePattern(param, 'param')
    }
  }

  /**
   * 进入块级作用域
   */
  enterBlockScope(path) {
    const scope = this.createScope('block', this.currentScope)
    this.currentScope = scope
  }

  /**
   * 退出当前作用域
   */
  exitScope() {
    this.currentScope = this.currentScope.parent
  }

  /**
   * 声明绑定
   */
  declareBinding(id, kind, path) {
    const name = id.name

    // var 声明提升到函数作用域
    const targetScope = kind === 'var'
      ? this.findFunctionScope()
      : this.currentScope

    // 创建绑定信息
    const binding = {
      name,
      kind,           // 'var', 'let', 'const', 'function', 'class', 'param', 'import'
      node: id,
      path,
      scope: targetScope,
      references: [],
      isExported: false,
      isUsed: false
    }

    targetScope.bindings.set(name, binding)
    targetScope.stats.declarations++

    return binding
  }

  /**
   * 处理解构模式
   */
  declarePattern(pattern, kind) {
    switch (pattern.type) {
      case 'Identifier':
        this.declareBinding(pattern, kind, null)
        break

      case 'ObjectPattern':
        for (const prop of pattern.properties) {
          if (prop.type === 'RestElement') {
            this.declarePattern(prop.argument, kind)
          } else {
            this.declarePattern(prop.value, kind)
          }
        }
        break

      case 'ArrayPattern':
        for (const element of pattern.elements) {
          if (element) {
            if (element.type === 'RestElement') {
              this.declarePattern(element.argument, kind)
            } else {
              this.declarePattern(element, kind)
            }
          }
        }
        break

      case 'AssignmentPattern':
        this.declarePattern(pattern.left, kind)
        break

      case 'RestElement':
        this.declarePattern(pattern.argument, kind)
        break
    }
  }

  /**
   * 记录变量引用
   */
  recordReference(name, path) {
    // 从当前作用域向上查找绑定
    let scope = this.currentScope

    while (scope) {
      const binding = scope.bindings.get(name)
      if (binding) {
        binding.references.push(path)
        binding.isUsed = true
        scope.stats.references++
        return
      }
      scope = scope.parent
    }

    // 未找到绑定,是全局变量引用
    this.currentScope.references.push({
      name,
      path,
      isGlobal: true
    })
  }

  /**
   * 判断 Identifier 是否为引用(而非声明)
   */
  isReference(path) {
    const parent = path.parent
    const node = path.node

    // 声明的左侧
    if (parent.type === 'VariableDeclarator' && parent.id === node) {
      return false
    }

    // 函数声明名称
    if (parent.type === 'FunctionDeclaration' && parent.id === node) {
      return false
    }

    // 类声明名称
    if (parent.type === 'ClassDeclaration' && parent.id === node) {
      return false
    }

    // 对象属性键(非计算属性)
    if (parent.type === 'Property' && parent.key === node && !parent.computed) {
      return false
    }

    // 对象方法名
    if (parent.type === 'MethodDefinition' && parent.key === node && !parent.computed) {
      return false
    }

    // 成员访问的属性(非计算)
    if (parent.type === 'MemberExpression' && parent.property === node && !parent.computed) {
      return false
    }

    // import 语句中的导入名
    if (parent.type === 'ImportSpecifier' && parent.imported === node) {
      return false
    }

    // export 语句中的导出名
    if (parent.type === 'ExportSpecifier' && parent.exported === node) {
      return false
    }

    return true
  }

  /**
   * 查找最近的函数作用域
   */
  findFunctionScope() {
    let scope = this.currentScope
    while (scope && scope.type === 'block') {
      scope = scope.parent
    }
    return scope || this.currentScope
  }

  /**
   * 检查块是否包含块级作用域声明
   */
  hasBlockScopedDeclarations(block) {
    for (const stmt of block.body) {
      if (stmt.type === 'VariableDeclaration' &&
          (stmt.kind === 'let' || stmt.kind === 'const')) {
        return true
      }
    }
    return false
  }

  /**
   * 获取未使用的绑定
   */
  getUnusedBindings() {
    const unused = []

    for (const scope of this.scopes) {
      for (const [name, binding] of scope.bindings) {
        if (!binding.isUsed && !binding.isExported) {
          unused.push({
            name,
            kind: binding.kind,
            scope: scope.type,
            loc: binding.node?.loc
          })
        }
      }
    }

    return unused
  }
}

第三章:robuild 完整架构设计

image.png

了解了 Bundler 的基本原理后,让我们深入 robuild 的架构设计。

3.1 核心设计原则

robuild 的设计遵循以下原则:

  1. 零配置优先:默认配置应该覆盖 90% 的使用场景
  2. 渐进式复杂度:简单任务简单做,复杂任务可配置
  3. 兼容性:支持 tsup 和 unbuild 的配置风格
  4. 性能:利用 Rust 工具链的性能优势
  5. 可扩展:插件系统支持自定义逻辑

3.2 架构总览

┌──────────────────────────────────────────────────────────────────┐
│                          CLI Layer                                │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │  cac(命令行解析)→ c12(配置加载)→ build()              │ │
│  └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
                                ↓
┌──────────────────────────────────────────────────────────────────┐
│                        Config Layer                               │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  │
│  │ normalizeTsup   │  │ inheritConfig   │  │ resolveExternal │  │
│  │ Config()       │→│      ()          │→│    Config()     │  │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘  │
└──────────────────────────────────────────────────────────────────┘
                                ↓
┌──────────────────────────────────────────────────────────────────┐
│                        Build Layer                                │
│  ┌─────────────────────────────┐  ┌─────────────────────────┐   │
│  │     rolldownBuild()         │  │    transformDir()       │   │
│  │  ┌─────────────────────┐    │  │  ┌─────────────────┐    │   │
│  │  │ Rolldown + DTS Plugin│   │  │  │ Oxc Transform   │    │   │
│  │  └─────────────────────┘    │  │  └─────────────────┘    │   │
│  └─────────────────────────────┘  └─────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘
                                ↓
┌──────────────────────────────────────────────────────────────────┐
│                       Plugin Layer                                │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐    │
│  │  Shims    │  │  Shebang  │  │  Node     │  │  Glob     │    │
│  │  Plugin   │  │  Plugin   │  │  Protocol │  │  Import   │    │
│  └───────────┘  └───────────┘  └───────────┘  └───────────┘    │
└──────────────────────────────────────────────────────────────────┘
                                ↓
┌──────────────────────────────────────────────────────────────────┐
│                      Transform Layer                              │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐    │
│  │  Banner   │  │  Clean    │  │  Copy     │  │  Exports  │    │
│  │  Footer   │  │  Output   │  │  Files    │  │  Generate │    │
│  └───────────┘  └───────────┘  └───────────┘  └───────────┘    │
└──────────────────────────────────────────────────────────────────┘

3.3 构建流程详解

robuild 的构建流程分为以下阶段:

                    build() 入口
                         │
          ┌──────────────┴──────────────┐
          ↓                             ↓
   normalizeTsupConfig()         performWatchBuild()
   (标准化配置格式)                  (Watch 模式)
          │
          ↓
   inheritConfig()
   (配置继承)
          │
          ↓
   ┌──────┴──────┐
   │   entries   │
   │   遍历      │
   └──────┬──────┘
          │
   ┌──────┴──────┬──────────────┐
   ↓             ↓              ↓
 Bundle       Transform      其他类型
 Entry        Entry          ...
   │             │
   ↓             ↓
rolldownBuild  transformDir
   │             │
   └──────┬──────┘
          ↓
   generateExports()
   (生成 package.json exports)
          │
          ↓
   executeOnSuccess()
   (执行回调)

让我们深入关键环节:

3.3.1 配置标准化

robuild 支持两种配置风格:

// tsup 风格(flat config)
export default {
  entry: ['./src/index.ts'],
  format: ['esm', 'cjs'],
  dts: true
}

// unbuild 风格(entries-based)
export default {
  entries: [
    { type: 'bundle', input: './src/index.ts', format: ['esm', 'cjs'] },
    { type: 'transform', input: './src/' }
  ]
}

配置标准化的核心代码:

// src/build.ts
function normalizeTsupConfig(config: BuildConfig): BuildConfig {
  // 如果已经有 entries,直接返回
  if (config.entries && config.entries.length > 0) {
    return config
  }

  // 将 tsup 风格的 entry 转换为 entries
  if (config.entry) {
    const entry: BundleEntry = inheritConfig(
      {
        type: 'bundle' as const,
        entry: config.entry,
      },
      config,
      { name: 'globalName' } // 字段映射
    )
    return { ...config, entries: [entry] }
  }

  return config
}

3.3.2 配置继承

顶层配置需要向下传递给每个 entry:

const SHARED_CONFIG_FIELDS = [
  'format', 'outDir', 'platform', 'target', 'minify',
  'dts', 'splitting', 'treeshake', 'sourcemap',
  'external', 'noExternal', 'env', 'alias', 'banner',
  'footer', 'shims', 'rolldown', 'loaders', 'clean'
] as const

function inheritConfig<T extends Partial<BuildEntry>>(
  entry: T,
  config: BuildConfig,
  additionalMappings?: Record<string, string>
): T {
  const result: any = { ...entry }

  // 只继承未定义的字段
  for (const field of SHARED_CONFIG_FIELDS) {
    if (result[field] === undefined && config[field] !== undefined) {
      result[field] = config[field]
    }
  }

  // 处理字段映射(如 name -> globalName)
  if (additionalMappings) {
    for (const [configKey, entryKey] of Object.entries(additionalMappings)) {
      if (result[entryKey] === undefined) {
        result[entryKey] = (config as any)[configKey]
      }
    }
  }

  return result
}

3.3.3 并行构建

所有 entries 并行构建,提升性能:

await Promise.all(
  entries.map(entry =>
    entry.type === 'bundle'
      ? rolldownBuild(ctx, entry, hooks, config)
      : transformDir(ctx, entry)
  )
)

3.4 Bundle Builder 实现

Bundle Builder 是 robuild 的核心,它封装了 Rolldown 的调用:

// src/builders/bundle.ts
export async function rolldownBuild(
  ctx: BuildContext,
  entry: BundleEntry,
  hooks: BuildHooks,
  config?: BuildConfig
): Promise<void> {
  // 1. 初始化插件管理器
  const pluginManager = new RobuildPluginManager(config || {}, entry, ctx.pkgDir)
  await pluginManager.initializeRobuildHooks()

  // 2. 解析配置
  const formats = Array.isArray(entry.format) ? entry.format : [entry.format || 'es']
  const platform = entry.platform || 'node'
  const target = entry.target || 'es2022'

  // 3. 清理输出目录
  await cleanOutputDir(ctx.pkgDir, fullOutDir, entry.clean ?? true)

  // 4. 处理外部依赖
  const externalConfig = resolveExternalConfig(ctx, {
    external: entry.external,
    noExternal: entry.noExternal
  })

  // 5. 构建插件列表
  const rolldownPlugins: Plugin[] = [
    shebangPlugin(),
    nodeProtocolPlugin(entry.nodeProtocol || false),
    // ... 其他插件
  ]

  // 6. 构建 Rolldown 配置
  const baseRolldownConfig: InputOptions = {
    cwd: ctx.pkgDir,
    input: inputs,
    plugins: rolldownPlugins,
    platform: platform === 'node' ? 'node' : 'neutral',
    external: externalConfig,
    resolve: { alias: entry.alias || {} },
    transform: { target, define: defineOptions }
  }

  // 7. 所有格式并行构建
  const formatResults = await Promise.all(formats.map(buildFormat))

  // 8. 执行构建结束钩子
  await pluginManager.executeRobuildBuildEnd({ allOutputEntries })
}

关键设计点:

多格式构建:ESM、CJS、IIFE 等格式同时构建,通过不同的文件扩展名避免冲突:

const buildFormat = async (format: ModuleFormat) => {
  let entryFileName = `[name]${extension}`

  if (isMultiFormat) {
    // 多格式构建时使用明确的扩展名
    if (format === 'cjs') entryFileName = `[name].cjs`
    else if (format === 'esm') entryFileName = `[name].mjs`
    else if (format === 'iife') entryFileName = `[name].js`
  }

  const res = await rolldown(formatConfig)
  await res.write(outConfig)
  await res.close()
}

DTS 生成策略:只在 ESM 格式下生成类型声明,避免冲突:

if (entry.dts !== false && format === 'esm') {
  formatConfig.plugins = [
    ...plugins,
    dts({ cwd: ctx.pkgDir, ...entry.dts })
  ]
}

3.5 Transform Builder 实现

Transform Builder 用于不打包的场景,保持目录结构:

// src/builders/transform.ts
export async function transformDir(
  ctx: BuildContext,
  entry: TransformEntry
): Promise<void> {
  // 获取所有源文件
  const files = await glob('**/*.*', { cwd: inputDir })

  const promises = files.map(async (entryName) => {
    const ext = extname(entryPath)

    switch (ext) {
      case '.ts':
      case '.tsx':
      case '.jsx': {
        // 使用 Oxc 转换
        const transformed = await transformModule(entryPath, entry)
        await writeFile(entryDistPath, transformed.code, 'utf8')

        // 生成类型声明
        if (transformed.declaration) {
          await writeFile(dtsPath, transformed.declaration, 'utf8')
        }
        break
      }
      default:
        // 其他文件直接复制
        await copyFile(entryPath, entryDistPath)
    }
  })

  await Promise.all(promises)
}

单文件转换使用 Oxc:

async function transformModule(entryPath: string, entry: TransformEntry) {
  const sourceText = await readFile(entryPath, 'utf8')

  // 1. 解析 AST
  const parsed = parseSync(entryPath, sourceText, {
    lang: ext === '.tsx' ? 'tsx' : 'ts',
    sourceType: 'module'
  })

  // 2. 重写相对导入(使用 MagicString 保持 sourcemap 兼容)
  const magicString = new MagicString(sourceText)
  for (const staticImport of parsed.module.staticImports) {
    // 将 .ts 导入重写为 .mjs
    rewriteSpecifier(staticImport.moduleRequest)
  }

  // 3. Oxc 转换
  const transformed = await transform(entryPath, magicString.toString(), {
    target: entry.target || 'es2022',
    sourcemap: !!entry.sourcemap,
    typescript: {
      declaration: { stripInternal: true }
    }
  })

  // 4. 可选压缩
  if (entry.minify) {
    const res = await minify(entryPath, transformed.code, entry.minify)
    transformed.code = res.code
  }

  return transformed
}

第四章:ESM/CJS 互操作处理

ESM 和 CJS 的互操作是库构建中最复杂的问题之一。让我详细解释 robuild 是如何处理的。

4.1 问题背景

ESM 和 CJS 有根本性的差异:

特性 ESM CJS
加载时机 静态(编译时) 动态(运行时)
导出方式 具名绑定 module.exports 对象
this 值 undefined module
__dirname 不可用 可用
require 不可用 可用
顶层 await 支持 不支持

4.2 Shims 插件设计

robuild 通过 Shims 插件解决兼容问题:

// ESM 中使用 CJS 特性时的 shim
const NODE_GLOBALS_SHIM = `
// Node.js globals shim for ESM
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
import { createRequire } from 'node:module'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const require = createRequire(import.meta.url)
`

// 浏览器环境的 process.env shim
const PROCESS_ENV_SHIM = `
if (typeof process === 'undefined') {
  globalThis.process = {
    env: {},
    platform: 'browser',
    version: '0.0.0'
  }
}
`

// module.exports shim
const MODULE_EXPORTS_SHIM = `
if (typeof module === 'undefined') {
  globalThis.module = { exports: {} }
}
if (typeof exports === 'undefined') {
  globalThis.exports = module.exports
}
`

关键是检测需要哪些 shim

function detectShimNeeds(code: string) {
  // 移除注释和字符串,避免误判
  const cleanCode = removeCommentsAndStrings(code)

  return {
    needsDirname: /\b__dirname\b/.test(cleanCode) ||
                  /\b__filename\b/.test(cleanCode),
    needsRequire: /\brequire\s*\(/.test(cleanCode),
    needsExports: /\bmodule\.exports\b/.test(cleanCode) ||
                  /\bexports\.\w+/.test(cleanCode),
    needsEnv: /\bprocess\.env\b/.test(cleanCode)
  }
}

function removeCommentsAndStrings(code: string): string {
  return code
    // 移除单行注释
    .replace(/\/\/.*$/gm, '')
    // 移除多行注释
    .replace(/\/\*[\s\S]*?\*\//g, '')
    // 移除字符串字面量
    .replace(/"(?:[^"\\]|\\.)*"/g, '""')
    .replace(/'(?:[^'\\]|\\.)*'/g, "''")
    .replace(/`(?:[^`\\]|\\.)*`/g, '``')
}

4.3 平台特定配置

不同平台需要不同的 shim 策略:

function getPlatformShimsConfig(platform: 'browser' | 'node' | 'neutral') {
  switch (platform) {
    case 'browser':
      return {
        dirname: false,  // 浏览器不支持
        require: false,  // 浏览器不支持
        exports: false,  // 浏览器不支持
        env: true        // 需要 polyfill
      }
    case 'node':
      return {
        dirname: true,   // 转换为 ESM 等价写法
        require: true,   // 使用 createRequire
        exports: true,   // 转换为 ESM export
        env: false       // 原生支持
      }
    case 'neutral':
      return {
        dirname: false,
        require: false,
        exports: false,
        env: false
      }
  }
}

4.4 Dual Package 支持

对于同时支持 ESM 和 CJS 的包,robuild 自动生成正确的 exports 字段:

// 输入配置
{
  entries: [{
    type: 'bundle',
    input: './src/index.ts',
    format: ['esm', 'cjs'],
    generateExports: true
  }]
}

// 生成的 package.json exports
{
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",  // TypeScript 优先
      "import": "./dist/index.mjs",    // ESM
      "require": "./dist/index.cjs"    // CJS
    }
  }
}

顺序很重要:types 必须在最前面,这是 TypeScript 的要求。


第五章:插件系统设计哲学

5.1 设计目标

robuild 的插件系统需要满足:

  1. 兼容性:支持 Rolldown、Rollup、Vite、Unplugin 插件
  2. 简洁性:简单需求不需要复杂配置
  3. 可组合:多个插件可以组合成一个
  4. 生命周期明确:robuild 特有的钩子

5.2 插件类型检测

robuild 自动识别插件类型:

class RobuildPluginManager {
  private normalizePlugin(pluginOption: RobuildPluginOption): RobuildPlugin {
    // 工厂函数
    if (typeof pluginOption === 'function') {
      return this.normalizePlugin(pluginOption())
    }

    // 类型检测优先级
    if (this.isRobuildPlugin(pluginOption)) return pluginOption
    if (this.isRolldownPlugin(pluginOption)) return this.adaptRolldownPlugin(pluginOption)
    if (this.isVitePlugin(pluginOption)) return this.adaptVitePlugin(pluginOption)
    if (this.isUnplugin(pluginOption)) return this.adaptUnplugin(pluginOption)

    // 兜底:当作 Rolldown 插件
    return this.adaptRolldownPlugin(pluginOption)
  }

  // Robuild 插件:有 robuild 特有钩子或标记
  private isRobuildPlugin(plugin: any): plugin is RobuildPlugin {
    return plugin.meta?.robuild === true
      || plugin.robuildSetup
      || plugin.robuildBuildStart
      || plugin.robuildBuildEnd
  }

  // Rolldown/Rollup 插件:有标准钩子
  private isRolldownPlugin(plugin: any): plugin is RolldownPlugin {
    return plugin.name && (
      plugin.buildStart || plugin.buildEnd ||
      plugin.resolveId || plugin.load || plugin.transform ||
      plugin.generateBundle || plugin.writeBundle
    )
  }

  // Vite 插件:有 Vite 特有钩子
  private isVitePlugin(plugin: any): boolean {
    return plugin.config || plugin.configResolved ||
           plugin.configureServer || plugin.meta?.vite === true
  }
}

5.3 Robuild 特有钩子

除了 Rolldown 标准钩子,robuild 添加了三个生命周期钩子:

interface RobuildPlugin extends RolldownPlugin {
  // 插件初始化时调用
  robuildSetup?: (ctx: RobuildPluginContext) => void | Promise<void>

  // 构建开始时调用
  robuildBuildStart?: (ctx: RobuildPluginContext) => void | Promise<void>

  // 构建结束时调用,可以访问所有输出
  robuildBuildEnd?: (ctx: RobuildPluginContext, result?: any) => void | Promise<void>
}

interface RobuildPluginContext {
  config: BuildConfig
  entry: BuildEntry
  pkgDir: string
  outDir: string
  format: ModuleFormat | ModuleFormat[]
  platform: Platform
  target: Target
}

5.4 插件工厂模式

robuild 提供工厂函数简化插件创建:

// 创建简单的 transform 插件
function createTransformPlugin(
  name: string,
  transform: (code: string, id: string) => string | null,
  filter?: (id: string) => boolean
): RobuildPlugin {
  return {
    name,
    meta: { robuild: true },
    transform: async (code, id) => {
      if (filter && !filter(id)) return null
      return transform(code, id)
    }
  }
}

// 使用示例
const myPlugin = createTransformPlugin(
  'add-banner',
  (code) => `/* My Library */\n${code}`,
  (id) => /\.js$/.test(id)
)

组合多个插件:

function combinePlugins(name: string, plugins: RobuildPlugin[]): RobuildPlugin {
  const combined: RobuildPlugin = { name, meta: { robuild: true } }

  for (const plugin of plugins) {
    // 链式组合 transform 钩子
    if (plugin.transform) {
      const prevHook = combined.transform
      combined.transform = async (code, id) => {
        let currentCode = code
        if (prevHook) {
          const result = await prevHook(currentCode, id)
          if (result) {
            currentCode = typeof result === 'string' ? result : result.code
          }
        }
        return plugin.transform!(currentCode, id)
      }
    }

    // 其他钩子类似处理...
  }

  return combined
}

第六章:性能优化策略

6.1 为什么 Rust 更快?

robuild 使用的 Rolldown 和 Oxc 都是 Rust 实现的。Rust 带来的性能优势主要来自:

1. 零成本抽象

Rust 的泛型和 trait 在编译时单态化,没有运行时开销:

// Rust: 编译时展开,没有虚函数调用
fn process<T: Transform>(input: T) -> String {
    input.transform()
}

// 等价于为每个具体类型生成特化版本
fn process_for_type_a(input: TypeA) -> String { ... }
fn process_for_type_b(input: TypeB) -> String { ... }

2. 无 GC 暂停

JavaScript 的垃圾回收会导致不可预测的暂停。Rust 通过所有权系统在编译时确定内存释放时机:

// Rust: 编译器自动插入内存释放
{
    let ast = parse(source);  // 分配内存
    let result = transform(ast);
    // ast 在这里自动释放,无需 GC
}

3. 数据局部性

Rust 鼓励使用栈分配和连续内存,对 CPU 缓存更友好:

// 连续内存布局
struct Token {
    kind: TokenKind,
    start: u32,
    end: u32,
}
let tokens: Vec<Token> = tokenize(source);
// tokens 在连续内存中,缓存命中率高

4. 真正的并行

Rust 的类型系统保证线程安全,可以放心使用多核:

use rayon::prelude::*;

// 多个文件并行解析
let results: Vec<_> = files
    .par_iter()  // 并行迭代
    .map(|file| parse(file))
    .collect();

6.2 robuild 的并行策略

robuild 在多个层面实现并行:

Entry 级并行:所有 entry 同时构建

await Promise.all(
  entries.map(entry =>
    entry.type === 'bundle'
      ? rolldownBuild(ctx, entry, hooks, config)
      : transformDir(ctx, entry)
  )
)

Format 级并行:ESM、CJS 等格式同时生成

const formatResults = await Promise.all(formats.map(buildFormat))

文件级并行:Transform 模式下所有文件同时处理

const writtenFiles = await Promise.all(promises)

6.3 缓存策略

robuild 在应用层做了一些优化:

依赖缓存:解析结果缓存

// 依赖解析缓存
const depsCache = new Map<OutputChunk, Set<string>>()
const resolveDeps = (chunk: OutputChunk): string[] => {
  if (!depsCache.has(chunk)) {
    depsCache.set(chunk, new Set<string>())
  }
  const deps = depsCache.get(chunk)!
  // ... 递归解析
  return Array.from(deps).sort()
}

外部模块判断缓存:避免重复的包信息读取

// 一次性构建外部依赖列表
const externalDeps = buildExternalDeps(ctx.pkg)
// 后续直接查表判断

第七章:为什么选择 Rust + JS 混合架构

7.1 架构选择的权衡

robuild 采用 Rust + JavaScript 混合架构。这个选择背后有深思熟虑的权衡:

为什么不是纯 Rust?

  1. 生态兼容性:npm 生态的插件都是 JavaScript,纯 Rust 无法复用
  2. 配置灵活性:JavaScript 配置文件可以动态计算、条件判断
  3. 开发效率:Rust 开发周期长,不利于快速迭代
  4. 用户学习成本:用户不需要学习 Rust 就能写插件

为什么不是纯 JavaScript?

  1. 性能瓶颈:AST 解析、转换、压缩都是 CPU 密集型任务
  2. 内存效率:大型项目的 AST 占用大量内存
  3. 并行能力:JavaScript 单线程无法利用多核

最佳策略:计算密集型用 Rust,胶水层用 JavaScript

┌─────────────────────────────────────────────────────────────┐
│                    JavaScript 层                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  配置加载、CLI、插件管理、构建编排、输出处理          │  │
│  └───────────────────────────────────────────────────────┘  │
│                           │                                  │
│                      NAPI 绑定                               │
│                           │                                  │
│  ┌───────────────────────────────────────────────────────┐  │
│  │              Rust 层(计算密集型)                     │  │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐     │  │
│  │  │ Parser  │ │Transform│ │ Bundler │ │ Minifier│     │  │
│  │  │ (Oxc)   │ │ (Oxc)   │ │(Rolldown)│ │ (Oxc)  │     │  │
│  │  └─────────┘ └─────────┘ └─────────┘ └─────────┘     │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

7.2 NAPI 绑定的成本

Rust 和 JavaScript 之间通过 NAPI(Node-API)通信。这有一定开销:

  1. 数据序列化:JavaScript 对象转换为 Rust 结构
  2. 跨边界调用:每次调用有固定开销
  3. 字符串复制:UTF-8 字符串需要复制

因此,robuild 的设计原则是减少跨边界调用次数

// 好的做法:一次调用完成整个解析
const parsed = parseSync(filePath, sourceText, options)

// 不好的做法:多次调用
const ast = parse(source)
const imports = extractImports(ast)  // 又一次跨边界
const exports = extractExports(ast)  // 又一次跨边界

7.3 与 Vite 生态的协同

robuild 的架构设计与 Vite 生态高度契合:

  1. Rolldown 是 Vite 的未来打包器:API 兼容 Rollup,便于迁移
  2. 插件复用:Vite 插件可以直接在 robuild 中使用
  3. 配置兼容:支持从 vite.config.ts 导入配置
// robuild 可以加载 Vite 配置
export default {
  fromVite: true,  // 启用 Vite 配置加载
  // robuild 特有配置可以覆盖
  entries: [...]
}

第八章:实战案例与最佳实践

8.1 最简配置

对于标准的 TypeScript 库,零配置即可工作:

// build.config.ts
export default {}

// 等价于
export default {
  entries: [{
    type: 'bundle',
    input: './src/index.ts',
    format: ['esm'],
    dts: true
  }]
}

8.2 多入口 + 多格式

// build.config.ts
export default {
  entries: [
    {
      type: 'bundle',
      input: {
        index: './src/index.ts',
        cli: './src/cli.ts'
      },
      format: ['esm', 'cjs'],
      dts: true,
      generateExports: true
    }
  ],
  exports: {
    enabled: true,
    autoUpdate: true
  }
}

8.3 带 shims 的 Node.js 工具

// build.config.ts
export default {
  entries: [{
    type: 'bundle',
    input: './src/index.ts',
    format: ['esm'],
    platform: 'node',
    shims: {
      dirname: true,   // __dirname, __filename
      require: true    // require()
    },
    banner: '#!/usr/bin/env node'
  }]
}

8.4 浏览器库 + UMD

// build.config.ts
export default {
  entries: [{
    type: 'bundle',
    input: './src/index.ts',
    format: ['esm', 'umd'],
    platform: 'browser',
    globalName: 'MyLib',
    minify: true,
    shims: {
      env: true  // process.env polyfill
    }
  }]
}

8.5 Monorepo 内部包

// build.config.ts
export default {
  entries: [{
    type: 'bundle',
    input: './src/index.ts',
    format: ['esm'],
    dts: true,
    noExternal: [
      '@myorg/utils',     // 打包内部依赖
      '@myorg/shared'
    ]
  }]
}

结语:构建工具的未来

回顾构建工具的三次革命:

  1. Webpack 时代:解决了"如何打包复杂应用"
  2. Rollup 时代:解决了"如何打包高质量的库"
  3. Rust Bundler 时代:解决了"如何更快地完成这一切"

robuild 是这场革命的参与者。它基于 Rolldown + Oxc 的 Rust 基础设施,专注于库构建场景,追求零配置、高性能、与现有生态兼容。

但构建工具的演进远未结束。我们可以期待:

  1. 更深的编译器集成:bundler 与类型检查器、代码检查器的融合
  2. 更智能的优化:基于运行时 profile 的优化决策
  3. 更好的开发体验:更快的 HMR、更精准的错误提示
  4. WebAssembly 的普及:让 Rust 工具链在浏览器中运行

构建工具的本质是将开发者的代码高效地转换为运行时需要的形态。技术在变,这个目标不变。作为工具开发者,我们的使命是让这个过程尽可能无感、高效、可靠。

感谢阅读。如果你对 robuild 感兴趣,欢迎查看 项目仓库


本文约 10000 字,涵盖了构建工具演进、bundler 核心原理(含完整 mini bundler 代码)、robuild 架构设计、ESM/CJS 互操作、插件系统、性能优化等主题。如有技术问题,欢迎讨论交流。

参考资料

前端构建产物里的 __esModule 是什么?一次讲清楚它的原理和作用

如果你经常翻构建后的代码,基本都会看到这样一行:

Object.defineProperty(exports, "__esModule", { value: true });

image.png

很多人第一次看到都会疑惑:

  • 这是干嘛的?
  • 能删吗?
  • 不加会怎么样?
  • 和 default 导出有什么关系?

这篇文章专门把这个现象讲清楚。


太长不看版

Object.defineProperty(exports, "__esModule", { value: true });

本质就是:

标记“这个 CommonJS 文件是从 ES Module 转译来的”,用于默认导出语义的互操作。

它不是功能代码,不是业务逻辑。

它只是模块系统演化过程中的一个兼容标志。

一、为什么会出现 __esModule

根本原因只有一个:

ES Module 和 CommonJS 的语义不一样。

我们简单对比一下。

ES Module

export default function foo() {}

CommonJS

module.exports = function foo() {}

两者看起来都叫“默认导出”,但内部机制完全不同。

当构建工具(TypeScript / Babel / Webpack / Rspack 等)把 ESM 转成 CJS 时,语义必须“模拟”出来。

于是就变成:

Object.defineProperty(exports, "__esModule", { value: true });
exports.default = foo;

关键问题来了:

如何区分“普通 CJS 模块”和“从 ESM 转过来的 CJS 模块”?

这就是 __esModule 存在的意义。


二、__esModule 到底做了什么?

它只是一个标记。

exports.__esModule = true

之所以用 Object.defineProperty,是为了:

  • 不可枚举
  • 更符合 Babel 的标准输出
  • 避免污染遍历结果

本质就是:

告诉别人:这个模块原本是 ES Module。

仅此而已。


三、真正的核心:默认导出的互操作问题

来看一个经典场景。

1️⃣ 原始 ESM

export default function foo() {}

2️⃣ 被编译成 CJS

exports.default = foo;

3️⃣ 用 CommonJS 引入

const foo = require('./foo');

得到的其实是:

{
  default: [Function: foo]
}

这就有问题了。

我们希望的是:

foo() // 直接调用

而不是:

foo.default()

于是构建工具会生成一个 helper:

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

逻辑是:

  • 如果模块带有 __esModule 标记 → 说明是 ESM 转的 → 直接用 default
  • 如果没有 → 说明是普通 CJS → 包一层 { default: obj }

这就是整个互操作的关键。


四、为什么不能只判断 default 属性?

因为普通 CJS 也可能写:

module.exports = {
  default: something
}

这时你没法区分:

  • 是 ESM 编译产物
  • 还是普通对象刚好有个 default 字段

所以必须有一个“官方标记”。

__esModule 就成了事实标准。


五、什么时候会生成它?

只要发生:

ESM → CJS 转译

基本都会生成。

常见场景:

  • TypeScript 编译为 module: commonjs
  • Babel preset-env 输出 CJS
  • Webpack / Rspack 输出 target 为 node + CJS
  • 老 Node 项目混用 import / require

如果你使用:

{
  "type": "module"
}

并且输出原生 ESM

那就不会有 __esModule

它只存在于“模块系统过渡时代”。


注意:它不是 JS 语言特性

非常重要的一点:

__esModule 不是语言规范的一部分。

它是:

  • Babel 约定
  • 构建器约定
  • 社区事实标准

是一种“工程层解决方案”。

换句话说:

它属于模块系统演化历史的一部分。

从更高层看:模块系统的过渡遗产

JavaScript 的模块系统经历了三代:

  1. 无模块(全局变量时代)
  2. CommonJS(Node 时代)
  3. ES Module(标准化)

但 Node 生态已经建立在 CJS 上。

所以必须有一个桥接层。

__esModule 就是这座桥的一块砖。

它存在的原因不是设计优雅,而是历史兼容。

❌