Rspack+生成AI向けReactプロジェクトの始め方

# 要約

- 大規模言語モデル（Claude、ChatGPTなど）にReactコンポーネントを作ってもらう時、shadcn/uiを指定すると良い感じにフォームを作ってもらえる。が、作ってもらったフォーム等を手元で動かそうとすると、色々と設定が必要になる。
- 本記事では[`Rspack`](https://rspack.dev/)と`pnpm`を使ってReactプロジェクトを作り、shadcn/uiを導入する手順を解説する。
  - `Rspack`とはRustで書かれたwebpackの互換性のあるツールで早いので筆者は気に入っている。
  - いくつかのプロジェクトで採用しているが、webpackと同様に扱えており不自由していない。
  - webpackを使いたいのであれば、webpack.config.jsを同様に書くだけで、あとは同じ手順で設定できると思われるが、本記事では扱わない。
- なお、設定しなければいけない手順として多くあるので、一括で行ってくれるツールを用意した。詳細は最後の方に記載しているが、コマンドは以下の通りである。

```bash
pnpm create @infodb/myproj@latest
```

## 補足

- `next.js`や`vite`を使うのであれば[shadcn/uiの公式ページを参照](https://ui.shadcn.com/docs/installation)して設定するか、または`v0`や`bolt`といったサービスを利用することでもプロジェクト等の設定まで行えるはず。但し、本記事では扱わない。
- また、文中は`pnpm`を使っているが、`npm`や`yarn`でも同じことができるはずである。（当方では未確認）

## 紹介動画

このツールの紹介動画。Claudeで作ったフォームを開発環境に持ってきて動かすまでを解説している。

<iframe width="560" height="315" src="https://www.youtube.com/embed/6nBY-P4VEwA?si=2LRzF5S8nCZvaWN8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 1. Rspackのプロジェクト作成からTailwind CSSの導入まで

- [Tailwind CSSの公式ページに記載](https://tailwindcss.com/docs/guides/rspack)されているが、それの転記となる。
- また後半にも同じファイルを修正する手順があるが、混乱を避けるために元の手順をそのまま採用している。

## 1-1. プロジェクトの作成

- 以下のコマンドを実行してプロジェクトを作成する。

```bash
pnpm create rspack@latest
```

- プロジェクト名やフレームワーク、言語を選択する画面が表示されるので、適宜入力する。
- ここではTypeScriptを選択したケースで記載している。

## 1-2. Tailwind CSSの導入

- 以下のコマンドを実行してTailwind CSSを導入する。

```bash
pnpm add -D tailwindcss postcss postcss-loader autoprefixer
pnpx tailwindcss init -p
```

## 1-3. rspack.config.tsの修正

- rulesの下にCSSの設定を追加する。

```typescript
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["postcss-loader"],
        type: "css",
      },
  // ...
```

## 1-4. tailwind.config.jsの修正

- contentに`./src/**/*.{js,ts,jsx,tsx}`を追加する。

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

## 1-5. index.cssの修正

- `index.css`を以下のように書き換える。
- Rspackでデフォルトで生成される`index.css`は、色々書かれているので全て消して構わない。

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

## 1-6. devサーバーの起動

- 以下のコマンドを実行してdevサーバーを起動する。
- ブラウザで`http://localhost:8080`にアクセスして、画面が表示されれば成功。
- また、`src/App.tsx`を編集して、tailwindcssのクラスが適用されることを確認する。

```bash
pnpm dev
```

- `src/App.tsx`の例

```typescript
export default function App() {
  return (
    <h1 className="text-3xl font-bold underline">
      Hello world!
    </h1>
  )
}
```

# 2. Shadcn/UIの導入

- `shadcn/ui`の公式ページで[マニュアルインストール](https://ui.shadcn.com/docs/installation/manual)に記載されているが、それの転記となる。
- 但し一部パス等の修正は行っている。
- また、公式ページの記載では足りていない部分があるので、それは次章で記載している。

## 2-1. 依存関係の導入

- 依存関係のあるパッケージ、アイコンライブラリ、@radix-ui/react-iconsをインストールする。

```bash
pnpm add tailwindcss-animate class-variance-authority clsx tailwind-merge
pnpm add lucide-react @radix-ui/react-icons
```

## 2-2. path aliasの設定

- `tsconfig.json`に`baseUrl`と`paths`を追加する。

```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    ...
  }
}
```

## 2-3. tailwind.config.jsの設定

- `tailwind.config.js`を以下のように修正する。（全て書き換えで構わない。）

```javascript
const { fontFamily } = require("tailwindcss/defaultTheme")

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: ["class"],
  content: ["./src/app/**/*.{ts,tsx}", "./src/components/**/*.{ts,tsx}"],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
      fontFamily: {
        sans: ["var(--font-sans)", ...fontFamily.sans],
      },
      keyframes: {
        "accordion-down": {
          from: { height: "0" },
          to: { height: "var(--radix-accordion-content-height)" },
        },
        "accordion-up": {
          from: { height: "var(--radix-accordion-content-height)" },
          to: { height: "0" },
        },
      },
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}
```

## 2-4. global.cssの作成

- ./src/global.cssを作成し、以下の内容を記述する。

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
	:root {
		--background: 0 0% 100%;
		--foreground: 222.2 47.4% 11.2%;

		--muted: 210 40% 96.1%;
		--muted-foreground: 215.4 16.3% 46.9%;

		--popover: 0 0% 100%;
		--popover-foreground: 222.2 47.4% 11.2%;

		--border: 214.3 31.8% 91.4%;
		--input: 214.3 31.8% 91.4%;

		--card: 0 0% 100%;
		--card-foreground: 222.2 47.4% 11.2%;

		--primary: 222.2 47.4% 11.2%;
		--primary-foreground: 210 40% 98%;

		--secondary: 210 40% 96.1%;
		--secondary-foreground: 222.2 47.4% 11.2%;

		--accent: 210 40% 96.1%;
		--accent-foreground: 222.2 47.4% 11.2%;

		--destructive: 0 100% 50%;
		--destructive-foreground: 210 40% 98%;

		--ring: 215 20.2% 65.1%;

		--radius: 0.5rem;
	}

	.dark {
		--background: 224 71% 4%;
		--foreground: 213 31% 91%;

		--muted: 223 47% 11%;
		--muted-foreground: 215.4 16.3% 56.9%;

		--accent: 216 34% 17%;
		--accent-foreground: 210 40% 98%;

		--popover: 224 71% 4%;
		--popover-foreground: 215 20.2% 65.1%;

		--border: 216 34% 17%;
		--input: 216 34% 17%;

		--card: 224 71% 4%;
		--card-foreground: 213 31% 91%;

		--primary: 210 40% 98%;
		--primary-foreground: 222.2 47.4% 1.2%;

		--secondary: 222.2 47.4% 11.2%;
		--secondary-foreground: 210 40% 98%;

		--destructive: 0 63% 31%;
		--destructive-foreground: 210 40% 98%;

		--ring: 216 34% 17%;

		--radius: 0.5rem;
	}
}

@layer base {
	* {
		@apply border-border;
	}
	body {
		@apply bg-background text-foreground;
		font-feature-settings: "rlig" 1, "calt" 1;
	}
}
```

## 2-5. lib/utils.tsの作成

- ./src/lib/utils.tsを作成し、以下の内容を記述する。

```typescript
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
```

# 3. 最終調整

- 以下の内容は、公式ページ等には書かれていないが、必要な設定である。

## 3-1. rspack.config.tsの名前修正とAliasの追加

- `rspack.config.ts`のままだと`path.resolve`で`TypeError: Cannot read properties of undefined (reading 'resolve')`のエラーとなってしまってaliasが設定できないので、`rspack.config.js`にリネームする。
  - 原因は不明。将来的なバージョンで修正される可能性があるし、`pnpm`を使っている副作用かもしれない。
  - jsファイルにすることで`path`が無事にインポートされて回避できるので、一旦これで対応する。

```javascript
const rspack = require("@rspack/core");
const RefreshPlugin = require("@rspack/plugin-react-refresh");
const path = require("node:path");

const isDev = process.env.NODE_ENV === "development";

// Target browsers, see: https://github.com/browserslist/browserslist
const targets = ["chrome >= 87", "edge >= 88", "firefox >= 78", "safari >= 14"];

module.exports = {
	context: __dirname,
	entry: {
		main: "./src/main.tsx",
	},
	resolve: {
		alias: {
			"@": path.resolve(__dirname, "./src"),
		},
		extensions: ["...", ".ts", ".tsx", ".jsx"],
	},
	module: {
		rules: [
			{
				test: /\.svg$/,
				type: "asset",
			},
			{
				test: /\.(jsx?|tsx?)$/,
				use: [
					{
						loader: "builtin:swc-loader",
						options: {
							jsc: {
								parser: {
									syntax: "typescript",
									tsx: true,
								},
								transform: {
									react: {
										runtime: "automatic",
										development: isDev,
										refresh: isDev,
									},
								},
							},
							env: { targets },
						},
					},
				],
			},
			{
				test: /\.css$/,
				use: ["postcss-loader"],
				type: "css",
			},
		],
	},
	plugins: [
		new rspack.HtmlRspackPlugin({
			template: "./index.html",
		}),
		isDev ? new RefreshPlugin() : null,
	].filter(Boolean),
	optimization: {
		minimizer: [
			new rspack.SwcJsMinimizerRspackPlugin(),
			new rspack.LightningCssMinimizerRspackPlugin({
				minimizerOptions: { targets },
			}),
		],
	},
	experiments: {
		css: true,
	},
};
```

## 3-2. src以下のファイルを修正

- `app`、`components`フォルダを作成する。
- `src/App.tsx`を`src/app/App.tsx`に移動する。
- `src/index.css`、`src/App.css`を削除する。
- main.tsxを以下のように修正する。

```typescript
import React from "react";
import ReactDOM from "react-dom/client";

import App from "@/app/App";
import "./global.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
	<React.StrictMode>
		<App />
	</React.StrictMode>,
);
```

## 3-3. components.jsonの作成

- `./components.json`を作成し、以下の内容を記述する。

```json
{
	"$schema": "https://ui.shadcn.com/schema.json",
	"style": "new-york",
	"rsc": true,
	"tsx": true,
	"tailwind": {
		"config": "tailwind.config.js",
		"css": "src/globals.css",
		"baseColor": "zinc",
		"cssVariables": true,
		"prefix": ""
	},
	"aliases": {
		"components": "@/components",
		"utils": "@/lib/utils",
		"ui": "@/components/ui",
		"lib": "@/lib",
		"hooks": "@/hooks"
	}
}
```

## 3-4. shadcn/uiのコンポーネントの追加

```bash
pnpx shadcn@latest add button
```

- `src/app/App.tsx`を以下のように修正する。

```typescript
import { Button } from "@/components/ui/button";

export default function App() {
	return (
		<div className="p-5">
			<h1 className="text-3xl font-bold underline text-red-400 mb-3">
				Hello world!
			</h1>
			<Button>Click me</Button>
		</div>
	);
}
```

## 3-5. devサーバーの起動

- 動作確認をする。以下のコマンドを実行してdevサーバーを起動する。

```bash
pnpm dev
```

- Hello world!とClick meボタンが表示されれば成功。
- あとはAIにshadcn/uiを使ったコンポーネントの作成指示を出すことで、コピペでフォームが表示できるようになる。（コンポーネントやライブラリなどは適宜入れる。）

# フォルダ構成と解説、課題など

- 最終的なフォルダ構成は以下のようになる。

```text
|--node_modules/* (中身は省略)
|--src
|  |--app
|  |  |--App.tsx
|  |--components
|  |  |--ui
|  |  |  |--button.tsx
|  |--lib
|  |  |--utils.ts
|  |--global.css
|  |--main.tsx
|--.gitignore
|--components.json
|--index.html
|--package.json
|--pnpm-lock.yaml
|--postcss.config.js
|--README.md
|--rspack.config.js
|--tailwind.config.js
|--tsconfig.json
```

- src直下にglobal.cssとmain.tsxを配置している。また、index.htmlはプロジェクト直下となっているので、好みに合わせて修正が必要。（筆者はこの構成を当面使用する予定）
- `pnpx shadcn@latest add <<component>>`を実行するために必要な最小限のファイルは、`components.json`、`package.json`、`tsconfig.json`の3ファイルである。(フォルダは無ければ作られる)
  - 依存関係がインストールされていないと、shadcnがnpmでインストールしてくれるようなので、事前にインストールしておく必要がある。
  - tsconfig.jsonはaliasとなっているフォルダパスを取得するために参照していると思われる。
  - 既存のプロジェクトなどに組み込む場合には、それらファイルを用意するだけでshadcn/uiを使えるようになると思われる。（ライブラリ等は要調整）

# ツール

- 手順が多いので3-3までの手順を行うツールを用意した。

```bash
pnpm create @infodb/myproj@latest
```

https://github.com/tamuto/infodb-cli