Node.js パッケージから自分自身を参照する

例えばサンプルコードを同梱したい場合や, コンパイラのセルフホストのような場合など, Node.js パッケージの内部でそのパッケージ自身を参照したくなることがあります. あるか? このときの方法がいくつかあるので軽くまとめます.

ここでは例として以下のようなディレクトリ構造になっている mypackage パッケージを仮定して, examples/ 以下のサンプルコードからパッケージ自身を参照することを考えます. CommonJS と ES modules で挙動が異なるので, どちらの形式であるかを明示するために拡張子には常に .cjs.mjs を使います.

+-- package.json
+-- lib/
|   \-- index.cjs
\-- examples/
    +-- greet.cjs
    \-- greet.mjs

package.jsonlib/index.cjs はひとまずこういう感じ.

{
  "name": "mypackage",
  "main": "lib/index.cjs"
}
/* lib/index.cjs */
"use strict";

function greet(name) {
  return `Hello, ${name}!`;
}

module.exports = { greet };

package.json のあるディレクトリを参照する

CommonJS の場合は package.json のあるディレクトリを require すると, そのパッケージ自身 (より正確には main フィールドに指定されたファイル) を参照できます (参考: Node.js のドキュメント, 特に LOAD_AS_DIRECTORY あたり).

/* examples/greet.cjs */
"use strict";

const { greet } = require("..");

console.log(greet("Alice"));
$ node examples/greet.cjs
Hello, Alice!

ただし ES modules の場合は, このようにディレクトリを import することは禁止されています.

/* examples/greet.mjs */
import { greet } from "..";

console.log(greet("Alice"));
$ node examples/greet.mjs
node:internal/errors:497
    ErrorCaptureStackTrace(err);
    ^

Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/path/to/mypackage/' is not supported resolving ES modules imported from /path/to/mypackage/examples/greet.mjs

自分自身の名前で参照する

package.jsonexports フィールドがあれば, 自身のパッケージ名を使って自分を参照できます (参考: Node.js のドキュメント). この方法であれば CommonJS と ES modules の両方で動作します.

{
  "name": "mypackage",
  "main": "lib/index.cjs",
  "exports": "./lib/index.cjs"
}
/* examples/greet.mjs */
import { greet } from "mypackage";

console.log(greet("Alice"));
/* examples/greet.mjs */
import { greet } from "mypackage";

console.log(greet("Alice"));
$ node examples/greet.cjs
Hello, Alice!
$ node examples/greet.mjs
Hello, Alice!

exports を追加することに問題がなければ1, パッケージをユーザーと同じ方法で参照できてわかりやすいので, 基本的にはこれがおすすめ.

自分自身の別のバージョンを参照する

コンパイラが自身をセルフホストする場合など, ビルドツールの開発では依存関係に自身の安定版を追加して, それを使って自身のビルドを行うといったことがあります (microsoft/TypeScript が好例).

package.jsonexports フィールドがない場合は, 以下のように自分自身を依存関係に追加するだけで, mypackage という名前で自身の安定版を参照できるようになります.

{
  "name": "mypackage",
  "version": "1.1.0",
  "main": "lib/index.cjs",
  "devDependencies": {
    "mypackage": "^1.0.0"
  }
}

ところが exports フィールドがある場合は, 上で説明した自分自身を名前で参照する挙動が優先されて, mypackage という名前では安定版ではなく開発中の自分自身を参照することになってしまいます.

この場合は npm, yarn, pnpm といった各種パッケージマネージャーが共通して提供しているエイリアス機能を使って, 安定版を別の名前 (例えば mypackage-stable など) でインストールして, 名前の重複を回避してやるとよいです.

{
  "name": "mypackage",
  "version": "1.1.0",
  "main": "lib/index.cjs",
  "exports": "./lib/index.cjs",
  "devDependencies": {
    "mypackage-stable": "npm:mypackage@^1.0.0"
  }
}

  1. exports フィールドがあると, そこで指定されているファイル以外は外部から参照できなくなる点には注意.