ESLint導入環境にprettierを追加して運用する

prettierというコードフォーマッタが最近OSSでよく使われていて、OSSに限らず複数人開発しているプロジェクトなどで非常に有用だと感じたので、導入した。以下はその際の覚え書き・注意点など。

prerrierとは

Node.js製のコードフォーマッタ。以下のコードに対応している。

  • JavaScript, including ES2017
  • JSX
  • Flow
  • TypeScript
  • CSS, LESS, and SCSS
  • JSON
  • GraphQL

ESLintとどう違うのか

似たようなツールでESLintが思い出されると思う。prettier公式のドキュメントにも言及があるが、ESLintのリンティングには大きく分けて二種類あって、

  • Formatting rules
  • Code-quality rules

prettierは’Formatting rules’のチェックと自動修正に特化したツールになっている。

しかし、ESLintにもeslint --fixで自動修正の機能がある。あるのだが、prettierのコードフォーマットがESLintの–fixオプションよりも優れていて、どう優れているかはこの記事が詳しい。

Unlike eslint, there aren’t a million configuration options and rules. But more importantly: everything is fixable.

There’s an extremely important piece missing from existing styling tools: the maximum line length. Sure, you can tell eslint to warn you when you have a line that’s too long, but that’s an after-thought (eslint never knows how to fix it). The maximum line length is a critical piece the formatter needs for laying out and wrapping code.

要するに、コードフォーマットに関するいろいろ(エラー/見づらいコード)全て修正できること、特にmax-lengthで引っかかったコードを修正できるということが明確なメリットということらしい。

つまり、両者で守備範囲は一部かぶる部分があるが、どちらか使わなくて良いということではなく、両方使ったほうがより楽にコードの品質の向上を見込めるということ。

自動修正する際にESLintと役割がかぶった部分をどうするか

前述の通り、ESLintがすでに導入されている環境の場合、どう共存させるかということが問題になる。(なにも考えずやるとprettierの結果コードがESLintでエラーという面倒なことになる) 対応は現状何種類かある。

  • 1 eslint-plugin-prettierを使って、prettierの記法ルールをESLintのルールとして定義し、それぞれで修正する
  • 2 prettierでフォーマットしたコードをeslint --fixにパイプで繋いで渡す

前者の方が設定ファイル一枚にまとまりわかりやすいと思ったため、今回は前者の方法で対応した。

対応の流れ

  • 1 まずeslint-config-prettierをインストールし、フォーマット関連のESLintのルールを全て無効にする
  • 2 eslint-plugin-prettierをインストールし、prettierのフォーマット設定をESLintの設定として読み込み、ESLintから指摘できるようにする。
  • 3 eslint --fixを実行すると、eslint-plugin-prettierによりprettierの実行とeslintの自動修正を同時に行ってくれる。

こんな感じ。

npm i -D prettier eslint-config-prettier eslint-plugin-prettier

した後、.eslintrcを修正。

{
  "parser": "babel-eslint",
  "extends": [
    "airbnb",
    "prettier",
    "prettier/react"
  ],
  "plugins": [
    "prettier"
  ],
  "rules": {
    "prettier/prettier": ["error", {
      "singleQuote": true,
      "bracketSpacing": true,
      "jsxBracketSameLine": true
    }]
  }
}

extendsにeslint-config-prettierを最後に指定するようにしないと上書きできないので注意。

git hookでコミット前にチェック/修正する

Pre-commit hookで、コミットする前にチェックと修正を走らせるとよいらしい。自分では考えたことがなかったが、それって最高なので導入した。こちら参照。

ドキュメントにある通りやり方は複数あるが、最初に出てきたものが一番手軽そうだったのでそれを採用した。

手順

  • 1 huskyをインストールし、precommitという名前のnpm-scriptがコミット時に走るようにする。
  • 2 lint-stagedをインストールし、ステージされたファイルに対してリンティングと自動修正が実行されるようにする。

huskyは、precommitという名前のnpm-scriptをコミット時に自動実行してくれるツール。仕組みとしては、インストールするとprojectDir/.git/hooks/pre-commitに以下のようなシェルスクリプトを自動で作成してくれて、それによりpre-commitを扱いやすくしてくれている。

#!/bin/sh
#husky 0.14.3

command_exists () {
  command -v "$1" >/dev/null 2>&1
}

has_hook_script () {
  [ -f package.json ] && cat package.json | grep -q "\"$1\"[[:space:]]*:"
}

cd "client"

# Check if precommit script is defined, skip if not
has_hook_script precommit || exit 0

load_nvm () {
  # If nvm is not loaded, load it
  command_exists nvm || {
    export NVM_DIR=/Users/KentaKatoh/.nvm
    [ -s "$1/nvm.sh" ] && . "$1/nvm.sh"
  }

  # If nvm has been loaded correctly, use project .nvmrc
  command_exists nvm && [ -f .nvmrc ] && nvm use
}

# Add common path where Node can be found
# Brew standard installation path /usr/local/bin
# Node standard installation path /usr/local
export PATH="$PATH:/usr/local/bin:/usr/local"

# nvm path with standard installation
load_nvm /Users/KentaKatoh/.nvm

# nvm path installed with Brew
load_nvm /usr/local/opt/nvm

# Check that npm exists
command_exists npm || {
  echo >&2 "husky > can't find npm in PATH, skipping precommit script in package.json"
  exit 0
}

# Export Git hook params
export GIT_PARAMS="$*"

# Run npm script
echo "husky > npm run -s precommit (node `node -v`)"
echo

npm run -s precommit || {
  echo
  echo "husky > pre-commit hook failed (add --no-verify to bypass)"
  exit 1
}

lint-stagedは、上述の通り。

2点踏まえてpackage.jsonに以下のように追加。

{
  "lint-staged": {
    "gitDir": "../",
    "*.{js,jsx}": [
      "eslint --fix",
      "git add"
    ]
  },
  "scripts": {
    "precommit": "lint-staged",
    "lint:fix-all": "eslint --fix --ext .js,.jsx'./javascripts/**'"
  }
}

自分のプロジェクトではフロントエンドのファイルが全てサブディレクトリにあるので、.gitの場所をlint-stagedのgitDirオプションで教えてあげる必要があった。

lint-stagedでハマった

これでコミット時に自動でeslint --fixが無事に実行されるのだが、npm run lint:fix-allを実行して既存の全ファイルに修正をかけた後コミットしようとすると、以下のようなエラーが出てしまいコミットできなかった。

husky > npm run -s precommit (node v6.9.1)

 ↓ Running tasks for gitDir [skipped]
   → No staged files match gitDir
 ❯ Running tasks for *.{js,jsx}
   ✔ eslint --fix
   ✖ git add
     → remove the file manually to continue.
🚫 git add found some errors. Please fix them and try committing again.

fatal: Unable to create '/projectDir/.git/index.lock': File exists.

Another git process seems to be running in this repository, e.g.
an editor opened by 'git commit'. Please make sure all processes
are terminated then try again. If it still fails, a git process
may have crashed in this repository earlier:
remove the file manually to continue.


husky > pre-commit hook failed (add --no-verify to bypass)

エラーメッセージの通り.git/index.lockを確認するのだが、このファイルが存在しない。lint-stagedのverboseオプションを付けてみてみたりしたのだが、さっぱりわからず。

ダメ元でlint-stagedのコードを見てみると、concurrentという記述を発見し、もしかして並列実行してる?と思ってtopコマンドで見てみると、並列実行してたことがわかった。

雑なスクショ

よくよくドキュメントを読むと、デフォルトで2つのプロセスで並列実行する仕様だった。だいぶクサイ気がする。subTaskConcurrencyを1に設定して実行してみる。

"lint-staged": {
  "gitDir": "../",
  "subTaskConcurrency": 1,
  "*.{js,jsx}": [
    "eslint --fix",
    "git add"
  ]
}
husky > npm run -s precommit (node v6.9.1)

 ↓ Running tasks for gitDir [skipped]
   → No staged files match gitDir
 ↓ Running tasks for subTaskConcurrency [skipped]
   → No staged files match subTaskConcurrency
 ✔ Running tasks for *.{js,jsx}
[test-lint-staged 8118f42c7] commit
 101 files changed, 1735 insertions(+), 1503 deletions(-)

いけた。。。

よくわかってないけど、片方のプロセスがgit addを実行しているときに、重複して実行できないようにgitがlockファイルを生成していて、その状態でもう片方のプロセスがgit addを実行しようとしたときに、lockファイルのせいで実行できない、とかなんだろうか。だれか詳しい人いたら教えて欲しいです。

本質的でないとこで時間を取られてしまった、ただこれでいい感じに運用できるんじゃないでしょうか。

おわり🐶