Claude Code 中的 bun:bundle 1.1 特性实现
build.mjs,相当于把 Node.js + npm + Webpack + Jest 等功能打包在一起,并且速度更快。总共涉及,覆盖了 Claude Code 的所有主要功能模块但用户可能没有 Bun,只有 Node.js,这个时候就需要构建脚本,把 "Bun 专有代码" 换为 "通用 Node.js 代码",为什么呢?因为这是Claude Code 原始设计• 用 Bun 编译(bun b
Bun 1.1 是一个让 JavaScript 开发更快、更简单的工具,相当于把 Node.js + npm + Webpack + Jest 等功能打包在一起,并且速度更快。
Bun 1.1 的核心特性:
- 编译时特性标记和死代码消除
- 运行时 Bun 特定 API (如 FFI)
- 构建时源码转换和优化
- 条件功能编译和模块加载
总共涉及 139+ 个文件,覆盖了 Claude Code 的所有主要功能模块
但用户可能没有 Bun,只有 Node.js,这个时候就需要构建脚本,把 "Bun 专有代码" 换为 "通用 Node.js 代码",为什么呢?因为这是Claude Code 原始设计
• 用 Bun 编译(bun build --compile)
• 利用 Bun 的编译时特性优化产物大小
• 使用 Bun 的运行时 API(bun:ffi 等)
注:FFI 是 JavaScript 和原生代码(C/C++等)之间的"桥梁",能在 JS 里直接调用用其他语言写的高性能函数。
阶段1脚本:prepare-src.mjs
这是一个源代码预处理脚本,在构建前修改原始源代码。
在理解这脚本之前,需要理解MACRO 对象,它是 Bun 提供的"编译时宏"系统,用于在打包阶段将某些标识符替换为具体的值,而不是在运行时计算,其实就类似于C/C++ 的 #define 宏定义,实现零运行时开销 值在编译时就确定了,不需要在代码里计算。
// 1. 替换 bun:bundle 导入
import { feature } from 'bun:bundle'
// ↓ 变成
import { feature } from '../stubs/bun-bundle.js'
// 2. 替换 MACRO 宏为实际值
MACRO.VERSION → '2.1.88'
MACRO.BUILD_TIME → '2026-04-01T09:43:...'
MACRO.FEEDBACK_CHANNEL → 'https://github.com/anthropics/claude-code/issues'
// 等等...
执行流程如下:
src/ 目录下的所有 .ts/.tsx 文件
↓
检查是否包含 bun:bundle 导入或 MACRO 引用
↓
替换并写回文件
↓
创建 stubs/ 目录下的填充文件(bun-ffi.ts, global.d.ts)
在 Claude Code 中,feature() 函数被广泛用于条件编译,这样的Bun编译时特性标记无处不在,而编译时常量渗透到各个层面.
新的文件可能也会使用 Bun 特性,需要自动包含在预处理范围内.
于是prepare-src.mjs 首先检查所有 .ts/.tsx 文件。
确保完整的 Bun-to-Node.js 兼容性转换,处理以下两类 Bun 特性:
- 编译时特性标记 (
feature()) - 控制代码是否被包含在最终构建中 - 编译时常量宏 (
MACRO.X) - 提供构建时确定的字符串值
接下来检查替换并写回之后创建的 Stub 存根文件,
这些存根文件是 Bun-to-Node.js 兼容性层 的关键组件:
bun-bundle.ts- 提供运行时 API,让代码能正常执行global.d.ts- 提供类型安全,让 TypeScript 编译通过bun-ffi.ts- 提供缺失的 Bun 特定 API
它们确保 Claude Code 能在没有 Bun 运行时的环境中编译和运行,同时保持代码的完整性和类型安全性。
附:
构建系统的整体架构
原始 Bun 代码 (src/)
├── 使用 feature() 进行条件编译
├── 使用 MACRO.X 编译时常量
└── 使用 bun:ffi 等 Bun 特性
prepare-src.mjs 预处理
├── 创建 stubs/ 目录
├── 替换导入路径
└── 生成类型声明
build.mjs 构建
├── 进一步转换 feature() 调用
├── 替换 MACRO.X 为字面量
├── 移除 Bun 特定代码
└── 生成最终的 dist/cli.js
阶段2脚本:build.mjs 构建过程
这是主构建脚本,使用 esbuild 打包项目 即复制→转换→迭代打包→生成可执行文件
┌───────────────────────────────────────────────────────────
│ Phase 1: Copy source
│ 把 src/ 复制到 build-src/(保持原始文件不变)
├───────────────────────────────────────────────────────────
│ Phase 2: Transform source
│ • feature('X') → false
│ • MACRO.X → 字符串字面量
│ • 删除 bun:bundle 导入
│ • 删除 global.d.ts 类型导入
├───────────────────────────────────────────────────────────
│ Phase 3: Create entry wrapper
│ 创建入口文件 entry.ts,导入 CLI 入口
├───────────────────────────────────────────────────────────
│ Phase 4: Iterative stub + bundle
│ 循环最多 5 次:
│ 1. 运行 esbuild
│ 2. 解析错误,找出缺失的模块
│ 3. 为缺失模块创建 stub
│ 4. 重试,直到成功或达到最大轮数
└───────────────────────────────────────────────────────────
关键转换详解
1. feature() 函数的处理
原始代码(Bun 环境):
import { feature } from 'bun:bundle'
if (feature('EXPERIMENTAL_API')) {
// 编译时决定是否包含这段代码
enableExperimentalFeature()
}
Bun 的行为:
-
feature()在编译时求值 -
如果返回
false,这段代码会被死码消除(tree-shaking),不会出现在最终产物中
脚本转换后:
// feature() replaced with false at build time
if (false) {
enableExperimentalFeature()
}
esbuild 会优化:
// 最终产物中这段代码完全消失
2. MACRO 宏的处理
console.log(`Version: ${MACRO.VERSION}`)
console.log(`Built at: ${MACRO.BUILD_TIME}`)
转换后:
console.log(`Version: ${'2.1.88'}`)
console.log(`Built at: ${''}`) // 或实际构建时间
3. Stub 文件的作用
// Stub for bun:bundle — feature() is compile-time in Bun; replaced by build script
export function feature(_flag: string): boolean {
return false
}
为什么返回 false?
| 原因 | 说明 |
|---|---|
| 安全默认 | 未知特性默认关闭,避免启用实验性功能导致问题 |
| 编译时行为 | 在 Bun 中 feature() 是编译时确定的,这里模拟"特性未启用"的情况 |
| 简化处理 | 不需要实现复杂的特性检测逻辑 |
迭代式 Stub 生成(Phase 4 的核心)
这是最巧妙的设计——因为不知道代码会依赖哪些模块,所以用试错法:
// 第 1 轮:运行 esbuild
// 报错:Could not resolve "./utils/helper"
// 创建 stubs/utils/helper.ts
//
// 第 2 轮:运行 esbuild
// 报错:Could not resolve "./assets/config.json"
// 创建 stubs/assets/config.json(空 JSON)
//
// 第 3 轮:运行 esbuild
// 成功!
具体如下:
.txt, .md —— 创建空文件
.ts, .tsx, .js —— 创建导出空函数的模块
.json ——创建 {}
总而言之,
这两构建脚本的作用:把 "Bun 专有代码" → "通用 Node.js 代码"
• 替换编译时特性为运行时兼容代码
• 创建 stub 填补缺失的 API
• 用 esbuild 替代 bun build
构建优化之树摇动:
未使用的特性代码在编译时被完全移除,树摇动是一种代码优化技术,用于自动删除未使用的代码,就像摇晃一棵树让枯叶掉落一样。
🌳 代码库就像一棵树
├── 主干(入口文件,一定会用到)
├── 树枝(被导入的模块)
│ ├── 绿叶(实际使用的代码)✅ 保留
│ └── 枯叶(从未使用的代码)❌ 删除
└── 树根(依赖的第三方库)
├── 用到的函数 ✅ 保留
└── 没用到的函数 ❌ 删除
术语来自 ES2015 模块系统(ESM) 的图形表示:
-
导入/导出关系形成一棵依赖树
-
构建工具"摇晃"这棵树,未引用的代码就像枯叶一样掉落
// ✅ 可以树摇动(ESM,静态导入)
import { usedFunction } from './utils.js' // 明确知道用了什么
// ❌ 难以树摇动(CommonJS,动态)
const utils = require('./utils.js') // 运行时才知道内容
关键区别:
-
ESM:
import/export是静态的,编译时就能确定依赖关系 -
CommonJS:
require是动态的,运行时才能确定
在 Claude Code 的构建中,树摇动是关键优化手段——它确保了只有实际使用的代码才会进入最终产物,而那些被 feature() 控制的实验性功能则不会增加包体积,最终产物更小、启动更快、不包含无用功能。
更多推荐



所有评论(0)