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

12

任务

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

问题

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

bootstrap-sass 为例。假设它被本地安装在项目文件夹 C:\dev\my-project 中。那么我要找的是 C:\dev\my-project\node_modules\bootstrap-sass。 而require.resolve('bootstrap-sass')将返回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文件夹中的所有软件包。

但是...

我想知道,这个相当简单的任务是否可以用更简单、不那么hacky的方式解决。在我看来,这似乎是Node.js应该内置的功能。有任何建议吗?


你真正想解决的问题是什么(例如,为什么你要获取这个路径)?你是在尝试获取模块中的其他资源的路径吗?通常来说,一个模块本身应该知道它自己的资源相对于位置在哪里,所以你只需要询问模块告诉你东西在哪里,它就可以给你答案。 - jfriend00
我猜第一个问题是,为什么你要找到模块的位置而不是它的主文件。 - tkone
其次,npm有几个模块提供其功能,包括查找node_modules目录的模块,例如read-package-tree - tkone
我想使用npm安装css框架(例如bootstrap),并获取包含文件的路径(例如.css/.scss/.svg/...)。 - x-ray
read-package-tree 函数期望作为第一个参数传入一个包的根路径... - x-ray
2个回答

12

试试这个:

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

返回:

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 文件,因此这应该总是有效的(请参见什么是软件包?)。


1
然而,请记住你所尝试实现的东西至少有点“hackish”(虽然这可能是最合理的做法)。 - Tomas Kulich
谢谢! :) 目前这似乎是最好的解决方案。使用npm安装的软件包将始终具有package.json文件(请参见什么是软件包?)。因此,path.dirname(require.resolve('bootstrap-sass/package.json'))就可以解决问题了。 - x-ray
是的,谢谢。根据您的评论,我编辑了答案,使其更完整和自包含。如果这对您有用,请接受答案?谢谢。 - Tomas Kulich
这将无法与包导出一起使用:Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package.json' is not defined by "exports" in /path/to/@scope/pkg/package.json - Benny Powers

5

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

此外,您不应这样做,因为没有确定的方法可以知道npm包将在npm树中的哪个位置结束(这就是您求助于require.resolve的原因)。

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

显示可解析的输出而不是树形视图。

例如:

$ npm ls my-dep -p

...将会输出类似于这样的内容:

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

请注意,此命令可能会输出一些与stderr无关的错误(例如有关多余安装的错误) - 为了解决此问题,请激活--silent标志(请参阅文档中的loglevel):

$ npm ls my-dep -ps

这个命令可以通过子进程集成到您的代码中,此时最好不要使用--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());
        });
    };
}

实际上,即使存在不必要的软件包,我也没有看到任何输出...也许他们更改了行为,或者只有在使用npm ls而没有指定软件包名称(例如,只是 npm ls --parseable 而不是 npm ls some-package --parseable)时才会发生这些错误。 - Matt Browne
@MattBrowne 我认为这与当时的npm版本有关,正如你所怀疑的那样。 - Eliran Malka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接