Node.jsにはrequire
というモジュールを読み込む機能があります。これはNode.js Module APIで提供されている機能で、割と簡単にハックすることができます。よく Module hack とか require hack と呼ばれるものです。
- @babel/register
- ts-node/resigter
などが有名どころでしょうか。これらのモジュールでModule hackをすると、本来のNode.jsでは動作しないTypeScriptや拡張されたJavaScriptが動作します。
この記事では Module hack のやり方について説明します。
Module hackのやり方
// TypeScriptの場合
export const Module = require('module') as any
まず module
を読み込みます。
拡張子に応じたハックをするのは簡単です。
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
にアクセスできないためです。
実例
// test.js
const Module = require('module')
Module._extensions['.js'] = function(m, name) {
return m._compile('console.log("piyopiyo")', name)
}
require('./hoge')
// hoge.js
console.log('hoge')
これら2つのファイルを同じディレクトリにおいて、node test.js
を実行すると、Module hack をしてなければ本来 hoge
と表示されるところが piyopiyo
と表示されます。
解説
Module._extensions
は、拡張子ごとにファイルの処理をする関数群です。.js
のようにドット込みの拡張子を指定します。
ts-node/registerや@babel/registerのように、ソースコードをトランスパイルして実行する場合には、大体定番となるのが、
Module._extensions['.js'] = function(m, name) {
return m._compile('console.log("piyopiyo")', name)
}
のように最終的にNode.jsが解釈できる状態にまでトランスパイルしたソースコードを、m._compile(code, name)
のようにすることです。
補足
Module._extensions
を書き換えると、もどす時に工夫は必要となります。
export const originalExts: { [props: string]: any } = {}
Object.keys(Module._extensions).forEach(ext => {
originalExts[ext] = Module._extensions[ext]
})
大体はこのように、本来の関数を保存しておきます。
もっとも、このような hack を元に戻す必要がある機会はあまりないでしょう。
モジュール読み込み自体をhackする
ここまでの hack で、拡張子ごとの hack は可能になりましたが、モジュールそのものを hack するためには別の隠しプロパティにアクセスする必要があります。
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のときと同じように任意のコードを実行して、その結果を返したいときもあるでしょう。
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
宣伝
技術書典7で出した本のPDF電子版を、Pixiv Boothで頒布しております。よろしければどうぞ。