Read OSS

代码转换与输出:Transformer、Minifier 与 Codegen

高级

前置知识

  • 第 1–4 篇:架构、AST、Parser/Semantic 以及 Visitor/Traverse
  • 熟悉 Babel 转换与代码压缩的基本概念

代码转换与输出:Transformer、Minifier 与 Codegen

前四篇文章聚焦于 Oxc 流水线的输入端:将源代码解析为 AST,并通过语义分析对其进行补充。本文转向输出端:对 AST 进行兼容性转换、体积压缩,最终将其还原为源代码。我们还会介绍兼容 Prettier 的 formatter,以及将 Oxc 暴露给 Node.js 生态的 NAPI 绑定。

Transformer:兼容 Babel 的代码转换

crates/oxc_transformer/src/lib.rs 中的 transformer 基于第 4 篇介绍的 Traverse 系统,实现了与 Babel 兼容的转译功能。它支持 ECMAScript 2015 到 ECMAScript 2026 的 preset、TypeScript 剥离、JSX 转换以及装饰器。

transformer 的结构与 Babel 保持一致,按 preset 模块组织:

flowchart TB
    subgraph "Transformer Presets"
        Common[Common transforms]
        TS[TypeScript]
        JSX[JSX / React]
        Decorator[Decorators]
        ES2015[ES2015]
        ES2016[ES2016]
        ES2017[ES2017]
        ES2018[ES2018]
        ES2019[ES2019]
        ES2020[ES2020]
        ES2021[ES2021]
        ES2022[ES2022]
        ES2026[ES2026]
        RegExp[RegExp]
    end
    
    TO[TransformOptions] -->|"target selection"| Common
    TO --> TS
    TO --> JSX
    TO --> ES2015
    TO --> ES2022

每个 preset 都是一个实现了 Traverse trait(见第 4 篇)的结构体,可以通过 TraverseCtx 访问完整的祖先上下文,并挂载 enter_*/exit_* 钩子。transformer 将这些 preset 组合成单次遍历完成所有转换。

入口方法 build_with_scoping 接收语义分析产出的 Scoping 结构体(即第 1 篇中 CompilerInterface 流水线里流转的数据),并返回包含更新后 scoping 信息的 TransformerReturn

pub struct TransformerReturn {
    pub errors: std::vec::Vec<OxcDiagnostic>,
    pub scoping: Scoping,
    pub helpers_used: FxHashMap<Helper, String>,
}

目标环境的选择由 EngineTargets 驱动:如果目标是 Chrome 100,那些 Chrome 100 已原生支持的特性对应的转换会被自动跳过。这与 Babel 的 @babel/preset-env 完全兼容,也支持通过 oxc-browserslist crate 使用 browserslist 查询语法。

提示: 使用 BabelOptions::from_json() 可以直接导入已有的 Babel 配置。Oxc 的 transformer 在设计上就是对 Babel 的直接替代,配置接口保持一致。

插件转换:注入与替换

在主 transformer 执行完毕后,还可以运行两个面向构建工具场景的插件转换,定义于 crates/oxc_transformer_plugins

  • ReplaceGlobalDefines — 将全局标识符替换为常量值(类似 Webpack 的 DefinePlugin 或 esbuild 的 --define
  • InjectGlobalVariables — 为全局变量注入 import 语句(类似 @rollup/plugin-inject

crates/oxc/src/compiler.rs#L169-L194CompilerInterface 流水线中可以看到,由于 transformer 可能使作用域树失效,这两个插件在运行前需要重新构建语义数据:

// Symbols and scopes are out of sync.
if inject_options.is_some() || define_options.is_some() {
    scoping = SemanticBuilder::new()
        .with_stats(stats)
        .build(&program)
        .semantic
        .into_scoping();
}

ReplaceGlobalDefines 执行完毕后,如果 minifier 未启用,会触发死代码消除(DCE),清理常量替换产生的不可达分支——例如将 if (process.env.NODE_ENV === 'production') 替换为 if (true) 后留下的冗余代码。

Minifier:不动点 Peephole 优化

crates/oxc_minifier/src/compressor.rs 中的压缩器受 Google Closure Compiler 启发,实现了不动点优化循环:反复执行 peephole 优化,直到 AST 不再发生变化为止。

flowchart TD
    Start[Input AST] --> Normalize[Normalize Pass]
    Normalize -->|"convert while→for,\nconst→let"| Loop{Peephole Loop}
    Loop -->|"changed=true"| PO[PeepholeOptimizations]
    PO --> Check{Changed?}
    Check -->|Yes, iteration < max| Loop
    Check -->|No, or max reached| Done[Optimized AST]
    
    style Loop fill:#ff9

compressor.rs#L69-L91 中的 run_in_loop 方法是核心逻辑:

fn run_in_loop(
    max_iterations: Option<u8>,
    program: &mut Program<'a>,
    ctx: &mut ReusableTraverseCtx<'a>,
) -> u8 {
    let mut iteration = 0u8;
    loop {
        PeepholeOptimizations.run_once(program, ctx);
        if !ctx.state().changed {
            break;
        }
        if let Some(max) = max_iterations {
            if iteration >= max {
                break;
            }
        } else if iteration > 10 {
            debug_assert!(false, "Ran loop more than 10 times.");
            break;
        }
        iteration += 1;
    }
    iteration
}

几个关键细节:

  • MinifierState.changed 追踪是否有任何优化被触发。若无变化,循环立即终止。
  • 最多迭代 10 次的安全上限防止无限循环。实际上,绝大多数代码在 2–3 次迭代后就会收敛。
  • ReusableTraverseCtx(见第 4 篇)支持高效的重复遍历,无需每次重新构建上下文。

循环开始前会先执行一次规范化处理:将 while 转换为 for 循环、const 转换为 let,并移除不必要的 "use strict" 指令,为后续优化创造更多空间。

Mangling 与 Codegen:从 AST 到输出

标识符 Mangling

crates/oxc_mangler 中的 mangler 将标识符重命名为尽可能短的名称。它利用语义分析产出的 Scoping 数据完成以下工作:

  1. 确定可 mangle 的符号 — 排除全局变量、导出项和需要保留的名称
  2. 按作用域分配槽位 — 兄弟作用域中的变量可以共用同一个短名称
  3. 使用 base-54 编码base54 函数依次生成 ab、...、zA、...、Zaa 等名称

crates/oxc_mangler/src/lib.rs#L21-L39 中的 MangleOptions 提供了 debug 模式,该模式会生成可读的名称(slot_0slot_1、...)而非 base-54 编码,方便调试 mangle 后的输出。

代码生成

crates/oxc_codegen/src/lib.rs 中的 codegen 打印器将 AST 还原为 JavaScript 源代码,使用以下两个 trait:

  • Gen — 用于无需优先级上下文的节点(语句、声明)
  • GenExpr — 用于表达式节点,需感知父节点的优先级,以决定是否插入括号
sequenceDiagram
    participant AST as Program AST
    participant CG as Codegen
    participant CB as CodeBuffer
    participant SM as SourcemapBuilder
    
    AST->>CG: build(program)
    loop For each AST node
        CG->>CG: call Gen/GenExpr trait method
        CG->>CB: write bytes
        CG->>SM: record source mapping
    end
    CG-->>AST: CodegenReturn { code, map }

lib.rs#L48-L61 中的 CodegenReturn 包含生成的代码、可选的 source map 以及提取出的版权注释:

pub struct CodegenReturn {
    pub code: String,
    #[cfg(feature = "sourcemap")]
    pub map: Option<oxc_sourcemap::SourceMap>,
    pub legal_comments: Vec<Comment>,
}

打印器使用来自 oxc_data_structuresCodeBuffer 高效构建字符串,避免频繁的内存分配。启用 mangling 时,打印器会接收 mangler 产出的 Scoping,并据此输出重命名后的标识符。

Formatter:兼容 Prettier 的输出

crates/oxc_formatter/src/lib.rs 中的 formatter 与 codegen 采用了截然不同的思路。codegen 直接从 AST 打印代码,而 formatter 先将 AST 转换为由 FormatElement 组成的中间表示(IR),再对 IR 进行打印。

pub struct Formatter<'a> {
    allocator: &'a Allocator,
    options: FormatOptions,
}

impl<'a> Formatter<'a> {
    pub fn build(self, program: &Program<'a>) -> String {
        let formatted = self.format(program);
        formatted.print().unwrap().into_code()
    }
}

IR 中包含以下元素:

  • Group — 可以单行(flat)或多行(expanded)打印的内容块
  • LineMode — 硬换行、软换行或空格
  • FormatElement — 文本、缩进、减少缩进、对齐

这种基于 IR 的架构与 Prettier 相同,使 formatter 能够在全局层面决定换行策略,这是简单的递归打印器做不到的。Format trait 为每种 AST 节点类型提供实现,生成 FormatElement 而非原始字符串。

NAPI 绑定:连接 Node.js 生态

Oxc 通过 napi-rs 向 Node.js 暴露其工具能力。napi/parser/src/lib.rs 中的 parser 绑定尤为值得关注——它实现了一套原始二进制传输协议。

Raw Transfer

在 64 位小端系统上,NAPI parser 支持"raw transfer"模式,完全绕过 JSON 序列化。传统方式需要将 AST 序列化为 JSON,再在 JavaScript 侧反序列化;而 raw transfer 直接将 AST 节点以已知布局写入二进制数据,JavaScript 侧通过 DataView 读取:

// Only enabled on 64-bit little-endian platforms
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
mod raw_transfer;

#[napi]
pub fn raw_transfer_supported() -> bool {
    cfg!(all(target_pointer_width = "64", target_endian = "little"))
}

这彻底消除了序列化/反序列化的开销——而这通常是原生到 JS 桥接性能的主要瓶颈。AST 类型上的 #[repr(C)] 布局注解(由第 2 篇介绍的 #[ast] 宏添加)在这里至关重要,它确保二进制布局是可预测且稳定的。

flowchart LR
    subgraph Rust
        Parser --> AST[AST in Arena]
        AST -->|raw binary| Buffer[SharedArrayBuffer]
    end
    subgraph JavaScript
        Buffer -->|DataView| JSAST[JS AST Objects]
    end
    
    style Buffer fill:#ff9

Transform 绑定

napi/transform/src/lib.rs 中的 transform NAPI 绑定将 transformer 和 isolated declarations(.d.ts 生成)暴露给 Node.js,Rolldown 及其他构建工具正是通过这里使用 Oxc 的转换流水线。

Isolated Declarations

oxc_isolated_declarations crate 无需完整的 TypeScript 类型检查器,即可直接从 AST 生成 .d.ts 类型声明文件——剥离实现细节,保留类型签名。该功能在 compiler.rs#L133-L135 处被集成进 CompilerInterface 流水线:

if let Some(options) = self.isolated_declaration_options() {
    self.isolated_declaration(options, &allocator, &program, source_path);
}

流水线全景回顾

经过这六篇文章,我们已经完整追踪了一个 JavaScript 文件在 Oxc 工具链中的完整旅程:

flowchart LR
    Source[Source Text] --> Alloc[Arena Allocator]
    Alloc --> Parser
    Parser --> AST[AST]
    AST --> Semantic[SemanticBuilder]
    Semantic --> Scoping
    
    Scoping --> Linter
    Linter --> Diagnostics
    
    Scoping --> Transformer
    Transformer --> Plugins[Inject/Define]
    Plugins --> Compressor[Minifier]
    Compressor --> Mangler
    Mangler --> Codegen
    Codegen --> Output[JavaScript Output]
    
    AST --> Formatter
    Formatter --> Formatted[Formatted Output]
  1. 第 1 篇:31 个 crate 的工作空间、三层架构与 CompilerInterface 流水线
  2. 第 2 篇:Arena 分配、无 Drop 的 Box<'a, T>/Vec<'a, T>,以及与 ESTree 有所差异的 AST 设计
  3. 第 3 篇:手写递归下降 parser、错误恢复,以及 SemanticBuilder 构建作用域与符号
  4. 第 4 篇:两套遍历系统——用于只读的 Visit 和支持修改及祖先追踪的 Traverse,以及 ast_tools 代码生成
  5. 第 5 篇:Oxlint 的并行 lint 架构、Rule trait、LintContext 与性能优化
  6. 本文:Transformer presets、不动点 minifier、codegen、formatter 与 NAPI 绑定

贯穿全部六篇的核心主线,是 arena 分配器与 Scoping 结构体。arena 让内存分配几乎零开销,并使遍历更加缓存友好。Scoping 在各流水线阶段之间传递语义信息,避免重复分析。正是这两者的组合,造就了 Oxc 今天的性能表现。

提示: 理解 Oxc 流水线最好的方式,就是动手实现点什么。从 CompilerInterface trait 入手——只需覆写你需要的钩子,其余部分由默认实现自动处理。compiler.rs 中的 Compiler 结构体就是这种模式的最简示例。