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

2020-03-14 03:09发布

问题:

Task

I'm looking for an universal way to get the (absolute) root path of an installed npm package in Node.js.

Problem

I know about require.resolve, but that will give me the entry point (path to the main module) rather than the root path of the package.

Take bootstrap-sass as an example. Say it's installed locally in a project folder C:\dev\my-project. Then what I'm looking for is C:\dev\my-project\node_modules\bootstrap-sass. require.resolve('bootstrap-sass') will return C:\dev\my-project\node_modules\bootstrap-sass\assets\javascripts\bootstrap.js.

I can think of several methods how to get the package's root path:

Solution #1

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

This will work fine for packages installed locally in node_modules folder. However, if I'm in a subfolder, I need to resolve ../node_modules/bootstrap-sass, and it get's more complicated with more nested folders. In addition, this does not work for globally installed modules.

Solution #2

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

This will work for local and global modules installed in node_modules folder. The regex will match everything up to the last node_modules path element plus the following path element. However this will fail if a package's entry point is set to another package (e.g. "main": "./node_modules/sub-package" in package.json).

Solution #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);

This function should work for all packages installed in a node_modules folder.

But...

I wonder if this rather simple task cannot be solved in a simpler and less hacky way. To me it looks like something that should already be built into Node.js. Any suggestions?

回答1:

Try this:

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

which returns:

path_to_my_project/node_modules/bootstrap-sass/package.json 

You can now get rid of 'package.json' path suffix such as:

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

Since it is mandatory for every package to contain package.json file, this should always work (see What is a package?).



回答2:

you don't need to look up the desired package's package.json file, or even refer to its location explicitly.

moreover, you shouldn't do it, as there is no sure way to know where will that npm package end up in npm's tree (which is why you turned to require.resolve for help).

instead, you can query the npm API (or CLI) by using npm ls with the --parseable flag, which will:

Show parseable output instead of tree view.

for example:

$ npm ls my-dep -p

… will output something like this:

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

you should be aware that this command can output some irrelevant errors as well to stderr (e.g. about extraneous installations) — to work around this, activate the --silent flag (see loglevel in the docs):

$ npm ls my-dep -ps

this command can be integrated into your code using a child process, in which case it's preferred to run the command without the --silent flag to allow capturing any error.

if an error is catched, you can then decide whether its fatal or not (e.g. the aforementioned error about extraneous package should be ignored).

so usage of the CLI via a child process can look like this:

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
    }
});

this can then be used (along with some closure magic…) in an async flow, e.g.:

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());
        });
    };
}