跳至內容
文件
外掛
ECMAScript
秘笈

外掛秘笈

💡

此頁面說明了實作 ecmascript 外掛的已知難點。

您可能會覺得 https://rustdoc.swc.rs/swc(在新分頁中開啟) 上的說明文件很有用,特別是當您在處理訪客或 Id 問題時。

了解類型

JsWord

String 會配置,而且原始碼的「文字」有特殊的特質。這些都是大量的重複。顯然地,如果您的變數名稱為 foo,您需要多次使用 foo。因此,SWC 會將字串內部化以減少配置次數。

JsWord 是一個已內部化的字串類型。您可以從 &strString 建立 JsWord。使用 .into() 轉換為 JsWord

IdentIdMarkSyntaxContext

SWC 使用一個特殊系統來管理變數。有關詳細資訊,請參閱 Ident 的 rustdoc(在新分頁中開啟)

常見問題

取得輸入的 AST 表示法

SWC Playground(在新分頁中開啟) 支援從輸入程式碼取得 AST。

SWC 的變數管理

錯誤回報

請參閱 rustdoc for swc_common::errors::Handler(在新分頁中開啟)

比較 JsWord&str

如果您不知道 JsWord 是什麼,請參閱 swc_atoms 的 rustdoc(在新分頁中開啟)

您可以透過執行 &val 來建立 &str,其中 val 是型別為 JsWord 的變數。

比對 Box<T>

您需要使用 match 來比對各種節點,包括 Box<T>。基於效能考量,所有表達式都儲存在框內形式中。(Box<Expr>

SWC 將呼叫表達式的被呼叫者儲存為 Callee 列舉,而且它有 Box<Expr>

use swc_core::ast::*;
use swc_core::visit::{VisitMut, VisitMutWith};
 
struct MatchExample;
 
impl VisitMut for MatchExample {
    fn visit_mut_callee(&mut self, callee: &mut Callee) {
        callee.visit_mut_children_with(self);
 
        if let Callee::Expr(expr) = callee {
            // expr is `Box<Expr>`
            if let Expr::Ident(i) = &mut **expr {
                i.sym = "foo".into();
            }
        }
    }
}
 
 

變更 AST 類型

如果您想將 ExportDefaultDecl 變更為 ExportDefaultExpr,您應該從 visit_mut_module_decl 執行此操作。

插入新節點

如果您想注入新的 Stmt,您需要將值儲存在結構中,並從 visit_mut_stmtsvisit_mut_module_items 注入。請參閱 解構核心轉換(在新分頁中開啟)

struct MyPlugin {
    stmts: Vec<Stmt>,
}
 

提示

測試時套用 resolver

SWC 在套用 resolver(在新分頁中開啟) 之後套用外掛,因此最好用它來測試您的轉換。如 resolver 的 rustdoc 中所寫,如果您需要參照全域變數(例如 __dirnamerequire)或使用者撰寫的頂層繫結,則必須使用正確的 SyntaxContext

fn tr() -> impl Fold {
    chain!(
        resolver(Mark::new(), Mark::new(), false),
        // Most of transform does not care about globals so it does not need `SyntaxContext`
        your_transform()
    )
}
 
test!(
    Syntax::default(),
    |_| tr(),
    basic,
    // input
    "(function a ([a]) { a });",
    // output
    "(function a([_a]) { _a; });"
);
 

讓您的處理常式無狀態

假設我們要在函式表達式中處理所有陣列表達式。您可以將旗標新增到訪客,以檢查我們是否在函式表達式中。您可能會想執行

 
struct Transform {
    in_fn_expr: bool
}
 
impl VisitMut for Transform {
    noop_visit_mut_type!();
 
    fn visit_mut_fn_expr(&mut self, n: &mut FnExpr) {
        self.in_fn_expr = true;
        n.visit_mut_children_with(self);
        self.in_fn_expr = false;
    }
 
    fn visit_mut_array_lit(&mut self, n: &mut ArrayLit) {
        if self.in_fn_expr {
            // Do something
        }
    }
}

但這無法處理

 
const foo = function () {
    const arr = [1, 2, 3];
 
    const bar = function () {};
 
    const arr2 = [2, 4, 6];
}
 

拜訪 bar 後,in_fn_expr 會是 false。你必須執行

 
struct Transform {
    in_fn_expr: bool
}
 
impl VisitMut for Transform {
    noop_visit_mut_type!();
 
    fn visit_mut_fn_expr(&mut self, n: &mut FnExpr) {
        let old_in_fn_expr = self.in_fn_expr;
        self.in_fn_expr = true;
 
        n.visit_mut_children_with(self);
 
        self.in_fn_expr = old_in_fn_expr;
    }
 
    fn visit_mut_array_lit(&mut self, n: &mut ArrayLit) {
        if self.in_fn_expr {
            // Do something
        }
    }
}

使用 @swc/jest 進行測試

你可以透過將外掛新增到 jest.config.js,使用 @swc/jest 測試轉換。

jest.config.js
module.exports = {
  rootDir: __dirname,
  moduleNameMapper: {
    "css-variable$": "../../dist",
  },
  transform: {
    "^.+\\.(t|j)sx?$": [
      "@swc/jest",
      {
        jsc: {
          experimental: {
            plugins: [
              [
                require.resolve(
                  "../../swc/target/wasm32-wasi/release/swc_plugin_css_variable.wasm"
                ),
                {
                  basePath: __dirname,
                  displayName: true,
                },
              ],
            ],
          },
        },
      },
    ],
  },
};
 

請參閱 https://github.com/jantimon/css-variable/blob/main/test/swc/jest.config.js(在新分頁中開啟)

Path 是 unix 的其中一項,而 FileName 則可以是其中一項主機作業系統

這是因為在編譯成 wasm 時,會使用 Path 程式碼的 linux 版本。因此,你可能需要在你的外掛中將 \\ 替換成 /。由於 / 是 windows 中有效的路徑分隔符號,因此這樣做是有效的。

擁有權模型(rust)

此章節並非關於 swc 本身。但由於它是幾乎所有 API 棘手問題的根源,因此在此說明。

在 rust 中,只有一個變數可以擁有資料,而且最多只有一個可變參考指向它。此外,如果您要修改資料,則需要擁有該值或擁有指向它的可變參考。

但最多只有一個擁有者/可變參考,這表示如果您擁有指向值的變數參考,則其他程式碼無法修改該值。每個更新作業都應由擁有該值或擁有指向它的可變參考的程式碼執行。因此,一些 babel API(例如 node.delete)的實作非常棘手。由於您的程式碼擁有 AST 的部分所有權或可變參考,因此 SWC 無法修改 AST。

棘手的操作

刪除節點

您可以分兩步驟刪除節點。

假設我們要刪除以下程式碼中名為 bar 的變數。

var foo = 1;
var bar = 1;

有兩種方法可以做到這一點。

標記並刪除

第一種方法是將它標記為無效,然後稍後刪除它。這通常比較方便。

 
use swc_core::ast::*;
use swc_core::visit::{VisitMut,VisitMutWith};
 
impl VisitMut for Remover {
    fn visit_mut_var_declarator(&mut self, v: &mut VarDeclarator) {
        // This is not required in this example, but you typically need this.
        v.visit_mut_children_with(self);
 
 
        // v.name is `Pat`.
        // See https://rustdoc.swc.rs/swc_ecma_ast/enum.Pat.html
        match v.name {
            // If we want to delete the node, we should return false.
            //
            // Note the `&*` before i.sym.
            // The type of symbol is `JsWord`, which is an interned string.
            Pat::Ident(i) => {
                if &*i.sym == "bar" {
                    // Take::take() is a helper function, which stores invalid value in the node.
                    // For Pat, it's `Pat::Invalid`.
                    v.name.take();
                }
            }
            _ => {
                // Noop if we don't want to delete the node.
            }
        }
    }
 
    fn visit_mut_var_declarators(&mut self, vars: &mut Vec<VarDeclarator>) {
        vars.visit_mut_children_with(self);
 
        vars.retain(|node| {
            // We want to remove the node, so we should return false.
            if node.name.is_invalid() {
                return false
            }
 
            // Return true if we want to keep the node.
            true
        });
    }
 
    fn visit_mut_stmt(&mut self, s: &mut Stmt) {
        s.visit_mut_children_with(self);
 
        match s {
            Stmt::Decl(Decl::Var(var)) => {
                if var.decls.is_empty() {
                    // Variable declaration without declarator is invalid.
                    //
                    // After this, `s` becomes `Stmt::Empty`.
                    s.take();
                }
            }
            _ => {}
        }
    }
 
    fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
        stmts.visit_mut_children_with(self);
 
        // We remove `Stmt::Empty` from the statement list.
        // This is optional, but it's required if you don't want extra `;` in output.
        stmts.retain(|s| {
            // We use `matches` macro as this match is trivial.
            !matches!(s, Stmt::Empty(..))
        });
    }
 
    fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
        stmts.visit_mut_children_with(self);
 
        // This is also required, because top-level statements are stored in `Vec<ModuleItem>`.
        stmts.retain(|s| {
            // We use `matches` macro as this match is trivial.
            !matches!(s, ModuleItem::Stmt(Stmt::Empty(..)))
        });
    }
}
 

從父處理常式刪除

刪除節點的另一種方法是從父處理常式中刪除它。如果您只有在父節點為特定類型時才要刪除節點,這會很有用。

例如,您不希望在刪除自由變數陳述式時觸及 for 迴圈中的變數。

use swc_core::ast::*;
use swc_core::visit::{VisitMut,VsiitMutWith};
 
struct Remover;
 
impl VisitMut for Remover {
    fn visit_mut_stmt(&mut self, s: &mut Stmt) {
        // This is not required in this example, but just to show that you typically need this.
        s.visit_mut_children_with(self);
 
        match s {
            Stmt::Decl(Decl::Var(var)) => {
                if var.decls.len() == 1 {
                    match var.decls[0].name {
                        Pat::Ident(i) => {
                            if &*i.sym == "bar" {
                                s.take();
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
 
 
    fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
        stmts.visit_mut_children_with(self);
 
        // We do same thing here.
        stmts.retain(|s| {
            !matches!(s, Stmt::Empty(..))
        });
    }
 
    fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
        stmts.visit_mut_children_with(self);
 
        // We do same thing here.
        stmts.retain(|s| {
            !matches!(s, ModuleItem::Stmt(Stmt::Empty(..)))
        });
    }
}
 

從子節點的處理常式參照父節點

這包括使用 pathsscope

快取關於 AST 節點的一些資訊

您可以使用兩種方式使用來自父節點的資訊。首先,您可以從父節點處理常式預先計算資訊。或者,您可以複製父節點並在子節點處理常式中使用它。

babel API 的替代方案

generateUidIdentifier

這會回傳一個具有單調遞增整數字尾的唯一識別碼。 swc 沒有提供 API 來執行此動作,因為有一個非常簡單的方法可以做到這一點。您可以在轉換器類型中儲存一個整數欄位,並在呼叫 quote_ident!private_ident! 時使用它。

 
struct Example {
    // You don't need to share counter.
    cnt: usize
}
 
impl Example {
    /// For properties, it's okay to use `quote_ident`.
    pub fn next_property_id(&mut self) -> Ident {
        self.cnt += 1;
        quote_ident!(format!("$_css_{}", self.cnt))
    }
 
    /// If you want to create a safe variable, you should use `private_ident`
    pub fn next_variable_id(&mut self) -> Ident {
        self.cnt += 1;
        private_ident!(format!("$_css_{}", self.cnt))
    }
}
 
 

path.find

swc 不支援向上遍歷。這是因為向上遍歷需要在子節點中儲存有關父節點的資訊,這需要在 rust 中使用 ArcMutex 等類型。

您應該從上到下進行,而不是向上遍歷。例如,如果您想從變數指派或指派中推斷 jsx 元件的名稱,您可以在拜訪 VarDecl 和/或 AssignExpr 時儲存元件的 name,並從元件處理常式中使用它。

state.file.get/state.file.set

您只需將值儲存在轉換結構中,因為轉換結構只會處理一個檔案。

最後更新於 2024 年 4 月 15 日