🧊

今度はVite + Shadcn (+ Vitest)で冷蔵庫内管理アプリをつくった

に公開

前回、Vite + Jest + Testing Libraryの環境を作りましたが、今回はVitestを使って環境作ってAIにコード書いてもらうことにします。

https://github.com/chiilog/reizouko-manager

準備

前回やってるViteとかPrettierとかは割愛します。

VitestとTesting Libraryをインストール

https://vitest.dev/guide/

npm install -D vitest @testing-library/react @testing-library/dom @types/react @types/react-dom jsdom @testing-library/jest-dom

インストールしたら、package.jsonのスクリプトにtestを追加します。

package.json
{
  "name": "reizouko-manager",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview",
    "test": "vitest"
  },
  ...
}

vite.config.ts を以下に書き換え

vite.config.ts
- import { defineConfig } from 'vite'
+ import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react-swc'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    setupFiles: './vitest.setup.ts',
+  },
})

vitest.setup.tsにインポート入れておきます。

vitest.setup.ts
import '@testing-library/jest-dom'

tsonfig.app.json の追記はこう。

tsconfig.app.json
{
  "compilerOptions": {
    ...

+    "esModuleInterop": true,
+    "types": ["vitest/globals", "@testing-library/jest-dom"]
  },
  "include": ["src"]
}

あとはテストファイルを作って動作確認して完了!

本題:冷蔵庫内の賞味期限を管理したい!

賞味期限ギリギリまで残してしまったり、見たら賞味期限切れてた………ということが結構あるので、買ったものの賞味期限を管理したいなーとずっと思っていました。
前に同じものをLovableで作ったのですが、コード読む気力がなくてそのままにしてたので、一度作り直すことにしました。

shadcnをインストール

UIライブラリはshadcnでやってみます。結構スキ。

https://ui.shadcn.com/docs/installation/vite

viteはすでに導入済みなので、その次のコマンドからやっていきます。

npm install tailwindcss @tailwindcss/vite

src/index.cssの中身を、Tailwindに置き換えます。

src/index.css
@import "tailwindcss";

tsconfig.jsontsconfig.app.jsonbaseUrlpathsを追記。

tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
}
tsconfig.app.json
{
  "compilerOptions": {
    ...

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

次に、アプリがエラーなしでパスを解決できるように@types/nodeをインストールして、vite.config.tsを編集します。

npm install -D @types/node
vite.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
+ import path from "path";
+ import tailwindcss from "@tailwindcss/vite";

// https://vite.dev/config/
export default defineConfig({
-  plugins: [react()],
+  plugins: [react(), tailwindcss()],
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "./src"),
+    },
  },
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: "./vitest.setup.ts",
  },
});

CLIを実行してセットアップします。

npx shadcn@latest init

https://ui.shadcn.com/colors

多分このベースカラーのslate gray zinc neutral stone が該当するっぽい。
なんでもいいけどとりあえずneutralにします。

選択すると

It looks like you are using React 19. 
Some packages may fail to install due to peer dependency issues in npm (see https://ui.shadcn.com/react-19).

黄色い文字で出てくるのですが、Use --forceでインストールします。

Lint対象からshadcn関連を無視するように追記していきます。

public
*.d.ts

+ src/lib/**/*
+ src/components/ui/**/*
eslint.config.js
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";

export default tseslint.config(
-  { ignores: ["dist"] },
+  { ignores: ["dist", "src/lib/**/*", "src/components/ui/**/*"] },
  ...
);

次に、テストからも除外するためvite.config.tsも書き換えます。

vite.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import tailwindcss from "@tailwindcss/vite";

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  test: {
    globals: true,
    environment: "jsdom",
    setupFiles: "./vitest.setup.ts",
+    exclude: ["src/lib/**/*", "src/components/ui/**/*"],
+    passWithNoTests: true,
  },
});

passWithNoTestsはテストなくてもエラーにならないようにしました。現状何のテストもないけど、shadcnは入れてるから「テストない!」ってエラーが出るんですよね。

TODOリストを作る

実装は基本Cursor(claude-3.7-sonnet)でやってもらうので、あいまいな仕様書ではなくがっちり作ります。

一旦MVPとして実装するため、こんな感じでまとめました。

TODO.md
- コンポーネントはshadcnを使用して実装すること
- レスポンシブでどのデバイスにも適切に表示されるようにすること
- 食材のリストはローカルストレージで管理する
- ドロワーで食材登録フォームが開く
  - [ ] 食品名の入力欄
  - [ ] 食品の賞味期限の入力欄
    - [ ] デフォルトでは5日間とする
  - [ ] 登録ボタン
  - [ ] 登録をキャンセルするボタン
- 食材リスト
  - [ ] フォームで登録した食材が1つずつカード型のレイアウトで表示される
    - [ ] 食品名(例:きゅうり、たまご)
    - [ ] 賞味期限までの日数を、賞味期限から自動計算する(例:あと3日)
      - [ ] カードに、賞味期限までの日数によって背景色をつける
        - 5日以上ある:緑
        - 5日〜3日:黄色
        - 3日〜当日:赤
        - 当日を過ぎた:黒
    - [ ] 削除ボタン

もちろんChatGPTによる添削つきです。日数の自動計算の明記とかが抜けてたのでありがたい。
あと、テストも併せて実装してもらうので

- 実装する各機能には、可能な限りテストも含めて実装すること
  - 使用ライブラリ:Vitest + @testing-library/react

これでAuto-runで実装してもらいます。
なお、あとから気づきましたがテストはクエリの優先度も明記しておいたほうがいいです。
最終的に

TODO.md
# アプリについて

- コンポーネントはshadcnを使用して実装すること
- レスポンシブでどのデバイスにも適切に表示されるようにすること
- 食材のリストはローカルストレージで管理する
- ドロワーで食材登録フォームが開く
  - [ ] 食品名の入力欄
  - [ ] 食品の賞味期限の入力欄
    - [ ] デフォルトでは5日間とする
  - [ ] 登録ボタン
  - [ ] 登録をキャンセルするボタン
- 食材リスト
  - [ ] フォームで登録した食材が1つずつカード型のレイアウトで表示される
    - [ ] 食品名(例:きゅうり、たまご)
    - [ ] 賞味期限までの日数を、賞味期限から自動計算する(例:あと3日)
      - [ ] カードに、賞味期限までの日数によって背景色をつける
        - 5日以上ある:緑
        - 5日〜3日:黄色
        - 3日〜当日:赤
        - 当日を過ぎた:黒
    - [ ] 削除ボタン
    - [ ]

# テストについて

- 実装する各機能には、可能な限りテストも含めて実装すること

  - 使用ライブラリ:Vitest + @testing-library/react
  - テストは主に振る舞い(behavior)に着目し、ユーザー視点で書くこと

- クエリの使用優先度([Testing Libraryのガイドライン](https://testing-library.com/docs/queries/about/)に準拠):

  ### 誰でもアクセスできるクエリ(最優先)

  1. `getByRole`
  2. `getByLabelText`
  3. `getByPlaceholderText`
  4. `getByText`
  5. `getByDisplayValue`

  ### セマンティッククエリ

  1. `getByAltText`
  2. `getByTitle`

  ### テスト専用クエリ(最終手段)

  1. `getByTestId`

- テストは可読性を意識し、3A(Arrange → Act → Assert)の流れで記述してください
  - それぞれのフェーズが分かるよう、空行やコメントで明確に区切るとより良いです

ざっくりテストみた時に「これは足したほうがいいかな〜」って思った最低限を足したので、コードレビュー後にまた追記するかも。

コードレビューについては後日。

GitHub Pagesに公開する

GitHub Actions等についてはViteに完璧にまとまってるのでそちらを参照します。

https://ja.vite.dev/guide/static-deploy#github-pages

が、今回インストールしたバージョンのreact-day-picker がReact19に対応してなくて色々作業したのでその備忘録を。

まずはパッケージを全部最新にする

インストールされているバージョンが古いのでは?と思ったので、ncuコマンドで調べてみました。

 @eslint/js                  ^9.21.0  →  ^9.24.0
 @vitejs/plugin-react-swc     ^3.8.0  →   ^3.8.1
 date-fns                     ^3.6.0  →   ^4.1.0
 eslint                      ^9.21.0  →  ^9.24.0
 eslint-plugin-react-hooks    ^5.1.0  →   ^5.2.0
 globals                    ^15.15.0  →  ^16.0.0
 react                       ^19.0.0  →  ^19.1.0
 react-day-picker            ^8.10.1  →   ^9.6.5
 react-dom                   ^19.0.0  →  ^19.1.0
 typescript                   ~5.7.2  →   ~5.8.3
 typescript-eslint           ^8.24.1  →  ^8.29.1
 vite                         ^6.2.0  →   ^6.2.6

これでただ更新しただけでうまくいけば「は〜よかったよかった〜」で済みますが、残念ながらそうはならず。

どうもCalendarコンポーネントのIconLeftIconRightがなくなった模様。

こう言う時は大元のドキュメントを参照します。

https://daypicker.dev/docs/styling

やっぱり手段はあるみたい。

The current shadcn/ui does not show the latest version. For a proper integration, please refer to date-picker.luca-felix.com.

https://date-picker.luca-felix.com/

と言うわけで、ここで書かれているCalendar.tsxをこのまま使わせていただきます。
これでnpm run buildができたので、今度こそGitHub Actionsが通るはず!

https://chiilog.github.io/reizouko-manager/

完成!ここまでで大体4時間くらいでしょうか。
あとはじっくりコードレビューと改善を繰り返して行こうと思います。(コードレビューはスクラップにしてから記事にまとめることになりそうな気がする)

Discussion