webpackを利用するkintoneプラグイン開発の流れ

kintoneプラグインの開発手法・ツールの利用については、最近色々とツール拡充がなされ、2015年の3月にサードパーティ開発が可能になって以来のちょっとした転換期がきているように思います。あくまでも**開発手法・ツール**についての話ですが・・・。
話が逸れましたが、例えばプラグインのパッケージング方法が `package.sh` から `@kintone/create-plugin` を用いた方法が今後メーカー推奨になるとの噂も出てきていますし、いわゆるモダンな開発方法というのも必要に応じて取り込んでいきたいものです。

kintoneのカスタマイズやプラグイン開発で webpack を利用されている方も多いと思いますが、プラグイン開発ツールとして webpack を適用するものがwebpackプラグインとして登場してきたので、今回はそれらを使った開発の流れを見ていきたいと思います。

# 大まかな流れ

1. `@kintone/create-plugin` でプラグインの開発に必要な雛形を展開する
2. `@kintone/webpack-plugin-kintone-plugin`等webpack経由でのプラグインパッケージングに必要なモジュールをインストールする
3. `webpack.config.js` を準備する
4. `manifest.json` を修正する
5. プラグインをパッケージングする

詳しく見ていきたいと思います。

## `@kintone/create-plugin` で雛形を展開する 
まずは、プラグイン開発に必要なフォルダ構成・ファイルの雛形を `@kintone/create-plugin` で展開します。後に利用する `@kintone/webpack-plugin-kintone-plugin` にはない部分（ファイル群の雛形が生成されるのも便利ですが鍵の生成が大きい）を補うために最初に実行します。

```
npx @kintone/create-plugin hello-webpack-plugin
```

設定のための対話を進め終えると、ここでは `hello-webpack-plugin` というフォルダが展開されますが、この時点での構成は次の通りです。

```
├── node_modules/
├── package-lock.json
├── package.json
├── private.ppk
├── scripts/
└── src/
```

`@kintone/create-plugin` によるwebpackは用いない従来のプラグイン開発が可能という状態です。

## webpack関連のモジュールの追加

次にwebpackを使ったパッケージングを行うために、`@kintone/webpack-plugin-kintone-plugin` とwebpackのバンドルに必要な `webpack`、 `webpack-cli` をインストールします。`@kintone/webpack-plugin-kintone-plugin`はwebpackのpluginとして提供されています。

```
npm install --save-dev webpack webpack-cli @kintone/webpack-plugin-kintone-plugin
```

この時点で、`package.json` は次のようになります。

```json:package.json
{
  "name": "hello-webpack-plugin",
  "version": "0.1.0",
  "scripts": {
    "start": "node scripts/npm-start.js",
    "upload": "kintone-plugin-uploader dist/plugin.zip --watch --waiting-dialog-ms 3000",
    "develop": "npm run build -- --watch",
    "build": "kintone-plugin-packer --ppk private.ppk --out dist/plugin.zip src",
    "lint": "eslint src"
  },
  "devDependencies": {
    "@kintone/plugin-packer": "^1.0.1",
    "@kintone/plugin-uploader": "^2.2.0",
    "@kintone/webpack-plugin-kintone-plugin": "^1.0.7",
    "eslint": "^4.19.1",
    "eslint-config-kintone": "^1.3.0",
    "npm-run-all": "^4.1.3",
    "webpack": "^4.28.2",
    "webpack-cli": "^3.1.2"
  }
}
```

## `webpack.config.js` を準備する

次に、webpackの設定ファイルである`webpack.config.js` を準備します。

- `entry` は`@kintone/create-plugin`で生成されたファイルを利用することにします。
- `output` は`./src/js`配下に`dist`フォルダを設けて、そこに出力するようにします（これがプラグインのパッケージング対象になります）。
- `plugins` では`@kintone/webpack-plugin-kintone-plugin`に必要なパラメータをセットしますが、`manifestJSONPath`、`privateKeyPath`は`@kintone/create-plugin`で生成されたファイルパスを指定します。

```js:webpack.config.js
const path = require("path");
const KintonePlugin = require("@kintone/webpack-plugin-kintone-plugin");

module.exports = {
    mode: 'development',
    entry: {
        desktop: './src/js/desktop.js',
        config: './src/js/config.js'
    },
    output: {
        path: path.resolve(__dirname, 'src', 'js', 'dist'),
        filename: '[name].js'
    },
    plugins: [
        new KintonePlugin({
            manifestJSONPath: './src/manifest.json',
            privateKeyPath: './private.ppk',
            pluginZipPath: './dist/plugin.zip'
        })
    ]
};
```

## `manifest.json`を修正する

最後のステップです。webpackでバンドルされたJSファイルをプラグインのパッケージングの設定ファイルである`manifest.json`に指定します。今回だと`js/dist/desktop.js`といったパスになります。

```json:manifest.json
{
  "manifest_version": 1,
  "version": 1,
  "type": "APP",
  "desktop": {
    "js": [
      "https://js.cybozu.com/jquery/3.3.1/jquery.min.js",
      "js/dist/desktop.js"
    ],
    "css": [
      "css/51-modern-default.css",
      "css/desktop.css"
    ]
  },
  "icon": "image/icon.png",
  "config": {
    "html": "html/config.html",
    "js": [
      "https://js.cybozu.com/jquery/3.3.1/jquery.min.js",
      "js/dist/config.js"
    ],
    "css": [
      "css/51-modern-default.css",
      "css/config.css"
    ],
    "required_params": [
      "message"
    ]
  },
  "name": {
    "en": "hello-webpack-plugin"
  },
  "description": {
    "en": "hello-webpack-plugin"
  }
}
```

# パッケージング

ここまでの準備が大変なのですが、これでやっとwebpackのバンドル経由でプラグインのパッケージングが可能になります。`webpack.config.js`を配置した階層で`webpack`を実行すれば、`webpack.config.js`で指定した`pluginZipPath`の階層にプラグインのZIPファイルが生成されます。production用のコマンドは次の通りです。

```
$(npm bin)/webpack --mode production
```

`package.json`の`scripts`は`@kintone/create-plugin`に適した形のままですので、次のようにwebpack利用が簡単になるよう自分で修正するといいと思います。

```json:package.json
{
  "name": "hello-webpack-plugin",
  "version": "0.1.0",
  "scripts": {
    "upload": "kintone-plugin-uploader dist/plugin.zip --watch --waiting-dialog-ms 3000",
    "watch": "webpack --mode development --watch",
    "build:dev": "webpack --mode development",
    "build:prod": "webpack --mode production",
    "lint": "eslint src"
  },
  "devDependencies": {
    "@kintone/plugin-packer": "^1.0.1",
    "@kintone/plugin-uploader": "^2.2.0",
    "@kintone/webpack-plugin-kintone-plugin": "^1.0.7",
    "eslint": "^4.19.1",
    "eslint-config-kintone": "^1.3.0",
    "npm-run-all": "^4.1.3",
    "webpack": "^4.28.2",
    "webpack-cli": "^3.1.2"
  }
}
```

これで、例えば`$(npm bin)/webpack --mode production`は`npm run build:prod`で実行できるようになります。

ちなみに、プラグインファイルは、`webpack.config.js`で指定した通り`./dist/plugin.zip`として生成されています。

# おまけ - ES6やReactを使いたい時
kintoneは業務システムとして利用されることが多い訳で、ES6やReactを利用したい場合にはwebpack時にBabelによるトランスパイルも実行されるようにして、IE11等モダンでないブラウザでも利用できるようにしておくのがいいでしょう。

## ES6

まずは、ES6への対応を見てみましょう。まずは`babel-loader`の[GitHubページ](https://github.com/babel/babel-loader)を参考に、トランスパイルに必要なモジュールをインストールしましょう。webpackもそうですが、Babelも結構アップデート頻度ありそうなので、バージョンへの意識が必要そうです。執筆時点では、`webpack 4.x | babel-loader 8.x | babel 7.x`です。

```
npm install --save-dev babel-loader @babel/core @babel/preset-env
```

これに伴い、`webpack.config.js`の`module.rules`に次のようにルールの追加を行います。

また、`resolve.extensions`も追加しておきます。これによって、別ファイルを`import`する際に拡張子を省略できます。

```js:webpack.config.js
const path = require("path");
const KintonePlugin = require("@kintone/webpack-plugin-kintone-plugin");

module.exports = {
    mode: 'development',
    entry: {
        desktop: './src/js/desktop.js',
        config: './src/js/config.js'
    },
    output: {
        path: path.resolve(__dirname, 'src', 'js', 'dist'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    },
    resolve: {
        extensions: ['.js']
    },
    plugins: [
        new KintonePlugin({
            manifestJSONPath: './src/manifest.json',
            privateKeyPath: './private.ppk',
            pluginZipPath: './dist/plugin.zip'
        })
    ]
};
```

## React

まず、Reactの利用に必要なモジュールをインストールします。

```
npm install --save react react-dom
```

また、ES6の時と同じようにBabelはじめトランスパイルに必要なモジュールをインストールします。React用の`@babel/preset-react`も今回必要になります。

```
npm install --save-dev babel-loader @babel/core @babel/preset-react
```

`webpack.config.js`の`module.rules`に次のようにルールの追加を行います。

```js:webpack.config.js
const path = require("path");
const KintonePlugin = require("@kintone/webpack-plugin-kintone-plugin");

module.exports = {
    mode: 'development',
    entry: {
        desktop: './src/js/desktop.js',
        config: './src/js/config.js'
    },
    output: {
        path: path.resolve(__dirname, 'src', 'js', 'dist'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-react']
                    }
                }
            }
        ]
    },
    resolve: {
        extensions: ['.js', '.jsx']
    },
    plugins: [
        new KintonePlugin({
            manifestJSONPath: './src/manifest.json',
            privateKeyPath: './private.ppk',
            pluginZipPath: './dist/plugin.zip'
        })
    ]
};
```

~~**ただし、`npm run build:dev`はファイルサイズが512kBを超えてエラーを吐きました。**~~
[追記] 2019年6月のアップデートで20MBまで拡張され、React等のライブラリも導入しやすくなりました（512kB制限で出ていた下記のエラーメッセージはそのまま残しておきます）。

```
Error: ".desktop.js[1]" should match format "https-url", ".desktop.js[1]" file size should be <= 512KB, ".desktop.js[1]" should match some schema in anyOf
    at validateManifest (/home/vagrant/workspace/hello-webpack-plugin/node_modules/@kintone/plugin-packer/src/zip.js:142:15)
    at preprocessToRezip.then (/home/vagrant/workspace/hello-webpack-plugin/node_modules/@kintone/plugin-packer/src/zip.js:36:7)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
```

~~`npm run build:prod`であれば収まりましたが、Reactまでを入れて1ファイルにバンドルするのはちょっと無理があるようです（"まとめ"で考察します）。~~

## ES6とReactを併用する場合

ここまでの合わせ技になりますが、慣れてないと`webpack.config.js`の書き方に迷う可能性があるので、一通り流していきましょう。

まずはモジュールのインストールです。

```
npm install --save react react-dom
npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-react
```

`webpack.config.js`の`module.rules`への追記は次のようになります。

```js:webpack.config.js
const path = require("path");
const KintonePlugin = require("@kintone/webpack-plugin-kintone-plugin");

module.exports = {
    mode: 'development',
    entry: {
        desktop: './src/js/desktop.js',
        config: './src/js/config.js'
    },
    output: {
        path: path.resolve(__dirname, 'src', 'js', 'dist'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react']
                    }
                }
            }
        ]
    },
    resolve: {
        extensions: ['.js', '.jsx']
    },
    plugins: [
        new KintonePlugin({
            manifestJSONPath: './src/manifest.json',
            privateKeyPath: './private.ppk',
            pluginZipPath: './dist/plugin.zip'
        })
    ]
};
```

以上がES6とReactの併用時の準備になります。

# まとめ
webpackプラグインである`@kintone/webpack-plugin-kintone-plugin`を用いて、webpackを利用したkintoneプラグインの開発の流れについて見てきました。

恐らく`plugin-packer`の機能だと思いますが、プラグインのパッケージング時に`manifest.json`に記述がないファイルはプラグインファイルから除外されるようです（`package.sh`は関係ないファイルもフォルダ中のファイルが内包されていた）。
プラグイン構成ファイルの上限である ~~512kB~~ 20MB制限を満たしているかもこの時チェックしているようです。
~~Reactのセクションでバンドルファイルが512kB超える可能性が高いお話をしましたが、ファイル容量の超過に対する方策は主に3つかと思います。~~
[追記] 2019年6月のアップデートで20MBまで拡張され、React等のライブラリも導入しやすくなりましたので、バシバシ活用すると良いかと思います。

- ~~プラグインファイルに内包するのではなくてCDNから配信する~~
- ~~webpackのファイル分割（`optimization.splitChunks`）を行う（試してません）~~
- ~~`externals`を利用して依存モジュールを外だし分割する~~

~~いずれの方法もこの開発の流れの延長で取れる方策だと思いますので、必要に応じて検討することになりそうです。~~

いずれにせよ、全体的な結論としてwebpackを利用するkintoneプラグインの開発は今回の流れが現状最適と言えそうです。

# 参考にしたサイト等
- [create-plugin - GitHub](https://github.com/kintone/create-plugin)
- [webpack-plugin-kintone-plugin - GitHub](https://github.com/kintone/webpack-plugin-kintone-plugin)
- [Babel公式サイト > Presets](https://babeljs.io/docs/en/presets)
- [babel-loader - GitHub](https://github.com/babel/babel-loader)
- [webpack公式サイト > Module](https://webpack.js.org/configuration/module/)