trivago 謹製の JavaScript UI library「Melody」を触ってみた

Melody is a library for building JavaScript web applications.

最近面白い記事を知った。

Melody - the sound of JavaScript for our Hotel Search

ざっくり見た感じ、Twig を parse して VDOM っぽいことをできるようにしているライブラリらしい。

ということは、Twig の lexer や parser がいるはずだろうと思って github を覗いてみたところ、多分こいつが肝心の parser のはず。

https://github.com/lidqqq/melody/tree/master/packages/melody-parser

perr も一緒に install して手元で readme にあるコードを実行してみる。

npm i -D melody-parser melody-types

const { parse } = require("melody-parser");

const code = "{% spaceless %} This is some Twig code {% endspaceless %}";
const abstractSyntaxTree = parse(code);

console.log(abstractSyntaxTree);

すると

Error: ERROR: Unknown tag "spaceless"
> 1 | {% spaceless %} This is some Twig code {% endspaceless %}
    |    ^^^^^^^^^

Expected a known tag such as

spacelss なんてタグねーぞと怒られる。readme に書かれてあって一番使われていそうなタグを解釈できないのは明らかにおかしいぞと思って Unknown tag で grep かけると以下が見つかった。

matchTag() {
        const tokens = this.tokens;
        const tagStartToken = tokens.la(-1);

        const tag = tokens.expect(Types.SYMBOL);
        let parser = this[TAG][tag.text];
        let isUsingGenericParser = false;
        if (!parser) {
            if (this.options.allowUnknownTags) {
                parser = this.getGenericParserFor(tag.text);
                isUsingGenericParser = true;
            } else {
                tokens.error(
                    `Unknown tag "${tag.text}"`,
                    tag.pos,
                    `Expected a known tag such as\n- ${Object.getOwnPropertyNames(
                        this[TAG]
                    ).join('\n- ')}`,
                    tag.length
                );
            }
        }

多分 this[TAG][tag.text] のへんで parser が見つけられなくてコケていそう。

次は this[TAG] で grep すると

addTag(tag) {
        this[TAG][tag.name] = tag;
        return this;
    }

絶対これやん。ここに入ってないやんこれ絶対。addTag で grep する。

applyExtension(ext) {
        if (ext.tags) {
            for (const tag of ext.tags) {
                this.addTag(tag);
~~~

そう言えば readme に ↓ みたいなのあったの思い出した。

There is also a third parameter called extensions. It collects the third parameter and all parameters that come after it, so you can pass as many extensions as you want:

npm i melody-extension-core -D

叩くと

Error: Cannot find module 'melody-traverse'

peerDependencies があるんやろな〜と思って覗くとめっちゃある

"peerDependencies": {
    "melody-idom": "^1.1.0",
    "melody-parser": "^1.1.0",
    "melody-runtime": "^1.1.0",
    "melody-traverse": "^1.1.0",
    "melody-types": "^1.1.0"
  },

全部入れる

npm i -D melody-idom melody-parser melody-runtime melody-traverse melody-types

:pray:

$ npm test
SequenceExpression {
  loc: {
    source: null,
    start: { line: 1, column: 0, index: 0 },
    end: { line: 1, column: 55, index: 57 }
  },
  expressions: [
    SpacelessBlock {
      loc: [Object],
      body: [Array],
      trimRightSpaceless: false,
      trimLeftEndspaceless: false,
      trimLeft: false,
      trimRight: false,
      tagNameLoc: [Object],
      [Symbol()]: []
    }
  ],
  [Symbol()]: []
}

これでひとまず動きそう。

parser の種類はここにあるものは動きそう。おおよそ PJ で使われているような major なものは揃っている感じがする。

面白いのが mount という Twig の doc には定義されていない parser があるが、これは melody が Twig file に JavaScript を埋め込むための独自構文を用意している。

Use mount statement to import components

https://melody.js.org/documentation/quickstart/rendering

<div id="app">
  {{ message }}
  {% mount './button.js' with {
    color: 'green',
    text: 'Increment'
  } %}
</div>

パッとみると .twig file に JS が埋め込まれるという悪魔のような実装だが(すでに JS 側でも .twig を import している)、こうすることで JS と Twig の境界線を無くして UI を構築できるので、資産の再利用性という意味ではかなりアツい実装だと思う。

思想として「twig から JS を生成」という流れなので、AST から twig file を generate するコードは見当たらなかった。

今回試したコードは ↓ かここで試せる。

npx degit "lidqqq/apps#hello-melody" my-app
cd my-app
npm ci
npm test