LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

複数リポジトリ間におけるeslint・prettierの設定共通化

転職会議事業部の srkw です。 今期事業部内で利用する eslint および prettier の共通ルールを管理するパッケージを作成したので、その工程と成果物をご紹介したいと思います。 なお、今回紹介するパッケージの内容には多分に要修正箇所があり、今後他のプロジェクトとの優先順位を鑑みて、都度改善される可能性があります。その際はこちらの記事も併せて更新できればと考えています。

TL;DR

最終成果物は以下のリポジトリで公開しています。利用リポジトリ側での設定等は README に記載しております。

https://github.com/livesense-inc/eslint-config-template

モチベーション

転職会議は現在ページごと・機能ごとにサーバーを別で管理するマイクロサービス構成で開発を行っています。その中で利用する静的コード分析やコードフォーマッタのルールは codingstyle.jobtalk というリポジトリで管理していました。codingstyle.jobtalk リポジトリの概要は以下の通りです。

  • ./target/配下のコードに対して eslint(+ eslint-plugin-prettier を経由した prettier 実行)と rubocop をそれぞれ実行する
  • codingstyle.jobtalk 側で eslint や rubocop およびそれらのプラグインパッケージのバージョン管理・ルールの管理を行う
  • codignstyle.jobtalk の circleci でイメージをビルドして ECR にプッシュする
  • 利用リポジトリ側では ECR から codingstyle.jobtalk のイメージを取得し、静的検査を行いたいコードを codingstyle.jobtalk/target/配下にコピーして静的検査を行う

この方法でも静的検査は可能なのですが、特に JavaScript の静的検査については以下の問題がありました

  • eslint-plugin-prettier が非推奨になっている
  • docker コンテナを立ち上げて静的検査を行う都合上、利用リポジトリ側に eslint(および prettier)の設定ファイルが存在せず、エディタの自動整形が利用できない

この問題を踏まえて、eslint および prettier の共通ルールを管理するパッケージを github package registry に立てることにしました。また、github package registry との相性を鑑みて、CI は github actions 上で行うことにしました。

期待する挙動

前項の裏返しになりますが、期待する挙動は以下の通りです。

  • 複数のリポジトリの eslint と prettier のルールを共通管理できること
  • npm パッケージとしてバージョン管理されていること
  • 利用者のローカル環境で eslint と prettier の設定ファイルを保持し、リポジトリごとにルールの個別指定ができること

以降、「パッケージ内部の実装」「パッケージ用の設定」「ワークフローの実装」「利用者側の実装」に分けて実装を紹介していこうと思います。

方針

今回のケースでは社外(organization外)にパッケージを公開することを想定していないため、scoped modulesとしてnpm packageを定義/publishします。

また、eslint および prettier の設定については、eslint と prettier の責務が類似していること、管理対象のリポジトリをむやみに増やしたくないことから、単一のパッケージとしてまとめて公開したいと考えました。 eslint の共用設定パッケージ(sharable config)については公式ドキュメントにもある通りeslint-config-xxxという命名規則に則ってパッケージ名を設定することで、継承する設定パッケージの指定がシンプルになります。

パッケージ名をhogeとした場合の利用側.eslintrc.js

module.exports = {
  extends: ['./node_modules/@orgname/hoge/eslint-config'], // 相対パスで継承したいパッケージのeslint設定ファイルを指定
  ...
}

パッケージ名をeslint-config-hogeとした場合の利用側.eslintrc.js

module.exports = {
  extends: ['@orgname/eslint-config-hoge'], // パッケージ名のみの指定でOK
  ...
}

prettierについては推奨されるパッケージ名(prettier-config-xxxx)が一応あるものの、今回のケースでは推奨されるパッケージ名の規則を無視してもデメリットが大きくなかったので、パッケージ名は @orgname/eslint-config とし、パッケージ内トップレベルに eslint と prettier の共通設定をそれぞれ配置する構成にします。

パッケージ内部の実装

パッケージで公開する成果物は eslint と prettier それぞれの設定が記載されたjsファイルになるので、パッケージのトップ階層に2つのファイルを配置します。 また、eslintのルールについてはテスト対象ファイルの拡張子ごとにルールを使い分けたい(後述)ため、lib/ 配下に各拡張子ごとの設定ファイルを配置します。

$ tree js-rules

js-rules
├── README.md
├── lib
│   └── eslint-config
│       ├── common.js
│       ├── react.js
│       └── typescript.js
├── package.json
├── eslint-config.js
├── prettier-config.js
└── yarn.lock

eslint 用の設定

eslint 用の設定は拡張子ごとに期待するルールが異なるケースが多いと思います。ネット上にある他の共通ルールパッケージなどでは「ルールを複数公開して利用者側で適宜必要なルールを選択して import +継承する」というような実装もありましたが、今回は

  • 事業部内でのみ利用する共通ルールであること、
  • 事業部内で拡張子ごとに適用するルールを分けたいニーズがなかったこと
  • 利用者側での共通ルールの利用のための記述を最小限にしたかったこと

などを踏まえて、./eslint-config.js で拡張子ごとに適用する共通ルールを適宜呼び出す実装にしました。

./eslint-config.js

module.exports = {
  root: true,
  overrides: [
    {
      files: ['*.js'],
      extends: ['./common.js'],
    },
    {
      files: ['*.jsx'],
      extends: ['./common.js', './react.js'],
    },
    {
      files: ['*.ts'],
      extends: ['./common.js', './typescript.js'],
    },
    {
      files: ['*.tsx'],
      extends: ['./common.js', './react.js', './typescript.js'],
    },
  ],
};

上記で呼び出すルールは以下のように配置していて、各所で必要なルールを定義しています。

lib
└── eslint-config
    ├── common.js
    ├── react.js
    └── typescript.js

prettier 用の設定

prettier についてはファイルの種類ごとに分けたいルールなどが特になかったため、prettier-config.js にルールを記載して export しています。

./prettier-config.js

module.exports = {
  singleQuote: true,
  printWidth: 120,
  tabWidth: 2,
};

ここまででパッケージ内部の実装は完了したので、次は publish するためのパッケージの設定を追加してゆきます。

パッケージ用の設定

パッケージの公開用の設定は package.json に記載してゆきます。

package.json

{
  "name": "@orgname/eslint-config", // @{organisation-name}/{package-name}
  "version": "0.0.1",
  "description": "eslintおよびprettierのルールをまとめるnpmパッケージ",
  "main": "eslint-config.js",
  "files": [ // 公開するファイルおよびディレクトリを全て指定する
    "lib",
    "prettier-config.js"
  ],
  "author": "Jobtalk Team",
  "license": "MIT",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com/" // github package registryに公開するための設定
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/orgname/{repo-name}.git"
  },
  "dependencies": {
    ...
  },
  "scripts": {
    ...
  }
}

ここまでで公開するための設定とパッケージの中身ができたので、次は github actions で公開するためのワークフローを実装してゆきます。

ワークフローの実装

パッケージの内容を更新するためのパッケージ pubish 用ワークフローを ./github/workflows/ 配下に追加します。 このワークフローはほぼ公式のサンプルそのままで動作しました。

name: publish

on:
  release:
    types:
      - created

jobs:
  test: # リリース前に行うテストジョブ
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: "12.x"
      - run: yarn
      - run: yarn lint

  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: "12.x"
          registry-url: "https://npm.pkg.github.com"
      - run: yarn
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # setup-nodeアクションでnpm認証用に利用される環境変数

ここまででリリース用の全ての実装が完了したので、次はリリース作業になります。

リリース作業

github action のパッケージ公開用ワークフローが created という release イベントによって発火するようになっているので、ローカルで適当な git tag を作成し、github 上でリリースを作成します。

ローカルでタグを作成して push

$ git tag 0.0.1
$ git push origin 0.0.1

github 上でリリースを作成

f:id:srkw_h:20210819113538p:plain f:id:srkw_h:20210819113600p:plain f:id:srkw_h:20210819113610p:plain

無事リリースができたようなので、次は利用者側のリポジトリの設定に移ります。

利用者側の設定

利用者側で主に行うことは以下の3点です。

  • package.jsonの更新
  • .npmrcの作成
  • eslint及びprettierの設定ファイルの作成

package.json の更新

package.json

{
  ...
  "dependencies": {
    "@orgname/eslint-config": "0.0.1",
    ...
  },
}

.npmrc の作成

デフォルトのnpm package registry以外のレジストリ(GPR)からパッケージをダウンロードするために、.npmrc ファイルを追加し、レジストリへの認証用の設定も記載しておきます。

.npmrc

registry=https://registry.yarnpkg.com/

@orgname:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GPR_READ_TOKEN}
//npm.pkg.github.com/orgname/:_authToken=${GPR_READ_TOKEN}
always-auth=true

認証に利用するトークン(GPR_READ_TOKEN)はgithubの設定ページから read:packages 権限を付与したトークンを生成して、環境変数経由で読み取るようにしています。

eslintおよびprettierの設定ファイルの作成

  • eslint

.eslintrc.js

module.exports = {
  extends: '@orgname/eslint-config',
  rules: {
    // 利用者側で個別に設定したいルールを記載
  }
};
  • prettier

.prettierrc.js

const prettierConfig = require('@orgname/eslint-config/prettier-config');

module.exports = {
  ...prettierConfig,
  // 利用者側で個別に設定したいルールを記載
};

おわりに

ここまでの作業で、「期待する挙動」に記載した要件を満たすことができ、その結果いままでよりも快適なJavaScriptの開発環境を構築することができました。 似たような課題感のある現場の方の参考になれば嬉しいです。