AST語法結構樹初學者完整教程

瘋狂的小蘑菇發表於2017-06-26

AST語法結構樹初學者完整教程

編寫你的第一個 Babel 外掛

不太喜歡上來就講大道理,先來個小栗子,做個簡單而又實用的功能,做完後,理論你就理解一大半了。

我們需要antd裡面的一個元件Button,程式碼如下:

import { Button } from 'antd'複製程式碼

我們只想引入Button元件,現在卻匯入antd/lib/index.js所有元件,導致打包後檔案大了好多,這顯然不是我們想要的。

我們想要的是隻引入antd/lib/button

import Button from 'antd/lib/button'複製程式碼

但是這樣寫又過於麻煩,多個元件得寫很多行,每行都有個相同的antd/lib程式碼冗餘,又不雅觀又不好維護。

我們可以使用Babel外掛來解決這個問題。(完整的程式碼示例

  1. 建立好如下目錄結構

    - root
        - node_modules
            - babel-plugin-import
                - index.js
                - package.json
        - test.js複製程式碼
  2. test.js檔案內容:

    const babel = require('babel-core');
    // 轉換前的程式碼
    const code = `import { Button } from 'antd'`;
    
    var result = babel.transform(code, {
      plugins:["import"]
    });
    
    // 轉換後的程式碼
    console.log(result.code);
    // 輸出:import Button from 'antd/lib/button'複製程式碼

    babel-core是babel的核心包,transform函式可以傳入code,返回轉換後的程式碼,ast語法樹和source-map對映地圖。具體API參考 babel-core

  3. package.json的main屬性指向index.js,不解釋

  4. 好了,開始編寫我們的外掛吧,index.js檔案內容:

    /* 單詞首字母小寫 */
    const toLowerCase = word =>
      Array.from(word).map((char, index) =>
        !index ? char.toLowerCase() : char).join('')
    
    module.exports = ({ types }) => (
      {
        // 外掛入口
        visitor: {
          // 處理型別: Import宣告
          ImportDeclaration(path) {
            const { node } = path
            const { type, specifiers, source } = node
            // 過濾掉預設引用,如 import antd from 'antd'
            const importSpecifiers = specifiers.filter(specifier =>
                types.isImportSpecifier(specifier))
            // 例子只處理了一個元件的引用
            if (importSpecifiers.length === 1) {
              const [importSpecifier] = importSpecifiers
              // 小寫處理元件引用,如Import { Button },改為: antd/lib/button
              const element = toLowerCase(importSpecifier.imported.name)
              // 引入改為預設引入,如Import { Button }, 改為: import Button
              importSpecifier.type = 'ImportDefaultSpecifier'
              source.value += `/lib/${element}`
            }
          }
        }
      })複製程式碼

    短短的幾行程式碼,即使看了註釋,相信親愛的讀者仍然一頭霧水。請登入 ast語法樹線上生成網站,輸入import Button from 'antd',對比右側生成的語法樹與程式碼,我們從程式碼開始講起

    • 程式碼中ImportDeclaration代表只處理程式碼中import這樣的程式碼(請注意網站生成的語法樹)。
    • 我們拿到specifiers是引用宣告,就是import { Button }中的Button,並且判斷長度為1(長度大於1的話得多行引入,要修改語法樹結構)。做了以下兩件事:

      • 拿到Button ,轉成小寫,拼接到source.value也就是'antd'後面。
      • 把Specifier型別為ImportSpecifier也就是{ Button }改為ImportDefaultSpecifier,這樣就把 Import { Button }改成了Import Button

    OK,就這麼簡單,也許你還沒有明白,沒關係,聯絡我吧。(mail: hongji_12313@163.com)

    然後我們再來看理論(理論的例子全部在ast倉庫中)。

基礎

Babel 是 JavaScript 靜態分析編譯器,更確切地說是原始碼到原始碼的編譯器,通常也叫做“轉換編譯器(transpiler)”。 意思是說你為 Babel 提供一些 JavaScript 程式碼,Babel 更改這些程式碼,然後返回給你新生成的程式碼。

靜態分析是在不需要執行程式碼的前提下對程式碼進行分析的處理過程 (執行程式碼的同時進行程式碼分析即是動態分析)。 靜態分析的目的是多種多樣的, 它可用於語法檢查,編譯,程式碼高亮,程式碼轉換,優化,壓縮等等場景。複製程式碼

抽象語法樹(ASTs)

這個處理過程中的每一步都涉及到建立或是操作抽象語法樹,亦稱 AST。

Babel 使用一個基於 ESTree 並修改過的 AST,它的核心說明文件可以在這裡找到。

    function square(n) {
      return n * n;
    }複製程式碼

AST Explorer 可以讓你對 AST 節點有一個更好的感性認識。 這裡是上述程式碼的一個示例連結。

同樣的程式可以表述為下面的列表:

- FunctionDeclaration:
  - id:
    - Identifier:
      - name: square
  - params [1]
    - Identifier
      - name: n
  - body:
    - BlockStatement
      - body [1]
        - ReturnStatement
          - argument
            - BinaryExpression
              - operator: *
              - left
                - Identifier
                  - name: n
              - right
                - Identifier
                  - name: n複製程式碼

或是如下所示的 JavaScript Object(物件):

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}複製程式碼

你會留意到 AST 的每一層都擁有相同的結構:

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}
{
  type: "Identifier",
  name: ...
}
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}複製程式碼
注意:出於簡化的目的移除了某些屬性複製程式碼

這樣的每一層結構也被叫做 節點(Node)。 一個 AST 可以由單一的節點或是成百上千個節點構成。 它們組合在一起可以描述用於靜態分析的程式語法。

每一個節點都有如下所示的介面(Interface):

interface Node {
  type: string;
}複製程式碼

字串形式的 type 欄位表示節點的型別(如: "FunctionDeclaration","Identifier",或 "BinaryExpression")。 每一種型別的節點定義了一些附加屬性用來進一步描述該節點型別。

Babel 還為每個節點額外生成了一些屬性,用於描述該節點在原始程式碼中的位置。

{
  type: ...,
  start: 0,
  end: 38,
  loc: {
    start: {
      line: 1,
      column: 0
    },
    end: {
      line: 3,
      column: 1
    }
  },
  ...
}複製程式碼

每一個節點都會有 start,end,loc 這幾個屬性。

Babel 的處理步驟

Babel 的三個主要處理步驟分別是: 解析(parse),轉換(transform),生成(generate)。.

解析

解析步驟接收程式碼並輸出 AST。 這個步驟分為兩個階段:詞法分析(Lexical Analysis) 和 語法分析(Syntactic Analysis)。.

詞法分析

詞法分析階段把字串形式的程式碼轉換為 令牌(tokens) 流。.

你可以把令牌看作是一個扁平的語法片段陣列:

n * n;
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]複製程式碼

每一個 type 有一組屬性來描述該令牌:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}複製程式碼

和 AST 節點一樣它們也有 start,end,loc 屬性。.

語法分析

語法分析階段會把一個令牌流轉換成 AST 的形式。 這個階段會使用令牌中的資訊把它們轉換成一個 AST 的表述結構,這樣更易於後續的操作。

轉換

轉換步驟接收 AST 並對其進行遍歷,在此過程中對節點進行新增、更新及移除等操作。 這是 Babel 或是其他編譯器中最複雜的過程 同時也是外掛將要介入工作的部分,這將是本手冊的主要內容, 因此讓我們慢慢來。

生成

程式碼生成步驟把最終(經過一系列轉換之後)的 AST轉換成字串形式的程式碼,同時建立原始碼對映(source maps)。.

程式碼生成其實很簡單:深度優先遍歷整個 AST,然後構建可以表示轉換後程式碼的字串。

遍歷

想要轉換 AST 你需要進行遞迴的樹形遍歷。

比方說我們有一個 FunctionDeclaration 型別。它有幾個屬性:id,params,和 body,每一個都有一些內嵌節點。

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}複製程式碼

於是我們從 FunctionDeclaration 開始並且我們知道它的內部屬性(即:id,params,body),所以我們依次訪問每一個屬性及它們的子節點。

接著我們來到 id,它是一個 Identifier。Identifier 沒有任何子節點屬性,所以我們繼續。

之後是 params,由於它是一個陣列節點所以我們訪問其中的每一個,它們都是 Identifier 型別的單一節點,然後我們繼續。

此時我們來到了 body,這是一個 BlockStatement 並且也有一個 body節點,而且也是一個陣列節點,我們繼續訪問其中的每一個。

這裡唯一的一個屬性是 ReturnStatement 節點,它有一個 argument,我們訪問 argument 就找到了 BinaryExpression。.

BinaryExpression 有一個 operator,一個 left,和一個 right。 Operator 不是一個節點,它只是一個值因此我們不用繼續向內遍歷,我們只需要訪問 left 和 right。.

Babel 的轉換步驟全都是這樣的遍歷過程。

Visitors(訪問者)

當我們談及“進入”一個節點,實際上是說我們在訪問它們, 之所以使用這樣的術語是因為有一個訪問者模式(visitor)的概念。.

訪問者是一個用於 AST 遍歷的跨語言的模式。 簡單的說它們就是一個物件,定義了用於在一個樹狀結構中獲取具體節點的方法。 這麼說有些抽象所以讓我們來看一個例子。

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};複製程式碼
注意: Identifier() { ... } 是 Identifier: { enter() { ... } } 的簡寫形式。.複製程式碼

這是一個簡單的訪問者,把它用於遍歷中時,每當在樹中遇見一個 Identifier 的時候會呼叫 Identifier() 方法。

所以在下面的程式碼中 Identifier() 方法會被呼叫四次(包括 square 在內,總共有四個 Identifier)。).

function square(n) {
  return n * n;
}複製程式碼
Called!
Called!
Called!
Called!複製程式碼

這些呼叫都發生在進入節點時,不過有時候我們也可以在退出時呼叫訪問者方法。.

假設我們有一個樹狀結構:

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)複製程式碼

當我們向下遍歷這顆樹的每一個分支時我們最終會走到盡頭,於是我們需要往上遍歷回去從而獲取到下一個節點。 向下遍歷這棵樹我們進入每個節點,向上遍歷回去時我們退出每個節點。

讓我們以上面那棵樹為例子走一遍這個過程。

  • 進入 FunctionDeclaration
    • 進入 Identifier (id)
    • 走到盡頭
    • 退出 Identifier (id)
    • 進入 Identifier (params[0])
    • 走到盡頭
    • 退出 Identifier (params[0])
    • 進入 BlockStatement (body)
      • 進入 ReturnStatement (body)
        • 進入 BinaryExpression (argument)
          • 進入 Identifier (left)
          • 走到盡頭
          • 退出 Identifier (left)
          • 進入 Identifier (right)
          • 走到盡頭
          • 退出 Identifier (right)
        • 退出 BinaryExpression (argument)
      • 退出 ReturnStatement (body)
    • 退出 BlockStatement (body)
  • 退出 FunctionDeclaration

所以當建立訪問者時你實際上有兩次機會來訪問一個節點。

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
};複製程式碼

Paths(路徑)

AST 通常會有許多節點,那麼節點直接如何相互關聯? 我們可以用一個巨大的可變物件讓你來操作以及完全訪問(節點的關係),或者我們可以用Paths(路徑)來簡化這件事情。.

Path 是一個物件,它表示兩個節點之間的連線。

舉例來說如果我們有以下的節點和它的子節點:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}複製程式碼

將子節點 Identifier 表示為路徑的話,看起來是這樣的:

{
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "square"
  }
}複製程式碼

同時它還有關於該路徑的附加後設資料:

{
  "parent": {...},
  "node": {...},
  "hub": {...},
  "contexts": [],
  "data": {},
  "shouldSkip": false,
  "shouldStop": false,
  "removed": false,
  "state": null,
  "opts": null,
  "skipKeys": null,
  "parentPath": null,
  "context": null,
  "container": null,
  "listKey": null,
  "inList": false,
  "parentKey": null,
  "key": null,
  "scope": null,
  "type": null,
  "typeAnnotation": null
}複製程式碼

當然還有成堆的方法,它們和新增、更新、移動和刪除節點有關,不過我們後面再說。

可以這麼說,路徑是對於節點在數中的位置以及其他各種資訊的響應式表述。 當你呼叫一個方法更改了樹的時候,這些資訊也會更新。 Babel 幫你管理著這一切從而讓你能更輕鬆的操作節點並且儘量保證無狀態化。(譯註:意即儘可能少的讓你來維護狀態)

Paths in Visitors(存在於訪問者中的路徑)

當你有一個擁有 Identifier() 方法的訪問者時,你實際上是在訪問路徑而不是節點。 如此一來你可以操作節點的響應式表述(譯註:即路徑)而不是節點本身。

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};複製程式碼
a + b + c;
Visiting: a
Visiting: b
Visiting: c複製程式碼

State(狀態)

狀態是 AST 轉換的敵人。狀態會不停的找你麻煩,你對狀態的預估到最後幾乎總是錯的,因為你無法預先考慮到所有的語法。

考慮下列程式碼:

function square(n) {
  return n * n;
}複製程式碼

讓我們寫一個把 n 重新命名為 x 的訪問者的快速實現:.

let paramName;

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    paramName = param.name;
    param.name = "x";
  },

  Identifier(path) {
    if (path.node.name === paramName) {
      path.node.name = "x";
    }
  }
};複製程式碼

對上面的程式碼來說或許能行,但我們很容易就能“搞壞”它:

function square(n) {
  return n * n;
}
n;複製程式碼

更好的處理方式是遞迴。那麼讓我們來像克里斯托佛·諾蘭的電影那樣來把一個訪問者放進另外一個訪問者裡面。

const updateParamNameVisitor = {
  Identifier(path) {
    if (path.node.name === this.paramName) {
      path.node.name = "x";
    }
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    const paramName = param.name;
    param.name = "x";

    path.traverse(updateParamNameVisitor, { paramName });
  }
};複製程式碼

當然,這只是一個刻意捏造的例子,不過它演示瞭如何從訪問者中消除全域性狀態。

Scopes(作用域)

接下來讓我們引入作用域(scope)的概念。 JavaScript 擁有詞法作用域,程式碼塊建立新的作用域並形成一個樹狀結構。

// global scope

function scopeOne() {
  // scope 1

  function scopeTwo() {
    // scope 2
  }
}複製程式碼

在 JavaScript 中,每當你建立了一個引用,不管是通過變數(variable)、函式(function)、型別(class)、引數(params)、模組匯入(import)、標籤(label)等等,它都屬於當前作用域。

var global = "I am in the global scope";

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    var two = "I am in the scope created by `scopeTwo()`";
  }
}複製程式碼

處於深層作用域程式碼可以使用高(外)層作用域的引用。

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
  }
}複製程式碼

低(內)層作用域也可以建立(和外層)同名的引用而無須更改它。

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
  }
}複製程式碼

當編寫一個轉換器時,我們須要小心作用域。我們得確保在改變程式碼的各個部分時不會破壞它。

我們會想要新增新的引用並且保證它們不會和已經存在的引用衝突。 又或者我們只是想要找出變數在哪裡被引用的。 我們需要能在給定作用域內跟蹤這些引用。

作用域可以表述為:

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
}複製程式碼

當你建立一個新的作用域時需要給它一個路徑及父級作用域。之後在遍歷過程中它會在改作用於內收集所有的引用(“繫結”)。

這些做好之後,你將擁有許多用於作用域上的方法。我們稍後再講這些。

Bindings(繫結)

引用從屬於特定的作用域;這種關係被稱作:繫結(binding)。.

function scopeOnce() {
  var ref = "This is a binding";

  ref; // This is a reference to a binding

  function scopeTwo() {
    ref; // This is a reference to a binding from a lower scope
  }
}複製程式碼

一個繫結看起來如下:

{
  identifier: node,
  scope: scope,
  path: path,
  kind: 'var',

  referenced: true,
  references: 3,
  referencePaths: [path, path, path],

  constant: false,
  constantViolations: [path]
}複製程式碼

有了這些資訊你就可以查詢一個繫結的所有引用,並且知道繫結的型別是什麼(引數,定義等等),尋找到它所屬的作用域,或者得到它的識別符號的拷貝。 你甚至可以知道它是否是一個常量,並檢視是哪個路徑讓它不是一個常量。

知道繫結是否為常量在很多情況下都會很有用,最大的用處就是程式碼壓縮。

function scopeOne() {
  var ref1 = "This is a constant binding";

  becauseNothingEverChangesTheValueOf(ref1);

  function scopeTwo() {
    var ref2 = "This is *not* a constant binding";
    ref2 = "Because this changes the value";
  }
}複製程式碼

API

Babel 實際上是一系列的模組。本節我們將探索一些主要的模組,解釋它們是做什麼的以及如何使用它們。

注意:本節內容不是詳細的 API 文件的替代品,正式的 API 文件將很快提供出來。複製程式碼

babylon

Babylon 是 Babel 的解析器。最初是 Acorn 的一份 fork,它非常快,易於使用,並且針對非標準特性(以及那些未來的標準特性)設計了一個基於外掛的架構。

首先,讓我們先安裝它。

$ npm install --save babylon複製程式碼

讓我們從解析簡單的字元形式程式碼開始:

import * as babylon from "babylon";

const code = `function square(n) {
  return n * n;
}`;

babylon.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }複製程式碼

我們還能傳遞選項給 parse():

babylon.parse(code, {
  sourceType: "module", // default: "script"
  plugins: ["jsx"] // default: []
});複製程式碼

sourceType 可以是 "module" 或者 "script",它表示 Babylon 應該用哪種模式來解析。 "module" 將會在嚴格模式下解析並且允許模組定義,"script" 則不會。

注意: sourceType 的預設值是 "script" 並且在發現 import 或 export 時產生錯誤。 使用 scourceType: "module" 來避免這些錯誤。複製程式碼

因為 Babylon 使用了基於外掛的架構,因此 plugins 選項可以開啟內建外掛。 注意 Babylon 尚未對外部外掛開放此 API 介面,不過未來會開放的。

可以在 Babylon README 檢視所有外掛的列表。.

babel-traverse

Babel Tranverse(遍歷)模組維護了整棵樹的狀態,並且負責替換、移除和新增節點。

執行以下命令安裝:

$ npm install --save babel-traverse複製程式碼

我們可以配合 Babylon 一起使用來遍歷和更新節點:

import * as babylon from "babylon";
import traverse from "babel-traverse";

const code = `function square(n) {
  return n * n;
}`;

const ast = babylon.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});複製程式碼

babel-types

Babel Types(型別)模組是一個用於 AST 節點的 Lodash 式工具庫。 譯註:Lodash 是一個 JavaScript 函式工具庫,提供了基於函數語言程式設計風格的眾多工具函式)它包含了構造、驗證以及變換 AST 節點的方法。 其設計周到的工具方法有助於編寫清晰簡單的 AST 邏輯。

執行以下命令來安裝它:

$ npm install --save babel-types複製程式碼

然後如下所示來使用:

import traverse from "babel-traverse";
import * as t from "babel-types";

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});複製程式碼

Definitions(定義)

Babel Types模組擁有每一個單一型別節點的定義,包括有哪些屬性分別屬於哪裡,哪些值是合法的,如何構建該節點,該節點應該如何去遍歷,以及節點的別名等資訊。

單一節點型別定義的形式如下:

defineType("BinaryExpression", {
  builder: ["operator", "left", "right"],
  fields: {
    operator: {
      validate: assertValueType("string")
    },
    left: {
      validate: assertNodeType("Expression")
    },
    right: {
      validate: assertNodeType("Expression")
    }
  },
  visitor: ["left", "right"],
  aliases: ["Binary", "Expression"]
});複製程式碼

Builders(構建器)

你會注意到上面的 BinaryExpression 定義有一個 builder 欄位。.

builder: ["operator", "left", "right"]複製程式碼

這是由於每一個節點型別都有構建器方法:

t.binaryExpression("*", t.identifier("a"), t.identifier("b"));複製程式碼

它可以建立如下所示的 AST:

{
  type: "BinaryExpression",
  operator: "*",
  left: {
    type: "Identifier",
    name: "a"
  },
  right: {
    type: "Identifier",
    name: "b"
  }
}複製程式碼

當列印出來(輸出)之後是這樣的:

a * b複製程式碼

構建器還會驗證自身建立的節點,並在錯誤使用的情形下丟擲描述性的錯誤。這就引出了接下來的一種方法。

Validators(驗證器)

BinaryExpression 的定義還包含了節點的 fields 欄位資訊並且指示瞭如何驗證它們。

fields: {
  operator: {
    validate: assertValueType("string")
  },
  left: {
    validate: assertNodeType("Expression")
  },
  right: {
    validate: assertNodeType("Expression")
  }
}複製程式碼

這可以用來建立兩種型別的驗證方法。第一種是 isX。.

t.isBinaryExpression(maybeBinaryExpressionNode);複製程式碼

此方法用來確保節點是一個二進位制表示式,不過你也可以傳入第二個引數來確保節點包含特定的屬性和值。

t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });複製程式碼

這些方法還有一種更加,嗯,斷言式的版本,會丟擲異常而不是返回 true 或 false。.

t.assertBinaryExpression(maybeBinaryExpressionNode);
t.assertBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });
// Error: Expected type "BinaryExpression" with option { "operator": "*" }複製程式碼

Converters(變換器)

babel-generator

Babel Generator模組是 Babel 的程式碼生成器。它將 AST 輸出為程式碼幷包括原始碼對映(sourcemaps)。

執行以下命令來安裝它:

$ npm install --save babel-generator複製程式碼

然後如下所示使用:

import * as babylon from "babylon";
import generate from "babel-generator";

const code = `function square(n) {
  return n * n;
}`;

const ast = babylon.parse(code);

generate(ast, null, code);
// {
//   code: "...",
//   map: "..."
// }複製程式碼

你也可以給 generate() 傳遞選項。.

generate(ast, {
  retainLines: false,
  compact: "auto",
  concise: false,
  quotes: "double",
  // ...
}, code);複製程式碼
babel-template

Babel Template模組是一個很小但卻非常有用的模組。它能讓你編寫帶有佔位符的字串形式的程式碼,你可以用此來替代大量的手工構建的 AST。

$ npm install --save babel-template
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";

const buildRequire = template(`
  var IMPORT_NAME = require(SOURCE);
`);

const ast = buildRequire({
  IMPORT_NAME: t.identifier("myModule"),
  SOURCE: t.stringLiteral("my-module")
});

console.log(generate(ast).code);
// var myModule = require("my-module");複製程式碼

相關文章