Open16

古いポモドーロタイマーのJavaScript実装をモダンに改修して技術の進歩を顧みる

TakashiAiharaTakashiAihara

背景

兼ねてよりポモドーロタイマーを作りたいなと思っていた。
とっかかりが欲しいなと思いつつ、イチから作るのも気が引けた。
ロジックとしては地味にでかい。

pomodoro react で検索すると、あるリポジトリが引っ掛かった。

https://github.com/astroud/pomodoro-react-app

約2年前のリポジトリらしい。
パッケージも多くなく、依存も少なそうだ。ちょうどいい。
本番運用中のサービスを想定し、それなりの段階を踏まえつつTypeScriptベースに置き換えていこう。

TakashiAiharaTakashiAihara

既存リポジトリの分析

package.json

package.json

{
"name": "pomodoro-react-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.3",
"@testing-library/user-event": "^12.6.0",
"react": "^17.0.1",
"react-circular-progressbar": "^2.0.3",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1",
"use-sound": "^2.0.1",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

  • react-scripts の local インストールが邪魔になるかも。
  • React用状態管理パッケージは入ってない。
  • CSSフレームワークは入ってない。
  • eslintのconfigは必要最低限。
  • browserslistは邪魔はしなさそう。Next.jsで流用できる。
  • Reactは17でちょっと古い。全体的にバージョンアップはしたほうがよさそう。
  • E2Eテストは入っていない。
  • CI/CDもない。

src 配下

  • 現状がcssのべた書きなので、ここの移行が最も辛そう。
  • ユニットテストは「レンダリングできること」レベルの必要最低限。
  • コンポーネントは細かく分けられているが、内部の if 分岐でコンポーネント自体が大きく切り替わっているため、共通化としては美しくないかな。
  • propsがでかい。
TakashiAiharaTakashiAihara

目次

  1. 不要なもの削除
  2. Nodeバージョン固定化
  3. TypeScriptの導入
  4. Formatter + Linter + VSCodeの設定 ( Prettier + ESLint )
  5. Gitフック系パッケージ導入 ( Husky + lint-staged )
  6. 静的コード品質管理 ( SonarCloud + GitHub Code Scanning )
  7. Pull Requestのステータスチェック設定
  8. UIカタログ導入 ( storybook )
  9. VRTの実装 ( reg-suit + storycap )
  10. E2Eテストの実装 ( Playwright )
  11. ユニットテストの実装 ( Vitest + Testing-library + happy-dom)
  12. CI/CDの設定 ( Github Actions )
  13. Nodeバージョンアップ
  14. パッケージ管理の入れ替え( pnpm )
  15. Next.js への移行
  16. パッケージ選別
  17. パッケージバージョンアップ
  18. 各コンポーネントのTypeScriptへの置き換え
  19. 状態管理の導入 ( zustand + Immer)
  20. コンポーネントのリファクタリング
  21. CSSフレームワークに移行 ( tailwindcss )
  22. PWA化 ( next-pwa )

おおまかな方針

  • TypeScriptは早めに導入。一時的にJS/TSの共存環境に
  • Linter/Formatterも早めに導入
  • 静的コード解析はなんぼあってもいいですからね
  • コーディング基盤ができたらTypeScriptベースで自動テスト実装して動作/デザインを担保
  • react-scriptsからNext.jsに移行
  • deprecatedなパッケージがあれば入れ替え、バージョンアップ
  • コンポーネントは細分化する。不要な共通化はしない。
  • CSSスタイリング移行は最後。ラスボス。

自動テストは全部乗せにするつもり。(知ってる範囲
資源流用しつつ、担保できるところからごろっと置き換えていく。

開発環境整備 -> テスト -> フレームワーク/パッケージ刷新 -> UI関係の移行

あとはGithubのDependabotあたり入れたいけど PR 汚されまくりそうなので、最後に余力があれば。

本番ドメイン

https://www.pomodoro-next.shop/

公開 storybook

https://page.pomodoro-next.shop/storybook/

直近のVRTレポート(作業中じゃなければ消えます)

https://page.pomodoro-next.shop/vrt-report/

TakashiAiharaTakashiAihara

所感

  • JavaScript向けのESlint/Prettier設定を考慮するのが非常にめんどくさい。既存のJavaScriptコードへの適用をしなくていいのなら、まずTypeScript向けに環境を作るのが吉。
  • パッケージ管理の入れ替えは実は結構鬼門。注意。
  • コンポーネントのリファクタリングが大きくなりそうなので、ユニットテストを入れるか迷ったが、本番想定ということで導入した。
TakashiAiharaTakashiAihara

Nodeバージョン固定化

Pull Request

package.jsonでのenginesの指定や.nvmrcなどが無く、そもそもNodeのバージョンが不明だった。
package-lock.json の lockfileVersion から、Node v16 以上であろうことは推測できる。

{
  "name": "pomodoro-react-app",
  "version": "0.1.0",
  "lockfileVersion": 2,

v18

念のため v18で npm i してから npm start してみた。
無理だった。

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/tokenize' is not defined by "exports" in /root/***

さすがにv18は無理か。潔くあきらめる。

v16

v16を試す。v14はさすがに古すぎる。
祈りながら npm start

Error: No version of chokidar is available. Tried chokidar@2 and chokidar@3.
You could try to manually install any chokidar version.
chokidar@3: Error: Cannot find module 'chokidar'

ダメだった。
ただ chokidar は 直接的な依存ではなかった。
package.json での各パッケージのバージョン指定も細かく、動作異常に至らなさそうという判断で package-lock.json の再作成をしてみた。

Compiled successfully!

You can now view pomodoro-react-app in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.0.222:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

動いた。

TakashiAiharaTakashiAihara

本番環境整備

ちょっとタイミング遅くなったが、本番想定ということで Vercel に デプロイしておく。
やることは npx vercel で対話形式で入れていってリポジトリ連携するだけなので、それほどタスク規模は大きくない。
"vercel" は通常 global に入れると思うけど、わかりやすくするため local にした。

Pull Request

モチベーション上げるためにドメインも登録しておいた。

https://www.pomodoro-next.shop/

TakashiAiharaTakashiAihara

TypeScriptの導入

Pull Request 1 導入
Pull Request 2 問題解決

導入部分

tsconfig.json はよく見る感じ。

checkJsは迷ったが falseにした。
極力既存のJSを尊重して見ないようにする。

https://github.com/TakashiAihara/pomodoro-next/blob/63f15f20c3e3a7cb619266120e680e13e032f8c7/tsconfig.json#L10

"js" は "react-jsx" にした。運よく React 17 だったので、最新に沿わせる。

https://github.com/TakashiAihara/pomodoro-next/blob/63f15f20c3e3a7cb619266120e680e13e032f8c7/tsconfig.json#L21

"include" は js も含めるので、"**/*" で全指定。

https://github.com/TakashiAihara/pomodoro-next/blob/63f15f20c3e3a7cb619266120e680e13e032f8c7/tsconfig.json#L33-L35

あとは、後々使うだろうということで alias 追加しておいたぐらい。

https://github.com/TakashiAihara/pomodoro-next/blob/63f15f20c3e3a7cb619266120e680e13e032f8c7/tsconfig.json#L27-L32


問題発生

ここまでは TypeScriptの導入。
この後に問題発生。

TypeScript導入後、ローカルで npm i をやり直すとエラーでなぜかインストールできない。

npm ERR! While resolving: react-scripts@5.0.1
npm ERR! Found: typescript@5.1.6
全文
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR! 
npm ERR! While resolving: react-scripts@5.0.1
npm ERR! Found: typescript@5.1.6
npm ERR! node_modules/typescript
npm ERR!   dev typescript@"^5.1.6" from the root project
npm ERR!   peer typescript@">= 2.7" from fork-ts-checker-webpack-plugin@6.5.3
npm ERR!   node_modules/fork-ts-checker-webpack-plugin
npm ERR!     fork-ts-checker-webpack-plugin@"^6.5.0" from react-dev-utils@12.0.1
npm ERR!     node_modules/react-dev-utils
npm ERR!       react-dev-utils@"^12.0.1" from react-scripts@5.0.1
npm ERR!       node_modules/react-scripts
npm ERR!         react-scripts@"^5.0.1" from the root project
npm ERR!   2 more (ts-node, tsutils)
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peerOptional typescript@"^3.2.1 || ^4" from react-scripts@5.0.1
npm ERR! node_modules/react-scripts
npm ERR!   react-scripts@"^5.0.1" from the root project
npm ERR! 
npm ERR! Conflicting peer dependency: typescript@4.9.5
npm ERR! node_modules/typescript
npm ERR!   peerOptional typescript@"^3.2.1 || ^4" from react-scripts@5.0.1
npm ERR!   node_modules/react-scripts
npm ERR!     react-scripts@"^5.0.1" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR! 
npm ERR! See /root/.npm/eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2023-08-04T06_33_33_521Z-debug-0.log

VercelではFailedは発生してない。

ログを見る限り、react-scripts x TypeScript の依存関係の問題。
npm はこういったことが稀に起こるのが怖い。
npm i {package} でのインストールはできたけど、再度 npm i するとエラーになる。というケース。
基本は pnpm 最低でも yarn を使っておきたい。

ちょっと話がそれたが、react-scripts を v5、TypeScriptを v4 にすると解決できた。
buildも問題ない。

TakashiAiharaTakashiAihara

Formatter + Linter + VSCodeの設定 ( Prettier + ESLint )

環境整備ということで一つの項目にまとめたが、PRは全5つになった。
まず別ステップに影響なさそうなPrettierから。

Prettier 設定

Pull Request Prettier設定
  • prettier + typescript 関係のものを導入(tailwindcssのプラグインも入ってしまっていたが影響なさそうなのでそのまま進む)
  • package.json の プラグイン と import 文のオーダー制御も導入。
  • npm exec prettier src/App.js あたりで動作することが確認できた。

Prettier 適用

Pull Request Prettier 適用
  • 設定とは別PRにして、prettier適用。対象は少ないけど。

ESLint設定

Pull Request ESLint設定
  • ありがちな内容。とりあえずChatGPTと相談しつつ。
  • JSはすべて解析対象外にするのでignore。
  • react-app-env.d.ts が自動作成されると 1:1 error Do not use a triple slash reference というエラーを吐くのでignore。
  • react/react-dom の types が入っておらずエラーになったのでここで導入。
  • このままではTypeScript実装のコンポーネントが入るまで解析対象がゼロ状態になるので、ダミーを作っておく(忘れずに削除)

VSCode設定

Pull Request VSCode設定
  • settings.json 追加、色々細かい設定はあるがファイル保存時のフォーマット適用のため。
  • いくつかファイルを保存してみて フォーマッターが動くことを確認。
  • デバッグ実行をするためにlaunch.json追加。
  • フォーマッターやコードルール系のExtensionはメンバー内で共通化したいのでextensions.json 追加。
  • 誤字脱字は美しくないのでcode-spell-checkerも追加、この時点で必ず利用するspellは登録しておく。
  • vitestのExtensionも積極的に使ってほしいのでこのタイミングで入れておく。

ESLintのwarn 修正

Pull Request ESLint の warn 修正
  • prettier.config.mjs が ESLintの対象に入ってて、「tsconfig.jsonの対象ではないよ~」とwarn出ていたのでignore。
TakashiAiharaTakashiAihara

Gitフック系パッケージ導入 ( Husky + lint-staged )

Pull Request Husky + lint-staged
  • Commit時に実行するのはeslint+prettier のみで、testは除外した。
  • 毎Commit or 毎Pushでテスト実行するのは、開発者体験として芳しくないと思っている(苦い過去)
  • できるだけGithub Actions の CIで賄う。
  • lint-staged の対象は*.ts, *.tsx のみにした、jsは完全に除外。
  • Dummy をいじってcommitしてみたところ動作が確認できた。
TakashiAiharaTakashiAihara

静的コード品質管理 ( SonarCloud + GitHub Code Scanning )

  • SonarCloudもPublicリポジトリだと無料だとは知らなかった。使う。
  • 機能重複してる感じで負荷過多な感じするけど、勉強のため。しんどいと感じたら後で消す。
  • ひとまず画面ポチポチだけなのでPRなし。

SonarCloud


画面上でポチポチ。


思ったよりもBugs/Code Smells 少ない。

Github Code Scanning



ポチポチ

アラート無し。

TakashiAiharaTakashiAihara

Pull Requestのステータスチェック設定

忘れていた。

  • Vercelのデプロイが成功したこと
  • SonarCloud の解析で問題がなかったこと

これらを Pull Requestのステータスチェックとして設定する。
(特にVercelチェックは早い段階でやっておくべきだったな)

画面ポチポチ

ついでにVercel のコメントも付けれるようにした。デプロイ先のURLをつぶやいてくれるらしい。
問題があったら修正する。

TakashiAiharaTakashiAihara

UIカタログ導入 ( Storybook )

👇成果物
https://page.pomodoro-next.shop/storybook/

大まかにやったこと

  • Storybook実装
  • storybook buildのワークフロー作成 (main プッシュ時)
  • 生成したものを Github Pages にデプロイする
  • 最新のStorybookの資源は docs/storybook というブランチで保持
  • docs/storybook ブランチ は Vercel のデプロイに含めない(ignoreする)。

導入と各コンポーネントの Storybook 作成まで

Storybook導入
  • 色が薄くて見づらいものがあったので backgrounds は決め打ちできるように。
  • 動作保証は別の自動テストでまかなうのでActionは無し。
  • べた書きcssはそのままimport。のちに削除したい。

Appも Storybook に追加する

page.stories追加 ※ 最下部
  • ページ全体像もVRTに含めたいのでstorybookに追加(一般的ではないと思う)
  • VRT実装のPRに含めた。

Storybookを gh-pages ブランチに Pushするワークフロー

試行錯誤したPRが多く、見づらいので最終結果だけ👇

.github/workflows/storybook_deploy.yaml
関係するPR
  • Github Pagesは直接デプロイ方式(Beta)を採用した。
    • gh-pagesブランチを使うことも考えたが、Historyサイズがでかくなりすぎることを危惧した。
    • PR 18 時点では gh-pages にゴネゴネしようとしてたけど方針転換。
  • Github Pagesのルートではなく、storybook のサブディレクトリで公開した。
    • VRTレポートやE2Eの結果など、公開したいものがほかにもある。
    • ディレクトリを作って mv して Deploy するという原始的な方法で実現した。
  • workflow中に使う設定値はenvに外だししてDRYに。
  • Vercelデプロイのignoreはこのあたりを参考にさせてもらった。

感想

  • Propsの数が多いほどStorybookの実装がしんどい。
    • Storybookだけでなくユニットテストも面倒くさいだろう。
    • 状態管理の導入や管理方針を予め決めておく重要性が分かった。
  • Copilot マジで助かる。
    • storybookのProps定義とかはCopilotにほぼお任せだった。
  • ChatGPTもかなり活用できたが、Github Actions と Github Pages の仕様はリアルタイムに変わっていっているので ご存じない機能もかなり多かった。
TakashiAiharaTakashiAihara

VRTの実装 ( reg-suit + storycap )

成果物👇(作業中じゃなければ消えてる可能性あり)
https://page.pomodoro-next.shop/test-results/vrt/

はじめに

やっとこさテスト実装に入っていく。
といっても今回は VRT だけなので、規模が大きいものではない。
ないが、試行錯誤が多くPRが増えてしまった。
特にGithub Pages周りの試行錯誤がすごい。
Github Actionsの動作検証がやりづらかったせいもある。
act あたりを今後使えるようにしておきたい。(戒め
なお、使うリソースはGithub周りのもののみにする ( Artifact/Pages/Actions )

やること

  • storycap + reg-suitの導入
  • それぞれの設定+storybookの修正
  • VRTを実行するGithub Actionsのワークフロー作成

導入

今回もPRが多くなったので結果だけ👇

package.json
  • storycap と reg-suit 関連のパッケージ追加。
  • VRT関連の npm scripts は vrt:* の形でおさめた。

regconfig.json (reg-suitの設定)
  • ほぼデフォルトだが、prComment関連を若干変えた
    • prCommentBehaviorは"new"にした。他のステータスチェックの結果が最後尾に来るので近くにあるほうが絶対いい。
    • shortDescriptionは"false"。コンポーネント数が少ない場合はfalseが見やすい。
  • clientIdが入っているが、このリポジトリのみの権限にした。
  • ちなみに、.reg も screenshots も .gitignoreに追加した。画像そのものは保持せず、ローカル実行かVRTレポート上のみで確認できるように。

.storybook/preview.ts (storycapの設定含む)
  • 設定の型定義は ScreenshotOptions が一番近そうだったのでこれにした。探した。
  • タブレット/スマホ/PCそれぞれのvariantsを設定。
  • delayは大きめにした。小さいとレンダリングが上手くいかないケースがあった。
  • waitAssetsもレンダリング待ちのためにtrueにした。
  • コンポーネントによっては背景色と被って見えなかったので、backgroundsプラグインでカラーを決め打ちできるようにした。

storybook_deploy.yaml (VRTレポートのデプロイworkflow)
  • envを利用してDRYに。
  • mainブランチのプッシュ時に実行
  • まずVRTレポートそのものを build
  • レポートをactions/upload-pages-artifact@v2 で Artifact に追加
  • actions-gh-pages で docs/storybook というブランチに最新化して保持する。
  • そのあと vrt-report/ というサブディレクトリに移動して再度Artifactに追加
  • この Artifact を Github Pages にデプロイすることでサブディレクトリ構成を実現。

参考にさせていただいた記事

https://zenn.dev/mimokmt/articles/16b0d30e8cd87f

所感

  • Github Actions上で実行する際、時間短縮のためと並行実行数を増やすと エラーになる可能性が高まる。エラーにならなくても renderingされてない白紙状態とか。
  • waitAssets は true にしておいたほうが安全な気がする。
  • delayもできれば大きいほうが良い。

gh-pages ではなく Github Pages 直接デプロイ方式を採用した理由

▼ gh-pages ブランチ方式のデメリット

GitのHistoryが残り続けること、それによるブランチサイズ増大が懸念点だった。
VRTレポートのように、Push毎に生成されるものは特に影響が大きい。
レポートの全体ファイルサイズを測ってみると8 MBほどだった。
最大で1Pushにつき8 MB増大すると考えると、Github 推奨上限の1 GB には簡単にたどり着いてしまいそうだった。

▼ ワークフローでのデプロイ方式(Beta)のメリット

Git Historyなど関係なく、デプロイ資源のみの管理。線ではなく点でしか見ない。
これは上記の gh-pages の懸念を無くせるものだった。

TakashiAiharaTakashiAihara

ユニットテストの実装 ( Vitest + Testing-library + happy-dom)

成果物👇
https://page.pomodoro-next.shop/test-results/unit/

E2Eテストとどちらを先に実装するか悩んだが、こちらを先に。
既存コードベースの理解が必要という部分と、フィードバックの速さを加味して。

バックエンドが無いので、E2Eテストはユニットテストで補えなかった部分

TakashiAiharaTakashiAihara

E2Eテストが無いと、vitest + happy-dom + testing-libraryでは動いているように見えたけど起動できないという可能性もある。。。