StorybookをCIでビルドしてPull Requestにコメントさせたらレビュー効率がアップする

最近Reactコンポーネントを実装、修正する機会が多い。コードレビューのときにコード的には大丈夫そうだけど見た目がどんな動きをするか気になるときがあり、レビュー時にStorybookを確認したい場面が増えた。

branchをcheckoutすれば手元で見れるが、自分も修正途中のときに面倒くさい。

https://circleci.com/docs/2.0/artifacts/
現在利用しているCircleCIにはartifactsという成果物を一定期間永続化できる機能がある。

解決方法として、StorybookをビルドしたHTMLファイルをartifactsに置き、index.htmlまでのURLをプルリクエストにコメントするという方法をとった。

ほぼこちらを参考に実装。
https://qiita.com/resessh/items/38ef6492d0cf21facec8

手順としては以下。

  • 環境変数からPull Request番号を取得
  • GitHubのAPIからPull Requestのターゲット(マージ先)ブランチを取得
  • ターゲットブランチとのgit diffにコンポーネントの修正があるか判断
  • 修正があればStorybookをビルドする
  • GitHub APIを叩いてStorybookまでのURLをPull Requestにコメント

上記のリンクではPull RequestへのコメントはJavaScriptで行っているが、全部shell scriptで書いた。

#!/bin/sh
set -eu

PULL_REQUEST_ID=$(echo ${CIRCLE_PULL_REQUEST} | awk -F'/' '{print $NF}')
if [ -z "${PULL_REQUEST_ID}" ]; then
  echo "Skip building storybook."
  exit 0
fi

# Get pull request target branch
TARGET_BRANCH=$(curl -H "Authorization: Bearer ${GITHUB_TOKEN}" "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/pulls/${PULL_REQUEST_ID}" | jq '.base.ref' | tr -d '"')

# Check fixed components in git diff
git fetch origin ${TARGET_BRANCH}
COMPONENT_FIXED=0
git diff origin/${TARGET_BRANCH}...HEAD --name-only | grep 'client/components/' || COMPONENT_FIXED=$?

if [ "${COMPONENT_FIXED}" != "0" ]; then
  echo "Skip building storybook because components not fixed."
  exit 0
fi

# build Storybook
$(npm bin)/build-storybook -c client/.storybook -o public/storybook --quiet

# add comment to pull request
COMMENT=":link: [Storybook](https://${CIRCLE_BUILD_NUM}-${REPO_NUMBER}-gh.circle-artifacts.com/0/storybook/index.html)";
curl -X POST \
  -H "Content-type: application/json" -H "Accept: application/json" \
  -H "Authorization: Bearer ${GITHUB_TOKEN}" \
  -d "{ \"body\": \"${COMMENT}\" }" \
  "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/issues/${PULL_REQUEST_ID}/comments"
version: 2.0
jobs:
  build_storybook:
    docker:
      - image: circleci/node:10.13.0-browsers
    resource_class: small
    steps:
      - checkout
      - run:
          name: Build Storybook
          command: |
            npm install
            client/bin/build_storybook.sh
          environment:
            REPO_NUMBER: [CircleCI repository number]
      - store_artifacts:
          path: public/storybook
          destination: storybook
workflows:
  version: 2
  build:
    jobs:
      - build_storybook

ちなみにGitHub Actionsはartifactsはあるもののzipでのダウンロードにしか対応していないので今のところ実現できない。今後に期待。
https://github.com/actions/upload-artifact/issues/3

Storybookから出力したHTMLファイルをサブディレクトリで公開するにはbaseタグを使う

Storybookは build-storybook コマンドを利用すると静的なHTML、JSファイルとして出力できるので、どこか静的ファイルを公開できるところにホスティングするとすぐに公開することができます。
(参考: https://storybook.js.org/docs/basics/exporting-storybook/)

ただ、 https://example.com/storybook/ のようなサブディレクトリにおいた場合にHTMLに入っているJSのリンクがファイル名のみになっていて、サブディレクトリを参照してくれないため404になってしまいます。

こんな感じ。

その場合はHTMLのbaseタグのhrefを使うと解決できます。

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

baseタグはaタグのhrefやimgタグのsrcなどのURLを指定する部分を相対パスで書いたときにベースになるURLを指定することができるタグです。(<img src="/hoge.png">のように絶対パスで書くと適用されません)

Storybookの設定ファイルを置くディレクトリ(通常は.storybook/)にmanager-head.htmlというファイルを置くと、Storybookを起動した際のheadタグにカスタムタグを追加することができます(参照)。これを利用しmanager-head.htmlに以下を記載します。

<base href="/storybook/">

こんなコマンドでどうぞ。

$ echo '<base href="/storybook/">' >> .storybook/manager-head.html

再度 build-storybook して見てみるとタグとしては相対パスでファイル名が出力されていますが

HTTPリクエストとしては /storybook/ をベースにしてリクエストされているためファイルが取得できページが見れるようになります。

JSでいう document.baseURL が今見ているURLではなくbaseタグで指定したURLになっています。

実はここにヒントがかいてありました。 Absolute versus relative paths
https://storybook.js.org/docs/configurations/serving-static-files/#absolute-versus-relative-paths

baseタグ知らなかったのでStorybook以外でも何かに使えそうだなと思いました。

FlowからTypeScriptに段階的に移行する

この記事は TypeScript Advent Calendar 2019 の 3日目の記事です。

私の所属するマネーフォワード クラウド経費ではフロントエンドの基盤整備が進んでいます。もともとFlowでの型チェックが入っていたJavaScriptのソースコードをTypeScriptへ移行しましたので知見を共有します。この記事ではなぜTypeScriptへ移行するのかの理由などは紹介せず、段階的に移行する方法について紹介します。

なぜ段階的に移行する必要があったか

既存のJavaScriptのソースコードの数がそれなりに多く、一気に移行し不具合が発生するリスクを抑えるため、少しずつ段階的に移行する戦法を取ることにしました。

% find client/ -name '*.js' | wc -l
384

% wc -l `find client/ -name '*.js'`
...
57075 total

JavaScriptのファイル数は384、行数は57075でした。コンポーネントのテスト自体はある程度書かれていて、テストが通り軽く打鍵テストをすれば問題なく移行できそうなのは幸いでした。

どうやって移行したか

TypeScriptに段階的に移行するということは、.js, jsxファイル(以下.jsx?)と.ts, .tsxファイル(以下.tsx?)を混在させるということです。.jsx?は既存の方法でビルドをしつつ、.tsx?はTypeScriptとしてビルドなどをするという拡張子別に処理を分ける戦法です。

以下について、拡張子別に処理を分ける方法を紹介します。

  • Webpackでのビルド
  • Jestによるテスト
  • ESLintでのlint

Webpackでのビルド

もともとBabelを使用していたので.jsx?はBabel(babel-loader)を使用し、.tsx?はts-loaderを利用してビルドする戦略をとりました。Babelの7から対応したTypeScriptのプリセットを使うと既存の.jsx?にまで影響が及んでしまうため、loaderを分けています。

webpack.config.jsのmodule設定は以下の感じです。

module.exports = {
  ...

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
      },
    ],
  },

  ...
}

Jestによるテスト

テストランナーとしてJestを利用しており、.tsx?のテストはts-jestを利用することでテスト時にビルドと型チェックをしてくれます。ts-jestを使うにはJestのtransform設定に.tsx?はts-jestを使う設定を追加します。(下記はpackage.jsonにJestの設定を書いた例)

  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

しかし、上記の設定を追加すると.jsx?ファイルのテストがうまく動かなくなります。Jestはデフォルトではbabel-jestというプラグインを利用して.jsx?をビルドしています。transform設定に.jsx?はbabel-jestを使用する設定を追加すると動くようになります。babel-jestはJestに依存してインストールされているため、あらたにnpm installをする必要はありません。

  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest",
      "^.+\\.jsx?$": "babel-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

また、ts-jestにはJestのpreset設定用に3つのPresetが用意されていますts-jest/presets/js-with-babel のPresetを利用すると.tsx?はts-jest、.jsx?ばbabel-jestを使ってビルドしてくれるので、transform設定ではなくpreset設定でも同じことができます。

  "jest": {
    "preset": "ts-jest/presets/js-with-babel",
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

ESlintによるlint

ESlintを利用していますが、ESLintの実行時に設定ファイルの指定と拡張子の指定ができます。.tsx?用の.eslintrcと.jsx?の.eslintrcを別々に用意し、eslintコマンドを別々に実行することで別々にlintすることができます。

  "scripts": {
    "lint": "npm run lint:ts && npm run lint:js",
    "lint:ts": "eslint -c .eslintrc.ts.yml --ext .ts,.tsx src/",
    "lint:js": "eslint -c .eslintrc.js.yml --ext .js,.jsx src/",
  },

ただlintは最悪あとからでも修正できるため、移行した.tsx?だけlintをかけるとか、後で全ファイルまとめて対応するでもよいと思います。私はlintはあとでまとめて全ファイルにかけるようにし、まずは移行を優先で進めるようにしています。

あとは気合で少しずつ移行していく

FlowからTypeScriptへ移行する際のビルド、テスト、lintを拡張子別に分けて段階的に移行していく方法を紹介しましたが、ここからFlowで書かれているコードを正規表現などで置換していき、TypeScript導入により厳密になった型チェックで怒られる部分を修正していく作業が待ち受けます。移行の際にはniieani/typescript-vs-flowtypeなどを参考にしつつ移行を進めました。

2019年11月頭頃から移行を始め、実はこの記事を書くまでにはすべてTypeScriptへ移行してリリースまでしている予定でしたが、思ったより手こずりまだリリースまでこぎつけていません。引き続きがんばります。