Vite+ + React

Vite+ pre-1.0 SPA React 19

ReScript と Vite+ ツールチェーンで動作するシングルページ React アプリケーション。Vite+(vite-plus)は Vite・Vitest・Oxlint・Oxfmt・Rolldown を統合したラッパーであり、1 つの CLI(vp)が dev・build・preview・test を処理する。テンプレートはオフラインフォールバックを備えた動作する greet フォーム (入力 + useState + fetch)と、vp test のスモークスイートを同梱する。

モダンな統合ツールチェーンで React SPA を組みたい場合に本テンプレートを選択する。.res + JSX のプロジェクトを ブラウザで動かす最速の手段である。Vite+ で詰まった場合(pre-1.0 のため)も、README と本ページに vanilla Vite への クリーンなフォールバック手順が記載されている — ロックインされることはない。

生成内容

my-project/
├── rescript.json                  # JSX mode "automatic", @rescript/react in bs-deps
├── package.json                   # ESM, "private": true, "vp" scripts
├── index.html                     # <script type="module" src="/src/Main.res.mjs">
├── vite.config.mjs                # imports defineConfig from "vite-plus" + react()
├── src/
│   ├── Main.res                   # ReactDOM.Client.createRoot + <App />
│   ├── App.res                    # form + useState + fetch + Validation
│   ├── Api.res                    # fetch("/api/greet") + offline fallback
│   ├── Validation.res             # zod or sury — selected in the wizard
│   └── __tests__/
│       └── App.test.mjs           # smoke test that App is a function component
├── README.md                      # script docs + About Vite+ section
├── LICENSE                        # MIT, holder = project name
├── .nvmrc                         # Node 24
├── .gitignore                     # node_modules + ReScript build + dist/ + .vite/
├── .editorconfig                  # 2-space indent, LF line endings
└── .github/
    ├── dependabot.yml             # weekly npm updates
    └── workflows/ci.yml           # install + rescript build + vp test

ウィザードオプション

オプション

効果

Project name

npm の name、LICENSE 保有者、および index.html<title> となる

Package manager

npm / pnpm / yarn / bun。packageManager フィールド、README のインストールコマンド、CI のキャッシュキーに影響する

Validation library

zodsury。同梱される src/Validation.res のバリアントを選択する — バリデータは フォームの submit ハンドラ内で動作する

主要な依存

パッケージ

用途

バージョン

rescript

ReScript コンパイラ

TemplateVersions.RESCRIPT

@rescript/core

標準ライブラリ

TemplateVersions.RESCRIPT_CORE

@rescript/runtime

コンパイル後の .res.mjs がランタイムでインポートするランタイムスタブ

TemplateVersions.RESCRIPT_RUNTIME

@rescript/react

React バインディング(フック・コンポーネント・イベント)

TemplateVersions.RESCRIPT_REACT

react / react-dom

React 19 ランタイム

TemplateVersions.REACT / TemplateVersions.REACT_DOM

zod または sury

フォーム入力のバリデーション(ウィザードで選択)

TemplateVersions.ZOD / TemplateVersions.SURY

vite-plus (dev)

Vite/Vitest/Oxlint を統合し vp CLI を提供するラッパー

TemplateVersions.VITE_PLUS

@voidzero-dev/vite-plus-core (dev)

Vite+ のコアエンジン

TemplateVersions.VITE_PLUS_CORE

vite (dev)

ドキュメント化されている Vite+ → Vite フォールバックが依存を変更せずに機能するよう、直接依存として追加されている

TemplateVersions.VITE

@vitejs/plugin-react (dev)

React Fast Refresh + JSX

TemplateVersions.VITEJS_PLUGIN_REACT

vitest (dev)

スモークテストランナー(vp test 経由でも駆動される)

TemplateVersions.VITEST

@vitest/coverage-v8 (dev)

test:coverage 用のカバレッジプロバイダ

TemplateVersions.VITEST_COVERAGE_V8

主要なファイル

rescript.json

本テンプレート固有の 3 つのポイント:

  • bs-dependencies@rescript/react が含まれているため JSX が型チェックされる

  • jsx.mode = "automatic" により、JSX は React.createElement ではなく react/jsx-runtime に展開される

  • ウィザードでの選択に応じて、バリデーションライブラリ(@rescript/zod シムまたは rescript-sury)が追記される

これらに手を加える必要はほとんどない — いずれもモダンな React + ReScript の規約と 1:1 で対応している。

index.html

Vite のエントリドキュメント。/src/Main.res.mjs をモジュールとして読み込む — Vite はこのパスを dev サーバ経由で解決する(dev の場合)か、ハッシュ付きのバンドルへ書き換える(build の場合)。

src/Main.res

ブートストラップ。3 行で構成される: #root を検索し、React ルートを作成し、<App /> をレンダリングする:

switch ReactDOM.querySelector("#root") {
| Some(rootEl) =>
  ReactDOM.Client.Root.render(ReactDOM.Client.createRoot(rootEl), <App />)
| None => Console.error("Could not find root element")
}

src/App.res

インタラクティブなデモ。制御された <input> + submit ボタン + 4 つの useState (name, greeting, error, loading)から成る。submit ハンドラは Validation.parseGreetForm で入力をバリデートしてから Api.greet を呼び出す:

let handleSubmit = async event => {
  ReactEvent.Form.preventDefault(event)
  switch Validation.parseGreetForm(name) {
  | Error(message) =>
    setError(_ => Some(message))
    setGreeting(_ => None)
  | Ok({name: validated}) =>
    setError(_ => None)
    setLoading(_ => true)
    try {
      let message = await Api.greet(validated)
      setGreeting(_ => Some(message))
    } catch {
    | JsExn(err) =>
      setGreeting(_ => Some("Error: " ++ err->JsExn.message->Option.getOr("unknown")))
    }
    setLoading(_ => false)
  }
}

validate してから fetch する順序が重要である: バリデーションは安価かつ同期的なので、ネットワークラウンドトリップの コストを払う前にチェックする。エラーはインライン(<p style={{color: "crimson"}}>)で描画され、リクエスト処理中は submit ボタンが無効化される。

src/Api.res

/api/greet を対象とした薄い fetch ラッパー。注目すべき点は オフラインフォールバック である: バックエンドが利用できなくても関数は使える挨拶を返すため、初回実行時にフォームが壊れて見えることがない。

let greet = async (name: string): string => {
  try {
    let response = await fetch("/api/greet", {...})
    if response->ok {
      let body = await response->json
      body["message"]
    } else {
      `Hello, ${name}! (offline fallback — no backend at /api/greet)`
    }
  } catch {
  | _ => `Hello, ${name}! (offline fallback — fetch failed)`
  }
}

実際のバックエンドを接続する場合(Full-StackMonorepo に移行するか、Hono (REST) テンプレートを立ち上げる場合)には、フォールバック分岐を削除すること。

src/Validation.res

parseGreetForm: string => result<greetForm, string> — fetch 呼び出しの前に実行される。zod 版・sury 版のいずれも、トリム後の name が空でなく、80 文字以下であることをチェックする。不正な入力はインラインのエラーメッセージを点灯させ、妥当な入力は Api.greet へ進む。

src/__tests__/App.test.mjs

スモークテストは コンパイル後App.res.mjs をインポートし、エクスポートされたコンポーネントを検証する:

import { describe, expect, it } from "vitest";
import { make as App } from "../App.res.mjs";

describe("App", () => {
  it("is a function component", () => {
    expect(typeof App).toBe("function");
  });
});

コンポーネントが到達可能であることを確認する最小限のサニティチェックである。フォーム自体はレンダリングしない。DOM の挙動を検証するには、@testing-library/react と JSDOM 環境を dev dependencies に追加し、<App /> をレンダリングして submit ボタンをクリックするテストを記述する。

vite.config.mjs

ボイラープレートに加えて 2 行:

import { defineConfig } from "vite-plus";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
});

defineConfig のインポート元が vite ではなく vite-plus である点に注意。vanilla Vite に フォールバックする場合はこのインポートを差し替える(下記の Notes を参照)。

npm スクリプト

スクリプト

説明

dev

vp dev — HMR 付きで Vite+ の dev サーバを起動

build

vp builddist/ にプロダクションバンドルを生成

preview

vp preview — 手動スモークテスト用にビルド済みの dist/ を配信

test

vp test — Vite+ CLI 経由で Vitest を実行

test:coverage

vp test --coverage — 同上に v8 のカバレッジレポートを付与

res:build

rescript — ワンショットの ReScript コンパイル

res:clean

rescript clean — 生成された .res.mjs を削除

res:dev

rescript -w — 保存時に再コンパイル(vp dev と並行して実行する)

推奨される開発ワークフローは 2 ターミナル構成である: 一方で pnpm res:dev を走らせ .res.mjs 出力を 最新に保ち、もう一方で pnpm dev を走らせて Vite+ の HMR サーバを動かす。Vite+ はファイルウォッチャ経由で 再コンパイルされたモジュールを拾う。

vanilla Vite へのフォールバック

pre-1.0 の Vite+ が誤動作する場合(よくあるのは vp build 中の vite/internal の解決失敗)、Vite+ から移行するのは 2 箇所の編集だけで、新しい依存は不要である:

  1. vite.config.mjs のインポートを差し替える:

    - import { defineConfig } from "vite-plus";
    + import { defineConfig } from "vite";
    
  2. package.jsonvp スクリプトを vanilla 相当のものに置き換える:

    "scripts": {
      "dev":     "vite",
      "build":   "vite build",
      "preview": "vite preview",
      "test":    "vitest run",
      "test:coverage": "vitest run --coverage"
    }
    

vitevitest のパッケージは既に devDependencies に含まれている。vite-plus@voidzero-dev/vite-plus-core はインストールしたまま残しておいてもよいし(設定からインポートされなければ 何もしない)、pnpm remove vite-plus @voidzero-dev/vite-plus-core で削除してもよい。

Vite+ の安定リリースが出たら、戻すのは同じ編集の逆操作で済む — 典型的な React プロジェクトでは、両者の間で vite.config の構文に違いはない。

2日目以降のレシピ

プロジェクトを開いた後の ReScript 側のエディタワークフローについては、機能概要 を参照すること。

補足

  • Vite+ は pre-1.0 である。 TemplateVersions.VITE_PLUS0.1.x レンジに固定している。安定リリース前に API・デフォルト値・CLI フラグが変わる可能性がある。バンプの際は Vite+ のチェンジログ を追跡すること。

  • 既知の非互換性: 現在の pre-1.0 Vite+ は @vitejs/plugin-react と組み合わせた際に vite/internal を常にクリーンに解決できるとは限らないため、vp build が失敗することがある。フォールバックは 1 箇所の編集である: vite.config.mjsimport { defineConfig } from "vite-plus"import { defineConfig } from "vite" に置き換え、npm スクリプト(dev / build / preview / test)を vp から vite / vite build / vite preview / vitest run に差し替える。テンプレートは既に直接の vite 依存を宣言しているため、この差し替えに npm install は不要である。

  • デフォルトは React 19 である。@rescript/react@types/react 系は TemplateVersions.kt で対応するメジャーに固定されている。一緒にバンプすること。

  • rescript.jsonjsx.mode"automatic" に設定されている。"classic" に戻す場合は、JSX を使うすべての .res ファイルの先頭で React をインポートする必要もある。

  • Vitest のスモークテストは import { make as App } from "../App.res.mjs" が関数を 返すことだけを確認する。フォームを実際にレンダリングはしない。DOM のアサーションが必要になったら @testing-library/react と JSDOM 環境を追加すること。

  • フォームは意図的にネットワーク呼び出しの Validation を呼ぶ。これにより契約がシンプルに保たれる: ネットワークは既にローカルチェックを通過した入力しか見ない。サーバ側のバリデーションは依然として必須である — 対応するバックエンドパターンについては Hono (REST) または Full-Stack テンプレートを参照すること。

  • .gitignore は ReScript のデフォルトに加えて dist/.vite/ を追加しており、ビルドアーティファクトや Vite のローカルキャッシュがコミットされないようになっている。

  • package.json"private": true を宣言している。SPA そのものを実際に npm に公開したい場合に限り、false に切り替え(同時に正式な versionlicense を追加する)こと — エンドユーザ向けアプリでは珍しい。

  • vp CLI は vp test においては Vitest の薄いラッパーである。Vite+ が公開しない Vitest 固有のフラグが 必要になったら、直接 vitest run を実行すること — vitest@vitest/coverage-v8 の依存は 既にインストール済みである。