技术栈与构建体系

📑 目录

在前三篇文章中,我们依次梳理了 Claude Code 的入口流程、对话循环与上下文管理。从这一篇开始,我们将把视角从"业务逻辑"转向"工程底座",拆解支撑这套复杂 AI Agent 运行的技术栈与构建体系。理解这些底层选择,有助于我们判断:为什么 Claude Code 能在终端里呈现出接近 GUI 的交互体验,又为何能以单文件形式分发到全球数百万开发者的机器上。

一、运行时与语言选型

1.1 TypeScript:严格模式下的 51 万行代码

Claude Code 的源码规模约为 1,900 个文件、512,000 行 TypeScript,全部采用严格类型检查。从泄露的源码中可以观察到几个显著特征:

  • 路径映射(Path Mapping):大量使用 src/ 绝对路径导入,如 import { logError } from 'src/utils/log.js',说明项目配置了 tsconfig.json 中的 paths 字段,避免深层相对路径 ../../../../ 的噩梦。
  • 显式文件后缀:所有 import 语句都带有 .js 后缀,即使在 TypeScript 源码中也是如此。这是 ESM(ECMAScript Modules)规范的要求——TypeScript 编译后文件扩展名变为 .js,源码中直接写 .js 可以确保编译前后路径一致。
  • 严格 ESLint 规则:源码顶部频繁出现 /* eslint-disable custom-rules/no-top-level-side-effects */ 之类的注释,说明团队编写了大量自定义 ESLint 规则来约束代码风格,甚至对"顶层副作用"这类通常被忽视的问题都进行了管控。
// src/main.tsx 顶部注释明确说明了三个 side-effect 的目的
// These side-effects must run before all other imports:
// 1. profileCheckpoint marks entry before heavy module evaluation begins
// 2. startMdmRawRead fires MDM subprocesses (plutil/reg query) so they run in
//    parallel with the remaining ~135ms of imports below
// 3. startKeychainPrefetch fires both macOS keychain reads in parallel

1.2 Bun:首选运行时

2025 年底 Anthropic 收购 Bun 后,Claude Code 的构建链路全面迁移到 Bun 工具链。源码中多处出现 import { feature } from 'bun:bundle',这是 Bun 独有的编译时宏(compile-time macro),用于在打包阶段进行条件编译和死代码消除(Dead Code Elimination, DCE)。

Bun 为 Claude Code 带来了几个关键优势:

  • 极速打包:将 4,500+ 个模块打包成单文件 cli.js,体积约 13MB。
  • 原生支持 TypeScript:无需 tsc 编译步骤,Bun 可以直接运行 .ts.tsx 文件。
  • 内置宏系统feature('COORDINATOR_MODE') 这类调用在打包时会被替换为布尔常量,未启用的功能分支会被彻底消除,不会进入最终产物。

尽管如此,Claude Code 仍然保持了对 Node.js 的兼容性。发布的 package.json 中明确声明了 "engines": { "node": ">=18.0.0" }cli.js 本身是一个自包含的 Node.js bundle,不依赖 node_modules。这意味着即使你没有安装 Bun,只要 Node.js 版本大于等于 18,也能直接运行。

1.3 从 package.json 看依赖生态

泄露的 npm 包中的 package.json 呈现出一种"极简"姿态:

{
  "name": "@anthropic-ai/claude-code",
  "version": "2.1.88",
  "bin": { "claude": "cli.js" },
  "engines": { "node": ">=18.0.0" },
  "type": "module",
  "dependencies": {},
  "optionalDependencies": {
    "@img/sharp-darwin-arm64": "^0.34.2",
    "@img/sharp-linux-x64": "^0.34.2"
  }
}

dependencies 字段为空,因为所有运行时依赖都已经被 Bun bundler 内联到了 cli.js 中。唯一留下的 optionalDependenciessharp 图像处理库的多平台二进制包,这类原生模块无法被轻易打包成纯 JavaScript,因此以外部依赖形式存在。

二、终端渲染架构:React in Terminal

2.1 为什么不是 oclif 或 commander?

传统 Node.js CLI 工具通常选用 oclifcommander.jsyargs 等框架。这些框架擅长参数解析、子命令路由和帮助文档生成,但它们都面向"命令行脚本"范式:执行、输出、退出。Claude Code 需要的是一个持续运行的交互式终端 UI(TUI),支持:

  • 实时流式渲染 AI 回复
  • 鼠标点击、拖拽选中文本
  • 滚动浏览历史消息
  • 多行输入框与语法高亮
  • 焦点管理和键盘事件路由

这些需求已经远远超出了传统 CLI 框架的能力边界。Claude Code 的做法是:用 Commander.js 只做参数解析,用 Ink 接管整个终端渲染

2.2 Ink:为终端而生的 React Reconciler

Ink 是一个将 React 组件树渲染到终端的框架。Claude Code 并没有直接使用开源的 Ink,而是在 src/ink/ 目录下维护了一个深度定制版本。其中 ink.tsx 单文件就达到 251KB,是整个终端渲染引擎的核心。

从技术架构上看,Ink 与 React DOM 的关键区别在于 Reconciler(协调器)

特性React DOMInk
渲染目标浏览器 DOM终端字符网格
Reconcilerreact-dom自定义 react-reconciler
节点类型HTML 元素(div, span…)虚拟 DOM 元素(ink-root, box, text…)
样式系统CSS内联样式对象(颜色、粗体、flex 布局)
布局引擎浏览器布局引擎Yoga(Facebook 的 C++ 布局引擎,WASM 版本)
事件系统合成事件原始 stdin 键码解析
flowchart TB
    subgraph React_Tree["React 组件树"]
        A[App.tsx] --> B[ScrollBox]
        A --> C[PromptInput]
        B --> D[MessageBubble]
        C --> E[TextArea]
    end

    subgraph Ink_Engine["Ink 渲染引擎"]
        F[Reconciler
reconciler.ts] --> G[DOM 节点树
dom.js] G --> H[Yoga 布局计算] H --> I[屏幕缓冲区
Screen Buffer] end subgraph Terminal["终端输出"] I --> J[diff 算法
log-update] J --> K[ANSI 转义序列] K --> L[stdout] end React_Tree --> F

2.3 Reconciler 与 DOM 抽象

src/ink/reconciler.ts 中,Claude Code 创建了一个自定义的 React Reconciler 实例:

import createReconciler from 'react-reconciler'
import {
  appendChildNode,
  createNode,
  createTextNode,
  insertBeforeNode,
  removeChildNode,
  setAttribute,
  setStyle,
  setTextNodeValue,
} from './dom.js'

// 创建自定义 reconciler,host config 指向终端 DOM 操作
const reconciler = createReconciler({
  createInstance: createNode,
  createTextInstance: createTextNode,
  appendChild: appendChildNode,
  removeChild: removeChildNode,
  insertBefore: insertBeforeNode,
  // ... 其他 host config
})

这里的 dom.js 不是浏览器的 DOM,而是 Ink 自己实现的轻量级虚拟 DOM。节点类型包括 ink-rootboxtext 等,每个节点可以挂载一个 yogaNode 用于 flex 布局计算。

值得注意的是,reconciler.ts 顶部有一段条件导入逻辑,用于在开发模式下连接 React DevTools:

if (process.env.NODE_ENV === 'development') {
  try {
    void import('./devtools.js')
  } catch (error: any) {
    if (error.code === 'ERR_MODULE_NOT_FOUND') {
      console.warn('Debugging with React Devtools requires react-devtools-core')
    }
  }
}

这说明开发团队确实会在终端里调试 React 组件树——一个在传统 CLI 开发中难以想象的能力。

2.4 帧渲染管线

ink.tsx 中的 onRender() 方法揭示了每一帧的完整渲染管线:

  1. flushInteractionTime:刷新延迟的交互时间戳
  2. renderer():将 DOM 树渲染到屏幕缓冲区(Screen Buffer)
  3. selection/highlight overlay:应用文本选中和搜索高亮的样式覆盖
  4. damage tracking:计算脏区域,只重绘变化的部分
  5. diff (log-update):对比前后帧,生成最小化的 ANSI 补丁
  6. optimize:合并相邻的 stdout 写入,减少系统调用
  7. writeDiffToTerminal:将补丁写入 stdout

这一管线的设计目标是在终端这种"带宽极低"的输出设备上实现 60fps 的交互体验。FRAME_INTERVAL_MS 控制了渲染节流,避免过度刷新导致终端闪烁。

三、核心依赖分析

虽然发布的 package.json 隐藏了所有依赖,但从源码 import 语句中可以还原出完整的技术图谱:

3.1 Commander.js:参数解析层

Claude Code 使用 @commander-js/extra-typings 而非基础版 commander。这个版本为所有命令和选项提供了完整的 TypeScript 类型推导,确保 program.opts() 返回的对象是严格类型的。

import { Command as CommanderCommand, InvalidArgumentError, Option } from '@commander-js/extra-typings'

const program = new CommanderCommand()
  .configureHelp(createSortedHelpConfig())
  .enablePositionalOptions()

3.2 React:组件体系

Claude Code 的 UI 完全由 React 组件构成,包括:

  • App.tsx:根组件,管理应用状态
  • ScrollBox:支持鼠标滚轮和键盘翻页的滚动容器
  • PromptInput:多行输入框,支持 IME 和声明式光标定位
  • MessageBubble:AI 回复的消息气泡,支持 Markdown 和代码块

源码中使用的是 React 18 的 Concurrent Root 模式:

import { ConcurrentRoot } from 'react-reconciler/constants.js'
this.container = reconciler.createContainer(
  this.rootNode,
  ConcurrentRoot,
  null,
  false,
  null,
  'id',
  noop,
  noop, // onUncaughtError
  noop, // onCaughtError
  noop, // onRecoverableError
  noop  // onDefaultTransitionIndicator
)

3.3 Zod:Schema 验证

在工具参数解析和配置校验中,Claude Code 大量使用 Zod 进行运行时类型检查。例如 ToolInputJSONSchema 的类型定义和 settings.json 的校验逻辑都依赖 Zod 确保数据安全。

3.4 KaTeX / markdown-it:内容渲染

当 AI 回复包含数学公式或 Markdown 内容时,Claude Code 需要将其转换为终端可显示的文本。KaTeX 负责数学公式的排版计算,markdown-it 负责 Markdown 到终端富文本的转换。

3.5 其他关键依赖

  • chalk:ANSI 颜色代码生成
  • lodash-es:工具函数(throttle、noop、mapValues 等)
  • signal-exit:跨平台的进程退出事件处理
  • yoga-layout:C++ flexbox 布局引擎的 WASM 绑定,用于终端 UI 的排版
  • log-update:终端屏幕的差异更新算法,是 Ink 高性能渲染的底层支撑

四、构建与发布链路

4.1 单文件打包策略

Claude Code 的构建产物是一个约 13MB 的 cli.js 文件,使用 Bun 的 bundler 将 4,500+ 个模块内联到一起。这种"单文件分发"策略有显著优势:

  • 零依赖安装:用户不需要等待 npm install 下载数百个包
  • 版本一致性:不会出现 "works on my machine" 的依赖版本问题
  • 快速启动:没有 require 遍历 node_modules 的文件系统开销

构建脚本的核心逻辑大致如下(基于社区重建的 build.ts):

// build.ts 简化示意
await Bun.build({
  entrypoints: ['./src/main.tsx'],
  outdir: './dist',
  target: 'node',
  format: 'esm',
  define: {
    'MACRO.VERSION': JSON.stringify(version),
    'MACRO.FEEDBACK_CHANNEL': JSON.stringify('stable'),
  },
  external: ['sharp', 'react-devtools-core'], // 原生模块和开发工具不外联
  sourcemap: 'external', // 生成外部 source map
})

4.2 Source Map 泄露事件分析

2026 年 3 月 31 日,Claude Code 的 npm 包意外包含了 57-60MB 的 cli.js.map 文件,导致约 51.2 万行 TypeScript 源码泄露。从技术角度看,这是一个典型的构建配置失误:

  1. Bun 默认生成 source map:Bun 的 bundler 在构建时默认会产出 .map 文件
  2. .npmignorefiles 字段缺失:发布团队没有将 *.map 加入忽略列表
  3. sourcesContent 包含完整源码:source map 的 sourcesContent 数组中嵌入了所有原始 TypeScript 文件内容

对于普通应用,source map 泄露通常只会暴露编译后的 JavaScript。但由于 Bun 直接打包 TypeScript,source map 中保存的是原始的 .ts.tsx 源码——包括注释、类型定义、甚至内部调试代码。

这一事件给所有使用现代打包工具的开发者敲响了警钟:

# 发布前检查 npm 包内容
npm pack --dry-run
# 或解压检查实际发布的文件
tar -tzf $(npm pack | tail -1) | grep -E '\.(map|ts|tsx)$'

4.3 npm 包结构

泄露的 @anthropic-ai/claude-code@2.1.88 包结构如下:

anthropic-ai-claude-code-2.1.88.tgz
├── package.json          # 发布配置(依赖为空)
├── cli.js                # 13MB 单文件可执行入口
├── cli.js.map            # 57MB source map(本不应发布)
└── README.md

安装后,cli.js 通过 package.json 中的 "bin": { "claude": "cli.js" } 映射为全局命令。这种极简包结构意味着 npm 在此场景中只扮演"分发管道"角色,实际的运行时完全自包含。

五、开发体验设计

5.1 性能预热:main.tsx 开头的 side-effects

打开 src/main.tsx,你会看到文件顶部有一系列在模块加载时就执行的副作用,这在常规 React 应用中极为罕见:

// 必须在所有其他 import 之前运行的 side-effects
import { profileCheckpoint } from './utils/startupProfiler.js'
profileCheckpoint('main_tsx_entry')

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js'
startMdmRawRead() // 并行启动 MDM 子进程

import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js'
startKeychainPrefetch() // 并行预取 macOS keychain

这些副作用的设计意图非常明确:

  • 并行化启动耗时startMdmRawRead()startKeychainPrefetch() 会在模块评估阶段就派生子进程,与后续约 135ms 的模块导入并行执行
  • 避免同步阻塞:如果不预取,macOS 的 keychain 读取会在 init() 阶段以同步 spawn 方式执行,每次启动额外增加约 65ms

这种"启动预热"模式将 CLI 的冷启动时间压缩到了极致,体现了产品团队对毫秒级性能的极致追求。

5.2 启动 Profiler

Claude Code 内置了一套精细的启动性能分析系统,定义在 src/utils/startupProfiler.ts 中:

// 两种采样模式
const DETAILED_PROFILING = isEnvTruthy(process.env.CLAUDE_CODE_PROFILE_STARTUP)
const STATSIG_SAMPLE_RATE = 0.005 // 0.5% 外部用户采样
const STATSIG_LOGGING_SAMPLED =
  process.env.USER_TYPE === 'ant' || Math.random() < STATSIG_SAMPLE_RATE

开启详细分析模式后,Claude Code 会在 ~/.claude/startup-perf/ 目录下为每个会话生成一份时间线报告:

================================================================================
STARTUP PROFILING REPORT
================================================================================

   0.0ms  (+0.0ms)  cli_entry
  12.3ms (+12.3ms)  main_tsx_entry
 147.2ms (+134.9ms)  main_tsx_imports_loaded
 198.5ms (+51.3ms)  run_commander_initialized
 245.1ms (+46.6ms)  preAction_after_init
 312.8ms (+67.7ms)  main_after_run

Total startup time: 312.8ms
================================================================================

报告中定义了多个关键阶段,如 import_time(模块加载)、init_time(初始化)、settings_time(配置加载)等。这些数据会上报到 Statsig(面向 Anthropic 内部员工 100% 采样,外部用户 0.5% 采样),用于持续优化启动性能。

5.3 开发模式与生产模式差异

Claude Code 通过环境变量和 Bun 的 feature() 宏区分开发与生产模式:

维度开发模式生产模式
React DevTools尝试连接完全剥离
Source Map生成并保留应排除在发布包外
Feature Flags所有分支保留DCE 消除未启用分支
Startup ProfilingCLAUDE_CODE_PROFILE_STARTUP=1 输出详细报告仅采样上报 Statsig
断言与检查保留消除

bun:bundlefeature() 宏在打包时会返回编译时常量。例如 feature('COORDINATOR_MODE') 在公开发布版中会被替换为 false,整个多 Agent 协调模块的分支都会被死代码消除,不会进入 cli.js

// 编译时条件分支——未启用的功能在打包后完全消失
const coordinatorModeModule = feature('COORDINATOR_MODE')
  ? require('./coordinator/coordinatorMode.js')
  : null

这种设计让 Anthropic 可以在同一份源码中维护内部高级功能(如 VOICE_MODEDAEMON 模式)和公开发布版,而不用担心内部代码泄露——至少在 source map 事件发生之前是如此。

六、总结

Claude Code 的技术栈选择展现了一个现代终端应用的工程典范:

  • TypeScript 严格模式支撑了 51 万行代码的可维护性
  • Bun 工具链提供了极速打包和编译时宏能力
  • React + 自定义 Ink Reconciler 将终端变成了可组件化、可声明式编程的 UI 平台
  • 单文件分发消除了依赖地狱,实现了秒级全球安装
  • 精细的启动预热和 Profiler 将 CLI 冷启动推向了性能极限

当然,source map 泄露事件也提醒我们:再优秀的技术架构,也需要发布流程的最后一道人工检查。对于构建终端应用的开发者而言,Claude Code 的工程实践——尤其是 Ink 的自定义 reconciler、Yoga 布局引擎在终端的应用、以及 Bun 宏驱动的条件编译——都值得深入研究甚至借鉴。

在下一篇文章中,我们将聚焦于 Claude Code 的状态管理体系,剖析它如何在复杂的异步对话流中维护一致且可恢复的 UI 状态。