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

作者:sunny_日期:2026/2/22

本文将从第一人称实战视角,深入探讨前端构建工具的技术演进,以及我在设计 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 的革命性在于它提出了一个统一的心智模型:一切皆模块

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

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 语句,只打包实际使用的代码。这在库构建场景下意义重大:

1// input.js
2import { add, multiply } from './math.js'
3console.log(add(2, 3))
4// multiply 未使用
5
6// math.js
7export function add(a, b) { return a + b }
8export function multiply(a, b) { return a * b }
9
10// output.js (Rollup 输出,multiply 被移除)
11function add(a, b) { return a + b }
12console.log(add(2, 3))
13

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

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

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

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

1// Webpack 风格的输出
2var __webpack_modules__ = {
3  "./src/a.js": (module) => { module.exports = 1 },
4  "./src/b.js": (module, exports, require) => {
5    const a = require("./src/a.js")
6    module.exports = a + 1
7  }
8}
9
10// Rollup 风格的输出
11const a = 1
12const b = a + 1
13

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

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 的统一替代品。

1传统工具链                    Oxc 工具链
2Babel (转换)                 oxc-transform
3ESLint (检查)               oxc-linter
4Prettier (格式化)            oxc-formatter
5Terser (压缩)                oxc-minify
6

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


第二章:理解 Bundler 核心原理

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

2.1 从零实现 Mini Bundler

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

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

下面是完整的实现代码:

1// mini-bundler.js
2// 一个完整的 Mini Bundler 实现,约 300 行代码
3// 支持 ES Module 解析、依赖图构建、打包输出
4
5import * as fs from 'node:fs'
6import * as path from 'node:path'
7import { parse } from '@babel/parser'
8import traverse from '@babel/traverse'
9import { transformFromAstSync } from '@babel/core'
10
11// ============================================
12// 第一部分:模块解析器
13// ============================================
14
15let moduleId = 0  // 模块计数器,用于生成唯一 ID
16
17/**
18 * 解析单个模块
19 * @param {string} filePath - 模块文件的绝对路径
20 * @returns {Object} 模块信息对象
21 */
22function parseModule(filePath) {
23  const content = fs.readFileSync(filePath, 'utf-8')
24
25  // 1. 使用 Babel 解析为 AST
26  // 这里我们支持 TypeScript  JSX
27  const ast = parse(content, {
28    sourceType: 'module',
29    plugins: ['typescript', 'jsx']
30  })
31
32  // 2. 收集依赖信息
33  const dependencies = []
34  const imports = []  // 详细的导入信息
35  const exports = []  // 详细的导出信息
36
37  traverse.default(ast, {
38    // 处理 import 声明
39    // import { foo, bar } from './module'
40    // import defaultExport from './module'
41    // import * as namespace from './module'
42    ImportDeclaration({ node }) {
43      const specifier = node.source.value
44      dependencies.push(specifier)
45
46      // 提取导入的具体内容
47      const importedNames = node.specifiers.map(spec => {
48        if (spec.type === 'ImportDefaultSpecifier') {
49          return { type: 'default', local: spec.local.name }
50        }
51        if (spec.type === 'ImportNamespaceSpecifier') {
52          return { type: 'namespace', local: spec.local.name }
53        }
54        // ImportSpecifier
55        return {
56          type: 'named',
57          imported: spec.imported.name,
58          local: spec.local.name
59        }
60      })
61
62      imports.push({
63        specifier,
64        importedNames,
65        start: node.start,
66        end: node.end
67      })
68    },
69
70    // 处理动态 import()
71    // const mod = await import('./module')
72    CallExpression({ node }) {
73      if (node.callee.type === 'Import' &&
74          node.arguments[0]?.type === 'StringLiteral') {
75        dependencies.push(node.arguments[0].value)
76        imports.push({
77          specifier: node.arguments[0].value,
78          isDynamic: true,
79          start: node.start,
80          end: node.end
81        })
82      }
83    },
84
85    // 处理 export 声明
86    // export { foo, bar }
87    // export const x = 1
88    // export default function() {}
89    ExportNamedDeclaration({ node }) {
90      if (node.declaration) {
91        // export const x = 1
92        if (node.declaration.declarations) {
93          for (const decl of node.declaration.declarations) {
94            exports.push({
95              type: 'named',
96              name: decl.id.name,
97              local: decl.id.name
98            })
99          }
100        }
101        // export function foo() {}
102        else if (node.declaration.id) {
103          exports.push({
104            type: 'named',
105            name: node.declaration.id.name,
106            local: node.declaration.id.name
107          })
108        }
109      }
110      // export { foo, bar }
111      for (const spec of node.specifiers || []) {
112        exports.push({
113          type: 'named',
114          name: spec.exported.name,
115          local: spec.local.name
116        })
117      }
118      // export { foo } from './module'
119      if (node.source) {
120        dependencies.push(node.source.value)
121      }
122    },
123
124    ExportDefaultDeclaration({ node }) {
125      exports.push({ type: 'default', name: 'default' })
126    },
127
128    // export * from './module'
129    ExportAllDeclaration({ node }) {
130      dependencies.push(node.source.value)
131      exports.push({
132        type: 'star',
133        from: node.source.value,
134        as: node.exported?.name  // export * as name
135      })
136    }
137  })
138
139  // 3. 转换代码:移除类型注解,转换为 CommonJS
140  // 这样我们可以在运行时执行
141  const { code } = transformFromAstSync(ast, content, {
142    presets: ['@babel/preset-typescript'],
143    plugins: [
144      ['@babel/plugin-transform-modules-commonjs', {
145        strict: true,
146        noInterop: false
147      }]
148    ]
149  })
150
151  return {
152    id: moduleId++,
153    filePath,
154    dependencies,
155    imports,
156    exports,
157    code,
158    ast
159  }
160}
161
162// ============================================
163// 第二部分:模块路径解析
164// ============================================
165
166/**
167 * 解析模块路径
168 *  import 语句中的相对路径转换为绝对路径
169 */
170function resolveModule(specifier, fromDir) {
171  // 相对路径
172  if (specifier.startsWith('.') || specifier.startsWith('/')) {
173    let resolved = path.resolve(fromDir, specifier)
174
175    // 尝试添加扩展名
176    const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json']
177
178    // 直接匹配文件
179    for (const ext of extensions) {
180      const withExt = resolved + ext
181      if (fs.existsSync(withExt)) {
182        return withExt
183      }
184    }
185
186    // 尝试 index 文件
187    for (const ext of extensions) {
188      const indexPath = path.join(resolved, `index${ext}`)
189      if (fs.existsSync(indexPath)) {
190        return indexPath
191      }
192    }
193
194    // 如果原路径存在(有扩展名的情况)
195    if (fs.existsSync(resolved)) {
196      return resolved
197    }
198
199    throw new Error(`Cannot resolve module: ${specifier} from ${fromDir}`)
200  }
201
202  // 外部模块(node_modules)
203  // 简化处理,返回原始标识符
204  return specifier
205}
206
207/**
208 * 判断是否为外部模块
209 */
210function isExternalModule(specifier) {
211  return !specifier.startsWith('.') && !specifier.startsWith('/')
212}
213
214// ============================================
215// 第三部分:依赖图构建
216// ============================================
217
218/**
219 * 构建依赖图
220 * 从入口开始,递归解析所有模块
221 * @param {string} entryPath - 入口文件路径
222 * @returns {Array} 模块数组(拓扑排序)
223 */
224function buildDependencyGraph(entryPath) {
225  const absoluteEntry = path.resolve(entryPath)
226  const entryModule = parseModule(absoluteEntry)
227
228  // 广度优先遍历
229  const moduleQueue = [entryModule]
230  const moduleMap = new Map()  // filePath -> module
231  moduleMap.set(absoluteEntry, entryModule)
232
233  for (const module of moduleQueue) {
234    const dirname = path.dirname(module.filePath)
235
236    // 存储依赖映射:相对路径 -> 模块 ID
237    module.mapping = {}
238
239    for (const dep of module.dependencies) {
240      // 跳过外部模块
241      if (isExternalModule(dep)) {
242        module.mapping[dep] = null  // null 表示外部依赖
243        continue
244      }
245
246      // 解析依赖的绝对路径
247      const depPath = resolveModule(dep, dirname)
248
249      // 避免重复解析
250      if (moduleMap.has(depPath)) {
251        module.mapping[dep] = moduleMap.get(depPath).id
252        continue
253      }
254
255      // 解析新的依赖模块
256      const depModule = parseModule(depPath)
257      moduleMap.set(depPath, depModule)
258      moduleQueue.push(depModule)
259      module.mapping[dep] = depModule.id
260    }
261  }
262
263  // 返回模块数组
264  return Array.from(moduleMap.values())
265}
266
267// ============================================
268// 第四部分:代码生成
269// ============================================
270
271/**
272 * 生成打包后的代码
273 * @param {Array} modules - 模块数组
274 * @returns {string} 打包后的代码
275 */
276function generateBundle(modules) {
277  // 生成模块定义
278  let modulesCode = ''
279
280  for (const mod of modules) {
281    // 每个模块包装为 [factory, mapping] 格式
282    // factory 是模块工厂函数
283    // mapping 是依赖映射表
284    modulesCode += `
285    // ${mod.filePath}
286    ${mod.id}: [
287      function(module, exports, require) {
288        ${mod.code}
289      },
290      ${JSON.stringify(mod.mapping)}
291    ],`
292  }
293
294  // 生成运行时代码
295  const runtime = `
296// Mini Bundler 输出
297// 生成时间: ${new Date().toISOString()}
298
299(function(modules) {
300  // 模块缓存
301  const cache = {}
302
303  // 自定义 require 函数
304  function require(id) {
305    // 如果是外部模块(id  null),使用原生 require
306    if (id === null) {
307      throw new Error('External module should be loaded via native require')
308    }
309
310    // 检查缓存
311    if (cache[id]) {
312      return cache[id].exports
313    }
314
315    // 获取模块定义
316    const [factory, mapping] = modules[id]
317
318    // 创建模块对象
319    const module = {
320      exports: {}
321    }
322
323    // 缓存模块(处理循环依赖)
324    cache[id] = module
325
326    // 创建本地 require 函数
327    // 将相对路径映射为模块 ID
328    function localRequire(name) {
329      const mappedId = mapping[name]
330
331      // 外部模块
332      if (mappedId === null) {
333        // 在实际环境中,这里应该使用 native require
334        // 为了演示,我们抛出错误
335        if (typeof window === 'undefined') {
336          return require(name)  // Node.js 环境
337        }
338        throw new Error(\`External module not available: \${name}\`)
339      }
340
341      return require(mappedId)
342    }
343
344    // 执行模块工厂函数
345    factory(module, module.exports, localRequire)
346
347    return module.exports
348  }
349
350  // 执行入口模块(ID  0)
351  require(0)
352
353})({${modulesCode}
354})
355`
356
357  return runtime
358}
359
360// ============================================
361// 第五部分:主入口
362// ============================================
363
364/**
365 * 打包入口
366 * @param {string} entryPath - 入口文件路径
367 * @param {string} outputPath - 输出文件路径
368 */
369function bundle(entryPath, outputPath) {
370  console.log(`\n📦 Mini Bundler`)
371  console.log(`   Entry: ${entryPath}`)
372  console.log(`   Output: ${outputPath}\n`)
373
374  // 1. 构建依赖图
375  console.log('1. Building dependency graph...')
376  const modules = buildDependencyGraph(entryPath)
377  console.log(`   Found ${modules.length} modules:`)
378  for (const mod of modules) {
379    console.log([`   - [${mod.id}] ${path.relative(process.cwd(), mod.filePath)}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.id.md))
380    console.log([`         Deps: ${mod.dependencies.join(', ') || '(none)'}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.join.md))
381  }
382
383  // 2. 生成打包代码
384  console.log('\n2. Generating bundle...')
385  const bundledCode = generateBundle(modules)
386
387  // 3. 写入文件
388  const outputDir = path.dirname(outputPath)
389  if (!fs.existsSync(outputDir)) {
390    fs.mkdirSync(outputDir, { recursive: true })
391  }
392  fs.writeFileSync(outputPath, bundledCode, 'utf-8')
393
394  // 4. 输出统计
395  const stats = fs.statSync(outputPath)
396  console.log(`\n3. Bundle stats:`)
397  console.log(`   Size: ${(stats.size / 1024).toFixed(2)} KB`)
398  console.log(`   Modules: ${modules.length}`)
399
400  console.log(`\n✅ Bundle created successfully!`)
401  console.log(`   ${outputPath}\n`)
402
403  return { modules, code: bundledCode }
404}
405
406// 导出 API
407export {
408  bundle,
409  parseModule,
410  buildDependencyGraph,
411  generateBundle,
412  resolveModule
413}
414
415// CLI 支持
416if (process.argv[2]) {
417  const entry = process.argv[2]
418  const output = process.argv[3] || './dist/bundle.js'
419  bundle(entry, output)
420}
421

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

1// src/index.js - 入口文件
2import { add, multiply } from './math.js'
3import { formatResult } from './utils/format.js'
4
5const result = add(2, 3)
6console.log(formatResult('2 + 3', result))
7console.log(formatResult('2 * 3', multiply(2, 3)))
8
1// src/math.js - 数学工具
2export function add(a, b) {
3  return a + b
4}
5
6export function multiply(a, b) {
7  return a * b
8}
9
10export function subtract(a, b) {
11  return a - b
12}
13
1// src/utils/format.js - 格式化工具
2export function formatResult(expression, result) {
3  return `${expression} = ${result}`
4}
5

运行打包:

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

输出:

1📦 Mini Bundler
2   Entry: src/index.js
3   Output: dist/bundle.js
4
51. Building dependency graph...
6   Found 3 modules:
7   - [0] src/index.js
8         Deps: ./math.js, ./utils/format.js
9   - [1] src/math.js
10         Deps: (none)
11   - [2] src/utils/format.js
12         Deps: (none)
13
142. Generating bundle...
15
163. Bundle stats:
17   Size: 1.84 KB
18   Modules: 3
19
20 Bundle created successfully!
21   dist/bundle.js
22

2.2 AST 依赖分析深入

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

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

1// 1. ImportDeclaration - 静态导入
2import defaultExport from './module.js'
3import { named } from './module.js'
4import * as namespace from './module.js'
5
6// 2. ExportNamedDeclaration - 具名导出(可能包含 re-export)
7export { foo } from './module.js'
8export { default as foo } from './module.js'
9
10// 3. ExportAllDeclaration - 星号导出
11export * from './module.js'
12export * as name from './module.js'
13
14// 4. ImportExpression - 动态导入
15const mod = await import('./module.js')
16

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

1/**
2 *  AST 中提取所有依赖信息
3 * 返回结构化的依赖对象
4 */
5function extractDependencies(ast, filePath) {
6  const result = {
7    // 静态导入
8    staticImports: [],
9    // 动态导入
10    dynamicImports: [],
11    // Re-export
12    reExports: [],
13    // CommonJS require(用于分析混合代码)
14    requires: [],
15    // 导出信息
16    exports: {
17      named: [],      // export { foo }
18      default: false, // export default
19      star: []        // export * from
20    }
21  }
22
23  traverse.default(ast, {
24    // ===== 导入 =====
25
26    ImportDeclaration({ node }) {
27      const specifier = node.source.value
28
29      // 提取导入的具体绑定
30      const bindings = node.specifiers.map(spec => {
31        switch (spec.type) {
32          case 'ImportDefaultSpecifier':
33            return {
34              type: 'default',
35              local: spec.local.name,
36              imported: 'default'
37            }
38          case 'ImportNamespaceSpecifier':
39            return {
40              type: 'namespace',
41              local: spec.local.name,
42              imported: '*'
43            }
44          case 'ImportSpecifier':
45            return {
46              type: 'named',
47              local: spec.local.name,
48              imported: spec.imported.name
49            }
50        }
51      })
52
53      result.staticImports.push({
54        specifier,
55        bindings,
56        // 位置信息用于 source map 和错误报告
57        loc: {
58          start: node.loc.start,
59          end: node.loc.end
60        }
61      })
62    },
63
64    // 动态 import()
65    ImportExpression({ node }) {
66      const source = node.source
67
68      if (source.type === 'StringLiteral') {
69        // 静态字符串
70        result.dynamicImports.push({
71          specifier: source.value,
72          isDynamic: false,
73          loc: node.loc
74        })
75      } else {
76        // 动态表达式,无法静态分析
77        result.dynamicImports.push({
78          specifier: null,
79          isDynamic: true,
80          expression: source,
81          loc: node.loc
82        })
83      }
84    },
85
86    // ===== 导出 =====
87
88    ExportNamedDeclaration({ node }) {
89      // export { foo, bar as baz }
90      for (const spec of node.specifiers || []) {
91        result.exports.named.push({
92          exported: spec.exported.name,
93          local: spec.local.name
94        })
95      }
96
97      // export const x = 1 / export function foo() {}
98      if (node.declaration) {
99        const decl = node.declaration
100        if (decl.declarations) {
101          // VariableDeclaration
102          for (const d of decl.declarations) {
103            result.exports.named.push({
104              exported: d.id.name,
105              local: d.id.name
106            })
107          }
108        } else if (decl.id) {
109          // FunctionDeclaration / ClassDeclaration
110          result.exports.named.push({
111            exported: decl.id.name,
112            local: decl.id.name
113          })
114        }
115      }
116
117      // export { foo } from './module'
118      if (node.source) {
119        result.reExports.push({
120          specifier: node.source.value,
121          bindings: node.specifiers.map(spec => ({
122            exported: spec.exported.name,
123            imported: spec.local.name
124          })),
125          loc: node.loc
126        })
127      }
128    },
129
130    ExportDefaultDeclaration({ node }) {
131      result.exports.default = true
132    },
133
134    ExportAllDeclaration({ node }) {
135      result.exports.star.push({
136        specifier: node.source.value,
137        as: node.exported?.name || null
138      })
139
140      result.reExports.push({
141        specifier: node.source.value,
142        bindings: '*',
143        as: node.exported?.name,
144        loc: node.loc
145      })
146    },
147
148    // ===== CommonJS =====
149
150    CallExpression({ node }) {
151      // require('module')
152      if (node.callee.type === 'Identifier' &&
153          node.callee.name === 'require' &&
154          node.arguments[0]?.type === 'StringLiteral') {
155        result.requires.push({
156          specifier: node.arguments[0].value,
157          loc: node.loc
158        })
159      }
160    }
161  })
162
163  return result
164}
165

2.3 Tree Shaking 简化实现

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

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

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

1/**
2 * 简化的 Tree Shaking 实现
3 * 核心思想:追踪哪些导出被使用了
4 */
5class TreeShaker {
6  constructor() {
7    this.modules = new Map()       // moduleId -> ModuleInfo
8    this.usedExports = new Map()   // moduleId -> Set<exportName>
9    this.sideEffectModules = new Set()
10  }
11
12  /**
13   * 添加模块到分析器
14   */
15  addModule(moduleInfo) {
16    this.modules.set(moduleInfo.id, moduleInfo)
17
18    // 分析模块导出
19    moduleInfo.exportMap = new Map()
20
21    for (const exp of moduleInfo.exports) {
22      if (exp.type === 'named' || exp.type === 'default') {
23        moduleInfo.exportMap.set(exp.name, {
24          type: exp.type,
25          local: exp.local,
26          // 追踪导出来源(本地声明 or re-export)
27          source: exp.from || null
28        })
29      }
30    }
31
32    // 检测副作用
33    moduleInfo.hasSideEffects = this.detectSideEffects(moduleInfo)
34  }
35
36  /**
37   * 检测模块是否有副作用
38   * 副作用包括:顶层函数调用、全局变量修改等
39   */
40  detectSideEffects(moduleInfo) {
41    const { ast } = moduleInfo
42
43    let hasSideEffects = false
44
45    traverse.default(ast, {
46      // 顶层表达式语句可能有副作用
47      ExpressionStatement(path) {
48        // 只检查顶层
49        if (path.parent.type === 'Program') {
50          const expr = path.node.expression
51
52          // 函数调用
53          if (expr.type === 'CallExpression') {
54            hasSideEffects = true
55          }
56
57          // 赋值表达式
58          if (expr.type === 'AssignmentExpression') {
59            // 检查是否是全局变量赋值
60            const left = expr.left
61            if (left.type === 'Identifier') {
62              // 简化判断:非 const/let/var 声明的赋值
63              hasSideEffects = true
64            }
65            if (left.type === 'MemberExpression') {
66              // window.foo = ... / global.bar = ...
67              hasSideEffects = true
68            }
69          }
70        }
71      }
72    })
73
74    return hasSideEffects
75  }
76
77  /**
78   * 从入口开始标记使用的导出
79   */
80  markFromEntry(entryId) {
81    const entryModule = this.modules.get(entryId)
82
83    // 入口模块的所有导出都被认为"使用"
84    const allExports = Array.from(entryModule.exportMap.keys())
85    this.markUsed(entryId, allExports)
86  }
87
88  /**
89   * 标记模块的导出为已使用
90   */
91  markUsed(moduleId, exportNames) {
92    // 初始化集合
93    if (!this.usedExports.has(moduleId)) {
94      this.usedExports.set(moduleId, new Set())
95    }
96
97    const used = this.usedExports.get(moduleId)
98    const module = this.modules.get(moduleId)
99
100    for (const name of exportNames) {
101      if (used.has(name)) continue  // 已处理
102      used.add(name)
103
104      // 查找导出定义
105      const exportInfo = module.exportMap.get(name)
106      if (!exportInfo) continue
107
108      // 如果是 re-export,递归标记源模块
109      if (exportInfo.source) {
110        const sourceModule = this.findModuleBySpecifier(module, exportInfo.source)
111        if (sourceModule) {
112          this.markUsed(sourceModule.id, [exportInfo.local])
113        }
114      }
115
116      // 追踪本地导出引用的导入
117      this.traceImports(module, exportInfo.local)
118    }
119
120    // 如果模块有副作用,标记为必须包含
121    if (module.hasSideEffects) {
122      this.sideEffectModules.add(moduleId)
123    }
124  }
125
126  /**
127   * 追踪导出绑定使用的导入
128   */
129  traceImports(module, localName) {
130    // 简化实现:标记该模块所有导入的模块
131    // 完整实现需要进行作用域分析
132
133    for (const imp of module.imports || []) {
134      // 检查导入的绑定是否被使用
135      for (const binding of imp.bindings || []) {
136        if (binding.local === localName) {
137          const sourceModule = this.findModuleBySpecifier(module, imp.specifier)
138          if (sourceModule) {
139            // 标记使用的具体导出
140            const usedExport = binding.imported === 'default'
141              ? 'default'
142              : binding.imported
143            this.markUsed(sourceModule.id, [usedExport])
144          }
145        }
146      }
147    }
148  }
149
150  /**
151   * 根据模块说明符查找模块
152   */
153  findModuleBySpecifier(fromModule, specifier) {
154    const targetId = fromModule.mapping?.[specifier]
155    if (targetId !== undefined && targetId !== null) {
156      return this.modules.get(targetId)
157    }
158    return null
159  }
160
161  /**
162   * 获取 Shake 后的结果
163   */
164  getShakeResult() {
165    const includedModules = []
166    const excludedExports = new Map()
167
168    for (const [moduleId, module] of this.modules) {
169      const used = this.usedExports.get(moduleId) || new Set()
170      const hasSideEffects = this.sideEffectModules.has(moduleId)
171
172      // 包含条件:有使用的导出 OR 有副作用
173      if (used.size > 0 || hasSideEffects) {
174        includedModules.push({
175          id: moduleId,
176          path: module.filePath,
177          usedExports: Array.from(used),
178          hasSideEffects
179        })
180
181        // 记录未使用的导出
182        const allExports = Array.from(module.exportMap.keys())
183        const unused = allExports.filter(e => !used.has(e))
184        if (unused.length > 0) {
185          excludedExports.set(moduleId, unused)
186        }
187      }
188    }
189
190    return {
191      includedModules,
192      excludedExports,
193      stats: {
194        totalModules: this.modules.size,
195        includedModules: includedModules.length,
196        removedModules: this.modules.size - includedModules.length
197      }
198    }
199  }
200}
201
202// 使用示例
203function performTreeShaking(modules, entryId) {
204  const shaker = new TreeShaker()
205
206  // 添加所有模块
207  for (const mod of modules) {
208    shaker.addModule(mod)
209  }
210
211  // 从入口开始标记
212  shaker.markFromEntry(entryId)
213
214  // 获取结果
215  return shaker.getShakeResult()
216}
217

2.4 作用域分析核心思路

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

1/**
2 * 作用域分析器
3 * 构建作用域树,追踪变量的声明和引用
4 */
5class ScopeAnalyzer {
6  constructor() {
7    this.scopes = []
8    this.currentScope = null
9  }
10
11  /**
12   * 分析 AST,构建作用域树
13   */
14  analyze(ast) {
15    // 创建全局/模块作用域
16    this.currentScope = this.createScope('module', null)
17
18    traverse.default(ast, {
19      // ===== 作用域边界 =====
20
21      // 函数创建新作用域
22      FunctionDeclaration: (path) => {
23        this.enterFunctionScope(path)
24      },
25      'FunctionDeclaration:exit': () => {
26        this.exitScope()
27      },
28
29      FunctionExpression: (path) => {
30        this.enterFunctionScope(path)
31      },
32      'FunctionExpression:exit': () => {
33        this.exitScope()
34      },
35
36      ArrowFunctionExpression: (path) => {
37        this.enterFunctionScope(path)
38      },
39      'ArrowFunctionExpression:exit': () => {
40        this.exitScope()
41      },
42
43      // 块级作用域(if、for、while 等)
44      BlockStatement: (path) => {
45        // 只有包含 let/const 声明时才创建块级作用域
46        if (this.hasBlockScopedDeclarations(path.node)) {
47          this.enterBlockScope(path)
48        }
49      },
50      'BlockStatement:exit': (path) => {
51        if (this.hasBlockScopedDeclarations(path.node)) {
52          this.exitScope()
53        }
54      },
55
56      // ===== 声明 =====
57
58      VariableDeclaration: (path) => {
59        const kind = path.node.kind  // var, let, const
60
61        for (const decl of path.node.declarations) {
62          this.declareBinding(decl.id, kind, path)
63        }
64      },
65
66      FunctionDeclaration: (path) => {
67        if (path.node.id) {
68          // 函数声明提升到外层作用域
69          this.declareBinding(path.node.id, 'function', path)
70        }
71      },
72
73      ClassDeclaration: (path) => {
74        if (path.node.id) {
75          this.declareBinding(path.node.id, 'class', path)
76        }
77      },
78
79      ImportDeclaration: (path) => {
80        for (const spec of path.node.specifiers) {
81          this.declareBinding(spec.local, 'import', path)
82        }
83      },
84
85      // ===== 引用 =====
86
87      Identifier: (path) => {
88        if (this.isReference(path)) {
89          this.recordReference(path.node.name, path)
90        }
91      }
92    })
93
94    return this.scopes
95  }
96
97  /**
98   * 创建新作用域
99   */
100  createScope(type, parent) {
101    const scope = {
102      id: this.scopes.length,
103      type,           // 'module', 'function', 'block'
104      parent,
105      children: [],
106      bindings: new Map(),    // name -> BindingInfo
107      references: [],         // 引用列表
108      // 统计信息
109      stats: {
110        declarations: 0,
111        references: 0
112      }
113    }
114
115    if (parent) {
116      parent.children.push(scope)
117    }
118
119    this.scopes.push(scope)
120    return scope
121  }
122
123  /**
124   * 进入函数作用域
125   */
126  enterFunctionScope(path) {
127    const scope = this.createScope('function', this.currentScope)
128    this.currentScope = scope
129
130    // 函数参数作为绑定
131    for (const param of path.node.params || []) {
132      this.declarePattern(param, 'param')
133    }
134  }
135
136  /**
137   * 进入块级作用域
138   */
139  enterBlockScope(path) {
140    const scope = this.createScope('block', this.currentScope)
141    this.currentScope = scope
142  }
143
144  /**
145   * 退出当前作用域
146   */
147  exitScope() {
148    this.currentScope = this.currentScope.parent
149  }
150
151  /**
152   * 声明绑定
153   */
154  declareBinding(id, kind, path) {
155    const name = id.name
156
157    // var 声明提升到函数作用域
158    const targetScope = kind === 'var'
159      ? this.findFunctionScope()
160      : this.currentScope
161
162    // 创建绑定信息
163    const binding = {
164      name,
165      kind,           // 'var', 'let', 'const', 'function', 'class', 'param', 'import'
166      node: id,
167      path,
168      scope: targetScope,
169      references: [],
170      isExported: false,
171      isUsed: false
172    }
173
174    targetScope.bindings.set(name, binding)
175    targetScope.stats.declarations++
176
177    return binding
178  }
179
180  /**
181   * 处理解构模式
182   */
183  declarePattern(pattern, kind) {
184    switch (pattern.type) {
185      case 'Identifier':
186        this.declareBinding(pattern, kind, null)
187        break
188
189      case 'ObjectPattern':
190        for (const prop of pattern.properties) {
191          if (prop.type === 'RestElement') {
192            this.declarePattern(prop.argument, kind)
193          } else {
194            this.declarePattern(prop.value, kind)
195          }
196        }
197        break
198
199      case 'ArrayPattern':
200        for (const element of pattern.elements) {
201          if (element) {
202            if (element.type === 'RestElement') {
203              this.declarePattern(element.argument, kind)
204            } else {
205              this.declarePattern(element, kind)
206            }
207          }
208        }
209        break
210
211      case 'AssignmentPattern':
212        this.declarePattern(pattern.left, kind)
213        break
214
215      case 'RestElement':
216        this.declarePattern(pattern.argument, kind)
217        break
218    }
219  }
220
221  /**
222   * 记录变量引用
223   */
224  recordReference(name, path) {
225    // 从当前作用域向上查找绑定
226    let scope = this.currentScope
227
228    while (scope) {
229      const binding = scope.bindings.get(name)
230      if (binding) {
231        binding.references.push(path)
232        binding.isUsed = true
233        scope.stats.references++
234        return
235      }
236      scope = scope.parent
237    }
238
239    // 未找到绑定,是全局变量引用
240    this.currentScope.references.push({
241      name,
242      path,
243      isGlobal: true
244    })
245  }
246
247  /**
248   * 判断 Identifier 是否为引用(而非声明)
249   */
250  isReference(path) {
251    const parent = path.parent
252    const node = path.node
253
254    // 声明的左侧
255    if (parent.type === 'VariableDeclarator' && parent.id === node) {
256      return false
257    }
258
259    // 函数声明名称
260    if (parent.type === 'FunctionDeclaration' && parent.id === node) {
261      return false
262    }
263
264    // 类声明名称
265    if (parent.type === 'ClassDeclaration' && parent.id === node) {
266      return false
267    }
268
269    // 对象属性键(非计算属性)
270    if (parent.type === 'Property' && parent.key === node && !parent.computed) {
271      return false
272    }
273
274    // 对象方法名
275    if (parent.type === 'MethodDefinition' && parent.key === node && !parent.computed) {
276      return false
277    }
278
279    // 成员访问的属性(非计算)
280    if (parent.type === 'MemberExpression' && parent.property === node && !parent.computed) {
281      return false
282    }
283
284    // import 语句中的导入名
285    if (parent.type === 'ImportSpecifier' && parent.imported === node) {
286      return false
287    }
288
289    // export 语句中的导出名
290    if (parent.type === 'ExportSpecifier' && parent.exported === node) {
291      return false
292    }
293
294    return true
295  }
296
297  /**
298   * 查找最近的函数作用域
299   */
300  findFunctionScope() {
301    let scope = this.currentScope
302    while (scope && scope.type === 'block') {
303      scope = scope.parent
304    }
305    return scope || this.currentScope
306  }
307
308  /**
309   * 检查块是否包含块级作用域声明
310   */
311  hasBlockScopedDeclarations(block) {
312    for (const stmt of block.body) {
313      if (stmt.type === 'VariableDeclaration' &&
314          (stmt.kind === 'let' || stmt.kind === 'const')) {
315        return true
316      }
317    }
318    return false
319  }
320
321  /**
322   * 获取未使用的绑定
323   */
324  getUnusedBindings() {
325    const unused = []
326
327    for (const scope of this.scopes) {
328      for (const [name, binding] of scope.bindings) {
329        if (!binding.isUsed && !binding.isExported) {
330          unused.push({
331            name,
332            kind: binding.kind,
333            scope: scope.type,
334            loc: binding.node?.loc
335          })
336        }
337      }
338    }
339
340    return unused
341  }
342}
343

第三章:robuild 完整架构设计

image.png

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

3.1 核心设计原则

robuild 的设计遵循以下原则:

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

3.2 架构总览

1┌──────────────────────────────────────────────────────────────────┐
2                          CLI Layer                                
3  ┌─────────────────────────────────────────────────────────────┐ 
4    cac(命令行解析)→ c12(配置加载)→ build()               
5  └─────────────────────────────────────────────────────────────┘ 
6└──────────────────────────────────────────────────────────────────┘
7                                
8┌──────────────────────────────────────────────────────────────────┐
9                        Config Layer                               
10  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐  
11   normalizeTsup      inheritConfig      resolveExternal   
12   Config()       │→│      ()          │→│    Config()       
13  └─────────────────┘  └─────────────────┘  └─────────────────┘  
14└──────────────────────────────────────────────────────────────────┘
15                                
16┌──────────────────────────────────────────────────────────────────┐
17                        Build Layer                                
18  ┌─────────────────────────────┐  ┌─────────────────────────┐   
19       rolldownBuild()               transformDir()          
20    ┌─────────────────────┐        ┌─────────────────┐       
21     Rolldown + DTS Plugin│        Oxc Transform          
22    └─────────────────────┘        └─────────────────┘       
23  └─────────────────────────────┘  └─────────────────────────┘   
24└──────────────────────────────────────────────────────────────────┘
25                                
26┌──────────────────────────────────────────────────────────────────┐
27                       Plugin Layer                                
28  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐    
29    Shims        Shebang      Node         Glob         
30    Plugin       Plugin       Protocol     Import       
31  └───────────┘  └───────────┘  └───────────┘  └───────────┘    
32└──────────────────────────────────────────────────────────────────┘
33                                
34┌──────────────────────────────────────────────────────────────────┐
35                      Transform Layer                              
36  ┌───────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐    
37    Banner       Clean        Copy         Exports      
38    Footer       Output       Files        Generate     
39  └───────────┘  └───────────┘  └───────────┘  └───────────┘    
40└──────────────────────────────────────────────────────────────────┘
41

3.3 构建流程详解

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

1                    build() 入口
2                         
3          ┌──────────────┴──────────────┐
4                                       
5   normalizeTsupConfig()         performWatchBuild()
6   (标准化配置格式)                  (Watch 模式)
7          
8          
9   inheritConfig()
10   (配置继承)
11          
12          
13   ┌──────┴──────┐
14      entries   
15      遍历      
16   └──────┬──────┘
17          
18   ┌──────┴──────┬──────────────┐
19                              
20 Bundle       Transform      其他类型
21 Entry        Entry          ...
22                
23                
24rolldownBuild  transformDir
25                
26   └──────┬──────┘
27          
28   generateExports()
29   (生成 package.json exports)
30          
31          
32   executeOnSuccess()
33   (执行回调)
34

让我们深入关键环节:

3.3.1 配置标准化

robuild 支持两种配置风格:

1// tsup 风格(flat config)
2export default {
3  entry: ['./src/index.ts'],
4  format: ['esm', 'cjs'],
5  dts: true
6}
7
8// unbuild 风格(entries-based)
9export default {
10  entries: [
11    { type: 'bundle', input: './src/index.ts', format: ['esm', 'cjs'] },
12    { type: 'transform', input: './src/' }
13  ]
14}
15

配置标准化的核心代码:

1// src/build.ts
2function normalizeTsupConfig(config: BuildConfig): BuildConfig {
3  // 如果已经有 entries,直接返回
4  if (config.entries && config.entries.length > 0) {
5    return config
6  }
7
8  //  tsup 风格的 entry 转换为 entries
9  if (config.entry) {
10    const entry: BundleEntry = inheritConfig(
11      {
12        type: 'bundle' as const,
13        entry: config.entry,
14      },
15      config,
16      { name: 'globalName' } // 字段映射
17    )
18    return { ...config, entries: [entry] }
19  }
20
21  return config
22}
23

3.3.2 配置继承

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

1const SHARED_CONFIG_FIELDS = [
2  'format', 'outDir', 'platform', 'target', 'minify',
3  'dts', 'splitting', 'treeshake', 'sourcemap',
4  'external', 'noExternal', 'env', 'alias', 'banner',
5  'footer', 'shims', 'rolldown', 'loaders', 'clean'
6] as const
7
8function inheritConfig<T extends Partial<BuildEntry>>(
9  entry: T,
10  config: BuildConfig,
11  additionalMappings?: Record<string, string>
12): T {
13  const result: any = { ...entry }
14
15  // 只继承未定义的字段
16  for (const field of SHARED_CONFIG_FIELDS) {
17    if (result[field] === undefined && config[field] !== undefined) {
18      result[field] = config[field]
19    }
20  }
21
22  // 处理字段映射(如 name -> globalName)
23  if (additionalMappings) {
24    for (const [configKey, entryKey] of Object.entries(additionalMappings)) {
25      if (result[entryKey] === undefined) {
26        result[entryKey] = (config as any)[configKey]
27      }
28    }
29  }
30
31  return result
32}
33

3.3.3 并行构建

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

1await Promise.all(
2  entries.map(entry =>
3    entry.type === 'bundle'
4      ? rolldownBuild(ctx, entry, hooks, config)
5      : transformDir(ctx, entry)
6  )
7)
8

3.4 Bundle Builder 实现

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

1// src/builders/bundle.ts
2export async function rolldownBuild(
3  ctx: BuildContext,
4  entry: BundleEntry,
5  hooks: BuildHooks,
6  config?: BuildConfig
7): Promise<void> {
8  // 1. 初始化插件管理器
9  const pluginManager = new RobuildPluginManager(config || {}, entry, ctx.pkgDir)
10  await pluginManager.initializeRobuildHooks()
11
12  // 2. 解析配置
13  const formats = Array.isArray(entry.format) ? entry.format : [entry.format || 'es']
14  const platform = entry.platform || 'node'
15  const target = entry.target || 'es2022'
16
17  // 3. 清理输出目录
18  await cleanOutputDir(ctx.pkgDir, fullOutDir, entry.clean ?? true)
19
20  // 4. 处理外部依赖
21  const externalConfig = resolveExternalConfig(ctx, {
22    external: entry.external,
23    noExternal: entry.noExternal
24  })
25
26  // 5. 构建插件列表
27  const rolldownPlugins: Plugin[] = [
28    shebangPlugin(),
29    nodeProtocolPlugin(entry.nodeProtocol || false),
30    // ... 其他插件
31  ]
32
33  // 6. 构建 Rolldown 配置
34  const baseRolldownConfig: InputOptions = {
35    cwd: ctx.pkgDir,
36    input: inputs,
37    plugins: rolldownPlugins,
38    platform: platform === 'node' ? 'node' : 'neutral',
39    external: externalConfig,
40    resolve: { alias: entry.alias || {} },
41    transform: { target, define: defineOptions }
42  }
43
44  // 7. 所有格式并行构建
45  const formatResults = await Promise.all(formats.map(buildFormat))
46
47  // 8. 执行构建结束钩子
48  await pluginManager.executeRobuildBuildEnd({ allOutputEntries })
49}
50

关键设计点:

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

1const buildFormat = async (format: ModuleFormat) => {
2  let entryFileName = `[name]${extension}`
3
4  if (isMultiFormat) {
5    // 多格式构建时使用明确的扩展名
6    if (format === 'cjs') entryFileName = `[name].cjs`
7    else if (format === 'esm') entryFileName = `[name].mjs`
8    else if (format === 'iife') entryFileName = `[name].js`
9  }
10
11  const res = await rolldown(formatConfig)
12  await res.write(outConfig)
13  await res.close()
14}
15

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

1if (entry.dts !== false && format === 'esm') {
2  formatConfig.plugins = [
3    ...plugins,
4    dts({ cwd: ctx.pkgDir, ...entry.dts })
5  ]
6}
7

3.5 Transform Builder 实现

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

1// src/builders/transform.ts
2export async function transformDir(
3  ctx: BuildContext,
4  entry: TransformEntry
5): Promise<void> {
6  // 获取所有源文件
7  const files = await glob('**/*.*', { cwd: inputDir })
8
9  const promises = files.map(async (entryName) => {
10    const ext = extname(entryPath)
11
12    switch (ext) {
13      case '.ts':
14      case '.tsx':
15      case '.jsx': {
16        // 使用 Oxc 转换
17        const transformed = await transformModule(entryPath, entry)
18        await writeFile(entryDistPath, transformed.code, 'utf8')
19
20        // 生成类型声明
21        if (transformed.declaration) {
22          await writeFile(dtsPath, transformed.declaration, 'utf8')
23        }
24        break
25      }
26      default:
27        // 其他文件直接复制
28        await copyFile(entryPath, entryDistPath)
29    }
30  })
31
32  await Promise.all(promises)
33}
34

单文件转换使用 Oxc:

1async function transformModule(entryPath: string, entry: TransformEntry) {
2  const sourceText = await readFile(entryPath, 'utf8')
3
4  // 1. 解析 AST
5  const parsed = parseSync(entryPath, sourceText, {
6    lang: ext === '.tsx' ? 'tsx' : 'ts',
7    sourceType: 'module'
8  })
9
10  // 2. 重写相对导入(使用 MagicString 保持 sourcemap 兼容)
11  const magicString = new MagicString(sourceText)
12  for (const staticImport of parsed.module.staticImports) {
13    //  .ts 导入重写为 .mjs
14    rewriteSpecifier(staticImport.moduleRequest)
15  }
16
17  // 3. Oxc 转换
18  const transformed = await transform(entryPath, magicString.toString(), {
19    target: entry.target || 'es2022',
20    sourcemap: !!entry.sourcemap,
21    typescript: {
22      declaration: { stripInternal: true }
23    }
24  })
25
26  // 4. 可选压缩
27  if (entry.minify) {
28    const res = await minify(entryPath, transformed.code, entry.minify)
29    transformed.code = res.code
30  }
31
32  return transformed
33}
34

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

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

4.1 问题背景

ESM 和 CJS 有根本性的差异:

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

4.2 Shims 插件设计

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

1// ESM 中使用 CJS 特性时的 shim
2const NODE_GLOBALS_SHIM = `
3// Node.js globals shim for ESM
4import { fileURLToPath } from 'node:url'
5import { dirname } from 'node:path'
6import { createRequire } from 'node:module'
7
8const __filename = fileURLToPath(import.meta.url)
9const __dirname = dirname(__filename)
10const require = createRequire(import.meta.url)
11`
12
13// 浏览器环境的 process.env shim
14const PROCESS_ENV_SHIM = `
15if (typeof process === 'undefined') {
16  globalThis.process = {
17    env: {},
18    platform: 'browser',
19    version: '0.0.0'
20  }
21}
22`
23
24// module.exports shim
25const MODULE_EXPORTS_SHIM = `
26if (typeof module === 'undefined') {
27  globalThis.module = { exports: {} }
28}
29if (typeof exports === 'undefined') {
30  globalThis.exports = module.exports
31}
32`
33

关键是检测需要哪些 shim

1function detectShimNeeds(code: string) {
2  // 移除注释和字符串,避免误判
3  const cleanCode = removeCommentsAndStrings(code)
4
5  return {
6    needsDirname: /\b__dirname\b/.test(cleanCode) ||
7                  /\b__filename\b/.test(cleanCode),
8    needsRequire: /\brequire\s*\(/.test(cleanCode),
9    needsExports: /\bmodule\.exports\b/.test(cleanCode) ||
10                  /\bexports\.\w+/.test(cleanCode),
11    needsEnv: /\bprocess\.env\b/.test(cleanCode)
12  }
13}
14
15function removeCommentsAndStrings(code: string): string {
16  return code
17    // 移除单行注释
18    .replace(/\/\/.*$/gm, '')
19    // 移除多行注释
20    .replace(/\/\*[\s\S]*?\*\//g, '')
21    // 移除字符串字面量
22    .replace(/"(?:[^"\\]|\\.)*"/g, '""')
23    .replace(/'(?:[^'\\]|\\.)*'/g, "''")
24    .replace(/`(?:[^`\\]|\\.)*`/g, '``')
25}
26

4.3 平台特定配置

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

1function getPlatformShimsConfig(platform: 'browser' | 'node' | 'neutral') {
2  switch (platform) {
3    case 'browser':
4      return {
5        dirname: false,  // 浏览器不支持
6        require: false,  // 浏览器不支持
7        exports: false,  // 浏览器不支持
8        env: true        // 需要 polyfill
9      }
10    case 'node':
11      return {
12        dirname: true,   // 转换为 ESM 等价写法
13        require: true,   // 使用 createRequire
14        exports: true,   // 转换为 ESM export
15        env: false       // 原生支持
16      }
17    case 'neutral':
18      return {
19        dirname: false,
20        require: false,
21        exports: false,
22        env: false
23      }
24  }
25}
26

4.4 Dual Package 支持

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

1// 输入配置
2{
3  entries: [{
4    type: 'bundle',
5    input: './src/index.ts',
6    format: ['esm', 'cjs'],
7    generateExports: true
8  }]
9}
10
11// 生成的 package.json exports
12{
13  "exports": {
14    ".": {
15      "types": "./dist/index.d.mts",  // TypeScript 优先
16      "import": "./dist/index.mjs",    // ESM
17      "require": "./dist/index.cjs"    // CJS
18    }
19  }
20}
21

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


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

5.1 设计目标

robuild 的插件系统需要满足:

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

5.2 插件类型检测

robuild 自动识别插件类型:

1class RobuildPluginManager {
2  private normalizePlugin(pluginOption: RobuildPluginOption): RobuildPlugin {
3    // 工厂函数
4    if (typeof pluginOption === 'function') {
5      return this.normalizePlugin(pluginOption())
6    }
7
8    // 类型检测优先级
9    if (this.isRobuildPlugin(pluginOption)) return pluginOption
10    if (this.isRolldownPlugin(pluginOption)) return this.adaptRolldownPlugin(pluginOption)
11    if (this.isVitePlugin(pluginOption)) return this.adaptVitePlugin(pluginOption)
12    if (this.isUnplugin(pluginOption)) return this.adaptUnplugin(pluginOption)
13
14    // 兜底:当作 Rolldown 插件
15    return this.adaptRolldownPlugin(pluginOption)
16  }
17
18  // Robuild 插件:有 robuild 特有钩子或标记
19  private isRobuildPlugin(plugin: any): plugin is RobuildPlugin {
20    return plugin.meta?.robuild === true
21      || plugin.robuildSetup
22      || plugin.robuildBuildStart
23      || plugin.robuildBuildEnd
24  }
25
26  // Rolldown/Rollup 插件:有标准钩子
27  private isRolldownPlugin(plugin: any): plugin is RolldownPlugin {
28    return plugin.name && (
29      plugin.buildStart || plugin.buildEnd ||
30      plugin.resolveId || plugin.load || plugin.transform ||
31      plugin.generateBundle || plugin.writeBundle
32    )
33  }
34
35  // Vite 插件:有 Vite 特有钩子
36  private isVitePlugin(plugin: any): boolean {
37    return plugin.config || plugin.configResolved ||
38           plugin.configureServer || plugin.meta?.vite === true
39  }
40}
41

5.3 Robuild 特有钩子

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

1interface RobuildPlugin extends RolldownPlugin {
2  // 插件初始化时调用
3  robuildSetup?: (ctx: RobuildPluginContext) => void | Promise<void>
4
5  // 构建开始时调用
6  robuildBuildStart?: (ctx: RobuildPluginContext) => void | Promise<void>
7
8  // 构建结束时调用,可以访问所有输出
9  robuildBuildEnd?: (ctx: RobuildPluginContext, result?: any) => void | Promise<void>
10}
11
12interface RobuildPluginContext {
13  config: BuildConfig
14  entry: BuildEntry
15  pkgDir: string
16  outDir: string
17  format: ModuleFormat | ModuleFormat[]
18  platform: Platform
19  target: Target
20}
21

5.4 插件工厂模式

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

1// 创建简单的 transform 插件
2function createTransformPlugin(
3  name: string,
4  transform: (code: string, id: string) => string | null,
5  filter?: (id: string) => boolean
6): RobuildPlugin {
7  return {
8    name,
9    meta: { robuild: true },
10    transform: async (code, id) => {
11      if (filter && !filter(id)) return null
12      return transform(code, id)
13    }
14  }
15}
16
17// 使用示例
18const myPlugin = createTransformPlugin(
19  'add-banner',
20  (code) => [`/* My Library */\n${code}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.code.md),
21  (id) => /\.js$/.test(id)
22)
23

组合多个插件:

1function combinePlugins(name: string, plugins: RobuildPlugin[]): RobuildPlugin {
2  const combined: RobuildPlugin = { name, meta: { robuild: true } }
3
4  for (const plugin of plugins) {
5    // 链式组合 transform 钩子
6    if (plugin.transform) {
7      const prevHook = combined.transform
8      combined.transform = async (code, id) => {
9        let currentCode = code
10        if (prevHook) {
11          const result = await prevHook(currentCode, id)
12          if (result) {
13            currentCode = typeof result === 'string' ? result : result.code
14          }
15        }
16        return plugin.transform!(currentCode, id)
17      }
18    }
19
20    // 其他钩子类似处理...
21  }
22
23  return combined
24}
25

第六章:性能优化策略

6.1 为什么 Rust 更快?

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

1. 零成本抽象

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

1// Rust: 编译时展开,没有虚函数调用
2fn process<T: Transform>(input: T) -> String {
3    input.transform()
4}
5
6// 等价于为每个具体类型生成特化版本
7fn process_for_type_a(input: TypeA) -> String { ... }
8fn process_for_type_b(input: TypeB) -> String { ... }
9

2. 无 GC 暂停

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

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

3. 数据局部性

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

1// 连续内存布局
2struct Token {
3    kind: TokenKind,
4    start: u32,
5    end: u32,
6}
7let tokens: Vec<Token> = tokenize(source);
8// tokens 在连续内存中,缓存命中率高
9

4. 真正的并行

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

1use rayon::prelude::*;
2
3// 多个文件并行解析
4let results: Vec<_> = files
5    .par_iter()  // 并行迭代
6    .map(|file| parse(file))
7    .collect();
8

6.2 robuild 的并行策略

robuild 在多个层面实现并行:

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

1await Promise.all(
2  entries.map(entry =>
3    entry.type === 'bundle'
4      ? rolldownBuild(ctx, entry, hooks, config)
5      : transformDir(ctx, entry)
6  )
7)
8

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

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

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

1const writtenFiles = await Promise.all(promises)
2

6.3 缓存策略

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

依赖缓存:解析结果缓存

1// 依赖解析缓存
2const depsCache = new Map<OutputChunk, Set<string>>()
3const resolveDeps = (chunk: OutputChunk): string[] => {
4  if (!depsCache.has(chunk)) {
5    depsCache.set(chunk, new Set<string>())
6  }
7  const deps = depsCache.get(chunk)!
8  // ... 递归解析
9  return Array.from(deps).sort()
10}
11

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

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

第七章:为什么选择 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

1┌─────────────────────────────────────────────────────────────┐
2                    JavaScript                              
3  ┌───────────────────────────────────────────────────────┐  
4    配置加载、CLI、插件管理、构建编排、输出处理            
5  └───────────────────────────────────────────────────────┘  
6                                                             
7                      NAPI 绑定                               
8                                                             
9  ┌───────────────────────────────────────────────────────┐  
10                Rust 层(计算密集型)                       
11    ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐       
12     Parser   │Transform│  Bundler   Minifier│       
13     (Oxc)     (Oxc)    (Rolldown)  (Oxc)         
14    └─────────┘ └─────────┘ └─────────┘ └─────────┘       
15  └───────────────────────────────────────────────────────┘  
16└─────────────────────────────────────────────────────────────┘
17

7.2 NAPI 绑定的成本

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

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

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

1// 好的做法:一次调用完成整个解析
2const parsed = parseSync(filePath, sourceText, options)
3
4// 不好的做法:多次调用
5const ast = parse(source)
6const imports = extractImports(ast)  // 又一次跨边界
7const exports = extractExports(ast)  // 又一次跨边界
8

7.3 与 Vite 生态的协同

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

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

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

8.1 最简配置

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

1// build.config.ts
2export default {}
3
4// 等价于
5export default {
6  entries: [{
7    type: 'bundle',
8    input: './src/index.ts',
9    format: ['esm'],
10    dts: true
11  }]
12}
13

8.2 多入口 + 多格式

1// build.config.ts
2export default {
3  entries: [
4    {
5      type: 'bundle',
6      input: {
7        index: './src/index.ts',
8        cli: './src/cli.ts'
9      },
10      format: ['esm', 'cjs'],
11      dts: true,
12      generateExports: true
13    }
14  ],
15  exports: {
16    enabled: true,
17    autoUpdate: true
18  }
19}
20

8.3 带 shims 的 Node.js 工具

1// build.config.ts
2export default {
3  entries: [{
4    type: 'bundle',
5    input: './src/index.ts',
6    format: ['esm'],
7    platform: 'node',
8    shims: {
9      dirname: true,   // __dirname, __filename
10      require: true    // require()
11    },
12    banner: '#!/usr/bin/env node'
13  }]
14}
15

8.4 浏览器库 + UMD

1// build.config.ts
2export default {
3  entries: [{
4    type: 'bundle',
5    input: './src/index.ts',
6    format: ['esm', 'umd'],
7    platform: 'browser',
8    globalName: 'MyLib',
9    minify: true,
10    shims: {
11      env: true  // process.env polyfill
12    }
13  }]
14}
15

8.5 Monorepo 内部包

1// build.config.ts
2export default {
3  entries: [{
4    type: 'bundle',
5    input: './src/index.ts',
6    format: ['esm'],
7    dts: true,
8    noExternal: [
9      '@myorg/utils',     // 打包内部依赖
10      '@myorg/shared'
11    ]
12  }]
13}
14

结语:构建工具的未来

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

  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 互操作、插件系统、性能优化等主题。如有技术问题,欢迎讨论交流。

参考资料


构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的》 是转载文章,点击查看原文


相关推荐


基于华为openEuler系统部署Gitblit服务器
江湖有缘2026/2/14

基于华为openEuler系统部署Gitblit服务器 前言一、相关服务介绍1.1 openEuler系统介绍1.2 Gitblit介绍 二、本次实践介绍2.1 本次实践介绍2.2 本次环境规划 三、本地环境检查3.1 检查系统版本3.2 检查内核版本3.3 检查本地IP3.4 检查Docker环境 四、下载Gitblit软件包4.1 新建安装目录4.2 下载Gitblit软件包 五、部署Gitblit服务器4.1 修改配置文件4.2 修改service-centos.sh文件


Jira部署在Windows完整流程
WangShade2026/2/5

目录 1 本文目标2 安装文件3 安装Jira4 安装java5 安装Mysql 8.05.1 解压Mysql安装包5.2 配置环境变量5.3 安装依赖5.4 安装mysql服务5.5 修改配置my.ini5.6 启动mysql5.7 访问数据库并修改密码5.8 安装驱动 6 配置java-agent6.1 查询Jira服务名称6.2 配置Java Agent 7 配置Jira7.1 生成注册码7.2 查看工作成果 8 生成插件注册码 1 本文目标 提供完整的软件安装包


grep一下
J船长2026/1/27

grep 实战指南:把日志过滤一下 grep (缩写来自Globally search a Regular Expression and Print)是一种强大的文本搜索工具,它能使用特定模式匹配(包括正则表达式)搜索文本,并默认输出匹配行。 0. 准备:创建示例日志文件 在终端执行: nano test.log 粘贴下面内容: 2026-01-27 10:01:12 INFO App started 2026-01-27 10:01:15 INFO User login success


如何将 Safari 标签转移到新 iPhone 17?
TheNextByte12026/1/18

当换用新 iPhone 17时,很多人都希望将 Safari 标签页无缝转移到新 iPhone 上,以便继续浏览未完成的网页内容。如何将 Safari 标签转移到另一部 iPhone?本文将介绍几种方法来帮助您轻松转移 Safari 标签页。 第 1 部分:如何通过 Handoff 将 Safari 标签转移到新 iPhone Handoff 是 Apple 设备之间强大的连续性功能之一,允许用户跨设备无缝传输任务,包括 Safari 选项卡。如果您想知道如何将 Safari 标签转移到另一


windows2025服务器系统如何开启多人远程?
网硕互联的小客服2026/1/10

在 Windows Server 2025 系统中,为了支持多人远程桌面会话,需要正确配置远程桌面服务(RDS,Remote Desktop Services)。Windows服务器系统默认只允许两个管理员会话用于远程管理。如果需要开启多人远程桌面功能(允许多个用户同时连接),需配置远程桌面会话主机(RDSH)或通过调整策略实现。 以下是实现多人远程桌面功能的详细步骤: 一、通过远程桌面服务(RDS)实现多人远程 Windows Server 提供了 远程桌面服务(RDS),这是开启多


赫蹏(hètí):为中文网页内容赋予优雅排版的开源利器
修己xj2026/1/2

fHJ9cZeOp.jpg 在当今信息爆炸的时代,内容呈现的形式往往决定了阅读体验的优劣。对于中文网站来说,一个长期存在的挑战是如何实现符合传统中文排版美学的网页展示。尽管现代CSS技术已经十分强大,但针对中文特点的排版优化仍然不够完善。今天,我们将介绍一个专门为解决这一问题而生的开源项目——赫蹏(hètí)。 什么是赫蹏? 赫蹏是一个专为中文内容展示设计的排版样式增强库,名称取自古代对纸张的雅称。这个项目由开发者Sivan创建,基于通行的中文排版规范,旨在为网站的读者提供更加舒适、专业的文章阅


AI中的网络世界
人生的方向随自己而走2025/12/23

灵光AI创作 第一条语法 语法: 在华三、华为、锐捷的组网过程中vlan是常用的,实现vlan的基本创建、access口,trunk口、hybrid口配置。 第二条语法 语法: 在华三、华为、锐捷的【交换机、防火墙】组网过程中vlan三层接口是常用的,实现vlan三层接口基本创建、并且配置好IPV4地址➕IPv6地址。 第三条语法 语法: 在华三、华为、锐捷的【交换机、防火墙】组网过程中stp 是常用的、给出stp工作原理和+基本配置命令+实战案例。 第四条语法 语法: 在华三、华为、锐捷的【交


解锁 Flutter 沉浸式交互:打造带物理动效的自定义底部弹窗
飛6792025/12/15

欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。 Flutter 下拉刷新组件深度开发指南 下拉刷新在移动应用中的重要性 下拉刷新是移动应用中列表类界面最基础也最关键的交互功能之一。根据2023年移动应用体验报告,超过92%的用户会在使用列表应用时自然尝试下拉刷新操作,其中78%的用户认为良好的刷新体验直接影响他们对应用的整体评价。 官方 RefreshIndicator 的局限性


公司内网部署大模型的探索之路
锋行天下2025/12/7

使用场景 公司的办公环境是内网,不和互联网相连(保密单位,别问为啥这样),要更新个项目依赖啥的,很麻烦,要使用U盘来回拷贝文件,这是前提,我现在要在内网环境部署大模型,也是一波三折,以下是我的探索之路 在外网使用docker 运行 ollama 镜像,由于我本地电脑是mac电脑,服务是linux,因为是要把容器导出为镜像文件拿到内网使用,所以拉取镜像的时候要指定宿主机架构,不然的话,导出的镜像文件在服务器无法运行 docker pull --flatform=linux/amd64 oll


Python高性能数据库操作实战:异步IO与多线程结合代码解析
2501_941800882025/11/28

在高并发数据库访问和大数据处理场景中,高性能数据库操作系统至关重要。Python结合异步IO和多线程,可实现快速、稳定的数据库操作平台。本文结合代码示例,讲解Python数据库操作实战方法。 一、基础数据库操作 使用sqlite3进行简单操作: import sqlite3 conn = sqlite3.connect('example.db') c = conn.cursor() c.execute('CREATE TABLE IF NOT EXISTS users (id INTEG

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客