44
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Babel Plugin を作りながら AST と Babel を学ぶ

Last updated at Posted at 2019-12-24

この記事は JavaScript Advent Calendar 2019 の 23日目の記事です。
前日の22日目は Vue-CLI 4を使用したJavaScript開発環境構築(プロトタイプ版とプロジェクト版) でした。

今回は表題通りBabel Pluginを作りながらASTBabelを学ぼうという記事です。

AST とは?

まずは根幹であるASTについて軽く説明します。
ASTAbstract Syntax Treeの略で、日本語では抽象構文木などと呼ばれるものです。

ASTはプログラムの構造を示したデータ構造体であり、JavaScriptではJSONデータの形で表現されることが一般的になり、基本的に仕様は、ESTree に準拠されています。

ASTBabel以外に ES Lintwebpack などにも使用されています。

実際にASTがどのようなものなのかをAST explorerというサイトで簡単に確認することができます。
今回はconst a = 1ASTの構造体にしてみました。

画面左側が実際の値、右側がASTの構造体になります。
これは acorn というミニマムな JavaScript の parser により生成されたものです。

スクリーンショット 2019-12-24 8.07.17.png

こちらがそのデータになります。

{
  "type": "Program",
  "start": 0,
  "end": 11,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 11,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

後ほど詳しく説明します。
今はASTはこのようなJSONなんだなというくらいの理解で大丈夫です。

Babel とは?

続いてBabelについて説明をします。
Babelとは JavaScript のコードを変換してくれる Compiler です。

元々Babel6 to 5という名称でした、その名の通り ES6 から ES5 にコードを変換するだけのものでした。
しかし、その後さまざまな要望を得た機能が実装され、現在のBabelという名称になりました。

Babelの機能

Babelは主に3つの機能を備えています。

  • 構文変換
  • Polyfill の提供
  • ソースコードの変換

Babelは上記のような機能をもって、ブラウザでは使用できない最新の機能を書いた JavaScript や TypeScript を、指定したブラウザでも使用できるようにコードを変換します。

例えば const a = () => {} のようなアロー関数は、@babel/plugin-transform-arrow-functions によって処理され、このようなコードになります。

https://babeljs.io/docs/en/babel-plugin-transform-arrow-functions
スクリーンショット 2019-12-24 8.25.37.png

イメージとしてはこのようになります。
スクリーンショット 2019-12-24 8.27.14.png

Babel はコードをどのように変換するのか?

Babelの変換にはこのように3つの段階があります。

  • Parsing
  • Transformation

  • Code Generat

図にするとこのようになります。
スクリーンショット 2019-12-24 8.32.30.png

実際に Babel Plugin を作りながら AST を学ぶ

ここからは実際にBabel Pluginを作りながらBabelがどのようにASTを駆使してコードを変換しているのか見ていきましょう。
今回はconstletをすべてvarに置き換えるものを作成します。

前準備

必要なパッケージを事前に落としておきます。

npm i -D @babel/parser @babel/generator @babel/traverse

1. Parse

@babel/parser を使用して、ソースコードをASTに変換しましよう。

// parser/index.js

const { parse } = require("@babel/parser");

// AST に変換
const ast = parse(`
const a = 1
`);

console.log(JSON.stringify(ast, null, 2));

このコードを実際に.jsonファイルに出力します。

node node parser/index.js

実際に出力されたものはこちらです。

{
  "type": "File",
  "start": 0,
  "end": 13,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 3,
      "column": 0
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 13,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 3,
        "column": 0
      }
    },
    "sourceType": "script",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 1,
        "end": 12,
        "loc": {
          "start": {
            "line": 2,
            "column": 0
          },
          "end": {
            "line": 2,
            "column": 11
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 7,
            "end": 12,
            "loc": {
              "start": {
                "line": 2,
                "column": 6
              },
              "end": {
                "line": 2,
                "column": 11
              }
            },
            "id": {
              "type": "Identifier",
              "start": 7,
              "end": 8,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 6
                },
                "end": {
                  "line": 2,
                  "column": 7
                },
                "identifierName": "a"
              },
              "name": "a"
            },
            "init": {
              "type": "NumericLiteral",
              "start": 11,
              "end": 12,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 10
                },
                "end": {
                  "line": 2,
                  "column": 11
                }
              },
              "extra": {
                "rawValue": 1,
                "raw": "1"
              },
              "value": 1
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

これでASTに変換することができました!

2. Generate

続いては @babel/generator を用いて先ほどASTにしたデータをソースコードに変換します。

// generator/index.js 

const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;

// ソースコードを AST に変換
const ast = parse(`
const a = 1
`);

// generate の第一引数に AST を格納
console.log(generate(ast).code);

このコードを実行してみましょう。

node generator/index.js        

実行結果はこのようになります。

const a = 1;

ASTに変更される前と変わりないコードが生成されました、これはASTになにも変更を加えずにコードに戻したからです。

3. Travers

ここから本題であるconstletをすべてvarに置き換える作業を行います。
もう一度1. Parseで吐き出したASTを見てましょう。

{
  "type": "File",
  "start": 0,
  "end": 13,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 3,
      "column": 0
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 13,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 3,
        "column": 0
      }
    },
    "sourceType": "script",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 1,
        "end": 12,
        "loc": {
          "start": {
            "line": 2,
            "column": 0
          },
          "end": {
            "line": 2,
            "column": 11
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 7,
            "end": 12,
            "loc": {
              "start": {
                "line": 2,
                "column": 6
              },
              "end": {
                "line": 2,
                "column": 11
              }
            },
            "id": {
              "type": "Identifier",
              "start": 7,
              "end": 8,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 6
                },
                "end": {
                  "line": 2,
                  "column": 7
                },
                "identifierName": "a"
              },
              "name": "a"
            },
            "init": {
              "type": "NumericLiteral",
              "start": 11,
              "end": 12,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 10
                },
                "end": {
                  "line": 2,
                  "column": 11
                }
              },
              "extra": {
                "rawValue": 1,
                "raw": "1"
              },
              "value": 1
            }
          }
        ],
        "kind": "const"
      }
    ],
    "directives": []
  },
  "comments": []
}

VariableDeclaration"kind": "const" という値があるのが分かります。
ここがかなり怪しいので、冒頭にも紹介した JavaScript の ASTの仕様に相当するEsTreeVariableDeclaration を検索してみましょう。
検索結果はこちら

このような検索結果コードが表示されています。

extend interface VariableDeclaration {
    kind: "var" | "let" | "const";
}

どうやらこのkindの値をvarにすればよさそうなので、これからASTを使って変換していきます。
今回は @babel/traverse を使用していきます。

// traverse/index.js

const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;

// AST を変換
const ast = parse(`
const a = 1
let b = 2
`);

// AST を第一引数に、変更内容を第二引数にする
traverse(ast, {
  // 変更したい部分(今回は VariableDeclaration )
  VariableDeclaration(path) { 
    // kind を var に変更
    path.node.kind = "var";
  },
});

// // generate の第一引数に AST を格納し、コードを生成
console.log(generate(ast).code);

これを実行します。

 node traverse/index.js

実行結果はこちらになります。

var a = 1;
var b = 2;

これで、constletvarに変更することができました。

ほかにもこのようにすることで値の変更もできます。

// traverse/index.js

const { parse } = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;

const ast = parse(`
const a = 1
let b = 2
`);

traverse(ast, {
  VariableDeclaration(path) {
    path.node.kind = "var";
  },
  VariableDeclarator(path) {
    if (path.node.id.name === "a") {
      path.node.id.name = "c";
      path.node.init.value = 3;
    }
  }
});

console.log(generate(ast).code);

実行。

 node traverse/index.js

実行結果。

var c = 3;
var b = 2;

このようにASTを駆使すれば正規表現では表現できないようなパターンも変更可能になります。

最後に

今回説明した部分は、Babel Pluginの肝となる部分になります。
ASTBabel Pluginの肝を理解すれば、 @babel/plugin-transform-arrow-functions のコード のようなプラグインがやっていることは案外簡単なことのように思えるかもしれません。

ASTを使って便利ツールを作っていきましょう!!

今回使用したソースコードはこちらのレポジトリにあります。
https://github.com/sakit0/babel-plugin-demo

44
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?