背景提要
今天就主要介绍 如何用 swc 这个 rust 知名库来自己实现一个 babel-import, 目前 swc 的文档 还仅仅停留在 transfrom 和 plugin 的层面,其实 swc 是一个丰富 rust 针对 js 语言的 parser 和 generator 库,笔者也是翻阅了很多的 swc的 源码 和 跑过了 swc 的单元测试 掌握了这个能力,希望看到的这篇文章有用的人可以适当点赞。
步骤拆分
-
(旧)已知 babel 的 实现 babel-import 大概需要以下3步
- 利用 @babel/parser 中 parser 把 code 改变 成 AST 结构
- 利用 @babel/traverse 中 traverse 把 ast 遍历寻找 对应需要修改的 node 节点。
- 利用 @babel/generator 中 把第二步 已修改的 ast 进行按 目标语言重新组装。
用 已知 推 未知
-
(新) 根据 babel 的类似步骤,了解 swc 对其自己 ast 整套流程,并脱离其 transform 和 plugin 的实现。
- 找到对应的 parser 方法 ,通过翻阅 swc/core 核心的 rust-doc 锁定 swc_ecma_parser 这个库 有通过该库 rust-doc (docs.rs/swc_ecma_pa…) 掌握了 调用方法。
- 继续翻阅 swc/project 的源码,swc_ecma_parser 查询 parser后 单元测试 掌握源码内容一般对 ast 都有进行过哪些修改,找到类似 @babel/traverse 的方法。
- 通过 trasnsform 的源码实现,和 parser的 ast 对象找到最后重新组合 ast 最简调用方法。来替代 generator 这个步骤。
具体源码实现
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::io::stderr;
use swc::Compiler;
use swc::config::SourceMapsConfig;
use swc::ecmascript::ast::{EsVersion, ImportDecl, ImportNamedSpecifier, ImportStarAsSpecifier, ModuleDecl};
use swc_common::sync::Lrc;
use swc_common::{DUMMY_SP, errors::{ColorConfig, Handler}, FileName, FilePathMapping, SourceMap};
use swc_ecma_parser::{EsConfig, lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
use swc::ecmascript::ast::{Decl, Module, ModuleItem, Pat, Stmt};
use swc_atoms::{js_word, JsWord};
use swc_ecma_visit::FoldWith;
use swc_ecma_ast::{ImportSpecifier, Str};
use swc_ecma_transforms::const_modules;
use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
use swc_bundler::Bundler;
#[test]
fn mut_change() {
let mut list = vec![1, 2, 3, 4];
let mut temp_list: Vec<i32> = vec![];
for num in list.iter_mut() {
let c = (*num) % 2;
if c == 0 {
*num = *num * 4;
temp_list.push(*num * 4);
}
}
list.extend(temp_list);
let str = list.iter().map(|x| { return x.to_string(); }).collect::<Vec<std::string::String>>().join("\n");
println!("{}", str);
}
#[test]
fn transform() {
let source = "
import React from "react";
import ReactDOM from "react-dom";
import {Button, Input} from "antd";
import Child from "./component/Child";
class Page extends React.Component {
render() {
return (
<div className={"test"}>
<div>Page</div>
<Child/>
<Button>click me</Button>
<Input/>
</div>
);
}
}
ReactDOM.render(<Page/>, document.getElementById("root"));
";
let source2 = "\
function abc(){\
console.log(123);
}";
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(
FileName::Custom("test.js".into()),
source.into(),
);
let compiler = Compiler::new(cm.clone());
let handler = Handler::with_emitter_writer(Box::new(stderr()), Some(compiler.cm.clone()));
let lexer = Lexer::new(
// We want to parse ecmascript
Syntax::Typescript(TsConfig {
tsx: true,
decorators: true,
dynamic_import: true,
dts: false,
no_early_errors: false,
import_assertions: false,
}),
// EsVersion defaults to es5
EsVersion::Es2016,
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let list_error = parser.take_errors();
if list_error.iter().len() > 0 {
let mut err_msg = "".to_owned();
for err in list_error {
let msg = err.into_kind().msg().to_string();
err_msg.push_str(msg.as_str());
}
}
let mut module = parser
.parse_module().unwrap();
println!("parser success");
let s = serde_json::to_string_pretty(&module).expect("failed to serialize");
println!("ast json is \n {}", s);
let mut specifiers = vec![];
for item in &mut module.body {
if let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item {
let source = &*var.src.value;
if source == "antd" {
for specifier in &var.specifiers {
match specifier {
ImportSpecifier::Named(ref s) => {
let ident = format!("{}", s.local.sym);
specifiers.push(format!("antd/es/{}/style/index.css", ident.to_lowercase()));
}
ImportSpecifier::Default(ref s) => {}
ImportSpecifier::Namespace(ref ns) => {}
}
}
}
}
}
for css_source in specifiers.clone() {
let css_source_ref = css_source.as_str();
let dec = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers: vec![],
src: Str {
span: DUMMY_SP,
value: JsWord::from(css_source_ref),
has_escape: false,
kind: Default::default(),
},
type_only: false,
asserts: None,
}));
module.body.insert(0, dec);
}
let new_res = compiler.print(&module,
None,
None,
false,
EsVersion::Es2020,
SourceMapsConfig::Bool(false),
&Default::default(),
None,
false,
None, ).unwrap();
println!("gen success");
}
}
源码导读 (rust 新手必看)
前置依赖
- 需要用到的 cargo 库 (这里有一些源码中 未提及 是因为 源码并未涉及到 ts 的 ast 和其他 vistit 的情况)
Parser 拆解 AST
- 拆解 AST 核心 3步骤 获得 AST 结构
// 声明一个 针对 swc 源码文件对象
// 如果 你的 code 是单纯 的 String 第二个文件名可以随便填写
// 第三个 则是 针对 js-code 的 string 值
// var a = 123;
// function abc(){ console.log(123);}
// .... 等等
let fm = cm.new_source_file(
FileName::Custom("test.js".into()),
source.into(),
);
// 声明 源码 的 Lexer 解析规则
// 说人话就是 是不是 ts 是不是 jsx 是不是 有 es2017+ 的语法
// 最后第三个参数 关联 上述 源码内容
let lexer = Lexer::new(
// We want to parse ecmascript
Syntax::Es(EsConfig {
jsx: true,
...
}),
// EsVersion defaults to es5
EsVersion::Es2016,
StringInput::from(&*fm),
None,
);
// 声明一个 Parser 对象
let mut parser = Parser::new_from(lexer);
// 在JS 中 每个文件一般默认为一个 Module 即按照 Module 来去 拆解
// 获取 "module" 就是整个 文件的 ast 对象
let mut module = parser.parse_module().unwrap();
根据上述方法 去拆解下列源码 获得 AST 数据 见下图2
import React from "react";
import ReactDOM from "react-dom";
import {Button, Input} from "antd";
import Child from "./component/Child";
class Page extends React.Component {
render() {
return (
<div className={"test"}>
<div>Page</div>
<Child/>
<Button>click me</Button>
<Input/>
</div>
);
}
}
ReactDOM.render(<Page/>, document.getElementById("root"));
AST 数据 github.com/zhusjfaker/…
通过上述 图片 我们可以看到 swc 和 AST结构 是和 Babel 有些高度类似的。
Traverse Node 遍历节点 以及修改
- 在 "module" ast 对象中 匹配到 对应的 node 节点,抽取出关键的信息,然后加工 ast对象
通过上述 图片 我们可以看到 swc 和 AST结构 是和 Babel 有些高度类似的。
Traverse Node 遍历节点 以及修改
- 在 "module" ast 对象中 匹配到 对应的 node 节点,抽取出关键的信息,然后加工 ast对象
for item in module.body {
if let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item {
let source = &*var.src.value;
if source == "antd" {
for specifier in &var.specifiers {
match specifier {
ImportSpecifier::Named(ref s) => {
let ident = format!("{}", s.local.sym);
specifiers.push(format!("antd/es/{}/style/index.css", ident.to_lowercase()));
}
ImportSpecifier::Default(ref s) => {}
ImportSpecifier::Namespace(ref ns) => {}
}
}
for css_source in specifiers.clone() {
let css_source_ref = css_source.as_str();
let dec = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers: vec![],
src: Str {
span: DUMMY_SP,
value: JsWord::from(css_source_ref),
has_escape: false,
kind: Default::default(),
},
type_only: false,
asserts: None,
}));
let body = &mut newmodule.body;
body.insert(0, dec);
}
}
}
}
-
以下列出 这段代码 的难点。
- 在 if 中居然存在 赋值语句 “let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item” 这个 是 rust 的 特有 “模式匹配” “结构用法” 可以理解为 在 Module -> ModuleItem 对象 中 -> ModuleDecl 接口 实现了 import 即为 ImportDeclaration 恰好符合 右边 遍历中的 item 时, 则把 item 的 ref 引用 赋值给 var 。
- 第二个在 JS 的AST 对象中 提取 字符串 并不是 rust 中 std::string:String 类型,而是 swc_atoms::JsWord 那这个两个 类型 怎么来实现 互相转换呢,其实很简单 有两种方法,
- 通过简单的 “拆箱” & “装箱”
let source = &*var.src.value;
if source == "antd"
- 通过 Rust 底层 Display 实现
// s 是 Js_Word
let ident = format!("{}", s.local.sym);
- DUMMY_SP “use swc_common::DUMMY_SP” 可以在你 插入新的 代码树 Node 时有效不出错。
- Rust match 类似 Switch case 代码块 的高级用户 doc.rust-lang.org/rust-by-exa… 可以参考 这篇 文档。
重新合成 指定语言
// 声明 compiler 对象
// 这个对象是 你只要调用 swc 必须声明 不管是否是 transform
// 是 swc::core 中的一个 核心对象
let compiler = Compiler::new(cm.clone());
// 根据 Rust 特性操作 ast 必须在新树上才可以 有权限。
let mut newmodule = module.clone() as Module;
// 有很多人 用 fold_with 来生成 代码
// 根据源码 查询后 发现不需要 只要需要在 上述 compiler 对象 调用 print 方法 即可
// 获得 新的 代码
let new_res = compiler.print(&newmodule,
None,
None,
false,
EsVersion::Es2020,
SourceMapsConfig::Bool(false),
&Default::default(),
None,
false,
None, ).unwrap();
后续规划
继续推出 rust 编译 wasm 和 napi-rs 和 cgo 调用链接 教程。有兴趣可以持续关注。