Vite で環境変数をバリデーションする

に公開

Vite で開発サーバ起動時やビルド時に環境変数のバリデーションができたら良いなと思い、試してみたら意外と簡単にできたので紹介します。

環境変数バリデーションの目的

  • 環境変数の設定ミスを早期発見するため。
  • TypeScript の型情報を付与し、静的解析やエディタでの補完ができるようにするため。

前提

  • Vite のバージョン … 6.2.0
  • プロジェクトは React + TypeScript + SWC で作成していますが、その他の構成でも同様にできるはずです。

方法

例として、以下の環境変数をアプリで使うと想定し、実行前にバリデーションを行うことを考えます。

  • Vite 側で設定される環境変数
    • MODE: "development", "staging", "production" のいずれかのみ許可する。
  • .env ファイルから読み込む環境変数
    • VITE_SAMPLE_TITLE: 文字列
    • VITE_SAMPLE_URL: URL 文字列
    • VITE_GENERATE_SOURCE_MAP: "true" または "false" を指定する。(本来は boolean で指定したいが、.env ファイルで指定できる値は文字列のみのため)

1. バリデーション用関数を作成する

アプリで使用する環境変数のバリデーション処理を作成します。
VITE_GENERATE_SOURCE_MAP は使用時のことを考えて boolean に変換しています。
今回は zod を使って作成していますが、他のライブラリの使用や自前でのバリデーション実装ももちろん可能です。

src/env/validation.ts
import { z } from "zod";

/** 環境変数のスキーマ */
const envSchema = z.object({
  MODE: z.enum(["development", "staging", "production"]),
  VITE_SAMPLE_TITLE: z.string(),
  VITE_SAMPLE_URL: z.string().url(),
  VITE_GENERATE_SOURCE_MAP: z
    .enum(["true", "false"])
    .transform((arg) => arg === "true"),
});

/** 環境変数のバリデーション */
export const validateEnv = envSchema.parse;

2. vite.config.ts にプラグインを追加する

以下のようにプラグインを追加し、configResolved フック内で先ほど作成したバリデーション処理を実行します。
configResolved は Vite の設定が解決された後に呼び出されるフックで、Vite によって最終的に解決された設定を config 引数から読み取ることができます。
解決された環境変数の情報は config.env に入っているので、こちらを先ほど作成したバリデーション関数に与えることでバリデーションができます。

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
+import { validateEnv } from "./src/env/validation";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
+   {
+     name: "environment variables validation",
+     configResolved: (config) => {
+       // 環境変数のバリデーション
+       validateEnv(config.env);
+     },
+   },
  ],
});

3. バリデーションされた環境変数をアプリで使う

まず適当なファイルで import.meta.env をバリデーションした後の環境変数 env を定義します。

src/env/env.ts
import { validateEnv } from "./validation";

export const env = validateEnv(import.meta.env);

アプリ内で環境変数を使いたい時は常にこの env を使用することで、静的解析やエディタによる補完が効くようになります。

src/App.tsx (例)
import { env } from "./env/env";

function App() {
  return (
    <>
      <h2>env</h2>
      <p>MODE: {env.MODE}</p>
      <p>VITE_SAMPLE_TITLE: {env.VITE_SAMPLE_TITLE}</p>
      <p>VITE_SAMPLE_URL: {env.VITE_SAMPLE_URL}</p>
      <p>{env.VITE_GENERATE_SOURCE_MAP && "source map is generated"}</p>
    </>
  );
}

動作確認

環境変数が正しく設定されていれば、開発サーバの起動・ビルドが成功します。

設定が正しくない時は、開発サーバの起動・ビルドが失敗します。

MODE のバリデーションもしっかりできています。

開発サーバ起動時はホットリロードにも対応しています。

サンプルコードはこちらです。

https://github.com/rendob/zenn-samples/tree/main/vite-env-validation-1

vite.config.ts 内でバリデーション後の環境変数を使う

vite.config.ts 内でバリデーション済みの環境変数を使いたい場合は、defineConfig を関数の形で書き、関数内の先頭で環境変数の読み込みとバリデーションを行えば良さそうです。

vite.config.ts
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react-swc";
import { validateEnv } from "./src/env/validation";

// 関数の形に書き換える
export default defineConfig(({ mode }) => {
  // mode に対応する環境変数の読み込み、バリデーション
  // ※ MODE など Vite 側で設定される環境変数もバリデーション対象になっているとエラーになる。後述
  const env = validateEnv(loadEnv(mode, process.cwd()));

  return {
    plugins: [
      react(),
      {
        name: "environment variables validation",
        configResolved: (config) => {
          validateEnv(config.env);
        },
      },
    ],
    build: {
      // バリデーション済み環境変数の使用例
      sourcemap: env.VITE_GENERATE_SOURCE_MAP,
    },
  };
});

環境変数の読み込みには Vite から提供されている loadEnv を使いますが、これで取得できるのは .env ファイルの環境変数のみであることに注意が必要です。
つまり、MODE などの Vite 側で設定される環境変数は loadEnv で読み込まれないので、それらのバリデーションも含む場合は上のコードではエラーになってしまいます。

これを回避するには、バリデーション関数を「Vite 側で設定される環境変数用のバリデーション関数」と「.env で設定する環境変数用のバリデーション関数」とに分けて定義します。

src/env/validation.ts
import { z } from "zod";

/** Vite 側で設定される環境変数のスキーマ */
const viteEnvSchema = z.object({
  MODE: z.enum(["development", "staging", "production"]),
});

/** .env で設定する環境変数のスキーマ */
const customEnvSchema = z.object({
  VITE_SAMPLE_TITLE: z.string(),
  VITE_SAMPLE_URL: z.string().url(),
  VITE_GENERATE_SOURCE_MAP: z
    .enum(["true", "false"])
    .transform((arg) => arg === "true"),
});

/** 上記2つを合わせた環境変数スキーマ */
const envSchema = viteEnvSchema.and(customEnvSchema);

/** Vite 側で設定される環境変数のバリデーション */
export const validateViteEnv = viteEnvSchema.parse;

/** .env で設定する環境変数のバリデーション */
export const validateCustomEnv = customEnvSchema.parse;

/** 全ての環境変数のバリデーション */
export const validateEnv = envSchema.parse;

そして vite.config.ts では、defineConfig に渡す関数の先頭で「.env で設定する環境変数用のバリデーション」を、configResolved 内で「Vite 側で設定される環境変数用のバリデーション」を実行します。

vite.config.ts
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react-swc";
import { validateCustomEnv, validateViteEnv } from "./src/env/validation";

export default defineConfig(({ mode }) => {
  // .env で設定する環境変数用のバリデーション
  const customEnv = validateCustomEnv(loadEnv(mode, process.cwd()));

  return {
    plugins: [
      react(),
      {
        name: "environment variables validation",
        configResolved: (config) => {
          // Vite 側で設定される環境変数用のバリデーション
          validateViteEnv(config.env);
        },
      },
    ],
    build: {
      // バリデーション済み環境変数の使用例
      sourcemap: customEnv.VITE_GENERATE_SOURCE_MAP,
    },
  };
});

これにより、全ての環境変数をバリデーションしつつ、.env で設定した環境変数を vite.config.ts 内で使うことができます。

こちらのサンプルコードも GitHub に置いています。

https://github.com/rendob/zenn-samples/tree/main/vite-env-validation-2

終わりに

環境変数の設定ミスはアプリを実行するまで気づけないことも多いので、今回紹介したバリデーションによってミスの早期発見ができる恩恵は大きいと思います。ぜひ使ってみてください。

参考文献

https://ja.vite.dev/guide/api-plugin#configresolved

Discussion