序言
在万字 esbuild 源代码批判,让你能够直接 PR 这篇文章中,深入研究了 esbuild 项目,为了达到极致的编译速度,esbuild 的编译过程被设计得非常紧凑,只包含两次对整个抽象语法树(AST)的全量传递。
然而,这种极致的效率并非没有代价。在 esbuild 中,解析 AST、进行语法降级以适应低版本环境、以及代码压缩等步骤是紧密耦合的。由于这种紧密耦合,基于 esbuild 开发 Vue 或 Svelte 编译器会变得困难(目前 esbuild 对 Vue 和 Svelte 的处理方式是使用 JavaScript 编写的编译器进行预处理,然后通过进程间通信传递转换后的结果)。此外,esbuild 的核心数据结构、类型和函数定义全部位于项目的 internal 目录下,这使开发者无法直接访问。
正因为如此,我们有必要深入研究 swc 项目。与 esbuild 不同,swc 提供了更灵活而且可扩展的架构,使得开发者可以更容易地基于 swc 开发出自己的编译器或工具。
目录结构
- bindings swc 对特定平台的绑定,每个子目录都是一个 Rust crate
- binding_core_node swc 的 Node.js 扩展
- binding_core_wasm swc 的 WASM 程序
- swc_cli swc 的命令行程序
- crates
- ast_node 定义 AST 节点的宏
- better_scoped_tls 实现线程局部变量
- binding_macros 用于构建 WASM 程序的宏
- preset_env_base 解析 browserslist
- string_enum 将枚举打印为字符串的宏
- swc swc 的主 crate
- swc_atoms 生成 JsWord
- swc_bundler swc 的 bundler
- swc_cached swc 的缓存配置
- swc_common swc 的工具方法
- swc_config swc 配置
- swc_config_macro 用于 swc 配置的宏
- swc_core 注册 swc 插件
- swc_css css 模块聚合
- swc_css_ast css AST 的结构体和枚举
- swc_css_codegen css 代码生成器
- swc_css_codegen_macros 用于 css 代码生成器的宏
- swc_css_lints css lint
- swc_css_minifier css 代码压缩
- swc_css_parser css 语法分析器
- swc_css_visit css AST 的 visitor
- swc_ecma_ast js AST 的结构体和枚举
- swc_ecma_codegen js 代码生成器
- swc_ecma_codegen_macros 用于 js 代码生成器的宏
- swc_ecma_visit js AST 的 visitor
- ...
- Cargo.toml Rust Cargo 清单文件
- ...
swc 通过 Cargo 的 workspace 管理多个相关的协同开发的 crate,它们位于 crates 目录下。
bindings 目录下的 crate 未放到 workspace 下的原因是,绑定程序使用已发布的 swc_core SDK 来构建特定平台的绑定。若放置到 workspace 下时,可能使用并未发布的依赖的版本,导致混乱。
使用 swc
阅读 swc 的源代码,一个好的起点是查看其示例代码。在 swc 项目的crates/swc/examples/transform.rs中,你可以找到一个简单的使用 swc 的例子。这段代码旨在展示如何使用 swc 进行代码转换。
use std::{path::Path, sync::Arc};
use anyhow::Context;
use swc::{self, config::Options, try_with_handler, HandlerOpts};
use swc_common::SourceMap;
fn main() {
let cm = Arc::<SourceMap>::default();
let c = swc::Compiler::new(cm.clone());
let output = try_with_handler(
cm.clone(),
HandlerOpts {
..Default::default()
},
|handler| {
let fm = cm
.load_file(Path::new("examples/transform-input.js"))
.expect("failed to load file");
c.process_js_file(
fm,
handler,
&Options {
..Default::default()
},
)
.context("failed to process file")
},
)
.unwrap();
println!("{}", output.code);
}
上面的程序做了以下事情:
- 首先,创建一个
SourceMap实例。SourceMap是 swc 用于追踪源代码的结构体。由于它需要在多个线程之间共享,所以使用Arc(自动引用计数)智能指针来持有它。 - 然后,创建一个
Compiler实例。Compiler是 swc 的主要接口,提供了处理 JavaScript 文件的方法。 - 使用
SourceMap的load_file方法加载一个JavaScript文件(具体的文件路径是"examples/transform-input.js")。 - 使用
Compiler的process_js_file方法处理加载的 JavaScript 文件。这个方法接受一个Handler实例作为参数,用于处理可能发生的错误。 try_with_handler函数用于创建一个Handler实例,并将它传递给process_js_file方法。它返回一个Result枚举,如果处理成功,我们可以使用unwrap方法从Ok(T)中取出结果;如果处理失败,程序将直接退出。
了解了这段代码的基本流程后,我们可以更详细地探讨 SourceMap 和 Compiler 这两个结构体。SourceMap 主要用于追踪源代码的位置信息,它可以帮助我们找到源代码中的错误;Compiler则提供了各种处理JavaScript文件的方法,包括解析、转换和生成代码等。
SourceMap 结构体
SourceMap 记录所有所使用的源代码,可以将字节位置映射到源代码中的具体位置。解析过程中的每个源数据(通常是文件、字符串或宏扩展)都存储在 SourceMap 中,表示为 SourceFiles。字节位置存储在 span 中,并在编译器中广泛使用。它是 SourceMap 绝对位置,可以将其转换为行列信息、源代码片段等。
SourceFile 结构体
SourceFile是swc中一个非常重要的结构体,它代表一个源代码文件。其结构如下:
struct SourceFile {
/// 源文件名。根据惯例,非源自文件系统的源码名位于尖括号之间,例如`<anon>`
pub name: FileName,
/// 全部源代码
pub src: Lrc<String>,
/// 源代码的 hash
pub src_hash: u128,
/// 此源代码位于 `SourceMap` 中的起始位置
pub start_pos: BytePos,
/// 此源代码位于 `SourceMap` 中的结束位置
pub end_pos: BytePos,
/// 源代码中所有行的起始位置
pub lines: Vec<BytePos>,
/// 文件名 hash
pub name_hash: u128,
}
Lrc 结构体
在swc中,Lrc 是一个被广泛使用的结构体,它是对 Rust 标准库中 Rc 或 Arc 的一个别名。它的具体定义在 swc_common 的 crate 中。
通过Rust的 cfg 宏,可以在编译时根据特性(feature)的配置来选择使用 Rc 或 Arc。如果特性配置中包含"concurrent",则 Lrc 会被定义为 Arc;否则,Lrc 会被定义为 Rc。
下面是这段代码的定义:
#[cfg(feature = "concurrent")]
mod concurrent {
pub use std::{
sync::Arc as Lrc,
};
}
#[cfg(not(feature = "concurrent"))]
mod single {
pub use std::{
rc::{Rc as Lrc, Weak},
};
}
这段代码使用了 Rust 的条件编译功能,根据是否启用"concurrent"特性,来选择导入 Arc 还是 Rc。"concurrent"特性在需要多线程共享数据时启用,这时 Lrc 会被定义为多线程安全的 Arc;在其他情况下,Lrc 则被定义为单线程环境下的 Rc。
Compiler 结构体
Compiler 是 swc 中一个核心的结构体,它包含了一个 SourceMap 实例,并提供了一系列用于处理 JavaScript 文件的方法。
struct Compiler {
pub cm: Arc<SourceMap>,
}
Handler 结构体
Compiler 提供的所有方法都需要传入一个 Handler 结构体,除了会导致 swc 立即退出的错误,其他错误会通过 Handler 结构体记录用于后续进行错误报告。Handler 结构体如下:
pub struct Handler {
// 如何对待不同等级的错误信息
pub flags: HandlerFlags,
err_count: AtomicUsize,
emitter: Lock<Box<dyn Emitter>>,
continue_after_error: LockCell<bool>,
// 所有记录的错误信息
delayed_span_bugs: Lock<Vec<Diagnostic>>,
taught_diagnostics: Lock<AHashSet<DiagnosticId>>,
emitted_diagnostic_codes: Lock<AHashSet<DiagnosticId>>,
// 该集合包含发出的每个错误信息的 hash,这些 hash 用于避免发出两次相同的错误。
emitted_diagnostics: Lock<AHashSet<u128>>,
}
swc 中提供了一个名为 HANDLER 的全局变量,它是一个线程局部的变量。在 try_with_handler 方法中,会创建一个 Handler 实例,并将这个实例注册到 HANDLER 全局变量上。这种设计使得在开发 swc 扩展时,能够更简单地进行错误报告。
以下是如何使用HANDLER进行错误报告的示例:
use swc_common::errors::HANDLER;
fn main() {
HANDLER.with(|handler| {
// 使用 HANDLER.with 访问当前文件的 Handler 实例。
// struct_span_err 方法接收错误信息。
// struct_span_err 方法接收的 span 用于定位错误代码的位置。
// 还可以通过 span_note 方法提供一些附加信息。
handler
.struct_span_err(
span,
&format!("`{}` used as parameter more than once", js_word),
)
.span_note(
old_span,
&format!("previous definition of `{}` here", js_word),
)
.emit();
});
}
处理流程
回到 Compiler的process_js_file方法,这是一个典型的编译过程,它将经历如下阶段:
-
词法分析(Lexical Analysis):该阶段将源代码分割成一系列的词素(tokens)。
-
语法分析(Syntax Analysis):语法分析会将词素组成抽象语法树(AST)。这个树状结构表示了源代码的语法结构。
-
代码生成(Code Generation):最后,编译器将通过遍历抽象语法树来生成目标代码。
这个流程是大多数编译器的基本工作流程,而 swc 的 process_js_file 方法也遵循了这一流程。
TransformOutput 结构体
process_js_file 方法返回一个 TransformOutput 结构体,它包含了转换后的代码和可能存在的 source map。
struct TransformOutput {
pub code: String,
pub map: Option<String>,
}
只有当 process_js_file 方法参数中配置了 source_maps 才会生成 source map,故其类型为 Option<String>:
c.process_js_file(
fm,
handler,
&Options {
source_maps: Some(swc::config::SourceMapsConfig::Bool(true)),
..Default::default()
},
)
最后
在此部分,我们主要介绍了 swc 的基本使用以及两个核心数据结构:SourceMap 和 Compiler。通过这些介绍,你应该已经对如何使用 swc 有了大致的理解,以及 SourceMap 和 Compiler 在其中起到的关键作用。
然而,要完全理解 swc 的内部工作机制,我们还需要深入其源码,特别是 process_js_file 方法的实现。在下一部分,我们将按照 process_js_file 方法的处理流程,逐步展开 swc 的内部实现,包括词法分析、语法分析和代码生成等阶段。这将帮助我们更深入地理解 swc 的工作原理和设计思路。