Vue CLI と kintone CLI の共存を探る（TypeScript 編）

`Vue CLI` でセットアップしたプロジェクトで `kintone CLI` を利用したカスタマイズ環境を作る目論見です。
どうにかこうにか形になりました。
ざっくり書いていますので読みづらいかも知れません。

こちらは TypeScript 編です。
JavaScript 編は以下をご覧ください。

[Vue CLI と kintone CLI の共存を探る（JavaScript 編）](https://qiita.com/iShinkai/items/5b9fb55518c47dc03e99)

# 目的

`Vue CLI` で作られるプロジェクトの整然さと `kintone CLI` で行えるカスタマイズの開発やデプロイの簡易性を両立させる。
`TypeScript` での開発を可能とし、`@kintone/rest-api-client` を利用する。
`kintone UI Component` は諦める。（`React` でしか動かないので）
場合により `Vuetify` の利用を検討する。

フォーマッティング・lint に関しては以下の方針。
`Vetur` はフォーマットを実行させず、シンタックスハイライトなどの役割に専念。
`eslint` は静的解析のみ担当。コードフォーマットはしない。
コードフォーマットは `Prettier` が実行。

# 前提

`Vue CLI` および `kintone CLI` がグローバルインストールされている事。

```bash
npm install -g @vue/cli
npm install -g git://github.com/kintone/kintone-cli.git
```

# プロジェクト作成手順

## Vue.js プロジェクトをセットアップする

`Vue CLI` でプロジェクトを開始する。

```bash
vue create vue-cli-kintone-cli-ts
```

以下の選択肢で進める。

`? Check the features needed for your project:`

- Babel
- TypeScript
- Vuex
- CSS Pre-processors
- Linter / Formatter
- Unit Testing

`? Use class-style component synytax?` **Yes**
`? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)?` **Yes**
`? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):` **Sass/SCSS (with dart-sass)**
`? Pick a linter / formatter config:` **ESLint + Prettier**
`? Pick additional lint features:` **Lint on save**
`? Pick a unit testing solution:` **Jest**
`? Where do you prefer placing config for Babel, ESLint, etc.?` **In dedicated config files**

## kintone CLI でアプリを作成する

作成されたプロジェクトを VS Code で開き、ターミナルを起動する。
`kintone CLI` でアプリをセットアップする。

```bash
kintone-cli create-template
```

`? What type of app you want to create ?` **Customization**
`? Do you want to set authentication credentials ?` **Yes**
`? What is your kintone domain ?` **(サブドメイン).cybozu.com**
`? What is your kintone username ?` **(ユーザー名)**
`? What is your kintone password ?` **(パスワード)**
`? Do you use proxy ?` **No**
`? Do you want to use React ?` **No**
`? Do you want to use TypeScript ?` **Yes**
`? Do you want to use webpack ?` **Yes**
`? What is the entry for webpack ?` **main.ts**
`? What is the app name ?` **app**
`? Do you want to use @cybozu/eslint-config for syntax checking ?` **No**
`? What is the app ID ?` **(アプリ ID)**
`? What is the scope of customization ?` **ALL**

`app` ディレクトリの下にソースが作成される。

## 開発サーバーをインストールする

この手順だとローカルプレビュー用の `local-web-server` がインストールされないため、手動で追加する。
現時点（2020 年 6 月）での最新版は `4.2` だが、このバージョンでは正しく動作しない模様。
`kintone-cli` でインストールされるものと同じバージョンの `2.6.1` をインストールする。

```bash
yarn add -D local-web-server@2.6.1
```

## その他の関連ツールをインストールする

`core-js` が二重に入っている。
いったんアンインストールして最新版を追加。

```bash
yarn remove core-js
yarn add core-js
```

`@kintone/rest-api-client`, `@kintone/dts-gen` をインストールする。

```bash
yarn add @kintone/rest-api-client
yarn add -D @kintone/dts-gen
```

## ESLint・Prettier の設定

以下を参考にする。
[続・VSCode 上で vue ファイルに対して ESLint と Prettier が快適に動作する設定](https://qiita.com/fukasawah/items/03fa205353ac03b06b7e)
[VS Code で ESLint × Prettier のベストかも知れないプラクティス](https://qiita.com/iShinkai/items/6f65b042618b76525659)

```bash
yarn add -D eslint-config-prettier
```

プロジェクトルート直下の `.eslintrc.js` を修正

```javascript
module.exports = {
  extends: [
    // --- 省略
    "prettier",
  ],
};
```

プロジェクトルート直下に `.eslintignore` ファイルを作成。
`dist` フォルダを追跡から除外する。

```text:.eslintignore
**/dist
```

標準のフォーマッティングを無効化し、`Prettier` が全て担当するようにする。
`.vscode/settings.json` に設定を加える。

```json:.vscode/settings.json
{
  "javascript.format.enable": false,
  "typescript.format.enable": false,
  "vetur.format.enable": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}
```

`.prettierrc` を作る。
設定はお好みで。

```json:.prettierrc
{
  "semi": false,
  "singleQuote": true
}
```

## ディレクトリ構成

プロジェクトルート直下の `src` 以下のファイルを `app/source` に移動。
`app/tests/unit` フォルダを作成。
残った `src` と `public`、`tests` は不要なので削除。

`app/source/main.ts` をとりあえず以下のようにする

```typescript:app/source/main.ts
import Vue from "vue";
import App from "./App.vue";
import store from "./store";

Vue.config.productionTip = false;

kintone.events.on("app.record.index.show", (event) => {
  console.log("Hello from kintone CLI");

  const elem: Element = kintone.app.getHeaderSpaceElement() as Element;
  new Vue({
    store,
    render: (h) => h(App),
  }).$mount(elem);

  return event;
});
```

## tsconfig.json の設定

プロジェクトルート直下と `app` の下に存在する。
プロジェクトルート直下の設定に寄せる。

`app` の下にある記述をプロジェクトルート直下の `tsconfig.json` にマージする。
この際 `include` や `paths`、`typeRoots` のパスを調整する。
最終的には以下の通りとなる。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "typeRoots": ["node_modules/@types", "**/source/*.d.ts"],
    "types": ["webpack-env", "jest"],
    "paths": {
      "@/*": ["~/*"]
    },
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  "include": [
    "**/source/**/*.ts",
    "**/source/**/*.tsx",
    "**/source/**/*.vue",
    "**/tests/**/*.ts",
    "**/tests/**/*.tsx"
  ],
  "exclude": ["node_modules"]
}
```

`app` 下の `tsconfig.json` は削除ないしリネーム。

## babel の設定

ビルドするために `@babel/plugin-proposal-decorators` のインストールが必要。

```bash
yarn add -D @babel/plugin-proposal-decorators
```

プロジェクトルート直下に `babel.config.js` が、 `app` の下に `.babelrc` が存在する。
プロジェクトルート直下の `babel.config.js` に `app/.babelrc` の内容をマージする。
以下のようになる。

```javascript:babel.config.js
module.exports = {
  plugins: [
    [
      "@babel/plugin-proposal-decorators",
      {
        legacy: true,
      },
    ],
    [
      "@babel/plugin-proposal-class-properties",
      {
        loose: true,
      },
    ],
    "@babel/plugin-syntax-dynamic-import",
  ],
  presets: [
    "@vue/cli-plugin-babel/preset",
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage",
        corejs: {
          version: 3,
          proposals: true,
        },
      },
    ],
  ],
};
```

その後 `app/.babelrc` は削除ないしリネーム。

## Webpack の設定

`.vue` ファイルをビルドするために `vue-loader`, `vue-template-compiler`, `ts-loader`, `url-loader`, `file-loader` をインストールする。
また `webpack` の設定は開発ビルドとプロダクションビルドで設定を変えたいので、`webpack-merge` もインストールする。
加えて、プロダクションビルドではミニファイも行うので、 `terser-webpack-plugin` もインストールする。

```bash
yarn add -D vue-loader vue-template-compiler ts-loader url-loader file-loader webpack-merge terser-webpack-plugin
```

`webpack` の設定は `app` 以下のものを使用する。
`app/webpack.config.js` を `app/webpack.common.js` にリネームし、内容を以下のようにする。

```javascript:app/webpack.common.js
const path = require("path");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  entry: path.resolve(__dirname, "./source/main.ts"),
  resolve: {
    extensions: [".ts", ".tsx", ".vue", ".js"],
  },
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: `${path.basename(__dirname)}.min.js`,
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: {
          loader: "vue-loader",
          options: {
            loaders: {
              js: {
                loader: "babel-loader",
              },
            },
          },
        },
      },
      {
        test: /.ts?$/,
        exclude: /node_modules/,
        use: {
          loader: "ts-loader",
          options: {
            transpileOnly: true,
            appendTsSuffixTo: ["\\.vue$"],
            happyPackMode: false,
          },
        },
      },
      {
        test: /.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.(jpe?g|png|gif|svg|ico)(\?.+)?$/,
        use: {
          loader: "url-loader",
          options: {
            esModule: false,
            limit: 10000,
            fallback: {
              loader: "file-loader",
              options: {
                name: "img/[name].[ext]",
              },
            },
          },
        },
      },
    ],
  },
  performance: {
    maxEntrypointSize: 10000000,
    maxAssetSize: 10000000,
  },
  plugins: [new VueLoaderPlugin()],
};
```

開発ビルドの設定 `app/webpack.dev.js` は以下の通りにする。

```javascript:app/webpack.dev.js
const merge = require("webpack-merge");
const common = require("./webpack.common.js");

module.exports = merge(common, {
  mode: "development",
});
```

プロダクションビルドの設定 `app/webpack.prod.js` は以下の通りにする。

```javascript:app/webpack.prod.js
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = merge(common, {
  mode: "production",
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: { compress: { drop_console: true } },
      }),
    ],
  },
});
```

新たに `app/webpack.config.js` を作り、以下のようにする。
このファイルでビルドターゲットを判定して、適切なファイルをロードするようにする。

```javascript:app/webpack.config.js
module.exports = (env, argv) => {
  const mode = argv.mode.includes("development") ? "dev" : "prod";
  return require(`./webpack.${mode}.js`);
};
```

## Jest の設定

特にない。
必要に応じてプロジェクトルート直下の `jest.config.js` を編集すればよい。

## package.json の設定

コマンドの入力を簡略化するため `scripts` を以下のようにする。
webpack の設定ファイルもここでコントロールする。

```json:package.json
  "scripts": {
    "dev": "ws --port ****",
    "serve-app": "kintone-cli dev --app-name app --watch --localhost",
    "devel-app": "webpack --mode development --config app/webpack.config.js",
    "build-app": "webpack --mode production --config app/webpack.config.js",
    "deploy-app": "kintone-cli deploy --app-name app",
    "test:unit": "jest"
  },
```

`ws --port ****` は任意のポート番号。
以下の記事で解説している。
[kintone CLI をざっくり試す](https://qiita.com/iShinkai/items/304f0345e12d69d2e0a7#%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%83%9D%E3%83%BC%E3%83%88-8000-%E5%95%8F%E9%A1%8C)

## .gitignore の設定

これまでの流れで `app/auth.json` は含まれているはず。
`app/dist` も弾くようにする。
その上で、今後仮に `app` が増えても大丈夫なようにする。

以下の通り修正。

```text:.gitignre
*/auth.json
*/dist
```

`.vscode/settings.json` は共有したいので、`.vscode` の行は削除する。

ここまでで開発準備は終了。
あとはひたすらに開発に注力する。

# 開発フェーズ

## 開発・テスト

実際の開発は `app/source/` 以下のファイルに対して行っていく事になる。

開発モード（ローカルサーバ）で起動

```bash
yarn serve-app
```

ローカルサーバへの参照が kintone アプリに適用され、テストが行えるようになる。

単体テストは以下のように実行。

```bash
yarn test:unit
```

テストファイルは `app/tests/unit/` 以下に `**.spec.ts` のファイル名パターンで記述する。

## ビルド

```bash
yarn build-app
```

`app` フォルダの下に `dist` フォルダが作成されファイルがビルドされる。
ビルドしたファイルを手動で運用環境に適用したい場合に使う。
`webpack.prod.js` では `TerserPlugin` によりミニファイと `console` の除去が行われる。

## デプロイ

```bash
yarn deploy-app
```

ビルドとデプロイを同時に実行するコマンド。
ビルドが実行され、終了後に運用環境にデプロイされる。

# 2 つめ以降のアプリのカスタマイズを作る場合

プロジェクトが複数のアプリカスタマイズで構成される場合、いくつかアプローチがある。

## kintone-cli create-template を使う

`kintone CLI` 的には王道パターンではある。

```bash
kintone-cli create-template --app-name (新しいアプリ名)
```

と実行し、カスタマイズを新規で作成する。

作成されたフォルダに、既存のアプリカスタマイズのファイルをかぶせてやる。（要微修正）

この方法だと改めて依存モジュール類を再取得しに行く動作をするため、時間が掛かる上にせっかく整えた `package.json` がまた汚れてしまう。
（`core-js` が二重になってしまうなど）
従って、決して良いやり方ではないと思われるが、以下の方法の方が簡単。

## 既存のアプリをコピーする

既に作成済みのアプリカスタマイズの構成ファイルをコピーして整えていくパターン。
以下、作成済みのアプリカスタマイズを `app`、 新しいアプリカスタマイズを `second-app` として説明する。

### フォルダ・ファイル構成

プロジェクトルート直下に新しいフォルダ `second-app` を作成する。
その下に、`source` フォルダ、 `tests` フォルダを作成する。
`second-app/source/` 以下に必要に応じて下記フォルダを作る。

- assets
- components
- css
- js
- store

`app/` の以下のファイルを `second-app/` フォルダにコピーする。

- auth.json
- config.json
- webpack.common.js
- webpack.config.js
- webpack.dev.js
- webpack.prod.js

`app/source/` の以下のファイルを `second-app/source/` フォルダにコピーする。

- global.d.ts
- shims-vue.d.ts
- shims-tsx.d.ts

### 認証情報

新しいアプリカスタマイズの認証情報が異なる場合は `second-app/auth.json` を編集する。

### アプリカスタマイズ情報

`second-app/config.json` を開き、以下の部分を編集する。

```json:second-app/config.json
{
  "appID": **, // カスタマイズを適用するアプリID
  "appName": "second-app",
  "type": "Customization",
  "scope": "ALL",
  "uploadConfig": {
    "desktop": {
      "js": ["second-app/dist/second-app.min.js"],
      "css": []
    },
    "mobile": {
      "js": ["second-app/dist/second-app.min.js"]
    }
  }
}
```

基本的には編集前に `app` となっていたものを `second-app` に置き換えれば良い。

### package.json の修正

`package.json` の `scripts` にコマンドを追加する。

作成済みの `serve-app`, `devel-app`,`build-app`,`deploy-app` をコピーし、`app` の部分を `second-app` に変えてやる。
合わせると以下の通りになる。

```json:package.json
  "scripts": {
    "dev": "ws --port 8800",
    "serve-app": "kintone-cli dev --app-name app --watch --localhost",
    "devel-app": "webpack --mode development --config app/webpack.config.js",
    "build-app": "webpack --mode production --config app/webpack.config.js",
    "deploy-app": "kintone-cli deploy --app-name app",
    "serve-second-app": "kintone-cli dev --app-name second-app --watch --localhost",
    "devel-second-app": "webpack --mode development --config second-app/webpack.config.js",
    "build-second-app": "webpack --mode production --config second-app/webpack.config.js",
    "deploy-second-app": "kintone-cli deploy --app-name second-app",
    "test:unit": "jest"
  },
```

あとは元のアプリカスタマイズを参考に `main.ts`、`App.vue`、`components/**.vue` 等を作り込んでいく。

**それなりに作業が多くオペミスが起こりやすいので注意が必要。**

# Vuetify を利用する場合

`Vue CLI` でインストールし、その成果を `app` 以下に移してやると言う流れ。
**※この流れでは IE11 で正しく動作しない。IE11 が要件に入らない場合のみ採用できる。**

```
vue add vuetify
```

インストールが始まる。
道中でプリセットの選択を求められる。
デフォルトで良い。

`? Choose a preset:` **Deafult (recommended)**

`public/index.html` が存在していないと最後にエラーが出るが、インストール自体は正常に行われているので問題ない。

プロジェクトルート直下の `src` 以下にファイルが作成される。
`src/App.vue` を `app/source/App.vue` に上書き。
`src/components/HelloWorld.vue` を `app/source/components/HelloWorld.vue` に上書き。
`src/assets/Logo.svg` を `app/source/assets/` に移動。
`src/plugins` フォルダを `app/source/` に移動。
`src` フォルダは不要なので削除。

`app/plugins/vuetify.ts` を以下のように修正。

```typescript:app/plugins/vuetify.ts
import "@mdi/font/css/materialdesignicons.css";
import Vue from "vue";
import Vuetify from "vuetify/lib";

Vue.use(Vuetify);

export default new Vuetify({
  icons: {
    iconfont: "mdi",
  },
});
```

`app/source/main.ts` を以下のように修正する。

```typescript:app/source/main.ts
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
import vuetify from "./plugins/vuetify";

Vue.config.productionTip = false;

kintone.events.on("app.record.index.show", (event) => {
  console.log("Hello from kintone CLI");

  const elem: Element = kintone.app.getHeaderSpaceElement() as Element;
  new Vue({
    store,
    vuetify,
    render: (h) => h(App),
  }).$mount(elem);

  return event;
});
```

`app/webpack.coomon.js` に以下の記述を追加。

```javascript:app/source/webpack.common.js
const VuetifyLoaderPlugin = require("vuetify-loader/lib/plugin");

module.exports = {
  // 省略
  module: {
    rules: [
      // 省略
      // CSS に対するルールの次に記述する
      {
        test: /\.s(c|a)ss$/,
        use: [
          'vue-style-loader',
          'css-loader',
          {
            loader: 'sass-loader',
            options: {
              implementation: require('sass'),
              sassOptions: {
                fiber: require('fibers'),
                indentedSyntax: true // optional
              }
            }
          }
        ]
      },
      // 省略
      // いちばん下に追加する
      {
        test: /\.(woff|woff2|eot|ttf)(\?.+)?$/,
        use: {
          loader: 'url-loader',
          options: {
            esModule: false,
            limit: 1000000
          }
        }
      }
    ]
  }
  plugins: [
    new VueLoaderPlugin(), // 既に記述済み
    new VuetifyLoaderPlugin(),
  ],
};
```

追加のライブラリをインストールする。

```bash
yarn add -D vue-style-loader fibers deepmerge @mdi/font
```

これで `Vuetify` が表示できるようになる。

# 雑感

`Vue CLI` と `kintone CLI` を良い感じに共存させようと言う考えから始めたが、あとでライブラリを追加したり多くの設定ファイルに手を入れたりファイルを移動させたりと、結局のところ両者の良さを良い感じに活かすと言うよりはどうにかこうにか妥協点を見出したと言う形になったように思う。
とは言え、`Vue.js` の環境をゼロから（`Vue CLI` を使わずに）作成するのは今となってはもう面倒この上ないし、`kintone CLI` のコマンド一発でデプロイできる便利さは開発工数の削減に効果があるのは間違いなく、無理っくり感が否めない構成とは言えテンプレート化できたのは十分なメリットをもたらすのではなかろうか。

※今回の成果は近日中に GitHub にアップする予定です。

# 参考

[VS Code に Prettier・ESLint・Stylelint を導入してファイル保存時にコードを自動整形させる方法](https://wemo.tech/3307)
[続・VSCode 上で vue ファイルに対して ESLint と Prettier が快適に動作する設定](https://qiita.com/fukasawah/items/03fa205353ac03b06b7e)
[VS Code で ESLint × Prettier のベストかも知れないプラクティス](https://qiita.com/iShinkai/items/6f65b042618b76525659)
[kintone CLI をざっくり試す](https://qiita.com/iShinkai/items/304f0345e12d69d2e0a7)