webpack + babelを使ってRPGツクールMVのプラグインを書いてみた その2

筆者はツクールシリーズは完全な初心者で、Javascriptは少し書いたことがある程度の趣味プログラマですので、誤りや不十分な点もあるかと思います。お気づきの点があればご指摘頂ければ幸いです。

# 作業環境
[RPGツクールMV 1.3.4](https://tkool.jp/mv/)
Windows10 64bit
node 7.3.0
npm 3.10.10

# 前回までのあらすじ
[その1](http://qiita.com/betto/items/91e1bdbc13c093e3ed94)
webpack+babelを導入してES2015・複数ファイルで書いておいてプラグインにビルドできるようになったけど、プラグインコメントが埋もれて困る。

# 今回やったこと
プラグインコメントを一番上に持ってくるようにした。Mocha、ESLint、ESDocを導入した。
[github](https://github.com/yatricia/mv-webpack-babel-sample)

# プラグインコメント対策
**webpackの仕組みの理解が十分でないので、この項目は他と比べても非常に怪しいです。**

ビルドの最後にプラグインコメントを正規表現で抜き出して移動するwebpackプラグインを書きました。

```javascript:
'use strict'
var ConcatSource = require('webpack-core/lib/ConcatSource')

// RPGツクールMVのプラグイン用のコメントをファイルの先頭に移動する
class TkoolMvCommentPlugin {
    apply(compiler) {
        compiler.plugin('emit', (compilation, callback) => {
            for (let key of Object.keys(compilation.assets)) {
                const source = compilation.assets[key].source()
                compilation.assets[key] = new ConcatSource(...this.splitHeader(source), '')
            }

            callback()
        })
    }

    // sourceを受け取ってプラグインコメントを抜き取る
    // 戻り値: header 抜き取ったプラグインコメント
    //         source 元のsourceからプラグインコメントを削除したもの
    splitHeader(source) {
        const header = source.match(/\/\*:([\s\S]+?)\*\//gm).join('\n').replace(/^\t/gm, '') + '\n'
        source = source.replace(/\/\*:([\s\S]+?)\*\//gm, '')
        return [header, source]
    }
}

module.exports = TkoolMvCommentPlugin
```

使うようにします。

```diff:webpack.config.js（差分）
+ const TkoolMvCommentPlugin = require('./lib/tkool-mv-comment-plugin.js')
（中略）
+  plugins: [
+    new TkoolMvCommentPlugin()
+  ],
   resolve: {
     extensions: ['', '.js']
   }
 }
```

出来た気がします。一応、`/*:`と`/*:jp`の両方ある場合や、プラグインを複数ビルドした場合にも動くことを確認していますが、まずそうなところを見つけたら教えてください。

# ツールの追加
せっかくnpmを使う環境を整えたので、他のツールもガンガン使っていきたいです。

## Mocha
ツクールのコアスクリプトと絡んでくるといろいろ難しそうですが、前回でいうCatクラスのような独立性の高い部分は簡単にテストが書けますので書いておくと安心できそうです。

インストールします。ライブラリはMocha + Chaiを選択しました。
`npm i -D mocha chai`

タスクを定義します。
testディレクトリを作って、ソースと対応した場所に置くことにします。

```diff:package.json（差分）
   "scripts": {
+    "test": "mocha test/**/*.test.js --compilers js:babel-register",
     "build": "webpack"
   },
```

Babelの設定をファイルに書いておきます。

```:.babelrc
{
  "presets": [
    "es2015",
  ],
}
```

テストを書いてみます。
importのfrom部分には式が使えないのでこうしてるんですが、何かいい方法ってあるんでしょうか。

```js:test/CatGreetingPlugin/Cat.test.js
import { expect } from 'chai'
const Cat = require(__dirname.replace('test', 'src') +'/Cat.js').default

describe('Cat', function() {
    let cat, name, message
    beforeEach(function() {
        name = 'ねこのなまえ'
        message = 'ねこのメッセージ'
        cat = new Cat(name, message)
    })

    describe('constructor', function() {
        it('渡した名前が設定される', function() {
            expect(cat.name).to.eq(name)
        })

        it('渡したメッセージが設定される', function() {
            expect(cat.message).to.eq(message)
        })

        describe('メッセージを渡さなかった場合', function() {
            it('メッセージとして"にゃー"が設定される', function() {
                cat = new Cat(name)
                expect(cat.message).to.eq('にゃー')
            })
        })
    })

    describe('#greet', function() {
        it('名前とメッセージに応じた挨拶を返す', function() {
            expect(cat.greet()).to.eq(`${name}: ${message}`)
        })
    })
})
```

わざとテストが落ちるようにしておきましょう。

```diff:Cat.js（差分）
    constructor(name, message = 'にゃー') {
-        this.name = name
-        this.message = message
+        //this.name = name
+        //this.message = message
    }
```

`npm run test`とやると、それぞれのテストケースで期待した値と違う旨が報告されますね。

```
  Cat
    constructor
      1) 渡した名前が設定される
      2) 渡したメッセージが設定される
      メッセージを渡さなかった場合
        3) メッセージとして"にゃー"が設定される
    #greet
      4) 名前とメッセージに応じた挨拶を返す


  0 passing (88ms)
  4 failing

  1) Cat constructor 渡した名前が設定される:
     AssertionError: expected undefined to equal 'ねこのなまえ'
      at Context.<anonymous> (C:/Users/XXXX/Documents/tkool/cat-greeting/test/CatGreetingPlugin/Cat.test.js:14:33)

  2) Cat constructor 渡したメッセージが設定される:
     AssertionError: expected undefined to equal 'ねこのメッセージ'
      at Context.<anonymous> (C:/Users/XXXX/Documents/tkool/cat-greeting/test/CatGreetingPlugin/Cat.test.js:18:36)

  3) Cat constructor メッセージを渡さなかった場合 メッセージとして"にゃー"が設定される:
     AssertionError: expected undefined to equal 'にゃー'
      at Context.<anonymous> (C:/Users/XXXX/Documents/tkool/cat-greeting/test/CatGreetingPlugin/Cat.test.js:24:40)

  4) Cat #greet 名前とメッセージに応じた挨拶を返す:

      AssertionError: expected 'undefined: undefined' to equal 'ねこのなまえ: ねこのメッセージ'
      + expected - actual

      -undefined: undefined
      +ねこのなまえ: ねこのメッセージ

      at Context.<anonymous> (C:/Users/XXXX/Documents/tkool/cat-greeting/test/CatGreetingPlugin/Cat.test.js:31:36)
```

Cat.jsを元に戻してもう一回`npm run test`とやると、全てパスしました。OKです。

```
  Cat
    constructor
      √ 渡した名前が設定される
      √ 渡したメッセージが設定される
      メッセージを渡さなかった場合
        √ メッセージとして"にゃー"が設定される
    #greet
      √ 名前とメッセージに応じた挨拶を返す


  4 passing (78ms)
```

## ESLint

インストールします。
`npm i -D eslint eslint-loader babel-eslint`

設定ファイルを書きます。ルールについてはeslint:recommendedをextendsしておけばとりあえず最低限OKなのではないかと。

```.eslintrc
{
  "parser": "babel-eslint",
  "env": {
    "browser": true,
    "es6": true,
    "node": true,
  },
  "ecmaFeatures": {
    "modules": true
  },
  "extends": "eslint:recommended",
  "rules": {
    "indent": 2,
    "comma-spacing": 2,
    "space-before-blocks": 2,
    "keyword-spacing": 2,
    "quotes": [2, "single"],
    "arrow-parens": [2, "always"],
    "arrow-spacing": 2,
  },
  "parserOptions": {
    "sourceType": "module",
  },
}
```

ビルドの際にチェックしてくれるようにします。
尚、個別に実行する場合は`.\node_modules\.bin\eslint ファイル名`でOKです。しょっちゅう使う場合はpackage.jsonに書いておけばいいと思います。

```diff:package.json（差分）
   module: {
+    preLoaders: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        loader: 'eslint-loader',
+      }
+    ],
     loaders: [
```

試しにビルドしてみます。
`npm run build`

```
ERROR in ./src/CatGreetingPlugin/CatGreetingPlugin.js

C:\Users\XXXX\Documents\tkool\cat-greeting\src\CatGreetingPlugin\CatGreetingPlugin.js
  25:41  error  'Game_Interpreter' is not defined  no-undef
  26:1   error  'Game_Interpreter' is not defined  no-undef
  29:9   error  '$gameMessage' is not defined      no-undef

✖ 3 problems (3 errors, 0 warnings)
```

定義していない変数として指摘されました。コアスクリプトで定義されるグローバル変数なので、CatGreetingPlugin.jsに外部で定義したグローバル変数を使う旨をコメントで追加します。
`/* globals Game_Interpreter, $gameMessage */`

出なくなりました。OKです。
（その1で始め書いていたコードはインデントが2文字になっているところがあってそれも指摘されました。現在は記事は修正済みなのでコピペした場合出ないかと思います。）

## ESDoc
インストールします。
`npm i -D esdoc`

設定ファイルを書きます。

```.esdoc.json
{
  "source": "./src",
  "destination": "./doc"
}
```

タスクを定義します。

```diff:package.json（差分）
   "scripts": {
+    "doc": "esdoc",
     "test": "mocha test/**/*.test.js --compilers js:babel-register",
     "build": "webpack"
   },
```

使ってみます。`npm run doc`
doc以下にいろいろ生成されるのでindex.htmlを見てみます。基本的にexportされているクラスのみが入るようです。
![ws_00000.JPG](https://qiita-image-store.s3.amazonaws.com/0/162022/9a8a9af8-87ae-0864-efb3-85033da42d78.jpeg)

Cat.jsにドキュメンテーションコメントを追加します。

```js:Cat.js
/**
 * 1匹の猫を表現するクラス
 */
export default class Cat {
    /**
     * nameとmessageを持つ猫を生成する
     * @param {string} name 新しい猫の名前
     * @param {string} [message = 'にゃー'] 新しい猫の挨拶メッセージ
     */
    constructor(name, message = 'にゃー') {
        /**
         * 猫の名前
         * @type {string}
         */
        this.name = name
        /**
         * 猫の挨拶メッセージ
         * @type {string}
         */
        this.message = message
    }

    /**
     * 挨拶文字列を得る
     * @return {string} 挨拶文字列
     */
    greet() {
        return `${this.name}: ${this.message}`
    }
}
```

もう一度`npm run doc`してindex.htmlを見てみます。書いた内容が反映されています。
![ws_00001.JPG](https://qiita-image-store.s3.amazonaws.com/0/162022/5acbe611-f81f-ebc3-53db-120b9b8c5bad.jpeg)

# まとめ
ツクールMVのプラグインをモダンなツールを使って開発できるようになりましたが、広く配布するプラグインを作る場合には成果物のjsが読みにくいのが問題になります。
自分自身で（プログラム的に）大きなゲームを作るとき向けかもしれません。その場合、コアスクリプトも含めて一つのjsにまとめてしまうほうがいいかもしれませんね。