Node.js: 获取已安装 npm 包的(绝对)根路径

Node.js: Get (absolute) root path of installed npm package

任务

我正在寻找一种通用方法来获取 Node.js 中已安装 npm 包的(绝对)根路径。

问题

我知道 require.resolve,但这会给我入口点(主模块的路径)而不是包的根路径。

bootstrap-sass为例。假设它本地安装在项目文件夹 C:\dev\my-project 中。那么我要找的就是C:\dev\my-project\node_modules\bootstrap-sassrequire.resolve('bootstrap-sass') 将 return C:\dev\my-project\node_modules\bootstrap-sass\assets\javascripts\bootstrap.js.

我想到了几种获取包根路径的方法:

解决方案 #1

var packageRoot = path.resolve('node_modules/bootstrap-sass');
console.log(packageRoot);

这适用于本地安装在 node_modules 文件夹中的包。但是,如果我在子文件夹中,我需要解析 ../node_modules/bootstrap-sass,并且嵌套文件夹越多,它就会变得越复杂。此外,这不适用于全局安装的模块。

解决方案 #2

var packageRoot = require.resolve('bootstrap-sass')
    .match(/^.*[\/\]node_modules[\/\][^\/\]*/)[0];
console.log(packageRoot);

这适用于安装在 node_modules 文件夹中的本地和全局模块。正则表达式将匹配所有内容,直到最后一个 node_modules 路径元素加上以下路径元素。但是,如果包的入口点设置为另一个包(例如 package.json 中的 "main": "./node_modules/sub-package"),这将失败。

解决方案 #3

var escapeStringRegexp = require('escape-string-regexp');

/**
 * Get the root path of a npm package installed in node_modules.
 * @param {string} packageName The name of the package.
 * @returns {string} Root path of the package without trailing slash.
 * @throws Will throw an error if the package root path cannot be resolved
 */
function packageRootPath(packageName) {
    var mainModulePath = require.resolve(packageName);
    var escapedPackageName = escapeStringRegexp(packageName);
    var regexpStr = '^.*[\/\\]node_modules[\/\\]' + escapedPackageName +
        '(?=[\/\\])';
    var rootPath = mainModulePath.match(regexpStr);
    if (rootPath) {
        return rootPath[0];
    } else {
        var msg = 'Could not resolve package root path for package `' +
            packageName + '`.'
        throw new Error(msg);
    }
}

var packageRoot = packageRootPath('bootstrap-sass');
console.log(packageRoot);

此功能适用于安装在 node_modules 文件夹中的所有软件包。

但是...

我想知道这个相当简单的任务是否不能用更简单、更简单的方法来解决。对我来说,它看起来应该已经内置到 Node.js 中了。有什么建议吗?

试试这个:

require.resolve('bootstrap-sass/package.json')

哪个returns:

path_to_my_project/node_modules/bootstrap-sass/package.json 

您现在可以摆脱 'package.json' 路径后缀,例如:

var path = require('path') // npm install path
var bootstrapPath = path.dirname(require.resolve('bootstrap-sass/package.json'))

由于每个包都必须包含 package.json 文件,因此这应该始终有效(参见 What is a package?)。

您不需要查找所需包的 package.json 文件,甚至不需要明确引用它的位置。

此外,你不应该这样做,因为没有确定的方法知道 npm 包最终会在 npm 树中的什么位置(这就是为什么你求助于 require.resolve寻求帮助)。

相反,您可以使用 npm ls with the --parseable 标志查询 npm API(或 CLI),这将:

Show parseable output instead of tree view.

例如:

$ npm ls my-dep -p

... 将输出如下内容:

/Users/me/my-project/node_modules/my-dep

你应该知道这个命令也会输出一些不相关的错误到 st​​derr(例如关于无关的安装)——要解决这个问题,激活 --silent 标志(见 loglevel 在文档):

$ npm ls my-dep -ps

此命令可以使用 child process 集成到您的代码中,在这种情况下,它比 运行 没有 --silent 标志的命令 更受欢迎 允许捕获任何错误。

如果捕获到错误,您可以决定它是否致命(例如,应忽略上述关于无关包的错误)。

因此通过子进程使用 CLI 可以如下所示:

const exec = require('child_process').exec;
const packageName = 'my-dep';

exec(`npm ls ${packageName} --parseable`, (err, stdout, stderr) => {
    if (!err) {
        console.log(stdout.trim()); // -> /Users/me/my-project/node_modules/my-dep
    }
});

然后可以在异步流中使用它(连同一些闭包魔法……),例如:

const exec = require('child_process').exec;
const async = require('async');

async.waterfall([
    npmPackagePathResolver('my-dep'),
    (packagePath, callback) => {
        console.log(packagePath) // -> /Users/me/my-project/node_modules/my-dep
        callback();
    }
], (err, results) => console.log('all done!'));

function npmPackagePathResolver(packageName) {
    return (callback) => {
        exec(`npm ls ${packageName} --parseable`, (err, stdout, stderr) => {
            callback(err, stdout.trim());
        });
    };
}