JavaScript
AST
vue.js
babel
Vuex

vuexのボイラープレートをbabelで飛び越えろ! -> babel pluginを書いてコード変換してみた、2歩目

前書き

前回、コード変換自体は出来ましたが、ファイルに書き込むところができなかったので、今回はbabelを使い、

  • 自作したbabel pluginを用いてコード変換を実施

    • (なお、コード変換内容は前回のものから拡張させている)
  • 変換したコードをファイルに書き込む

というところまで実現させてみようと思います。

注意事項

なお、試行錯誤を繰り返しながらコードは書いたので、コード内にダメダメな箇所が散在していると思います。そのため実装内容自体は参考にならないということを先に書いておきます。

また、vue.js(とvuex)も、まだ触り始めて間もなく、理解できていない部分もあるため、間違いが混ざっているかもしれません。

vuexにおけるボイラープレート

vuexを使用して、vueを書く際はmutation-types.jsなどに操作を定義するなどして使用することが多いかと思います。

ここではindex.jsにこれらのimport文を記載しますが、index.jsにてimportした操作はmutation-types.jsを開いてコードをいちいち書かなくても自動的に記述してもらいたいです。
また、ここでimportしたものはactinosmutationsにも記述することになるので、そこの部分も自動生成させたいです。
(自動生成するプログラムの記述内容は、ひとまず定義した操作名称を元に生成し、決まった内容のテンプレートに落とし込むという形で実装を行っています。)

目指すゴール

index.jsに下記の一行を書くだけで対応する名称をbabelのコード変換の仕組みを使って生成したいと思います。

// input.js(変換対象ファイル)
import { CREATE, SELECT, UPDATE, DELETE } from './mutation-types';

babelで変換をかけたあとのコード

下ではstate,actions,mutations,getters,storeがbabel pluginによって自動生成されています。
(意図せぬ改行が入っていますが、内容に差異はないです。Babel側の整形によるものなのかな?)

// output.js(変換後のファイル)

import { CREATE, SELECT, UPDATE, DELETE } from './mutation-types';
const state = {};
const actions = {
  [CREATE]({
    commit
  }, state) {
    commit(CREATE, state);
  },

  [SELECT]({
    commit
  }, state) {
    commit(SELECT, state);
  },

  [UPDATE]({
    commit
  }, state) {
    commit(UPDATE, state);
  },

  [DELETE]({
    commit
  }, state) {
    commit(DELETE, state);
  }

};
const mutations = {
  [CREATE](state) {
    state = state;
  },

  [SELECT](state) {
    state = state;
  },

  [UPDATE](state) {
    state = state;
  },

  [DELETE](state) {
    state = state;
  }

};
const getters = {};
export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations
});

また、定義した操作名称に応じて、ファイル(mutation-types.js)にコードを自動生成。

// mutation-types.js
export const CREATE = "CREATE";
export const SELECT = "SELECT";
export const UPDATE = "UPDATE";
export const DELETE = "DELETE";

実際に書いたpluginのコード

下記のようなプラグインを書き、babel-preset-envを使って変換をかけて使用しています。
下は変換する前のコードです。

babel-plugin内でファイルに書き込みを行っていたり、コードが冗長で汚かったりで、行儀悪くてあまり参考にならないコードですが、求めていた変換処理はこちらで実現することが出来ました。

import template from 'babel-template'
import * as t from 'babel-types'
import generate from 'babel-generator'
import { writeFileSync } from 'fs'
import { join } from 'path'

// importリスト
let importNameList = []

// 生成用テンプレート
const builder = (code) => {
  return template(code, { sourceType: 'module' });
}

const newVuexBuilder = () => {
  const b = builder(`
  export default new Vuex.Store({
    state,
    getters,
    actions,
    mutations
  })
  `)
  return b()
}

const mutationTypesBuilder = (codeObj) => {
  const b = builder(`export const ACTION_NAME = ACTION_STR`)
  return b(codeObj)
}

const stateInitilize = () => {
  const b = builder(`
    const state = {
    }
    `)
  return b()
}

const gettersInitilize = () => {
  const b = builder(`
    const getters = {
    }`)
  return b()
}

const actionsInitilize = (actionsCount) => {
  let codes = []
  const method = `
      [ACTION_NAME] ({ commit }, state) {
        commit(ACTION_NAME, state)
      }
  `
  const beforeCode = "const actions = {"
  const afterCode = "}"

  for(let i = 0; i < actionsCount; i++) {
    codes.push(method)
  }
  const buildCode = beforeCode
                  + codes.join(',')
                  + afterCode

  const b = builder(buildCode)
  return b()
}

const mutationsInitilize = (actionsCount) => {
  let codes = []
  const method = `
      [ACTION_NAME] (state) {
        state = state
      }
  `
  const beforeCode = "const mutations = {"
  const afterCode = "}"

  for(let i = 0; i < actionsCount; i++) {
    codes.push(method)
  }
  const buildCode = beforeCode
                  + codes.join(',')
                  + afterCode

  const b = builder(buildCode)
  return b()
}

export default () => {
  return {
    visitor: {
      ImportDeclaration: path => {
        // 定義数
        const actionsCount = path.node.specifiers.length;

        // 書き込み先のファイル名(今回の場合、"./mutationsTypes.js")を取得しておく
        const filepath = join(__dirname, (path.node.source.value + ".js"))

        // mutationTypes内のコードを保持
        const mutationTypesCode = [];

        // new Vuexの生成
        path.insertAfter(newVuexBuilder())

        // vuexのテンプレートを記述
        path.insertAfter(gettersInitilize())
        path.insertAfter(mutationsInitilize(actionsCount))
        path.insertAfter(actionsInitilize(actionsCount))
        path.insertAfter(stateInitilize())

        path.node.specifiers.forEach((n) => {
          importNameList.push(n.imported.name)
          const buildAst = mutationTypesBuilder({
            ACTION_NAME: t.identifier(n.imported.name),
            ACTION_STR: t.StringLiteral(n.imported.name)
          });

          // mutationTypes.js用のコードを追加していく
          mutationTypesCode.push(generate(buildAst).code)
        })

        // "./mutation-types.js"に書き込み
        writeFileSync(filepath, mutationTypesCode.join("\n"))
      },

      // 書き出したactions, mutationsの内容を、定義した操作名称に書き換えていく
      VariableDeclaration: path => {
        path.node.declarations.forEach((n1) => {
          if(n1.id.name === 'actions') {
            path.node.declarations.forEach((d) => {
              d.init.properties.forEach((n, i) => {
                d.init.properties[i].key.name = importNameList[i]
                d.init.properties[i].body.body[0].expression.arguments[0].name = importNameList[i]
              })
            })
          }
          if(n1.id.name === 'mutations') {
            path.node.declarations.forEach((d) => {
              d.init.properties.forEach((n, i) => {
                d.init.properties[i].key.name = importNameList[i]
              })
            })
          }
        })
      }
    }
  }
}


.babelrcにこのpluginを使用するように設定し、あとはbabel {変換対象ファイル名} --out-file {変換後のファイル名}などのようにして実行します。

プロジェクト内にbabelをインストールしていて、CLIから直接叩いて実行する際は下記のようになるでしょうか

node_modules/.bin/babel {変換対象ファイル名} --out-file {変換後のファイル名}

.babelrcの記述
babel-plugin.jsというのが今回作成したコード

.babelrc
{
  "plugins": ["./babel-plugin.js"]
}

最後に

今回vuexのBoilerPlateをbabelで生成してみようとしましたが、実際にこの変換を実現させようとするとコード自体の品質以外にも、色々と考えなければならないところが出てきて、さらなる鍛錬が必要だと痛感しました。
(babel pluginには実際にどこまでの責務を与えるべきか、とか、vuexを使用したときのプロジェクトの構成とか、、、)
が、まずは希望した自動生成が実現できたことは、単純に嬉しいものでした。初めてコードを動かしたときのような、プリミティブな喜びです。
このあとはbabel関連のドキュメントを読みかえしたりしつつ、理解を深めていこうと思います。

あと、簡単JavaScript AST入門 - 東京ラビットハウス - BOOTH を購入して読んでいましたが、こちらの内容がめちゃくちゃ分かりやすくてとても良かったです。
babelのpluginとはなんぞや?という場所から始まった自分にとっては、欲しい情報がかなり盛り込まれており、荒れ狂った海上で遥か彼方に見える灯台的なポジションでありました。
何度も読み返しつつ、血肉としていきたい。

参照したドキュメントやポストなど

Babel types

BABEL - API

Babel Plugin Handbook

Babelで簡単に最適化プラグインを作ってみよう

babel@6プラグインの単純な作成方法について

vuex - Core Concepts