Il modo migliore per eseguire l'installazione di npm per le cartelle nidificate?


128

Qual è il modo più corretto per eseguire l'installazione npm packagesin sottocartelle nidificate?

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

Qual è il modo migliore per avere packagesin /my-sub-moduleessere installato automaticamente durante npm installl'esecuzione in my-app?


Penso che la cosa più idiomatica sia avere un singolo file package.json all'inizio del tuo progetto.
Robert Moskal

Un'idea potrebbe essere quella di utilizzare uno script npm che esegue un file bash.
Davin Tryon

Non potrebbe questo essere fatto con una modifica al modo in cui funzionano i percorsi locali ? :
stackoverflow.com/questions/14381898/…

Risposte:


26

Se si desidera eseguire un singolo comando per installare i pacchetti npm in sottocartelle nidificate, è possibile eseguire uno script tramite npme main package.jsonnella directory principale. Lo script visiterà ogni sottodirectory e verrà eseguito npm install.

Di seguito è riportato uno .jsscript che raggiungerà il risultato desiderato:

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

Notare che questo è un esempio tratto da un file articolo di StrongLoop che affronta specificamente una struttura di node.jsprogetto modulare (inclusi componenti e package.jsonfile nidificati ).

Come suggerito, potresti anche ottenere la stessa cosa con uno script bash.

EDIT: ha fatto funzionare il codice in Windows


1
A destra complicata però, grazie per il collegamento dell'articolo.
WHITECOLOR

Sebbene la struttura basata su "componenti" sia un modo abbastanza pratico per configurare un'app di nodi, è probabilmente eccessivo nelle prime fasi dell'app suddividere file package.json separati ecc. L'idea tende a realizzarsi quando l'app cresce e vuoi legittimamente moduli / servizi separati. Ma sì, decisamente troppo complicato se non necessario.
snozza

3
Anche se sì, uno script bash andrà bene, ma preferisco il modo nodejs di farlo per la massima portabilità tra Windows che ha una shell DOS e Linux / Mac che ha la shell Unix.
truthadjustr

270

Preferisco usare la post-installazione, se conosci i nomi della sottodirectory annidata. In package.json:

"scripts": {
  "postinstall": "cd nested_dir && npm install",
  ...
}

10
che dire di più cartelle? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre

1
@Emre yes - questo è tutto.
Guy,

2
@Scott non puoi quindi inserire la cartella successiva nel package.json interno come "postinstall": "cd nested_dir2 && npm install"per ogni cartella?
Aron

1
@Aron E se si volessero due sottodirectory all'interno della directory principale del nome?
Alec

29
@Emre Dovrebbe funzionare, le subshell potrebbero essere leggermente più pulite: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Alec

49

Secondo la risposta di @ Scott, lo script install | postinstall è il modo più semplice fintanto che i nomi delle sottodirectory sono noti. Ecco come lo eseguo per più directory secondarie. Ad esempio, far finta che abbiamo api/, web/e shared/sotto-progetti in una radice monorepo:

// In monorepo root package.json
{
...
 "scripts": {
    "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
  },
}

1
Soluzione perfetta. Grazie per la condivisione :-)
Rahul Soni

1
Grazie per la risposta. Lavorando per me.
AMIC MING

5
Buon uso di ( )per creare subshell ed evitare cd api && npm install && cd ...
Cameron Hudson

4
Questa dovrebbe essere la risposta selezionata!
tmos

3
Ricevo questo errore quando npm install"(cd was unexpected at this time."
eseguo

22

La mia soluzione è molto simile. Pure Node.js

Lo script seguente esamina tutte le sottocartelle (in modo ricorsivo) finché hanno package.jsone viene eseguito npm installin ciascuna di esse. Si possono aggiungere eccezioni: cartelle consentite non avere package.json. Nell'esempio sotto una di queste cartelle è "packages". Si può eseguirlo come uno script di "preinstallazione".

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

3
il tuo copione è carino. Tuttavia, per i miei scopi personali preferisco rimuovere la prima "condizione if" per ottenere una "installazione npm" nidificata in profondità!
Guilherme Caraciolo

21

Solo per riferimento nel caso in cui le persone incontrino questa domanda. Tu puoi ora:

  • Aggiungi un package.json a una sottocartella
  • Installa questa sottocartella come link di riferimento nel package.json principale:

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


2
Notare che le dipendenze vengono installate nella cartella principale. Ho il sospetto che se stai anche solo considerando questo modello, vuoi le dipendenze della sottodirectory package.json nella sottodirectory.
Cody Allan Taylor,

Cosa intendi? Le dipendenze per la sottocartella-pacchetto si trovano in package.json nella sottocartella.
Jelmer Jellema

(utilizzando npm v6.6.0 e nodo v8.15.0) - Imposta un esempio per te stesso. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Ora aspetta ... hai appena installato manualmente le dipendenze in "b", non è quello che succede quando cloni un nuovo progetto. rm -rf node_modules ; cd .. ; npm install --save ./b. Ora elenca node_modules, quindi elenca b.
Cody Allan Taylor,

1
Ah, intendi i moduli. Sì, i node_modules per b verranno installati in a / node_modules. Il che ha senso, perché richiederai / includerai i moduli come parte del codice principale, non come un modulo nodo "reale". Quindi un "require ('throug2')" cercherà attraverso2 in un / node_modules.
Jelmer Jellema

Sto cercando di generare codice e voglio un pacchetto di sottocartelle che sia completamente pronto per essere eseguito, inclusi i propri node_modules. Se trovo la soluzione, mi assicurerò di aggiornare!
ohsully

19

Caso d'uso 1 : se vuoi essere in grado di eseguire comandi npm da ogni sottodirectory (dove si trova ogni package.json), dovrai usare postinstall.

Come spesso uso npm-run-allcomunque, lo uso per mantenerlo bello e breve (la parte nella postinstallazione):

{
    "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:*"
}

Questo ha l'ulteriore vantaggio di poter installare tutto in una volta o individualmente. Se non ne hai bisogno o non vuoi npm-run-allcome dipendenza, controlla la risposta di demisx (usando le subshell in postinstallazione).

Caso d'uso 2 : se eseguirai tutti i comandi npm dalla directory root (e, ad esempio, non utilizzerai gli script npm nelle sottodirectory), potresti semplicemente installare ogni sottodirectory come faresti con qualsiasi dipendenza:

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

In quest'ultimo caso, non essere sorpreso di non trovare alcun file node_moduleso package-lock.jsonnelle sottodirectory: tutti i pacchetti verranno installati nella rootnode_modules , motivo per cui non sarai in grado di eseguire i tuoi comandi npm (che richiedono dipendenze) da qualsiasi sottodirectory.

Se non sei sicuro, il caso d'uso 1 funziona sempre.


È bello avere ogni sottomodulo con il proprio script di installazione e quindi eseguirli tutti in postinstallazione. run-pnon è necessario, ma è più dettagliato"postinstall": "npm run install:a && npm run install:b"
Qwerty

Sì, puoi usare &&senza run-p. Ma come dici tu, è meno leggibile. Un altro svantaggio (che run-p risolve perché le installazioni vengono eseguite in parallelo) è che se uno fallisce, nessun altro script è interessato
Don Vaughn,

3

Aggiunta del supporto di Windows alla risposta di snozza , oltre a saltare la node_modulescartella se 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' })
})

Certo che puoi. Ho aggiornato la mia soluzione per saltare la cartella node_modules.
Ghostrydr

2

Ispirato dagli script forniti qui, ho creato un esempio configurabile che:

  • può essere configurato per utilizzare yarnonpm
  • puòessere impostato per determinare il comando da usare in base ai file di blocco in modo che se lo imposti per l'uso yarnma una directory ha solo un package-lock.json, lo userànpm per quella directory (il valore predefinito è true).
  • configurare la registrazione
  • esegue installazioni in parallelo utilizzando cp.spawn
  • può fare corse a secco per farti vedere cosa farebbe prima
  • può essere eseguito come una funzione o eseguito automaticamente utilizzando env vars
    • quando viene eseguito come una funzione, fornire opzionalmente un array di directory da controllare
  • restituisce una promessa che si risolve una volta completata
  • consente di impostare la profondità massima per guardare se necessario
  • sa di interrompere la ricorrenza se trova una cartella con yarn workspaces (configurabile)
  • consente di saltare le directory utilizzando una env var separata da virgole o passando alla configurazione un array di stringhe con cui confrontare o una funzione che riceve il nome del file, il percorso del file e l'oggetto fs.Dirent e si aspetta un risultato 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;

E con esso utilizzato:

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

installRecursively(process.cwd(), { dry: true })

1

Se hai findun'utilità sul tuo sistema, puoi provare a eseguire il seguente comando nella directory principale dell'applicazione:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Fondamentalmente, trova tutti i package.jsonfile ed esegui npm installin quella directory, saltando tutte le node_modulesdirectory.


1
Bella risposta. Solo una nota che puoi anche omettere percorsi aggiuntivi con:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.