¿La mejor manera de ejecutar npm install para carpetas anidadas?

128

¿Cuál es la forma más correcta de instalar npm packagesen subcarpetas anidadas?

my-app
  /my-sub-module
  package.json
package.json

¿Cuál es la mejor manera de tener packagesen /my-sub-moduleinstalarse automáticamente cuando npm installse ejecute en my-app?

EL COLOR BLANCO
fuente
Creo que lo más idiomático es tener un solo archivo package.json al final de su proyecto.
Robert Moskal
Una idea sería utilizar un script npm que ejecute un archivo bash.
Davin Tryon
¿No se podría hacer esto con una modificación de cómo funcionan las rutas locales ?: stackoverflow.com/questions/14381898/…
Evanss

Respuestas:

26

Si desea ejecutar un solo comando para instalar paquetes npm en subcarpetas anidadas, puede ejecutar un script a través de npmy main package.jsonen su directorio raíz. El script visitará todos los subdirectorios y se ejecutará npm install.

A continuación se muestra un .jsscript que logrará el resultado deseado:

var fs = require('fs')
var resolve = require('path').resolve
var join = require('path').join
var cp = require('child_process')
var os = require('os')

// get library path
var lib = resolve(__dirname, '../lib/')

fs.readdirSync(lib)
  .forEach(function (mod) {
    var modPath = join(lib, mod)
// ensure path has package.json
if (!fs.existsSync(join(modPath, 'package.json'))) return

// npm binary based on OS
var npmCmd = os.platform().startsWith('win') ? 'npm.cmd' : 'npm'

// install folder
cp.spawn(npmCmd, ['i'], { env: process.env, cwd: modPath, stdio: 'inherit' })
})

Tenga en cuenta que este es un ejemplo tomado de StrongLoop artículo de que aborda específicamente una estructura de node.jsproyecto modular (incluidos componentes anidados ypackage.json archivos ).

Como se sugirió, también podría lograr lo mismo con un script bash.

EDITAR: hizo que el código funcionara en Windows

snozza
fuente
1
Aunque complicado, gracias por el enlace del artículo.
WHITECOLOR
Si bien la estructura basada en 'componentes' es una forma bastante práctica de configurar una aplicación de nodo, probablemente sea excesivo en las primeras etapas de la aplicación dividir archivos package.json separados, etc. La idea tiende a concretarse cuando la aplicación crece y legítimamente desea módulos / servicios separados. Pero sí, definitivamente demasiado complicado si no es necesario.
snozza
3
Si bien sí, un script bash servirá, pero prefiero la forma de nodejs de hacerlo para una máxima portabilidad entre Windows que tiene un shell de DOS y Linux / Mac que tiene el shell de Unix.
truthadjustr
270

Prefiero usar post-install, si conoce los nombres del subdirectorio anidado. En package.json:

"scripts": {
  "postinstall": "cd nested_dir && npm install",
  ...
}
Scott
fuente
10
¿qué pasa con varias carpetas? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre
1
@Emre sí, eso es todo.
Guy
2
@Scott, ¿no puedes poner la siguiente carpeta en el interior package.json como "postinstall": "cd nested_dir2 && npm install"para cada carpeta?
Aron
1
@Aron ¿Qué sucede si desea dos subdirectorios dentro del directorio principal del nombre?
Alec
29
@Emre Eso debería funcionar, las subcapas podrían ser un poco más limpias: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Alec
49

Según la respuesta de @ Scott, el script install | postinstall es la forma más sencilla siempre que se conozcan los nombres de los subdirectorios. Así es como lo ejecuto para múltiples subdirectorios. Por ejemplo, pretender que tenemos api/, web/y shared/sub-proyectos en una raíz monorepo:

// In monorepo root package.json
{
...
 "scripts": {
    "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
  },
}
demisx
fuente
1
Solución perfecta. Gracias por compartir :-)
Rahul Soni
1
Gracias por la respuesta. Trabajando para mi.
AMIC MING
5
Buen uso de ( )para crear subcapas y evitar cd api && npm install && cd ...
Cameron Hudson
4
¡Esa debería ser la respuesta seleccionada!
tmos
3
Recibo este error cuando npm install"(cd was unexpected at this time."
corro
22

Mi solución es muy similar. Pure Node.js

La siguiente secuencia de comandos examina todas las subcarpetas (de forma recursiva) siempre que tengan package.jsony se ejecute npm installen cada una de ellas. Se le pueden agregar excepciones: carpetas permitidas no tener package.json. En el ejemplo siguiente, una de estas carpetas es "paquetes". Se puede ejecutar como un script "preinstalado".

const path = require('path')
const fs = require('fs')
const child_process = require('child_process')

const root = process.cwd()
npm_install_recursive(root)

// Since this script is intended to be run as a "preinstall" command,
// it will do `npm install` automatically inside the root folder in the end.
console.log('===================================================================')
console.log(`Performing "npm install" inside root folder`)
console.log('===================================================================')

// Recurses into a folder
function npm_install_recursive(folder)
{
    const has_package_json = fs.existsSync(path.join(folder, 'package.json'))

    // Abort if there's no `package.json` in this folder and it's not a "packages" folder
    if (!has_package_json && path.basename(folder) !== 'packages')
    {
        return
    }

    // If there is `package.json` in this folder then perform `npm install`.
    //
    // Since this script is intended to be run as a "preinstall" command,
    // skip the root folder, because it will be `npm install`ed in the end.
    // Hence the `folder !== root` condition.
    //
    if (has_package_json && folder !== root)
    {
        console.log('===================================================================')
        console.log(`Performing "npm install" inside ${folder === root ? 'root folder' : './' + path.relative(root, folder)}`)
        console.log('===================================================================')

        npm_install(folder)
    }

    // Recurse into subfolders
    for (let subfolder of subfolders(folder))
    {
        npm_install_recursive(subfolder)
    }
}

// Performs `npm install`
function npm_install(where)
{
    child_process.execSync('npm install', { cwd: where, env: process.env, stdio: 'inherit' })
}

// Lists subfolders in a folder
function subfolders(folder)
{
    return fs.readdirSync(folder)
        .filter(subfolder => fs.statSync(path.join(folder, subfolder)).isDirectory())
        .filter(subfolder => subfolder !== 'node_modules' && subfolder[0] !== '.')
        .map(subfolder => path.join(folder, subfolder))
}
cataanfetamina
fuente
3
tu guión es bueno. Sin embargo, para mis propósitos personales, prefiero eliminar la primera 'condición if' para obtener una 'instalación npm' anidada en profundidad.
Guilherme Caraciolo
21

Solo como referencia en caso de que las personas se encuentren con esta pregunta. Tu puedes ahora:

  • Agregar un package.json a una subcarpeta
  • Instale esta subcarpeta como enlace de referencia en el paquete principal.json:

npm install --save path/to/my/subfolder

Jelmer Jellema
fuente
2
Tenga en cuenta que las dependencias se instalan en la carpeta raíz. Sospecho que si está considerando este patrón, querrá las dependencias del subdirectorio package.json en el subdirectorio.
Cody Allan Taylor
¿Qué quieres decir? Las dependencias para el paquete de subcarpetas se encuentran en package.json en la subcarpeta.
Jelmer Jellema
(usando npm v6.6.0 & node v8.15.0) - Configure un ejemplo para usted. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Ahora espera ... acabas de instalar manualmente las dependencias en "b", eso no es lo que sucede cuando clonas un proyecto nuevo. rm -rf node_modules ; cd .. ; npm install --save ./b. Ahora enumere node_modules, luego enumere b.
Cody Allan Taylor
1
Ah, te refieres a los módulos. Sí, los node_modules para b se instalarán en a / node_modules. Lo cual tiene sentido, porque necesitará / incluirá los módulos como parte del código principal, no como un módulo de nodo "real". Entonces, un "require ('throug2')" buscaría a través de2 en un / node_modules.
Jelmer Jellema
Estoy tratando de generar código y quiero un paquete de subcarpetas que esté completamente preparado para ejecutarse, incluidos sus propios módulos de nodo. Si encuentro la solución, ¡me aseguraré de actualizar!
ohsully
19

Caso de uso 1 : si desea poder ejecutar comandos npm desde cada subdirectorio (donde se encuentra cada package.json), deberá usarpostinstall .

Como lo uso a menudo de npm-run-alltodos modos, lo uso para mantenerlo agradable y corto (la parte en el postinstall):

{
    "install:demo": "cd projects/demo && npm install",
    "install:design": "cd projects/design && npm install",
    "install:utils": "cd projects/utils && npm install",

    "postinstall": "run-p install:*"
}

Esto tiene el beneficio adicional de que puedo instalarlo todo a la vez o individualmente. Si no necesita esto o no quiere npm-run-allcomo dependencia, consulte la respuesta de demisx (usando subshells en postinstall).

Caso de uso 2 : si va a ejecutar todos los comandos npm desde el directorio raíz (y, por ejemplo, no usará scripts npm en subdirectorios), simplemente puede instalar cada subdirectorio como lo haría con cualquier dependencia:

npm install path/to/any/directory/with/a/package-json

En el último caso, no se sorprenda de no encontrar ningún archivo node_moduleso package-lock.jsonarchivo en los subdirectorios; todos los paquetes se instalarán en la raíz node_modules, por lo que no podrá ejecutar sus comandos npm (que requieren dependencias) de cualquiera de sus subdirectorios.

Si no está seguro, el caso de uso 1 siempre funciona.

Don Vaughn
fuente
Es bueno que cada submódulo tenga su propio script de instalación y luego ejecutarlos todos en postinstall. run-pno es necesario, pero luego es más detallado"postinstall": "npm run install:a && npm run install:b"
Qwerty
Sí, puedes usar &&sin run-p. Pero como dices, eso es menos legible. Otro inconveniente (que run-p resuelve porque las instalaciones se ejecutan en paralelo) es que si uno falla, ningún otro script se ve afectado
Don Vaughn
3

Agregar soporte de Windows a la respuesta de snozza , así como omitir la node_modulescarpeta si está presente.

var fs = require('fs')
var resolve = require('path').resolve
var join = require('path').join
var cp = require('child_process')

// get library path
var lib = resolve(__dirname, '../lib/')

fs.readdirSync(lib)
  .forEach(function (mod) {
    var modPath = join(lib, mod)
    // ensure path has package.json
    if (!mod === 'node_modules' && !fs.existsSync(join(modPath, 'package.json'))) return

    // Determine OS and set command accordingly
    const cmd = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';

    // install folder
    cp.spawn(cmd, ['i'], { env: process.env, cwd: modPath, stdio: 'inherit' })
})
Ghostrydr
fuente
Seguro que puedes. Actualicé mi solución para omitir la carpeta node_modules.
Ghostrydr
2

Inspirado por los scripts proporcionados aquí, construí un ejemplo configurable que:

  • se puede configurar para usar yarnonpm
  • se puede configurar para determinar el comando que se usará en función de los archivos de bloqueo, de modo que si lo configura para usar yarnpero un directorio solo tiene un package-lock.json, se usaránpm para ese directorio (el valor predeterminado es verdadero).
  • configurar el registro
  • ejecuta instalaciones en paralelo usando cp.spawn
  • puede hacer pruebas para que veas lo que haría primero
  • se puede ejecutar como una función o ejecutar automáticamente usando env vars
    • cuando se ejecuta como una función, opcionalmente proporcione una matriz de directorios para verificar
  • devuelve una promesa que se resuelve cuando se completa
  • permite establecer la profundidad máxima para mirar si es necesario
  • sabe dejar de recurrir si encuentra una carpeta con yarn workspaces (configurable)
  • permite omitir directorios usando una var env separada por comas o pasando a la configuración una matriz de cadenas para hacer coincidir o una función que recibe el nombre del archivo, la ruta del archivo y el obj fs.Dirent y espera un resultado booleano.
const path = require('path');
const { promises: fs } = require('fs');
const cp = require('child_process');

// if you want to have it automatically run based upon
// process.cwd()
const AUTO_RUN = Boolean(process.env.RI_AUTO_RUN);

/**
 * Creates a config object from environment variables which can then be
 * overriden if executing via its exported function (config as second arg)
 */
const getConfig = (config = {}) => ({
  // we want to use yarn by default but RI_USE_YARN=false will
  // use npm instead
  useYarn: process.env.RI_USE_YARN !== 'false',
  // should we handle yarn workspaces?  if this is true (default)
  // then we will stop recursing if a package.json has the "workspaces"
  // property and we will allow `yarn` to do its thing.
  yarnWorkspaces: process.env.RI_YARN_WORKSPACES !== 'false',
  // if truthy, will run extra checks to see if there is a package-lock.json
  // or yarn.lock file in a given directory and use that installer if so.
  detectLockFiles: process.env.RI_DETECT_LOCK_FILES !== 'false',
  // what kind of logging should be done on the spawned processes?
  // if this exists and it is not errors it will log everything
  // otherwise it will only log stderr and spawn errors
  log: process.env.RI_LOG || 'errors',
  // max depth to recurse?
  maxDepth: process.env.RI_MAX_DEPTH || Infinity,
  // do not install at the root directory?
  ignoreRoot: Boolean(process.env.RI_IGNORE_ROOT),
  // an array (or comma separated string for env var) of directories
  // to skip while recursing. if array, can pass functions which
  // return a boolean after receiving the dir path and fs.Dirent args
  // @see https://nodejs.org/api/fs.html#fs_class_fs_dirent
  skipDirectories: process.env.RI_SKIP_DIRS
    ? process.env.RI_SKIP_DIRS.split(',').map(str => str.trim())
    : undefined,
  // just run through and log the actions that would be taken?
  dry: Boolean(process.env.RI_DRY_RUN),
  ...config
});

function handleSpawnedProcess(dir, log, proc) {
  return new Promise((resolve, reject) => {
    proc.on('error', error => {
      console.log(`
----------------
  [RI] | [ERROR] | Failed to Spawn Process
  - Path:   ${dir}
  - Reason: ${error.message}
----------------
  `);
      reject(error);
    });

    if (log) {
      proc.stderr.on('data', data => {
        console.error(`[RI] | [${dir}] | ${data}`);
      });
    }

    if (log && log !== 'errors') {
      proc.stdout.on('data', data => {
        console.log(`[RI] | [${dir}] | ${data}`);
      });
    }

    proc.on('close', code => {
      if (log && log !== 'errors') {
        console.log(`
----------------
  [RI] | [COMPLETE] | Spawned Process Closed
  - Path: ${dir}
  - Code: ${code}
----------------
        `);
      }
      if (code === 0) {
        resolve();
      } else {
        reject(
          new Error(
            `[RI] | [ERROR] | [${dir}] | failed to install with exit code ${code}`
          )
        );
      }
    });
  });
}

async function recurseDirectory(rootDir, config) {
  const {
    useYarn,
    yarnWorkspaces,
    detectLockFiles,
    log,
    maxDepth,
    ignoreRoot,
    skipDirectories,
    dry
  } = config;

  const installPromises = [];

  function install(cmd, folder, relativeDir) {
    const proc = cp.spawn(cmd, ['install'], {
      cwd: folder,
      env: process.env
    });
    installPromises.push(handleSpawnedProcess(relativeDir, log, proc));
  }

  function shouldSkipFile(filePath, file) {
    if (!file.isDirectory() || file.name === 'node_modules') {
      return true;
    }
    if (!skipDirectories) {
      return false;
    }
    return skipDirectories.some(check =>
      typeof check === 'function' ? check(filePath, file) : check === file.name
    );
  }

  async function getInstallCommand(folder) {
    let cmd = useYarn ? 'yarn' : 'npm';
    if (detectLockFiles) {
      const [hasYarnLock, hasPackageLock] = await Promise.all([
        fs
          .readFile(path.join(folder, 'yarn.lock'))
          .then(() => true)
          .catch(() => false),
        fs
          .readFile(path.join(folder, 'package-lock.json'))
          .then(() => true)
          .catch(() => false)
      ]);
      if (cmd === 'yarn' && !hasYarnLock && hasPackageLock) {
        cmd = 'npm';
      } else if (cmd === 'npm' && !hasPackageLock && hasYarnLock) {
        cmd = 'yarn';
      }
    }
    return cmd;
  }

  async function installRecursively(folder, depth = 0) {
    if (dry || (log && log !== 'errors')) {
      console.log('[RI] | Check Directory --> ', folder);
    }

    let pkg;

    if (folder !== rootDir || !ignoreRoot) {
      try {
        // Check if package.json exists, if it doesnt this will error and move on
        pkg = JSON.parse(await fs.readFile(path.join(folder, 'package.json')));
        // get the command that we should use.  if lock checking is enabled it will
        // also determine what installer to use based on the available lock files
        const cmd = await getInstallCommand(folder);
        const relativeDir = `${path.basename(rootDir)} -> ./${path.relative(
          rootDir,
          folder
        )}`;
        if (dry || (log && log !== 'errors')) {
          console.log(
            `[RI] | Performing (${cmd} install) at path "${relativeDir}"`
          );
        }
        if (!dry) {
          install(cmd, folder, relativeDir);
        }
      } catch {
        // do nothing when error caught as it simply indicates package.json likely doesnt
        // exist.
      }
    }

    if (
      depth >= maxDepth ||
      (pkg && useYarn && yarnWorkspaces && pkg.workspaces)
    ) {
      // if we have reached maxDepth or if our package.json in the current directory
      // contains yarn workspaces then we use yarn for installing then this is the last
      // directory we will attempt to install.
      return;
    }

    const files = await fs.readdir(folder, { withFileTypes: true });

    return Promise.all(
      files.map(file => {
        const filePath = path.join(folder, file.name);
        return shouldSkipFile(filePath, file)
          ? undefined
          : installRecursively(filePath, depth + 1);
      })
    );
  }

  await installRecursively(rootDir);
  await Promise.all(installPromises);
}

async function startRecursiveInstall(directories, _config) {
  const config = getConfig(_config);
  const promise = Array.isArray(directories)
    ? Promise.all(directories.map(rootDir => recurseDirectory(rootDir, config)))
    : recurseDirectory(directories, config);
  await promise;
}

if (AUTO_RUN) {
  startRecursiveInstall(process.cwd());
}

module.exports = startRecursiveInstall;

Y con su uso:

const installRecursively = require('./recursive-install');

installRecursively(process.cwd(), { dry: true })
Braden Rockwell Napier
fuente
1

Si tiene findutilidad en su sistema, puede intentar ejecutar el siguiente comando en el directorio raíz de su aplicación:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Básicamente, busque todos los package.jsonarchivos y ejecútelos npm installen ese directorio, saltándose todos los node_modulesdirectorios.

Moha el camello todopoderoso
fuente
1
Gran respuesta. Solo una nota de que también puede omitir rutas adicionales con:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran