TypeScriptのコード品質を高めるnpmパッケージ構成を試した(ESLint+Prettier+CSpell+npm-run-all+VSCode)

# 概要
TypeScriptの開発環境を整備していくうえで、ある程度必要そうなチェックが楽にできる環境が出来上がったので記事にします。

### 環境
- VSCode
- TypeScript
- Node(npm利用のため)
- npm(yarnでも可)


## 利用する拡張機能・パッケージの紹介
|名称|説明|
|---|---|
|ESLint|静的コード分析ツール|
|Prettier|自動フォーマットツール|
|CSpell|打ち間違い検出ツール|
|npm-run-all|複数のnpm scriptをまとめて実行|

### ESLint
javasciptの開発を行う際に、ソースコードが決められた構文に沿っているかをチェックするためのツールとして提供されています。  
(このようなプログラムを静的に解析して問題点を発見するツールを***リンター Linter***と呼びます)  
ECMAScriptというJavaScriptの標準仕様は年１のペースでバージョンが上がっているため最新の仕様に対応できているかを人の目だけで監視するのは困難です。  
また、チーム内で定めたコーディング規約についても命名規則が合っているかをコードレビュー内で確認するのは手間がかかります。  
ESLintはこれらの悩みを解決します。

### Prettier
綺麗にフォーマットされたコードは可読性を高め、扱いやすいソースコードとなります。
また、複数人で開発する際のフォーマットの仕方を統一することでフォーマットずれによる差分の抑制となります。

### CSpell
キーボードでの英単語の打ち間違いの事をタイポ(typo)とも呼びます。  
ソースコード上の変数名や関数名のタイポはエンドユーザーに見える部分ではありませんが、それでもシステムの保守性を高めるならタイポ無しを目指すべきです。
また、APIの項目名やテーブルの列名などはタイポが原因でエラーが発生する事もあります。  
それに対しエラーが発生した際に気づくよりは打ち間違いをしたタイミングで気づけるのが好ましいです。

### npm-run-all
npmにはnpmスクリプトという、CLIで実行するようなコマンドを定義できる機能があります。
例えばビルドコマンドやテストの実行などをコマンドで行う場合に、npmスクリプト機能を用いれば毎回同じコマンドを打ち直す必要は無くなります。
`npm run [scriptname]`
の形式で実行することが可能です。

但しデフォルトのままでは1個の`npm run [scriptname]`で実行できるコマンドは1個なのでシェルスクリプトなどを作成しそれを呼び出すといった手間が発生します。  
npm-run-allはnpmスクリプトの仕組みの中で複数のコマンドを実行するための仕組みが備わっています。

## ゴール
コマンド１つで
- JavaScriptやTypeScriptの規約チェック
- プロジェクト内のコーディング規約チェック
- スペルチェック
- TypeScriptファイルのコンパイルによるエラーチェック
- デバック開始

までが行えて、かつ開発時に随時フォーマットと規約チェックが行われる状態を目指します。

最終的なプロジェクトの中身は以下の画像の通りになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/3fb519cc-1f13-0b22-4439-2dcc24d623fc.png)


## 構築手順
1. プロジェクト用のフォルダ作成
1. package.jsonの生成
1. npmパッケージのインストール
1. ESLint設定ファイルの作成
1. Prettier設定ファイルの作成
1. CSpell用ファイルの作成
1. VSCodeの設定ファイルの作成
1. package.jsonにnpmスクリプトを定義
1. 動作確認

### プロジェクト用のフォルダ作成

まず`.vscode`フォルダを直下に作成します。
この中には現在開いているプロジェクト内にのみ影響のある設定を残すことが出来ます。
VSCodeの設定はユーザ単位での設定とプロジェクトごとの設定の2種類があり、同じ設定項目に対しユーザごとの設定とプロジェクトに対する設定の2種類が存在する場合は、プロジェクトの設定が優先されたはずです。
そのためチームで開発する際にVSCode関連の設定の統制を取りたい場合はこのフォルダごとソース管理をする事で可能となります。

次に`src`フォルダを作成します。
ここは名前の通りソースコードを格納するフォルダです。また、`__test__`というフォルダを作成していますがこちらはJestなどでテストをする際のテストコードを格納するフォルダとなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/858dd29a-2ada-1ae1-07bb-437f258319d3.png)

### VSCode拡張機能のインストール

設定は後ほど行いますので、まずは以下３つの拡張機能をインストールまででお願いいたします。

ESLint

https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint

Prettier

https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode

Code Spell Checker

https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker

### package.jsonの作成
:::note warn
npmは導入完了しているものとして進めます。
:::

`npm init`コマンドを実行したら後はひたすらエンターを押して、最後の確認で`yes`と打てばpackage.jsonが出来上がります。

```shell-session
$ npm init
...
package name: (typescript_project) 
version: (1.0.0)                                                                                                                                                                                      
description:                                                                                                                                                                                          
entry point: (index.js)                                                                                                                                                                               
test command:                                                                                                                                                                                         
git repository:                                                                                                                                                                                       
keywords:                                                                                                                                                                                             
author:                                                                                                                                                                                               
license: (ISC)                                                                                                                                                                                        
About to write to C:...\typescript_project\package.json:

{
  "name": "typescript_project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this OK? (yes) yes
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/e60d9bdd-82b8-1ebb-65c7-8f9f99c3214d.png)

mainの項目がindex.jsになっているなど直すところはありますが、一旦先に進みます。

### npmパッケージのインストール

#### コマンド一覧
TypeScript本体
```bash
npm install typescript
```
ESLint本体＋TypeScript検証用パッケージ
```bash
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
```
Prettier本体＋ESLintとPrettierを連携させるためのパッケージ
```bash
npm install -D prettier eslint-config-prettier
```
CSpell本体
```bash
npm install -D cspell
```
npm-run-all本体
```bash
npm install -D npm-run-all
```

加えてお好みでJestを入れたりTypeScriptで利用するフレームワークを入れたりしてください。

###  ESLint設定ファイルの作成
直下に`.eslintrc.json`ファイルを作成します。
今回は
- ES2022の規約に基づいたチェック
- 変数名などの命名規則のチェック

をESLintで行います。
全ての設定の説明は省略します。

```eslintrc.json
{
  "root": true,
  "env": {
    "browser": true,
    "es2022": true
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2022
  },
  "plugins": ["@typescript-eslint"],
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "rules": {
    "@typescript-eslint/naming-convention": [
      "error",
      {
        "selector": "variable",
        "modifiers": ["const"],
        "format": ["strictCamelCase", "UPPER_CASE"]
      },
      {
        "selector": "variable",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "function",
        "format": ["strictCamelCase"],
        "leadingUnderscore": "allow"
      },
      {
        "selector": "parameter",
        "format": ["camelCase"]
      },
      {
        "selector": "class",
        "format": ["StrictPascalCase"]
      },
      {
        "selector": "accessor",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "typeAlias",
        "format": ["StrictPascalCase", "UPPER_CASE"]
      },
      {
        "selector": "method",
        "format": ["strictCamelCase"]
      },
      {
        "selector": "interface",
        "format": ["StrictPascalCase"]
      }
    ]
  }
}

```

extendsに設定している`"eslint:recommended", "plugin:@typescript-eslint/recommended"`が
ESLintが推奨している規約チェックを行うといった設定内容になっています。
また、rulesには個別のルールが設定できるので、その中の一つである命名規則関連のルールを細かく定義しています。
詳しくは公式ドキュメントなどをご参照ください。

https://eslint.org/docs/latest/use/getting-started

https://typescriptbook.jp/tutorials/eslint


###  Prettier設定ファイルの作成
直下に`.prettierrc.json`ファイルを作成します。
作成が出来たら以下公式ドキュメントなどを参考に適用したいフォーマットの設定を記述します。
色々調べて考えた結果
- printWidthは120程度、但しPrettierの仕様的に改行がいまいちと感じる場合はもっと長めに設定する
- インデントはデフォルトと同じスペース2文字
- デフォルトはダブルクォーテーションだがシングルクォーテーションの方が派閥が多い
- 改行コードはLinuxかWIndowsかで違うところで、かつデフォルトはLFなのでWindowsユーザはCRLFにする
- それ以外はデフォルトでも良さそう

という結論に至りました。

```prettierrc.json
{
  "printWidth": 120,
  "singleQuote": true,
  "endOfLine": "crlf",
  "overrides": [
    {
      "files": "*.vue",
      "options": {
        "printWidth": 240
      }
    }
  ]
}

```

:::note info
overridesを用いるとファイルの形式ごとに設定を分けることが出来ます。
この書き方の場合vueコンポーネントではprintWidthが240となります。
Vueの場合template部分にHTMLとかを書く関係で120でも短いと感じたため、値を倍にしています。
:::


https://prettier.io/docs/en/options.html

###  CSpell用ファイルの作成
.vscodeフォルダ配下に`cspell.json`ファイルを作成します。
例えば`Enshu(遠州)`のような固有名詞が出た場合、もちろんこれは英単語ではないのでスペルチェックに引っかかります。
しかしこれはスペルミスではない為辞書に登録することでエラーを回避します。
`cspell.json`ファイルはこの辞書を管理する役目を果たします。

例えばこのような変数があったとして、青線の部分がタイポではないかと指摘されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/d0336f0d-80ce-f448-93a6-5195fbc1e548.png)

青線にカーソルを当てるとクイックフィックスが選択可能なので、その中から「Enshuをcspell.jsonに追加」を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/d3b51b97-10c4-aa50-3842-38e04868fb1e.png)

エラーが消えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/da6bd5a3-9643-b0b0-839c-796339d22cbe.png)

ユーザー側の辞書ではなくプロジェクトごとのフォルダにワードを登録するメリットとしては、これもまたチーム内で同じ辞書を使うことによる統一ができることとなります。
反面、複数のプロジェクトがある場合に何度も同じワードを辞書に登録する必要がありますが、cspell.jsonの中身をコピーすればその手間は省けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/57ea41f7-d8f2-cacc-e23d-499d29e948de.png)

### TypeScriptコンパイル用のファイル作成
`npx tsc --init`コマンドで`tsconfig.json`ファイルを作成します。

```shell-session
$ npx tsc --init

Created a new tsconfig.json with:                                                                                       
                                                                                                                     TS 
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig

```

完了するとやけにコメントアウトがたくさんな設定ファイルが直下に出来上がります。
本来であれば作成するプロジェクトごとに必要な項目を精査して設定ファイルを作りますが今回は割愛します。
strictを指定して厳格なチェックを行うのが一般的で、targetやmoduleの指定をどうするかをちゃんと考えるのが大事です。
※targetはコンパイル成功時にどのバージョンのjavascriptで出力するかを示す設定値となります。

```tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,

    "strict": true ,
    "skipLibCheck": true ,

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/*.ts", "src/**/*.ts", "src/**/*.d.ts"]
}
```

https://typescriptbook.jp/reference/tsconfig/tsconfig.json-settings

###  VSCodeの設定ファイルの作成
.vscodeフォルダ配下に`setting.json`ファイルを作成します。
ここには主にESLintやPrettierをファイル保存時に実行できるような設定を記述します。
下の方にあるインデントをスペース2個に強制する設定は恐らく書かなくてもPrettierで自動フォーマットしてくれるので問題ないです。
`editor.formatOnSave`がセーブ時にフォーマットを自動実行
`editor.formatOnPaste`がテキストの貼り付け時にフォーマットを自動実行
`editor.formatOnType`が文字入力を行った行のフォーマットを自動実行
の設定項目なので全てTrueにし、都度Prettierによるフォーマットが行われるようにしています。
また、`editor.codeActionsOnSave`ではESLintによるチェックをセーブのたびに行えるように設定しています。
```setting.json
{

  "editor.formatOnSave": true, 
  "editor.formatOnPaste": true, 
  "editor.formatOnType": true, 
  "editor.defaultFormatter": "esbenp.prettier-vscode", 
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true 
  },
  "editor.insertSpaces": true,
  "editor.indentSize": 2
}
```

### package.jsonにnpmスクリプトを定義
現在はpackage.jsonにscriptsという項目に
```json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
```
このような内容が入っていると思います。
これを
```json
"scripts": {
    "pipeline:debug": "run-s cspell lint:report compile debug",
    "pipeline:build": "run-s cspell lint:report compile build",
    "compile": "tsc",
    "cspell": "cspell src/**/*",
    "lint": "eslint src",
    "lint:report": "eslint src --format=html > eslint_result.html",
    "debug": "echo debug start!!",
    "build": "echo build success!!"
  }
```
に書き換えてください。
`pipeline:debug`はデバッグ実行する際に実行するコマンド
`pipeline:build`はビルドする際に実行するコマンドを想定しています。
デバッグ実行やビルドを行う前に
1. スペルチェック
1. ESLintによるチェック
1. コンパイルによるエラーチェック

を行い、途中でエラーが発生したら終了する仕組みにしています。
`pipeline:○○`のコマンドの中に`run-s`とありますが、これがnpm-run-allの機能の1つです。
`run-s`の後ろに指定したnpmスクリプトを順番に実行してくれます。
こうすることで開発者は`npm run pipeline:debug`や`npm run pipeline:build`を使うことさえ意識すれば開発時にチェックしてもらいたい諸々をツールたちが自動で確認して問題ないと判断したうえでデバッグ実行やビルドを行うフローが出来上がります。
なお、ESLintはWARNINGレベル以下のみの場合はエラーとして途中終了しない為、WARNINGレベルで必ず対応してほしいチェック項目が存在する場合はエラーレベルまで引き上げるように.eslintrc.jsonのrulesに記載する必要があります。


### 設定完了後の再起動
ここまで終わったらVSCodeを再起動してください。

###  動作確認
①打ち間違いが存在する場合
拡張機能によりPROBLEMSにお知らせしてくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/db1e320b-3569-cd70-6d7b-f83066fddf2b.png)

対応せずにデバッグ実行を実施した場合はエラーとして弾かれて途中で終了します。

```shell-session
$ npm run pipeline:debug

> typescript_project@1.0.0 pipeline:debug
> run-s cspell lint:report test compile debug


> typescript_project@1.0.0 cspell
> cspell src/**/*

1/2 .\src\__test__\hogehoge.spec.ts 655.99ms
2/2 .\src\hogehoge.ts 33.58ms X
C:...\typescript_project\typescript_project\src\hogehoge.ts:1:11 - Unknown word (Hoge)
CSpell: Files checked: 2, Issues found: 1 in 1 files
ERROR: "cspell" exited with 1.
```

②ES2022の規約に沿っていなかったり、命名規則が間違っている場合
試しにvarを使用したり、本来キャメルケースまたはアッパーケースの変数名をパスカルケースで記述すると問題の部分にエラーとして表示されます。
警告の方は、用意した変数が他の処理で全く使われていないので警告として出ています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/4a667afe-4fb2-3b10-dc71-5773872ab124.png)

ESLintの機能だと思われますが、保存するときにvarはconstに変更されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/15c3fbbc-1b5a-415e-f5e9-c39279580baf.png)

エラーをすべて対応せずにデバッグ実行を実施した場合はエラーとして弾かれて途中で終了します。

```shell-session
$ npm run pipeline:debug

> typescript_project@1.0.0 pipeline:debug
> run-s cspell lint:report test compile debug


> typescript_project@1.0.0 cspell
> cspell src/**/*

1/2 .\src\__test__\hogehoge.spec.ts 571.36ms
2/2 .\src\hogehoge.ts 18.22ms
CSpell: Files checked: 2, Issues found: 0 in 0 files

> typescript_project@1.0.0 lint:report
> eslint src --format=html > eslint_result.html

ERROR: "lint:report" exited with 1.
```

ESLintの結果はコンソールに大量に出ると見づらいためHTMLに出力しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/9ea5ca7e-e683-b4bb-9329-b29632c0d903.png)

③コンパイルエラーが発生する場合

存在しない変数の値を代入しようとしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2781188/6f777935-d97f-c976-7b31-09be5eb7d275.png)


```shell-session
$ npm run pipeline:debug

> typescript_project@1.0.0 pipeline:debug
> run-s cspell lint:report compile debug


> typescript_project@1.0.0 cspell
> cspell src/**/*

1/1 .\src\hogehoge.ts 622.14ms
CSpell: Files checked: 1, Issues found: 0 in 0 files

> typescript_project@1.0.0 lint:report
> eslint src --format=html > eslint_result.html


> typescript_project@1.0.0 compile
> tsc

src/hogehoge.ts:3:19 - error TS2552: Cannot find name 'fileName3'. Did you mean 'fileName'?

3 const fileName2 = fileName3;
                    ~~~~~~~~~

  src/hogehoge.ts:1:7
    1 const fileName = 'test.txt';
            ~~~~~~~~
    'fileName' is declared here.


Found 1 error in src/hogehoge.ts:3

ERROR: "compile" exited with 2.

```

## まとめ
これらのパッケージと拡張機能によりコーディング規約の統一、ミスの事前発見が可能となりました。
また、開発環境のインストール以外はリポジトリを落としてもらえれば簡単に導入可能となっています。
今回はVueやJestなどを使う際の考慮は除いていますが、環境作成のサポートになれば幸いです。