106

Can I install a NPM package from a javascript file running in Node.js? For example, I'd like to have a script, let's call it "script.js" that somehow (...using NPM or not...) install a package usually available through NPM. In this example, I'd like to install "FFI". (npm install ffi)

Justin
  • 1,063
  • 2
  • 8
  • 5

10 Answers10

127

Update: As of November 2021, use of the programmatic API is deprecated. Consider using child_process to call the npm CLI.


It is indeed possible to use npm programmatically, and it was outlined in older revisions of the documentation. It has since been removed from the official documentation, but still exists on source control with the following statement:

Although npm can be used programmatically, its API is meant for use by the CLI only, and no guarantees are made regarding its fitness for any other purpose. If you want to use npm to reliably perform some task, the safest thing to do is to invoke the desired npm command with appropriate arguments.

The semantic version of npm refers to the CLI itself, rather than the underlying API. The internal API is not guaranteed to remain stable even when npm's version indicates no breaking changes have been made according to semver.

In the original documentation, the following is the code sample that was provided:

var npm = require('npm')
npm.load(myConfigObject, function (er) {
  if (er) return handlError(er)
  npm.commands.install(['some', 'args'], function (er, data) {
    if (er) return commandFailed(er)
    // command succeeded, and data might have some info
  })
  npm.registry.log.on('log', function (message) { ... })
})

Since npm exists in the node_modules folder, you can use require('npm') to load it like any other module. To install a module, you will want to use npm.commands.install().

If you need to look in the source then it's also on GitHub. Here's a complete working example of the code, which is the equivalent of running npm install without any command-line arguments:

var npm = require('npm');
npm.load(function(err) {
  // handle errors

  // install module ffi
  npm.commands.install(['ffi'], function(er, data) {
    // log errors or data
  });

  npm.on('log', function(message) {
    // log installation progress
    console.log(message);
  });
});

Note that the first argument to the install function is an array. Each element of the array is a module that npm will attempt to install.

More advanced use can be found in the npm-cli.js file on source control.

hexacyanide
  • 88,222
  • 31
  • 159
  • 162
  • 6
    in case this helps anyone- make sure you do `npm install npm --save` first. Example works great :) – mikermcneil Dec 15 '13 at 22:12
  • 6
    Also, beware-- `npm` has lots of dependencies, so adding it to your module will most likely result in it taking MUCH longer to download. Check out one of the `child_process` answers to leverage the global npm already installed on your users' machines. – mikermcneil Dec 16 '13 at 01:07
  • 1
    Don't pass `npm.config` to `npm.load`! Even @isaacs doesn't know what kind of weird things will happen then! See https://github.com/npm/npm/issues/4861#issuecomment-40533836 Instead, you can just skip the 1st argument. – Georgii Ivankin Feb 12 '15 at 17:39
  • The link to the documentation seems to be resulting in a 404. Is there a new url or did something happen? – Saad Nov 29 '15 at 12:28
  • 2
    How do I set the destination path? (when it is different that the `process.cwd()`) – Gajus Jan 04 '16 at 16:34
  • I'm not entirely sure if that's possible. The first parameter to `npm.load` is just an object of the command-line arguments you'd normally supply to `npm install`, which doesn't currently appear to have a destination path option. There does exist a `--prefix` option for installation (see [here](http://stackoverflow.com/questions/14469515/how-to-npm-install-to-a-specified-directory)) but that appears to require a `package.json` to exist in the target directory. – hexacyanide Jan 04 '16 at 20:46
  • I thought I'm getting insane when I remembered this was in the manual, but could not find it there. What a sucker punch. I've opened a ticket to get it back. – polkovnikov.ph Nov 11 '16 at 15:44
  • 1
    For those that wish to import NPM despite the warnings, [global-npm](https://www.npmjs.com/package/global-npm) is better (smaller, no dependencies) than `npm install npm --save` – Xunnamius Sep 12 '18 at 15:34
  • As for 2022 this solution doesn't work anymore, npm >8.0.0 deprecated programmatic API, therefore `var npm = require("npm");` inside the project is no longer possible. – Sorin GFS Jun 24 '22 at 07:49
44

You can use child_process.exec or execSync to spawn a shell then execute the desired command within that shell, buffering any generated output:

var child_process = require('child_process');
child_process.execSync('npm install ffi',{stdio:[0,1,2]});

If a callback function is provided, it is called with the arguments (error, stdout, stderr). This way you can run the installation like you do it manualy and see the full output.

The child_process.execSync() method is generally identical to child_process.exec() with the exception that the method will not return until the child process has fully closed.

krankuba
  • 1,283
  • 1
  • 17
  • 20
  • 4
    this is the only option from all the answers that for instance lets you run npm install and get the full output as if you were executing the command manually! thank you! – Jörn Berkefeld May 21 '20 at 11:50
  • 2
    What does `stdio: [0,1,2]` do? – Zach Smith Aug 29 '20 at 07:09
  • 1
    if a callback function is provided to child_process.exec, it is called with the arguments equivalent to [process.stdin, process.stdout, process.stderr] or [0,1,2] as per [api doc](https://nodejs.org/api/child_process.html) – krankuba Aug 30 '20 at 08:36
30

yes. you can use child_process to execute a system command

var exec = require('child_process').exec,
    child;

 child = exec('npm install ffi',
 function (error, stdout, stderr) {
     console.log('stdout: ' + stdout);
     console.log('stderr: ' + stderr);
     if (error !== null) {
          console.log('exec error: ' + error);
     }
 });
TheBrain
  • 5,528
  • 2
  • 25
  • 26
  • 2
    Yes, you can, however some dependencies WILL fail to install (talking from experience, because once upon a time I actually wrote a [CI](https://github.com/matejkramny/node-ci) server for node.js) – Matej Sep 30 '13 at 19:06
  • 5
    On windows this doesn't work! You have to call `npm.cmd` instead. – DUzun Feb 27 '16 at 21:10
11

it can actually be a bit easy

var exec = require('child_process').exec;
child = exec('npm install ffi').stderr.pipe(process.stderr);
  • 2
    This also has the advantage that stderr (and stdout) are printed as they occur, not at the end of the execution! – mvermand Jun 16 '17 at 07:42
  • 1
    This doesn't print out to the same extent as the answer from @krankuba below, so far as I can tell. – Zach Smith Aug 29 '20 at 07:12
7

I had a heck of a time trying to get the first example to work inside a project directory, posting here in case anyone else finds this. As far as I can tell, NPM still works fine loaded directly, but because it assumes CLI, we have to repeat ourselves a little setting it up:

// this must come before load to set your project directory
var previous = process.cwd();
process.chdir(project);

// this is the part missing from the example above
var conf = {'bin-links': false, verbose: true, prefix: project}

// this is all mostly the same

var cli = require('npm');
cli.load(conf, (err) => {
    // handle errors
    if(err) {
        return reject(err);
    }

    // install module
    cli.commands.install(['ffi'], (er, data) => {
        process.chdir(previous);
        if(err) {
            reject(err);
        }
        // log errors or data
        resolve(data);
    });

    cli.on('log', (message) => {
        // log installation progress
        console.log(message);
    });
});
Megamind
  • 162
  • 5
  • 9
  • It seems this has changed in `npm >v7.x`: `commands.install` doesn't follow `process.chdir` behavior anymore, forcing a `cli.prefix = ` inside `cli.load` callback seems to solve it (albeit there might be better ways to do so but I haven't found yet) – Alexander Fortin Feb 05 '21 at 05:00
  • Here an example using npm `v7.5.2`: https://github.com/probot/create-probot-app/blob/c373bafebc514f4c12436a55f579e641b0fa61ff/src/helpers/run-npm.ts#L46 – Alexander Fortin Feb 05 '21 at 21:02
4

pacote is the package that npm uses to fetch package metadata and tarballs. It has a stable, public API.

James A. Rosen
  • 64,193
  • 61
  • 179
  • 261
4

Another option, which wasn't mentioned here, is to do fork and run CLI right from ./node_modules/npm/bin/npm-cli.js

For example you want to be able to install node modules from running script on machine, which do not have NPM installed. And you DO want to do it with CLI. In this case just install NPM in your node_modules locally while building your program (npm i npm).

Then use it like this:

// Require child_process module
const { fork } = require('child_process');
// Working directory for subprocess of installer
const cwd = './path-where-to-run-npm-command'; 
// CLI path FROM cwd path! Pay attention
// here - path should be FROM your cwd directory
// to your locally installed npm module
const cli = '../node_modules/npm/bin/npm-cli.js';
// NPM arguments to run with
// If your working directory already contains
// package.json file, then just install it!
const args = ['install']; // Or, i.e ['audit', 'fix']

// Run installer
const installer = fork(cli, args, {
  silent: true,
  cwd: cwd
});

// Monitor your installer STDOUT and STDERR
installer.stdout.on('data', (data) => {
  console.log(data);
});
installer.stderr.on('data', (data) => {
  console.log(data);
});

// Do something on installer exit
installer.on('exit', (code) => {
  console.log(`Installer process finished with code ${code}`);
});

Then your program could be even packed to binary file, for example with PKG package. In this case you need to use --ignore-scripts npm option, because node-gyp required to run preinstall scripts

tarkh
  • 2,424
  • 1
  • 9
  • 12
2

I'm the author of a module that allow to do exactly what you have in mind. See live-plugin-manager.

You can install and run virtually any package from NPM, Github or from a folder.

Here an example:

import {PluginManager} from "live-plugin-manager";

const manager = new PluginManager();

async function run() {
  await manager.install("moment");

  const moment = manager.require("moment");
  console.log(moment().format());

  await manager.uninstall("moment");
}

run();

In the above code I install moment package at runtime, load and execute it. At the end I uninstall it.

Internally I don't run npm cli but actually download packages and run inside a node VM sandbox.

Davide Icardi
  • 11,919
  • 8
  • 56
  • 77
1

A great solution by @hexacyanide, but it turned out that NPM doesn't emit "log" event anymore (at least as of version 6.4.1). Instead they rely on a standalone module https://github.com/npm/npmlog. Fortunately it's a singleton, so we can reach the very same instance NPM uses for logs and subscribe for log events:

const npmlog = require( "npm/node_modules/npmlog" ),
      npm = require( "npm" );

npmlog.on( "log", msg => {
   console.log({ msg });
});

 process.on("time", milestone => {
   console.log({ milestone });
 });

 process.on("timeEnd", milestone => {
   console.log({ milestone });    
 });

 npm.load({
    loaded: false,
    progress: false,
    "no-audit": true
  }, ( err ) => {

 npm.commands.install( installDirectory, [
      "cross-env@^5.2.0",
      "shelljs@^0.8.2"
    ], ( err, data ) => {
       console.log( "done" );    
    });

  });

As you can see from the code, NPM also emits performance metrics on the process, so we can also use it to monitor the progress.

Dmitry Sheiko
  • 2,130
  • 1
  • 25
  • 28
0

Adding on to a little more to tarkh's great answer. If you don't intend to install npm CLI explicitly and already are installing npm through the package.json file. Then the file in the ./node_modules/npm/bin/npm-cli.js is the CLI that can be used to run commands.

In order to make this globally available, run the following command and add the binary to the path.

ln -sf /usr/app/node_modules/npm/bin/npm-cli.js /usr/bin/npm

Additionally, if it helps someone. Here's how you can mimic the npm install command in order to install all packages in the package.json.

const npm = require('npm');
const Bluebird = require('bluebird');

    async installDependencies() {
            await Bluebird.promisify(npm.load)({
                loglevel: 'silent',
                progress: false,
            });
            await Bluebird.promisify(npm.install)(
                "/path/to/directory/where/package.json/is",
            );
        }
vipulgupta2048
  • 141
  • 1
  • 14