😡

.envに書いてあるものはundefinedにならないで欲しい

2023/02/18に公開

.env に書いてあるものは undefined にならないで欲しい

と、一度は思ったことがあると思います。

それで、 process.env.API_KEY!import.meta.env.API_BASE_URL as string 的なキャストを書いて消耗していたり。
というかそもそも import.meta.env が長いです。
あと、TYPO してても気付けないという問題もあります。

実は SvelteKit では、この問題が解決されています。参考 (感動〜 SvelteKit さんって細かいところまで気が効くのね!)
今回は、Next.js や Astro などのフレームワークでも汎用的に使える typedotenv を紹介します。

typedotenv とは

私が作ったパッケージ群です。

Playground が用意してあるので触ればなんとなくわかると思いますが、 .env ファイルを読んで、かんたんな TypeScript(JavaScript) コードを生成するだけのものです。

例えば

API_KEY=qwertyuiop
NEXT_PUBLIC_API_ENDPOINT=http://example.com/

という .env ファイルがあれば

/* Auto generated by typedotenv */
if (typeof process.env.API_KEY !== 'string') throw new Error('API_KEY is not defined in .env');
export const API_KEY = process.env.API_KEY;
if (typeof process.env.NEXT_PUBLIC_API_ENDPOINT !== 'string') throw new Error('NEXT_PUBLIC_API_ENDPOINT is not defined in .env');
export const NEXT_PUBLIC_API_ENDPOINT = process.env.NEXT_PUBLIC_API_ENDPOINT;

という TypeScript のコードが吐かれます。
やってること言えば、環境変数の値が string であることを保証しつつ export しているだけです。

つかいかた

npm i -D unplugin-typedotenv して、各種フレームワークのプラグインとして挿入します。

Next.js

next.config.js
const typedotenv = require('unplugin-typedotenv/webpack') // 型が欲しければ `.default` をつける
module.exports = {
  webpack: (config) => {
    config.plugins.push(
      typedotenv({ output: 'src/env.ts', env: process.env.NODE_ENV })
    );
    return config;
  },
};

Vite 系

vite.config.ts
import typedotenv from 'unplugin-typedotenv/vite'
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
  plugins: [
    typedotenv({ output: 'src/env.ts', env: mode, envObject: 'import.meta.env' })
  ],
})

あとは、 .env.development などから src/env.ts などにコードが生成されます。
もちろんホットリロードにも対応しており、 .env ファイルを編集したら再出力されます。

もちろん、フレームワーク以外でも使えるように CLI が用意してあります。
npm i -D @typedotenv/cli して package.json に npm-scripts を書いて使えます。

package.json
{
  "scripts": {
    "generate": "typedotenv generate src/env.ts"
  }
}

@typedotenv/core ではコード生成部分だけが生えていますが、紹介は省略します。

.env のバリデーション

使うことはあまりないかもしれませんが、「特定の変数名しか許さない」、「特定の変数名は含めてはいけない」、「特定の変数名を含める必要がある」、「この変数はこういう値を持つべき」といったチェックもできます。

それぞれ allowList(--allow) / denyList(--deny) / required(--required) / patterns(現状 API 経由のみ) です。(括弧内は CLI オプション)

CI 文脈の開発/本番差異チェック

.env.development には設定してあるけど .env.production には設定してない変数があると事故る可能性がある上、気付きづらいです。
@typedotenv/cli では先のバリデーションもですが、環境差異判定にも使える typedotenv check コマンドが用意しています。

開発時には .env.development を利用しつつ、 CI で typedotenv check -e production などで .env.production と比較させることで、変数の過不足がないかを判定できます。

その他オプション

先に紹介した例では、ランタイムの型チェックで絞り込みをしていましたが、本番環境では仮に undefined でも throw してほしくないことがあると思います。
その為、ランタイム型チェックを無効化する disableRuntimeTypeCheck(--disable-type-check) が生えてます。

そうすると今度は、 string | undefined で困るというケースもあるかもしれません。
その為、型アサーションを付与する enableTypeAssertion(--enable-type-assertion) が生えてます。

双方を有効にすると以下のようになります。

/* Auto generated by typedotenv */
export const API_KEY = process.env.API_KEY as string;
export const NEXT_PUBLIC_API_ENDPOINT = process.env.NEXT_PUBLIC_API_ENDPOINT as string;

他にも、Vite の例でしれっと使っている envObject(--env-object) では process.env 以外のオブジェクトから環境変数を取得したり、 prefix(現状 API 経由のみ) で冒頭コメント部分を /* eslint-ignore */ などの任意の文字列に変更できるなどがあります。

unplugin とは

最後に、今回のパッケージの unplugin-typedotenv で使われている unplugin について紹介します。

unplugin は、Vue.js や Vite のコントリビュータである Anthony Fu 氏がメインで開発している、統一プラグインシステムです。

webpack や Vite にはプラグインシステムがありますが、API が統一されておらず、プラグイン開発者としては両方対応するのは面倒なものでした。(ちなみに Vite のプラグインは Rollup のプラグインシステムが基本になっており高い互換性があります)
そこで、 Rollup のプラグイン API をベースに1つのコードベースで Webpack や Vite にも対応できるようにしたのが unplugin (unified plugin) というわけです。

おわりに

typedotenv という .env ファイルから TypeScript コードを生成するパッケージ群を紹介しました。

3 日ほどで書いたのでテストが不足しています。Issue や PR、Discussion お待ちしてます。

Discussion