kintoneカスタマイズ開発におけるTypeScript導入の流れ

kintoneのカスタマイズ開発もモダン化傾向を感じる今日この頃ですが、ちょっと前に「[TypeScriptでkintoneカスタマイズ開発をしてみよう](https://developer.cybozu.io/hc/ja/articles/360023293091)」といった記事が掲載されました。この記事の中で言及されている`eslint`や`prettier`の導入と`@kintone/customize-uploader`を利用したカスタマイズのデプロイまでを含めた開発の準備をまとめてみました。

# 準備

## TypeScript関連パッケージのインストール

まずは、[記事](https://developer.cybozu.io/hc/ja/articles/360023293091)を参考にTypeScriptに関するパッケージをインストールします。

```sh
npm install -D @kintone/dts-gen \
               @babel/core @babel/plugin-proposal-class-properties \
               @babel/preset-env \
               @babel/preset-typescript \
               babel-loader \
               fork-ts-checker-webpack-plugin \
               typescript \
               webpack \
               webpack-cli
```

```sh
npm install -S @babel/polyfill # https://babeljs.io/docs/en/usage#polyfill
```

## ESLint、Prettier関連パッケージのインストール

続けて、`eslint`、`prettier`周辺のパッケージをインストールします。

```sh
npm install -D eslint @typescript-eslint/eslint-plugin \
               prettier eslint-config-prettier eslint-plugin-prettier
```

```sh
npm install -D @cybozu/eslint-config
```

`.eslintrc.json`は次のように準備します。この辺（[1](https://teppeis.hatenablog.com/entry/2019/02/typescript-eslint), [2](https://qiita.com/madono/items/a134e904e891c5cb1d20)）の内容ベースですが、`eslint:@cybozu/eslint-config`部分を修正したりしています。Prettierルールとの衝突等考慮点がありますが、まだ試行錯誤中です。

```json:.eslintrc.json
{
  "extends": [
    "@cybozu/eslint-config",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "env": {
    "browser": true,
    "node": true,
    "es6": true
  },
  "parserOptions": {
    "sourceType": "module"
  },
  "globals": {
    "kintone": false
  },
  "rules": {
    "indent": ["error", 2]
  }
}
```

インデント2で使いたいので`rules`にそれだけ指定しつつ、`.editorconfig`も生成しておきます。

```:.editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
```

## TypeScript、Webpack関連の設定ファイルの準備

この辺で、対象のkintoneアプリに合わせたTypeScriptの型定義を生成します。生成された`fields.d.ts`はソースを整理する`src`を作成して移動しておきます。

```sh
npx kintone-dts-gen --host https://xxx.cybozu.com \
                    -u user \
                    -p password \
                    --app-id 596
```

```
mkdir src; mv fields.d.ts src 
```

[記事](https://developer.cybozu.io/hc/ja/articles/360023293091)の通り、`tsconfig.json`、`babel.config.js`も準備します。

```json:tsconfig.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "esModuleInterop": true,
    "noUnusedLocals": true,
    "noImplicitAny": true,
    "declarationDir": "dist/types",
    "declaration": true,
    "target": "es5",
    "module": "es2015",
    "strict": true
  },
  "files" : [
    "./node_modules/@kintone/dts-gen/kintone.d.ts",
    "./src/fields.d.ts"
  ],
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "dist",
    "dest",
    "node_modules"
  ]
}
```

```js:babel.config.js
module.exports = function (api) {
    api.cache(true);
    const presets = [
        [
            "@babel/preset-env",
            {
              "useBuiltIns": "usage"
            }
        ],
        "@babel/typescript"
    ];
    const plugins = [
        [
            "@babel/proposal-class-properties"
        ]
    ];
    return {
        presets,
        plugins,
    }
}
```

`webpack.config.js`は次のように整えておきます。

```js:webpack.config.js
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  mode: "development",

  entry: {
    "customize" : "./src/customize.ts"
  },

  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
  },

  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.json']
  },

  module: {
    rules: [{ test: /\.(ts|js)x?$/, loader: 'babel-loader', exclude: /node_modules/ }],
  },

  plugins: [
    new ForkTsCheckerWebpackPlugin(),
  ]
};
```

## @kintone/customize-uploaderの設定

そして、`@kintone/customize-uploader`のインストールと初期設定を行います。

```sh
npm install -D @kintone/customize-uploader
```

```
$(npm bin)/kintone-customize-uploader init
```

```json:dest/customize-manifest.json
{
    "app": "596",
    "scope": "ALL",
    "desktop": {
        "js": [
            "dist/customize.js"
        ],
        "css": []
    },
    "mobile": {
        "js": [
            "dist/customize.js"
        ]
    }
}
```

## npm-run-allを利用したwatchの設定
TypeScript等を含めたビルドを担うwebpackのwatchと、その出力をアップロードするための`@kintone/customize-uploader`のwatchを共存するために、`@kintone/create-plugin`と同様に`npm-run-all`を導入し`scripts/npm-start.js`を準備します。

```sh
npm i -D npm-run-all
```

```js:scripts/npm-start.js
"use strict";
const runAll = require("npm-run-all");

runAll(["develop", "upload"], {
  parallel: true,
  stdout: process.stdout,
  stdin: process.stdin
}).catch(({results}) => {
  results
    .filter(({code}) => code)
    .forEach(({name}) => {
      console.log(`"npm run ${name}" was failed`);
    })
  ;
});
```

## package.jsonとフォルダ構成を確認

最後に`package.json`の`scripts`に後々の作業を楽にできるように幾つか足して、次のようにしておきます。`update:fields`はパスワード埋め込みになるので、ちょっと悩ましいところです。`start`ではtsファイルの変更で、webpackのバンドルからjsファイルのデプロイまで行えるようになります。

```json:package.json
{
  "name": "kintone-customize-bolierplate-ts",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "update:fields": "npx kintone-dts-gen --host https://xxx.cybozu.com -u user -p passowrd --app-id 596; mv fields.d.ts src",
    "start": "node scripts/npm-start.js",
    "upload": "kintone-customize-uploader --watch dest/customize-manifest.json",
    "develop": "npm run build -- --watch",
    "build": "webpack",
    "build:prod": "webpack --mode production",
    "lint": "eslint src/*"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.4.4",
    "@babel/plugin-proposal-class-properties": "^7.4.4",
    "@babel/preset-env": "^7.4.4",
    "@babel/preset-typescript": "^7.3.3",
    "@cybozu/eslint-config": "^4.0.1",
    "@kintone/customize-uploader": "^1.5.3",
    "@kintone/dts-gen": "^1.0.2",
    "@typescript-eslint/eslint-plugin": "^1.9.0",
    "babel-loader": "^8.0.6",
    "eslint": "^5.16.0",
    "eslint-config-prettier": "^4.3.0",
    "eslint-plugin-prettier": "^3.1.0",
    "fork-ts-checker-webpack-plugin": "^1.3.3",
    "npm-run-all": "^4.1.5",
    "prettier": "^1.17.1",
    "typescript": "^3.4.5",
    "webpack": "^4.31.0",
    "webpack-cli": "^3.3.2"
  }
}
```

```
❯ tree -L 1 -a
.
├── .editorconfig
├── .eslintrc.json
├── .git
├── .gitignore
├── babel.config.js
├── dest
├── dist
├── node_modules
├── package-lock.json
├── package.json
├── scripts/npm-start.js
├── src
├── tsconfig.json
└── webpack.config.js
```

以上で設定は終了です。

# kintoneカスタマイズコーディング

準備が整ったら、[記事](https://developer.cybozu.io/hc/ja/articles/360023293091)にあるように次のようにコーディングを進めましょう。

```ts:src/customize.ts
const HANDLE_EVENT = "app.record.create.show";
interface KintoneEvent {
    record: kintone.types.SavedFields;
}
kintone.events.on(HANDLE_EVENT, (event: KintoneEvent) => {
    event.record.単価.value = "1";
    event.record.ユーザー選択.value = [{name: "名前", code: "コード"}];
    return event;
});
```

# 所感

- kintoneの型が収められている`kintone.d.ts`が便利です！
    - `kintone.d.ts`は`tsconfig.json`を見ると`files`で`fields.d.ts`と共に`./node_modules/@kintone/dts-gen/kintone.d.ts`として指定されています
    - これだけ`reference`タグで読み込んで使うというのも便利です
- kintoneでよくある複数のアプリを対象にしたカスタマイズでは、`kintone-dts-gen`する際に`--namespace`で`fields.d.ts`を作り分けて対応すると良さそうです（[ドキュメント](https://kintone.github.io/dts-gen/docs/#/README)より）
- kintoneのカスタマイズ開発でもコード量が多くなることが見込まれるケースには、TypeScriptの利用は有効かと思いました。TypeScriptでの一般的なメリットをkintoneカスタマイズ開発でも享受できます
    - 逆に、コード量が増える見込みがないケースでは無理して使う必要もなさそうかなぁと思いました（ただ、`kintone.d.ts`の利用は幸せになれると思います）
    - 同様に、kintoneカスタマイズ初学者の方はTypeScript自体の導入・学習コストが高く感じられると思いますし、いきなりコード量が多いカスタマイズに挑むケースは考えづらいとすれば、従来のJavaScriptのカスタマイズに慣れてきて、Webpackを利用する開発が必要になったり、そのメリットが見えてくるケースに遭遇するようになったらTypeScript導入を検討頂くと良いのかなと思いました
- ESLint、Prettierの設定が良い感じなのかが自信なく、試行錯誤を続けたいと思います

# 参考にした情報

- [TypeScriptでkintoneカスタマイズ開発をしてみよう](https://developer.cybozu.io/hc/ja/articles/360023293091)
- [https://kintone.github.io/dts-gen/docs/#/README](https://kintone.github.io/dts-gen/docs/#/README)
- [@kintone/customize-uploader](https://github.com/kintone/customize-uploader)
- [@typescript-eslint ことはじめ/teppeis blog](https://teppeis.hatenablog.com/entry/2019/02/typescript-eslint)
- [@cybozu/eslint-config](https://github.com/cybozu/eslint-config)
- [VSCodeでESLint+@typescript-eslint+Prettierを導入する](https://qiita.com/madono/items/a134e904e891c5cb1d20)