TypeScriptで文字列をモジュールとして実行する
-
TypeScript にて、任意の文字列を Node.js のモジュールに変換して実行してみます。
例えば module.exports = 1
という文字列が存在するとき、この文字列が変換されたモジュールを実行すると 1
が得られる、そんな動作をする関数についてです。
変換コード
以下のような関数に文字列を渡すと、Node.js で実行できるモジュールに変換されます。ビルドインモジュールの型の拡張方法がわからないので、一部 @ts-ignore
でエラーを無視します。
import { dirname } from 'path';
const requireFromString = (code: string, filename: string = '') => {
const parent = module.parent || undefined;
const m = new Module(filename, parent);
m.filename = filename;
// @ts-ignore
const paths: string[] = Module._nodeModulePaths(dirname(filename));
m.paths = paths;
// @ts-ignore
m._compile(code, filename);
return m.exports;
};
使うときはこんな感じ。
const code = `
module.exports = function add(n, m) {
return n + m;
};
`;
const add = requireFromString(code);
console.log(add(1, 2)); // 3
以下は、Module システムの流れとコードの解説です。
Node.js の Module システム
Node.js の Module
について。
// @types/node/globals.d.ts
class Module {
static runMain(): void;
static wrap(code: string): string;
static createRequireFromPath(path: string): (path: string) => any;
static builtinModules: string[];
static Module: typeof Module;
exports: any;
require: NodeRequireFunction;
id: string;
filename: string;
loaded: boolean;
parent: Module | null;
children: Module[];
paths: string[];
constructor(id: string, parent?: Module);
}
Module._load
node filename.js
が実行されるときや、require('filename')
が呼ばれるときになどに実行されます。
モジュールを読み込むとき、読み込み先がキャッシュされていればキャッシュされた Module オブジェクトのモジュールを、そうでなければ新規に作成した Module オブジェクトのモジュールを返します。モジュールは Module オブジェクトの exports
からアクセスできます。
// キャッシュされている場合
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// Module オブジェクト作成
const module = new Module(filename, parent);
// load を実行
module.load(filename);
return module.exports;
Module.prototype.load
ファイル名やディレクトリ名、拡張子などの情報を Module オブジェクトに追加します。また、拡張子ごとに Module._extensions を実行します。
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
const extension = findLongestRegisteredExtension(filename);
Module._extensions[extension](this, filename);
Module._extensions
拡張子が .js
のとき、fs.readFileSync
でファイルを読み込み、module._compile
でコンパイルします。
Module._extensions['.js'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
};
.json
のときは、読み込んだファイルを JSON.parse
で JSON オブジェクトに変換して Module オブジェクトの exports
に保存します。
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module.exports = JSON.parse(stripBOM(content));
};
変換コードの詳細
Module システムの流れの雰囲気を理解したところで、今回の変換関数を詳しくみていきます。
まず、Module オブジェクト parent
と filename
を Module に渡して Module オブジェクトを初期化します。
const parent = module.parent || undefined;
const m = new Module(filename, parent);
つぎに、Module オブジェクトのパスを追加します。パスは _nodeModulePaths
から取得することができます。このパスはモジュールを探す際に使われます。
const paths: string[] = Module._nodeModulePaths(dirname(filename)); // ['/app/node_modules', '/node_modules'];
m.paths = paths;
そのため、パスの設定をしないまま node_modules のライブラリを読み込もうとするとエラーが発生します。
const code = `
const express = require('express');
const app = express();
module.exports = app;
`;
try {
const app = requireFromString(code);
app('*', (_, res) => res.send('ok!'));
} catch (error) {
console.log(error);
/*
{ Error: Cannot find module 'express'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:582:15)
at Function.Module._load (internal/modules/cjs/loader.js:508:25)
at Module.require (internal/modules/cjs/loader.js:637:17)
at require (internal/modules/cjs/helpers.js:22:18)
at Object.<anonymous> (<anonymous>:2:17)
at Module._compile (internal/modules/cjs/loader.js:701:30)
at requireFromString (/app/src/sandbox/index.ts:36:5)
at Object.<anonymous> (/app/src/sandbox/index.ts:52:15)
at Module._compile (internal/modules/cjs/loader.js:701:30)
at Module.m._compile (/usr/local/share/.config/yarn/global/node_modules/ts-node/src/index.ts:473:23) code: 'MODULE_NOT_FOUND'
}
*/
}
さいごに、Module オブジェクトのコンパイルメソッドを実行して、文字列からモジュールに変換します。
m._compile(code, filename);
return m.exports;
おしまい。