0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

acorn-jsxを使うとacornでJSXのトークン化ができるらしいのでオリジナルJSXトランスパイラーを作ってみた

Last updated at Posted at 2025-04-25

acorn-jsxを使えばオリジナルJSXトランスパイラーを作れると思ったのでやってみます。
これはbun向けに作られていますが、bunをnpmに入れ替えたりtsxを使えばnpmなどでもできるはずです。

環境

WSL version: 2.4.13.0
Kernel version: 5.15.167.4-1
WSLg version: 1.0.65
MSRDC version: 1.2.5716
Direct3D version: 1.611.1-81528511
DXCore version: 10.0.26100.1-240331-1435.ge-release
Windows version: 10.0.19045.5796
Bun 1.2.10

準備

$ bun init

✓ Select a project template: Blank

 + .gitignore
 + index.ts
 + tsconfig.json (for editor autocomplete)
 + README.md

To get started, run:

    bun run index.ts

bun install v1.2.10 (db2e7d7f)

+ typescript@5.8.3
+ @types/bun@1.2.10

5 packages installed [1144.00ms]

$ bun i acorn acorn-jsx escodegen
bun add v1.2.10 (db2e7d7f)

installed acorn@8.14.1 with binaries:
 - acorn
installed acorn-jsx@5.3.2
installed escodegen@2.1.0 with binaries:
 - esgenerate
 - escodegen

3 packages installed [17.00ms]

$ bun i -D @types/escodegen
bun add v1.2.10 (db2e7d7f)

installed @types/escodegen@0.0.10

1 package installed [259.00ms]

書いてみよう!

1. AST(抽象構文木)の出力

index.ts
import { Parser } from "acorn";
import jsx from "acorn-jsx";

const jsxParser = Parser.extend(jsx());

const ast = jsxParser.parse(`<Bar/>`, {ecmaVersion: "latest"});

console.log(JSON.stringify(ast))

これの結果をJSON Beautifyしたものはこちら

{
  "type": "Program",
  "start": 0,
  "end": 6,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 6,
      "expression": {
        "type": "JSXElement",
        "start": 0,
        "end": 6,
        "openingElement": {
          "type": "JSXOpeningElement",
          "start": 0,
          "end": 6,
          "attributes": [],
          "name": {
            "type": "JSXIdentifier",
            "start": 1,
            "end": 4,
            "name": "Bar"
          },
          "selfClosing": true
        },
        "closingElement": null,
        "children": []
      }
    }
  ],
  "sourceType": "script"
}

とこんな感じにJSXElementという新しいタイプが出来てます。
これをうまく入れ替えればいいのです。

2. とりあえすJSXElementを通常のトークンに変換

で、ここがJSXトランスパイルをするわけですが、どう変換するかです。
とりあえず自分はサイズを追求する人なので
<AAA name="tanaka">BBB{"C"}</AAA>AAA.name("tanaka")("BBB","C")というようにしたいです。
誰も分からないと思いますがTNTSuperMan/Rjs /src/seg.tsみたいにしようと思います。

トークンをどう書くかわからない人は、型をたよりにしたり実際にacornで変換して調べてください。

その型についてなんですが、typescriptJsxElementあるやんと思ってたらacornと形式が違くて書き直すはめになりました。

jsxType.ts
import type { Expression, Node } from "acorn";

export interface JSXText extends Node{
    type: "JSXText",
    value: string,
    raw: string
}

export interface JSXExpressionContainer extends Node{
    type: "JSXExpressionContainer"
    expression: Expression
}

export type JSXChild =
    JSXText |
    JSXElement |
    JSXExpressionContainer | 
    JSXFragment;

export interface JSXIdentifier extends Node{
    type: "JSXIdentifier"
    name: string
}

export interface JSXAttribute extends Node{
    type: "JSXAttribute"
    name: JSXIdentifier
    value: Expression
}

export interface JSXOpeningElement extends Node{
    type: "JSXOpeningElement"
    name: JSXIdentifier
    attributes: JSXAttribute[]
    selfClosing: boolean
}

export interface JSXClosingElement extends Node{
    type: "JSXClosingElement"
    name: JSXIdentifier
}

export interface JSXElement extends Node{
    type: "JSXElement"
    openingElement: JSXOpeningElement
    closingElement: JSXClosingElement | null
    children: JSXChild[]
}

export interface JSXOpeningFragment extends Node{
    type: "JSXOpeningFragment"
    attributes: JSXAttribute[]
    selfClosing: boolean
}

export interface JSXClosingFragment extends Node{
    type: "JSXClosingFragment"
}

export interface JSXFragment extends Node{
    type: "JSXFragment"
    openingFragment: JSXOpeningFragment
    closingFragment: JSXClosingFragment
    children: JSXChild[]
}

そしてトランスパイルするコードです。

jsx2token.ts
import type { CallExpression, Expression, Node, SpreadElement } from "acorn";
import type { JSXChild, JSXElement } from "./jsxType";

function JSXChild2Token(token: JSXChild): (Expression|SpreadElement)[]{
    switch(token.type){
        case "JSXElement": return [JSXEl2Token(token)];
        case "JSXExpressionContainer": return [token.expression];
        case "JSXFragment": return token.children.map(e=>JSXChild2Token(e)).flat();
        case "JSXText": return [{
            ...token,
            type: "Literal"
        }]
    }
}

export function JSXEl2Token(token: JSXElement): Expression{
    console.log(token.children.map(e=>JSXChild2Token(e)))
    const jsxT: CallExpression = {
        type: "CallExpression",
        callee: { ...token.openingElement.name, type: "Identifier" },
        arguments: token.children.map(e=>JSXChild2Token(e)).flat(),
        optional: false,
        start: token.start,
        end: token.end
    };
    token.openingElement.attributes.forEach(e=>{
        jsxT.callee = {
            type: "CallExpression",
            arguments: [e.value] as any as Expression[],
            optional: false,
            start: e.value.start,
            end: e.value.end,
            callee: {
                type: "MemberExpression",
                object: jsxT.callee,
                property: { ...e.name, type: "Identifier" },
                computed: false,
                optional: false,
                start: e.name.start,
                end: e.name.end
            }
        }
    })

    return jsxT;
}

がんばってかきました。つかれますた。(放心状態)

3. 最終形態 トランスパイル後のコードを出力するモジュール

index.ts
import { Parser } from "acorn";
import jsx from "acorn-jsx";
import { JSXEl2Token } from "./jsx2token";
import { generate } from "escodegen";

const jsxParser = Parser.extend(jsx());

export const parseJSX = (code: string) => {
    
    const ast = jsxParser.parse(code, {ecmaVersion: "latest", sourceType: "module"});

    const json = JSON.stringify(ast, (_, v)=>
        v && typeof v == "object" && v.type == "JSXElement" ?
            JSXEl2Token(v) : v
    )

    const transpiledAST = JSON.parse(json);

    const result = generate(transpiledAST);
    return result;
}

JSXのトークン生成を分離したおかげでスッキリです。
構文木をすべて検索するのは大変なので、JSON.stringifyの第二引数でオブジェクト内のすべてを置換できるのですごく便利でした。

感想

JSXの出力形式を構文木の形式として書くのが大変でした
しかし構文木として書くことで様々な形式のコードに柔軟に対応できるでしょう。

まあ自分で0からJSXを認識するコードを書くよりはましですね。

結論

  • acorn+acorn-jsxでJSXをAST化できる
  • ASTを上手く操作してescodegenにASTを渡せばオリジナルJSXトランスパイラーを(比較的)簡単に作れる

作ったもの

今回作ったものに色々したものがこちらです。ぜひ参考にしてください。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?