Node.jsでパッケージが参照しているnode_modulesのパスを取得する

結論

require.resolveを使う

Use the internal require() machinery to look up the location of a module, but rather than loading the module, just return the resolved filename.

const path = require.resolve(<PACKAGE>)

https://nodejs.org/dist/latest-v16.x/docs/api/modules.html#requireresolverequest-options

環境

% node -v
v16.13.2
% npm -v
8.10.0

動作確認

npm init -y
npm init -y -w apps/api-a
npm i cowsay --save-exact
npm i express --save-exact
npm i express@4.0.0 --save-exact -w apps/api-a
require-resolve % tree -L 3 -I node_modules
.
├── apps
│   └── api-a
│       ├── index.js
│       └── package.json
├── package-lock.json
└── package.json

apps/api-a/package.json

{
  "name": "api-a",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "4.0.0"
  }
}

package.json

{
  "name": "require-resolve",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "workspaces": [
    "apps/api-a"
  ],
  "dependencies": {
    "cowsay": "1.5.0",
    "express": "4.18.1"
  }
}

CommonJSの場合

apps/api-a/index.js

const cowsay = require("cowsay");
console.log(require.resolve("cowsay"));

const express = require("express");
console.log(require.resolve("express"));

実行結果

node apps/api-a/index.js
/.../require-resolve/node_modules/cowsay/index.js # さかのぼって参照
/.../require-resolve/apps/api-a/node_modules/express/index.js # 最も近いもの 

挙動の説明

https://nodejs.org/dist/latest-v16.x/docs/api/modules.html#loading-from-node_modules-folders

上のドキュメントにある通り、node_modulesをさかのぼって参照する。
ディレクトリルートにexpress@4.18.1、exapi-aにexpress@4.0.0がインストールされており、
apps/api-a/index.jsは一番近いexpressを見に行く。
どのnode_modulesを利用しているかはrequrie.resolveで取得できる。

ES Modulesの場合

apps/api-a/index.js

console.log(await import.meta.resolve("cowsay"));
console.log(await import.meta.resolve("express"));

apps/api-a/package.json

  "type": "module",

実行結果

% node --experimental-import-meta-resolve apps/api-a/index.js
file:///.../require-resolve/node_modules/cowsay/index.js
file:///.../require-resolve/apps/api-a/node_modules/express/index.js

もしくは

import { createRequire } from "module";
const require = createRequire(import.meta.url);
console.log(require.resolve("cowsay"));
console.log(require.resolve("express"));
% node apps/api-a/index.js
/.../require-resolve/node_modules/cowsay/index.js
/.../require-resolve/apps/api-a/node_modules/express/index.js

参考ドキュメント

https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#importmetaresolvespecifier-parent https://nodejs.org/dist/latest-v16.x/docs/api/esm.html#no-requireresolve

まとめ

どのnode_modulesを参照しているのかパスを取得する方法をまとめた。
モノレポはnode_modulesが複数できるので、node_module内のファイルを参照するときにパスを考慮する必要がある。