本文将从第一人称实战视角,深入探讨前端构建工具的技术演进,以及我在设计 robuild 过程中的架构思考与工程实践。
引言:为什么我们需要又一个构建工具?
在开始正文之前,我想先回答一个无法回避的问题:在 Webpack、Rollup、esbuild、Vite 已经如此成熟的今天,为什么还要设计一个新的构建工具?
答案很简单:库构建与应用构建是两个本质不同的问题域。
Webpack 为复杂应用而生,Vite 为开发体验而生,esbuild 为速度而生。但当我们需要构建一个 npm 库时,我们需要的是:
- 零配置:库作者不应该花时间在配置上
- 多格式输出:ESM、CJS、甚至 UMD 一键生成
- 类型声明:TypeScript 项目的
.d.ts自动生成 - Tree-shaking 友好:输出代码必须对消费者友好
- 极致性能:构建速度不应该成为开发瓶颈
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 的架构基于以下核心概念:
- 依赖图(Dependency Graph):从入口点出发,递归解析所有依赖
- Loader 机制:将非 JS 资源转换为模块
- Plugin 系统:基于 Tapable 的事件驱动架构
- 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 具有以下静态特性:
- 静态导入:
import语句必须在模块顶层,不能动态 - 静态导出:
export的绑定在编译时就能确定 - 只读绑定:导入的值不能被重新赋值
这使得编译器可以在构建时进行精确的依赖分析,而不需要运行代码。
作用域提升(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 成为了这场革命的主角?
- 零成本抽象:高级语言特性不带来运行时开销
- 内存安全:编译器保证没有数据竞争和悬空指针
- 真正的并行:无 GC 停顿,能充分利用多核
- 可编译到 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 需要完成以下步骤:
- 解析:将源代码转换为 AST
- 依赖收集:从 AST 中提取 import 语句
- 依赖图构建:递归处理所有依赖,构建完整的模块图
- 打包:将所有模块合并为单个文件
下面是完整的实现代码:
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 的核心是标记-清除算法:
- 标记阶段:从入口点开始,标记所有"活"的导出
- 清除阶段:移除所有未标记的代码
下面是一个简化的 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 完整架构设计
了解了 Bundler 的基本原理后,让我们深入 robuild 的架构设计。
3.1 核心设计原则
robuild 的设计遵循以下原则:
- 零配置优先:默认配置应该覆盖 90% 的使用场景
- 渐进式复杂度:简单任务简单做,复杂任务可配置
- 兼容性:支持 tsup 和 unbuild 的配置风格
- 性能:利用 Rust 工具链的性能优势
- 可扩展:插件系统支持自定义逻辑
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 有根本性的差异:
| 特性 | ESM | CJS |
|---|---|---|
| 加载时机 | 静态(编译时) | 动态(运行时) |
| 导出方式 | 具名绑定 | module.exports 对象 |
| this 值 | undefined | module |
| __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 的插件系统需要满足:
- 兼容性:支持 Rolldown、Rollup、Vite、Unplugin 插件
- 简洁性:简单需求不需要复杂配置
- 可组合:多个插件可以组合成一个
- 生命周期明确: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?
- 生态兼容性:npm 生态的插件都是 JavaScript,纯 Rust 无法复用
- 配置灵活性:JavaScript 配置文件可以动态计算、条件判断
- 开发效率:Rust 开发周期长,不利于快速迭代
- 用户学习成本:用户不需要学习 Rust 就能写插件
为什么不是纯 JavaScript?
- 性能瓶颈:AST 解析、转换、压缩都是 CPU 密集型任务
- 内存效率:大型项目的 AST 占用大量内存
- 并行能力: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)通信。这有一定开销:
- 数据序列化:JavaScript 对象转换为 Rust 结构
- 跨边界调用:每次调用有固定开销
- 字符串复制: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 生态高度契合:
- Rolldown 是 Vite 的未来打包器:API 兼容 Rollup,便于迁移
- 插件复用:Vite 插件可以直接在 robuild 中使用
- 配置兼容:支持从 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
结语:构建工具的未来
回顾构建工具的三次革命:
- Webpack 时代:解决了"如何打包复杂应用"
- Rollup 时代:解决了"如何打包高质量的库"
- Rust Bundler 时代:解决了"如何更快地完成这一切"
robuild 是这场革命的参与者。它基于 Rolldown + Oxc 的 Rust 基础设施,专注于库构建场景,追求零配置、高性能、与现有生态兼容。
但构建工具的演进远未结束。我们可以期待:
- 更深的编译器集成:bundler 与类型检查器、代码检查器的融合
- 更智能的优化:基于运行时 profile 的优化决策
- 更好的开发体验:更快的 HMR、更精准的错误提示
- WebAssembly 的普及:让 Rust 工具链在浏览器中运行
构建工具的本质是将开发者的代码高效地转换为运行时需要的形态。技术在变,这个目标不变。作为工具开发者,我们的使命是让这个过程尽可能无感、高效、可靠。
感谢阅读。如果你对 robuild 感兴趣,欢迎查看 项目仓库。
本文约 10000 字,涵盖了构建工具演进、bundler 核心原理(含完整 mini bundler 代码)、robuild 架构设计、ESM/CJS 互操作、插件系统、性能优化等主题。如有技术问题,欢迎讨论交流。
参考资料:
《构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的》 是转载文章,点击查看原文。