Node.jsでのモジュールインポートと環境変数の取り扱いに関して

最近、モジュールのインポートに関連して次の二つの問題が発生した。

- フロントエンドで単純なヘルパー関数をインポートする際に、Webpackを使ってビルドすると多くのモジュールresolveエラーが発生
- `import "dotenv/config"`を使用しているにも関わらず、インポートしているライブラリ内で使用する環境変数がundefinedになってる


これらの問題をきっかけに、Node.jsのモジュールシステムについて再学習した。

# TL;DR

- Node.jsでは、1ファイルが1モジュールとして扱われる
- モジュールをインポートする際、Node.jsはそのモジュール内のすべてのコードを実行し、メモリにキャッシュする
- インポートするモジュールが他のモジュールをさらにインポートしている場合、インポート文の順序に従って上記の処理を繰り返す

そのため
- 単純なヘルパー関数をインポートする場合でも、その関数が属するモジュールの中身全体が読み込まれるため、フロントエンドのWebpack環境でサポートされていない関連モジュールがあるとModule not found Errorが発生
- もし`import "dotenv/config"`が後ろに配置されている場合、その前に環境変数の初期化がうまく行われず、値がundefinedになる

Node.jsでのモジュール処理の流れ詳細について、下記のリンクを参照してください

- [Everything you should know about ‘module’ & ‘require’ in Node.js](https://www.freecodecamp.org/news/require-module-in-node-js-everything-about-module-require-ccccd3ad383/)
- [Node.js Module System](https://www.codementor.io/@oparaprosper79/node-js-module-system-15r2ud9v1c)
- [node api docs](https://nodejs.org/api/modules.html#modules-commonjs-modules)
- [Requiring modules in Node.js: Everything you need to know](https://www.freecodecamp.org/news/requiring-modules-in-node-js-everything-you-need-to-know-e7fbd119be8/)

# 問題① Module not found Error

## 背景
今回のプロジェクトはReact, Express, MongoDBのモノレポで、フロント側とサーバー側が一部コードをシェアすることがある。
※ただ、フロント側が直接/server以下からimportしたりすることは実はNGだと思うが、歴史的な経緯から、一旦今回の作業では対象外とする。

バックエンド側で担当している修正で、COSTモジュールに新しい関数処理を追加。
その変更をマージリクエストに提出した後、フロント側のbuildで多数の`Module not found Error`が発生。

```typescript:src/server/lib/cost.ts
import { BrandRepository } from "../repositories/brand"

// フロント側がimportしている関数
export const isOverCost = (total: number, target: number): boolean => {
	return total > target;
};

// 追加のコードイメージ
export const makeEstimate = (brandId: string): number => {
  const brand = BrandRepository.getOne(brandId)
  // ...見積計算
}

```

webpackでbuildするときのエラーイメージ：
```shell
ERROR in ./node_modules/@mongodb-js/saslprep/dist/code-points-data.js 3:15-30
Module not found: Error: Can't resolve 'zlib' in '/home/v12/new-sj-testit/node_modules/@mongodb-js/saslprep/dist'

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add a fallback 'resolve.fallback: { "zlib": require.resolve("browserify-zlib") }'
        - install 'browserify-zlib'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.fallback: { "zlib": false }
 @ ./node_modules/@mongodb-js/saslprep/dist/memory-code-points.js 8:43-72
 @ ./node_modules/@mongodb-js/saslprep/dist/index.js 2:29-60
 @ ./node_modules/mongodb/lib/deps.js 58:4-54
 @ ./node_modules/mongodb/lib/index.js 115:13-30
 @ ./src/server/repositories/brand.ts 11:0-35 18:72-80 26:39-47 43:37-45 67:37-45
 @ ./src/server/lib/estimate.ts 5:0-62 28:4-23
 @ ./src/client/scripts/containers//job/index.tsx 8:0-82 10:47-54 10:56-65
 @ ./src/client/scripts/routes/index.tsx 64:0-83 300:92-118
 @ ./src/client/scripts/index.tsx 6:0-34 9:50-56
 @ ./src/client/index.ts 1:0-19

ERROR in ./node_modules/mongodb/lib/cmap/auth/mongodb_aws.js 4:15-32
Module not found: Error: Can't resolve 'crypto' in '/home/v12/new-sj-testit/node_modules/mongodb/lib/cmap/auth'

ERROR in ./node_modules/mongodb/lib/cmap/auth/mongodb_aws.js 5:13-28
Module not found: Error: Can't resolve 'http' in '/home/v12/new-sj-testit/node_modules/mongodb/lib/cmap/auth'

ERROR in ./node_modules/mongodb/lib/cmap/connect.js 4:12-26
Module not found: Error: Can't resolve 'net' in '/home/v12/new-sj-testit/node_modules/mongodb/lib/cmap'

ERROR in ./node_modules/mongodb/lib/connection_string.js 4:12-26
Module not found: Error: Can't resolve 'dns' in '/home/v12/new-sj-testit/node_modules/mongodb/lib'

ERROR in ./node_modules/mongodb/lib/connection_string.js 5:11-24
Module not found: Error: Can't resolve 'fs' in '/home/v12/new-sj-testit/node_modules/mongodb/lib'

ERROR in ./node_modules/mongodb/lib/connection_string.js 7:14-28
Module not found: Error: Can't resolve 'url' in '/home/v12/new-sj-testit/node_modules/mongodb/lib'

ERROR in ./node_modules/mongodb/lib/cursor/abstract_cursor.js 4:17-34
Module not found: Error: Can't resolve 'stream' in '/home/v12/new-sj-testit/node_modules/mongodb/lib/cursor'

....

```

## 原因

エラーメッセージから、MongoDBが必要とするNode.jsのコアモジュール(zlib, os, dns, crypto, http, url, net, etc.)が見つからないことは原因だとわかる。

Webpack 5以降、Node.jsのコアモジュールのpolyfillはデフォルトでは提供されなくなったため、これらのモジュールが見つからないというエラーが発生した。

なぜフロントエンドが単純な数値比較関数をインポートしているにもかかわらず、MongoDB関連のモジュールが必要になっただろう。

それは、**モジュールをimportするとき、importする関数だけでなく、その関数の属するモジュール全体が読み込まれるため**

今回の修正で、repository操作を追加したことで、そのrepositoryモジュールをimportするとき、MongoDB関連のモジュールもロードする必要があった。

その結果、本来DBが必要ないフロント側でも、DB関連の依存パッケージの見つからないエラーが発生した。

## 解決案

本来であれば、フロント側が直接サーバー側の関数をimportするべきではないが、今回の作業ではその問題を対象外とする。一旦臨時措置として、DB操作必要な部分を別モジュールに分離した。

また、今回のケースではないが、フロントエンドで必要なパッケージだった場合、エラーメッセージに記載されているように、Webpackの設定にfallbackを追加し、適切なpolyfillライブラリをインストールする必要があります。

```

        - add a fallback 'resolve.fallback: { "zlib": require.resolve("browserify-zlib") }'
        - install 'browserify-zlib'
```


# 問題② 環境変数値がundefined

## 背景

問題が発生した箇所は下記のような感じ。
Analyzerが外部ライブラリdata-processorのRawdataProviderを使用してデータを処理しているが、その出力先が`.env`に定義した`BASE_DIR`ではなく、常にデフォルトの`base`となっている。

```typescript:analyzer.ts
import { RawdataProvider} from "data-processor";
import "dotenv/config";

export class Analyzer {
	 async analyze(): Promise<void> {
  
          console.log(process.env.BASE_DIR)
      
		const provider = new RawdataProvider();
        // そのほかの処理
	}

}
```

```typescript:@data-processor/rawdataProvider.ts
import { baseDir } from "../lib/helper"

export class RawdataProvider {
	 async getOutputDir(): Promise<void> {
		return baseDir
	}

}
```

```typescript:@data-processor/lib/helper.ts
// RawdataProviderクラスで使用されるbaseDirを定義
const baseDir = process.env.BASE_DIR ?? "base";
```

```.env
BASE_DIR = "output"
```

`console.log(process.env.BASE_DIR)`で出力すると、.envファイルに定義した`output`が問題なく出力されている。

なぜプログラムの実行中にライブラリ内の`process.env.BASE_DIR`が正しく読み込まれていないだろう。

## 原因

Node.jsでは、importの順番に基づいてモジュールが実行される。そのため、`import "dotenv/config";`が依存ライブラリ"data-processor"のimportより後ろにある場合、`RawdataProvider`の含まれるモジュールが先に実行される。

その際、`const baseDir = process.env.BASE_DIR ?? "base";`の行が実行される時点で、まだ`process.env`が初期化されていないため、`process.env.BASE_DIR `がundefinedとなり、baseDirがデフォルト値に設定される。

```typescript:analyzer.ts
import { RawdataProvider} from "data-processor";
import "dotenv/config";
```

## 解決策

`import "dotenv/config";`を最上部に移動する。

```typescript:analyzer.ts
import "dotenv/config";
import { RawdataProvider} from "data-processor";
```

best practiceとしては、`import "dotenv/config";`はアプリのentry file( index.tsなど)のの最上部に配置すべき。

ただし、今回はモジュール単体でテストを行っているため、通常は`index.ts`に`import "dotenv/config";`を配置している。そのため、今回のような環境変数の読み込むタイミングを意識していなかった。