かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Webpack TypeScript な Express アプリを環境ごとに変数を埋め込んで build したい。

前回までのあらすじ

Express を TypeScript で書いて webpack で build する構成で作成していて、環境毎に切り替えたい情報を直接バンドルして build したいと思いやって見たメモ。

WHY?

webpack をビルドする際に modeprocess.env環境変数を渡せるので、例えばこんな風に process.env.NODE_ENV を利用して読み込む環境変数を切り替えようとしていました。

import * as dotenv from 'dotenv';

if (process.env.NODE_ENV === 'production') {
  dotenv.config({ path:  path.join(__dirname, '.env.prod')});
} else {
  dotenv.config({ path:  path.join(__dirname, '.env.dev')});
}

しかし、build されたコードを見ていると process.env はそのままだったので、実行環境でも process.env に変数を渡す方法でアプリを起動させる必要がありそうでした。なので環境変数を直接バンドルしてしまえば実行環境の設定が楽になりそう。(セキュリティに関わる値は外から直接見れないようにした .env とかを使うのが良いのだとは思いますが)

構成
/app
  |- /dist # build されたアプリの出力先
  |- /src
  |    |- index.ts # express entrypoint
  |- /webpack
  |    |- base.config.js # 共通設定
  |    |- dev.config.js
  |    |- prod.config.js
  |- nodemon.json
  |- package.json
  |- tsconfig.json
  • 開発中は nodemon で ts-node を実行して src 内の TypeScript を監視して開発
  • development / production それぞれのモードで build して確認できるようにする

webpack resolve.alias を使ってファイルをバンドルする

resolve.alias にファイルの場所を設定するとスクリプトからはエイリアスのプロパティ名をルートとして読み込めるようになる。

e.g.
// webpack.config.js
const path = require('path');
module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'foo/bar/utilities/'),
    }
  }
};
webpack の対象スクリプトでは `/foo/bar/utilities/` を `Utilities/` で import できるようになる
import MyUtility from 'Utilities/my-utility';
cf. Resolve | webpack

webpack.config を dev / prod モード別で使い分けているので、それぞれで別の設定ファイルを alias として読み込ませるようにすれば環境に合わせて別の設定ファイルを import でバンドルさせることができる。

環境変数ファイルの作成

$ mkdir .env
$ touch .env/dev.config.ts # Development 用
$ touch .env/prod.config.ts # Production 用

設定ファイルはそれぞれ同じプロパティを作成しておく

.env/dev.config.ts

export default {
  ENV_MODE: 'development',
  PORT: 3000,
};

.env/prod.config.ts

export default {
  ENV_MODE: 'production',
  PORT: 8080,
};

webpack resolve.alias の作成

共通で使用している webpack/base.config.js で実行時の process.env を使って alias を動的に設定する

webpack/base.config.js

const node_env = process.env.NODE_ENV || 'dev';
module.exports = {
  // …
  resolve: {
    alias: {
      //...
     UserEnv$: path.resolve(__dirname, `../.env/${node_env}.config.ts`),
    }
  }
};

npm script で NODE_ENV を渡すようにする

環境変数のパスを作成するのに NODE_ENV の値を利用するようにするので、build 用の npm-script から変数を渡すようにします。

package.json

"scripts": {
  "build:dev": "NODE_ENV=dev webpack --config ./webpack/dev.config.js",
  "build": "NODE_ENV=prod webpack --config ./webpack/prod.config.js",
}

メインファイルで alias 指定した設定ファイルを import する

/src/index.ts

//@ts-ignore
const USER_ENV = require('UserEnv').default;
const PORT = USER_ENV.PORT || 3000;
// …
app.listen(PORT, () => {
  console.log(`Mode: ${USER_ENV.ENV_MODE}`);
  console.log(`listening on port http://localhost:${PORT}`);
});

本当は型定義ファイルを作るのが正しいのだと思うけど、まだ理解が足りないので //@ts-ignore で lint を誤魔化し…

$ npm run build:dev のときは Mode: development と表示され localhost:3000,
$ npm run build のときは Mode: production と表示され localhost:8080 で動作していれば OK

nodemon で実行する開発モードでも設定ファイルを読み込ませるようにする

ここまでで webpack を使って build する際には alias を使った設定ファイルを読み込ませることが出来るようになりましたが、開発モードでは webpack を通さずに nodemon + ts-node のホットリロードで実行させるようにしているので UserEnv が見つからずエラーになってしまうので、.env/dev.config.tsUserEnv として読み込めるようにする必要があります。

tsconfig-paths を使って tsconfig に alias を作成する。

tsconfig-paths というライブラリを使うと tsconfig で alias import が出来るようになるみたいです!
tsconfig.json に alias を設定します。

tsconfig.json

{
  // ...
  "paths": {
    // webpack の resolve.alias で設定したエイリアス名と同じ名前で登録する
    "UserEnv": ["../.env/dev.config"]
  }
}

tsconfig の paths を alias import するには ts-node に -r tsconfig-paths/register オプションが必要なようなので、nodemon.json の実行コマンドを編集します

nodemon.json

{
-   "exec": "ts-node ./src/index.ts"
+   "exec": "ts-node -r tsconfig-paths/register ./src/index.ts"  
}

これで nodemon -L で開発モードを実行した時は require('UserEnv').default.env/dev.config.ts が読み込まれるようになりました! ₍ ᐢ. ̫ .ᐢ ₎ ヤッタネ!

TypeScript の設定ファイルなので、webpack で build する際にも読み込まれてエイリアスがコンフリクトするのでは?と思ったのですが、webpack で build する際は -r tsconfig-paths/register のオプションの指定がないので webpack の resolve.alias が読み込まれます。ヨシ!

所感

実務で環境を作った知見が無いので、これが本当に良いやり方なのか分かってない部分がありますが、とりあえず自分の思う実現したかったことは実現することができました!nodemon から global 変数を渡せる方法が分かれば webpack の DefinePlugin で変数を渡す方が美しい気がしていますが…


[参考]