¿Puedo instalar un paquete NPM desde javascript ejecutándose en Node.js?

91

¿Puedo instalar un paquete NPM desde un archivo javascript que se ejecuta en Node.js? Por ejemplo, me gustaría tener un script, llamémoslo "script.js" que de alguna manera (... usando NPM o no ...) instale un paquete generalmente disponible a través de NPM. En este ejemplo, me gustaría instalar "FFI". (npm instalar ffi)

Justin
fuente

Respuestas:

109

De hecho, es posible usar npm mediante programación, y se describió en revisiones anteriores de la documentación. Desde entonces se ha eliminado de la documentación oficial, pero todavía existe en el control de código fuente con la siguiente declaración:

Aunque npm se puede utilizar de forma programática, su API está diseñada para ser utilizada únicamente por la CLI y no se ofrecen garantías con respecto a su idoneidad para cualquier otro propósito. Si desea utilizar npm para realizar alguna tarea de manera confiable, lo más seguro es invocar el comando npm deseado con los argumentos adecuados.

La versión semántica de npm se refiere a la CLI en sí, en lugar de a la API subyacente. No se garantiza que la API interna permanezca estable incluso cuando la versión de npm indique que no se han realizado cambios importantes según semver .

En la documentación original, el siguiente es el ejemplo de código que se proporcionó:

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) { ... })
})

Dado que npm existe en la node_modulescarpeta, puede usarlo require('npm')para cargarlo como cualquier otro módulo. Para instalar un módulo, querrá usar npm.commands.install().

Si necesita buscar en la fuente, también está en GitHub . Aquí hay un ejemplo de trabajo completo del código, que es el equivalente a ejecutarse npm installsin ningún argumento de línea de comandos:

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

Tenga en cuenta que el primer argumento de la función de instalación es una matriz. Cada elemento de la matriz es un módulo que npm intentará instalar.

Se puede encontrar un uso más avanzado en el npm-cli.jsarchivo sobre control de código fuente.

hexacianuro
fuente
5
en caso de que esto ayude a alguien, asegúrese de hacerlo npm install npm --saveprimero. El ejemplo funciona muy bien :)
mikermcneil
6
Además, npmtenga cuidado, tiene muchas dependencias, por lo que agregarlo a su módulo probablemente resultará en que demore MUCHO más en descargarse. Consulte una de las child_processrespuestas para aprovechar el npm global ya instalado en las máquinas de sus usuarios.
mikermcneil
1
¡No pases npm.configa npm.load! ¡Incluso @isaacs no sabe qué tipo de cosas extrañas sucederán entonces! Consulte github.com/npm/npm/issues/4861#issuecomment-40533836 En su lugar, puede omitir el primer argumento.
Georgii Ivankin
2
¿Cómo configuro la ruta de destino? (cuando es diferente al process.cwd())
Gajus
1
Para aquellos que desean importar NPM a pesar de las advertencias, global-npm es mejor (más pequeño, sin dependencias) quenpm install npm --save
Xunnamius
26

si. puede usar child_process para ejecutar un comando del sistema

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);
     }
 });
El cerebro
fuente
2
Sí, puede, sin embargo, algunas dependencias NO se instalarán (hablando por experiencia, porque una vez escribí un servidor CI para node.js)
Matej
5
¡En Windows esto no funciona! Tienes que llamar en su npm.cmdlugar.
DUzun
26

Puede usar child_process . exec o execSync para generar un shell y luego ejecutar el comando deseado dentro de ese shell, almacenando en búfer cualquier salida generada:

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

Si se proporciona una función de devolución de llamada, se llama con los argumentos (error, stdout, stderr). De esta manera, puede ejecutar la instalación como lo hace manualmente y ver el resultado completo.

El método child_process.execSync () es generalmente idéntico a child_process.exec () con la excepción de que el método no regresará hasta que el proceso hijo se haya cerrado por completo.

krankuba
fuente
2
esta es la única opción de todas las respuestas que, por ejemplo, le permite ejecutar npm install y obtener el resultado completo como si estuviera ejecutando el comando manualmente. ¡gracias!
Jörn Berkefeld
1
¿Qué hace stdio: [0,1,2]?
Zach Smith
si se proporciona una función de devolución de llamada a child_process.exec, se llama con los argumentos equivalentes a [process.stdin, process.stdout, process.stderr] o [0,1,2] según api doc
krankuba
11

en realidad puede ser un poco fácil

var exec = require('child_process').exec;
child = exec('npm install ffi').stderr.pipe(process.stderr);
Vyacheslav Shebanov
fuente
2
Esto también tiene la ventaja de que stderr (y stdout) se imprimen a medida que ocurren, ¡no al final de la ejecución!
mvermand
1
Esto no se imprime en la misma medida que la respuesta de @krankuba a continuación, por lo que puedo decir.
Zach Smith
6

Tuve un gran tiempo tratando de que el primer ejemplo funcionara dentro de un directorio de proyecto, publicando aquí en caso de que alguien más encuentre esto. Por lo que puedo decir, NPM todavía funciona bien cargado directamente, pero debido a que asume CLI, tenemos que repetirnos un poco configurándolo:

// 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);
    });
});
Megamente
fuente
3

pacote es el paquete que utiliza npm para recuperar metadatos y archivos tar del paquete. Tiene una API pública estable.

James A. Rosen
fuente
2

Soy el autor de un módulo que permite hacer exactamente lo que tienes en mente. Ver live-plugin-manager .

Puede instalar y ejecutar prácticamente cualquier paquete desde NPM, Github o desde una carpeta.

Aquí un ejemplo:

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

En el código anterior instalo el momentpaquete en tiempo de ejecución, lo cargo y lo ejecuto. Al final lo desinstalo.

Internamente no ejecuto npmcli, pero en realidad descargo paquetes y los ejecuto dentro de una caja de arena de VM de nodo.

Davide Icardi
fuente
1

Una gran solución de @hexacyanide, pero resultó que NPM ya no emite eventos "log" (al menos a partir de la versión 6.4.1). En su lugar, confían en un módulo independiente https://github.com/npm/npmlog . Afortunadamente es un singleton, por lo que podemos llegar a la misma instancia que NPM usa para los registros y suscribirnos para los eventos de registro:

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

  });

Como puede ver en el código, NPM también emite métricas de rendimiento en process, por lo que también podemos usarlo para monitorear el progreso.

Dmitry Sheiko
fuente
1

Otra opción, que no se mencionó aquí, es hacer un fork y ejecutar CLI directamente desde ./node_modules/npm/bin/npm-cli.js

Por ejemplo, desea poder instalar módulos de nodo desde un script en ejecución en la máquina, que no tiene NPM instalado. Y quieres hacerlo con CLI. En este caso, simplemente instale NPM en sus node_modules localmente mientras construye su programa ( npm i npm).

Entonces úsalo así:

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

Entonces su programa podría incluso empaquetarse en un archivo binario, por ejemplo con el paquete PKG . En este caso, debe usar la --ignore-scriptsopción npm, porque se requiere node-gyp para ejecutar scripts de preinstalación

tarkh
fuente