Node.js には --require=module と --import=module というオプションがあります。このオプションを使うと、エントリポイントとなるプログラムよりも前に、任意のモジュールを実行できます。
例えば以下のようなコマンドを実行すると、Node.js ランタイムはまず最初に preload.cjs を実行し、それから main.mjs を実行できます。
node --require ./preload.cjs main.mjs
エントリポイントよりも前に、何かしらの処理を実行したい時に使うことを想定しています。
--require と --import の違い
--import も --require と同じように、モジュールをプリロードするためのオプションです。両者の違いはプリロードするモジュールの読み込み方です。
--require は require(...); 相当、--import は import '...'; 相当のコードでモジュールを読み込みます。
# `require('./preload.cjs');` 相当 node --require ./preload.cjs main.mjs # `import './preload.mjs';` 相当 node --import ./preload.mjs main.mjs
ところで、import 文では ES module 形式だけでなく、CommonJS 形式のモジュールも読み込めます。つまり --import で CommonJS 形式のモジュールもプリロードできるのです。
node --import ./preload.cjs main.mjs
また、Node.js v22.0.0 からは --experimental-require-module オプションを用いることで、top-level await を含まない ES module 形式のモジュールに限って require(esm) が可能になりました (参考1, 参考2)。従って --experimental-require-module を併用することで、ES module 形式のモジュールを --require で読み込めます。
node --experimental-require-module=module --require ./preload.mjs main.mjs
従って、--experimental-require-module=module + --require と --import のどちらを使っても、ほぼ変わりないです。 とはいえ、--experimental-require-module=module + --require は top-level await 非対応なので、基本的には --import を使っておくと良いのではないかと思います。
npm package を --require/--import で読み込む
--require/--import はユーザ定義のモジュールだけでなく、npm package も読み込めます。import '3rd-party-preload'; と書くように、node --import 3rd-party-preload とするだけです。
npm i -S 3rd-party-package node --import 3rd-party-package main.mjs
複数のモジュールをプリロードする
--require/--import を複数個指定して、複数のモジュールをプリロードできます。プリロードされるモジュールは、左から順に実行されます。
例えば以下のようなコマンドを実行した場合、3rd-party-package-1 => 3rd-party-package-2 => 3rd-party-package-3 => main.mjs の順で実行されます。
node \ --import 3rd-party-package-1 \ --import 3rd-party-package-2 \ --import 3rd-party-package-3 \ main.mjs
--require/--import の主な用途
主にモジュールの読み込み方法をカスタマイズするために利用されます。
例えば ts-node を使うと、事前のトランスパイルをせずとも *1 Node.js で直接 .ts が読み込めるようになります。
npm install -D ts-node node --import ts-node/register main.mts
更に tsconfig-paths と組み合わせると、tsconfig.json の compilerOptions.paths を Node.js でも解決できるようになります。
npm install -D ts-node tsconfig-paths node \ --import ts-node/register \ --import tsconfig-paths/register \ main.mts
僕が作った @mizdra/node-next-image-loader を使うと、画像を base64 化しつつモジュールとして読み込むこともできます。
npm install -D @mizdra/node-next-image-loader node --import @mizdra/node-next-image-loader main.mjs
import burnAllGIFs from "./assets/burnallgifs.png"; console.log(burnAllGIFs); // { // src: 'data:image/png;base64,...', // width: 199, // height: 117, // }
また、power-assert をサポートするための @power-assert/node というパッケージもあります (以下、公式 example より引用)。
npm install -D @power-assert/node node \ --enable-source-maps \ --import @power-assert/node \ --test \ demo.test.mjs
> node --enable-source-maps --import @power-assert/node --test demo.test.mjs ▶ power-assert demo ✖ Array#indexOf (8.774208ms) AssertionError [ERR_ASSERTION]: assert(ary.indexOf(zero) === two) | | | | | | | | | 2 | | | false | | 0 | 0 [0,1,2] 0 === 2 at TestContext.<anonymous> (/path/to/demo.test.mjs:9:5) at Test.runInAsyncScope (node:async_hooks:206:9) at Test.run (node:internal/test_runner/test:824:25) at Test.start (node:internal/test_runner/test:721:17) at node:internal/test_runner/test:1181:71 at node:internal/per_context/primordials:488:82 at new Promise (<anonymous>) at new SafePromise (node:internal/per_context/primordials:456:29) at node:internal/per_context/primordials:488:9 at Array.map (<anonymous>) { generatedMessage: false, code: 'ERR_ASSERTION', actual: 0, expected: 2, operator: '===' }
余談: tsx を使う
完全に余談ですが、ts-node + tsconfig-paths 相当のことをやりたいのであれば、tsx を使いるのがオススメです。
npm install -D tsx node --import tsx main.mjs
ts-node よりもトランスパイルが高速で、compilerOptions.paths を使ったモジュールの解決を組み込みでサポートしています。他にも色々と ts-node + tsconfig-paths との違いがあります。詳しくは以下の FAQ を参照してください。
--require/--import は合成可能
何個もモジュールをプリロードしていると、コマンドラインオプションのリストが長くなってしまいます。
node \ --enable-source-maps \ --import tsx \ --import @mizdra/node-next-image-loader \ --import @power-assert/node \ --test \ demo.test.mts
ここで、--require/--import が require(...);/import '...'; 相当の挙動をすることを思い出してみましょう。そう、以下のように書くと --require/--import を合成できるのです。
node --enable-source-maps --import ./preload.mjs --test demo.test.mts
// preload.mjs import 'tsx'; import '@mizdra/node-next-image-loader'; import '@power-assert/node';
プリロードしたいモジュールが増えても安心ですね。
プリロードされるモジュールをカスタマイズ可能にする
./preload.mjs を用意すればプログラマブルなことができるという点に着目すると、面白いことができます。例えば、@mizdra/node-next-image-loader から API を export しておいて、ユーザが自由に @mizdra/node-next-image-loader をカスタマイズできるように、といったことが可能です。
(理論上の話で、実際にはこの機能は @mizdra/node-next-image-loader に実装されてないことに注意)
import { registerCustomHook } from '@mizdra/node-next-image-loader/custom'; registerCustomHook({ export: { src: { // base64 文字列ではなく Buffer として export する type: 'buffer', }, // width, height は export しない width: false, height: false, }, });
あとがき
Node.js の --require/--import は、シンプルながらも柔軟なオプションです。本当に色々なことができて面白いので、皆さんも是非機会があれば触ってみてください。
*1:node コマンドを実行するよりも前にトランスパイルが不要なだけで、node コマンドの実行中にオンデマンドでトランスパイルしています。見かけ上のビルドプロセスが単純化されるメリットはありますが、最終的にトランスパイルしてから実行することに変わりはありません。