Serverless+Typescriptで指定フォルダのモジュールを一括require

モジュールをファイルごとに分割したけど、importする時は一個のファイルから一気にラッパーオブジェクトを呼び出して、モジュールファイルの名前でアクセスして関数を使えたらいいな！
という時に使うWebpackの小技みたいなやつを、Vue.js勉強してるときにJSでのサンプルを手に入れたものの、typescriptのserverlessプロジェクトでも使おうとしたら中々上手くいかなかったので、そっちのやり方をまとめておきます。

使っている`require.context()`というWebpackの機能についてはこの記事でもコード解説の時に書くつもりですが、包括的に知りたい場合は下記の記事を見たほうが分かりやすいです。
[Webpackでフォルダ内の全ファイルを一気にrequireする](https://qiita.com/jkr_2255/items/d23e66323857d3189a00)
あと公式も
[Dependency Management](https://webpack.js.org/guides/dependency-management/#requirecontext)

## 前提
- serverlessは [aws-nodejs-typescript](https://github.com/serverless/serverless/tree/master/lib/plugins/create/templates/aws-nodejs-typescript)を指定してAWS用のtypescriptテンプレートを使用する。よって**Webpack**は最初から入っている

（※20年末のテンプレートのアップデートでなにやら構成がさらに複雑になって私にはhello worldさえ出来なくなったので一個前のバージョンを使っています…（誰か解説記事を書いてほしい…））


- ファイル構成

```
src/
├─ index.ts (*1)
└─ modules/
   ├─ handler.ts (*2)
   ├─ create.ts
   └─ update.ts
```
*1. modulesフォルダ内にある分割されたモジュールをimportしたいファイル

*2. `create.ts`,`update.ts`内でdefault exportされた関数を含むラッパーオブジェクトを作成するファイル

---
**実際のコードの書き方が異なるわけではないので、動作の説明が見たい方はjavascriptの項目を、
上記の環境でどう使うか知りたい方はそこは飛ばしてtypescriptの項目を見てください**

## JavaScriptの時の構成

```js:handler.js
const requireModule = require.context(".", false, /\.js$/); //extract js files inside modules folder
const modules = {};

requireModule.keys().forEach((fileName) => {
	if (fileName === "./handler.js") return; //reject the index.js file
	const moduleName = fileName.replace(/(\.\/|\.js)/g, "");
	modules[moduleName] = requireModule(fileName).default;
});
export default modules;
```

簡単にコードの説明をしますと、
1行目：`require.context(対象ディレクトリとの相対パス, 子ディレクトリ内も含めるか, 対象ファイルを取得する正規表現)` で同じディレクトリ内のすべての.jsファイルをrequireします。（handler.jsも含む）

4行目：`require.context`によって得られた`requireModule`という変数にはcontext moduleから操作可能なファイル情報の配列が入っているらしいです。配列といっても使用できるメソッドはWebpack側が提供しているresolve()・keys()・id()の3つだけです。
keys()はObject.keys()と直感的に似た動きをして、requireされたファイル名の配列を取得するので、forEachを繋げることで一つ一つのファイルを処理することができます。
（ここではcreate.tsといった拡張子まで含むファイル名）

5行目：このファイル自体はラッパーオブジェクト内に配置する必要がないので早期returnで除外

6行目：拡張子部分`.js`をreplaceで取り除いて純粋なファイル名を取得

7行目：fileNameを持つモジュールにアクセスして、その中で export defaultされた関数を取得し、modulesオブジェクトに純粋なファイル名をkeyとしたプロパティに格納。

9行名：modulesオブジェクトをexport defaultする。
これでindex.jsでは下記のように各モジュールにアクセス出来てかつ、新しいモジュールを追加した際にはファイルを作るだけで呼び出せるようになる。

```ts:index.ts
import handler from "./modules/handler";
handler.create() //create.tsでexport defaultされた関数が実行される
handler.update() //update.tsで(以下同文)

handler.delete() //delete.tsを作れば自動でアクセス可能になる
```

ちなみにcreate.ts側はこんな感じ、default付けるのを忘れないってだけですが。あとasync付かどうかも関係ないです。

```ts:create.ts
export default async function create() {
  // create something
}
```

## TypeScriptの時の構成
```ts:handler.ts
const requireModule = require.context(".", false, /\.ts$/); //extract ts files inside modules folder
const modules = {};

requireModule.keys().forEach((fileName) => {
	if (fileName === "./handler.ts") return; //reject the index.js file
	const moduleName = fileName.replace(/(\.\/|\.ts)/g, "");
	modules[moduleName] = requireModule(fileName).default;
});
export const handler= modules
```
拡張子の部分変えただけでjsの時と何も変わりませんね…強いて言えばexportを名前付きにしたのでimportする際はこうなりますが…

```ts:index.ts
import { handler } from "./modules/handler";
```

同じコードでで動くならなぜコピペで上手くいかなかったのか？
エラーメッセージはこんな感じでした。

```
Property `context` does not exist on type 'NodeRequire'
```

まぁググったらすぐ出てきたんですが、` npm i @types/webpack-env -D`でwebpack-envをインストールしたら一発解決でした。すでに入ってる人は再インストールで治ることもあるみたいです。

あと変えるのは呼び出し側でtypescriptはhandlerにその関数があるか判断できずに警告してくるので、ブラケット型での参照にするくらいですかね。

```ts:index.ts
import { handler } from "./modules/handler";
  handler ["create"]
  handler ["update"]
```

以上です。