mizdra's blog

ぽよぐらみんぐ

Node.js の --require/--import オプションについて

Node.js には --require=module--import=module というオプションがあります。このオプションを使うと、エントリポイントとなるプログラムよりも前に、任意のモジュールを実行できます。

例えば以下のようなコマンドを実行すると、Node.js ランタイムはまず最初に preload.cjs を実行し、それから main.mjs を実行できます。

node --require ./preload.cjs main.mjs

エントリポイントよりも前に、何かしらの処理を実行したい時に使うことを想定しています。

--require--import の違い

--import--require と同じように、モジュールをプリロードするためのオプションです。両者の違いはプリロードするモジュールの読み込み方です。

--requirerequire(...); 相当、--importimport '...'; 相当のコードでモジュールを読み込みます。

# `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.jsoncompilerOptions.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/--importrequire(...);/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 コマンドの実行中にオンデマンドでトランスパイルしています。見かけ上のビルドプロセスが単純化されるメリットはありますが、最終的にトランスパイルしてから実行することに変わりはありません。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.