Node.jsでModule hack （require hack）する

Node.jsには`require`というモジュールを読み込む機能があります。これは[Node.js Module API](https://nodejs.org/api/modules.html)で提供されている機能で、割と簡単にハックすることができます。よく Module hack とか require hack と呼ばれるものです。

* [@babel/register](https://www.npmjs.com/package/@babel/register)
* [ts-node](https://github.com/TypeStrong/ts-node)/resigter

などが有名どころでしょうか。これらのモジュールでModule hackをすると、本来のNode.jsでは動作しないTypeScriptや拡張されたJavaScriptが動作します。

この記事では Module hack のやり方について説明します。

## Module hackのやり方

```ts
// TypeScriptの場合
export const Module = require('module') as any
```

まず `module` を読み込みます。

拡張子に応じたハックをするのは簡単です。

```ts
Module._extensions[ext] = function(m: any, filename: string) {
  return m._compile('console.log("hoge")', filename)
}
```

先ほどから、Module を `as any` で `any` アサーションをしたり、今回の関数の第一引数 `m` もanyで定義していますが、これは、`@types/node` で定義されている `Module` 型の定義では、非公開プロパティ、たとえば `_extensions`や`_compile` にアクセスできないためです。



### 実例

```js
// test.js
const Module = require('module')

Module._extensions['.js'] = function(m, name) {
  return m._compile('console.log("piyopiyo")', name)
}

require('./hoge')
```

```js
// hoge.js
console.log('hoge')
```

これら2つのファイルを同じディレクトリにおいて、`node test.js`を実行すると、Module hack をしてなければ本来 `hoge` と表示されるところが `piyopiyo` と表示されます。

### 解説

`Module._extensions` は、拡張子ごとにファイルの処理をする関数群です。`.js` のようにドット込みの拡張子を指定します。

ts-node/registerや@babel/registerのように、ソースコードをトランスパイルして実行する場合には、大体定番となるのが、

```js
Module._extensions['.js'] = function(m, name) {
  return m._compile('console.log("piyopiyo")', name)
}
```

のように最終的にNode.jsが解釈できる状態にまでトランスパイルしたソースコードを、`m._compile(code, name)` のようにすることです。

### 補足

`Module._extensions`を書き換えると、もどす時に工夫は必要となります。

```js
export const originalExts: { [props: string]: any } = {}

Object.keys(Module._extensions).forEach(ext => {
  originalExts[ext] = Module._extensions[ext]
})
```

大体はこのように、本来の関数を保存しておきます。

もっとも、このような hack を元に戻す必要がある機会はあまりないでしょう。

## モジュール読み込み自体をhackする

ここまでの hack で、拡張子ごとの hack は可能になりましたが、モジュールそのものを hack するためには別の隠しプロパティにアクセスする必要があります。

```js
const Module = require('module')

const originalLoad = Module._load

Module._load = function(name, parent, isMain) {
  if (name === 'hoge') {
    return { hoge: () => console.log('hoge') }
  }
  return originalLoad(name, parent, isMain)
}

const { hoge } = require('hoge')
hoge()
```

`Module._load`を書き換えると、モジュール読み込みそのものにちょっかいを出せます。ただし本来の関数は保存しておいた方が良いため、`const originalLoad = Module._load`で保存しておきます。

このコードでは、`hoge`という名前のモジュールだけhackします。他のモジュールを読み込んだ場合は、従来読み込むべきモジュールが読み込まれます。

### 任意のソースコードをモジュールとして返す

先ほどのhackでは、ダイレクトに書いたコードをそのまま実行しています。もちろん、それはそれで用途もあるかもしれませんが、拡張子hackのときと同じように任意のコードを実行して、その結果を返したいときもあるでしょう。

```js
const module = new Module()
module._compile(code, filename)
return module.exports
```

さきほどもでてきた `_compile`は、Node.jsの実行環境に応じた処理をやってくれるので便利です。同等の処理を自前、たとえば`eval` `Function` `vm.runInContext`などでやろうとするとかなり面倒です。

## 注意点

ここに書いた undocumented なものです。これで今後も含めて正しく動作することは保証されていません。また他の黒魔術を使ったソースコードで問題が生じる可能性があります。

`pirates` というnpmモジュールを使うと、より安全に Module hack を試すことはできます。自前でやるよりはいいかもしれません。

参考: https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js

## 宣伝

* [Effective React Hooks \- 東京ラビットハウス \- BOOTH](https://tk-rabbit-house.booth.pm/items/1477000)
* [東京ラビットハウス \- BOOTH](https://tk-rabbit-house.booth.pm/)

技術書典7で出した本のPDF電子版を、Pixiv Boothで頒布しております。よろしければどうぞ。