La meilleure façon d'exécuter l'installation de npm pour les dossiers imbriqués?

128

Quelle est la manière la plus correcte d'installer npm packagesdans des sous-dossiers imbriqués?

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

Quelle est la meilleure façon d'avoir packagesà /my-sub-moduleinstaller automatiquement de npm installfonctionner en my-app?

COULEUR BLANCHE
la source
Je pense que la chose la plus idiomatique est d'avoir un seul fichier package.json à la fin de votre projet.
Robert Moskal
Une idée serait d'utiliser un script npm qui exécute un fichier bash.
Davin Tryon
Cela pourrait - il pas être fait avec un modificaiton à des chemins d' accès locaux travaillent ?: stackoverflow.com/questions/14381898/...
Evanss

Réponses:

26

Si vous souhaitez exécuter une seule commande pour installer les packages npm dans des sous-dossiers imbriqués, vous pouvez exécuter un script via npmet main package.jsondans votre répertoire racine. Le script visitera chaque sous-répertoire et s'exécutera npm install.

Vous trouverez ci-dessous un .jsscript qui atteindra le résultat souhaité:

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

Notez qu'il s'agit d'un exemple tiré d'un article StrongLoop qui traite spécifiquement d'une node.jsstructure de projet modulaire (y compris les composants et package.jsonfichiers imbriqués ).

Comme suggéré, vous pouvez également réaliser la même chose avec un script bash.

EDIT: fait fonctionner le code sous Windows

snozza
la source
1
Trop compliqué, merci pour le lien de l'article.
WHITECOLOR
Bien que la structure basée sur les `` composants '' soit un moyen assez pratique de configurer une application de nœud, il est probablement excessif dans les premières étapes de l'application de casser des fichiers package.json séparés, etc. L'idée a tendance à se concrétiser lorsque l'application se développe et vous voulez légitimement des modules / services séparés. Mais oui, définitivement trop compliqué sinon nécessaire.
snozza
3
Certes, un script bash fera l'affaire, mais je préfère la manière nodejs de le faire pour une portabilité maximale entre Windows qui a un shell DOS et Linux / Mac qui a le shell Unix.
truthadjustr
270

Je préfère utiliser la post-installation, si vous connaissez les noms du sous-répertoire imbriqué. Dans package.json:

"scripts": {
  "postinstall": "cd nested_dir && npm install",
  ...
}
Scott
la source
10
qu'en est-il de plusieurs dossiers? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre
1
@Emre oui - c'est tout.
Guy
2
@Scott ne pouvez-vous pas simplement mettre le dossier suivant dans le package.json intérieur comme "postinstall": "cd nested_dir2 && npm install"pour chaque dossier?
Aron
1
@Aron Que faire si vous voulez deux sous-répertoires dans le répertoire parent du nom?
Alec
29
@Emre Cela devrait fonctionner, les sous-shell pourraient être légèrement plus propres: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Alec
49

Selon la réponse de @ Scott, le script install | postinstall est le moyen le plus simple tant que les noms de sous-répertoires sont connus. C'est ainsi que je l'exécute pour plusieurs sous-répertoires. Par exemple, prétendez que nous avons api/, web/et shared/sous-projets dans une racine monorepo:

// In monorepo root package.json
{
...
 "scripts": {
    "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
  },
}
demisx
la source
1
Solution parfaite. Merci d'avoir partagé :-)
Rahul Soni
1
Merci d'avoir répondu. Travailler pour moi.
AMIC MING
5
Bonne utilisation de ( )pour créer des sous-shell et éviter cd api && npm install && cd ...
Cameron Hudson le
4
Cela devrait être la réponse choisie!
tmos
3
npm install"(cd was unexpected at this time."
J'obtiens
22

Ma solution est très similaire. Pure Node.js

Le script suivant examine tous les sous-dossiers (de manière récursive) tant qu'ils ont package.jsonet s'exécute npm installdans chacun d'eux. On peut y ajouter des exceptions: les dossiers autorisés ne l'ont pas package.json. Dans l'exemple ci-dessous, l'un de ces dossiers est "packages". On peut l'exécuter en tant que script de "pré-installation".

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))
}
catamphétamine
la source
3
votre script est sympa. Cependant, pour mes besoins personnels, je préfère supprimer la première condition «if» pour obtenir une «installation npm» imbriquée en profondeur!
Guilherme Caraciolo
21

Juste pour référence au cas où les gens rencontreraient cette question. Tu peux maintenant:

  • Ajouter un package.json à un sous-dossier
  • Installez ce sous-dossier en tant que lien de référence dans le package principal.json:

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

Jelmer Jellema
la source
2
Notez que les dépendances sont installées dans le dossier racine. Je soupçonne que si vous envisagez même ce modèle, vous voulez les dépendances du sous-répertoire package.json dans le sous-répertoire.
Cody Allan Taylor
Que voulez-vous dire? Les dépendances pour le sous-dossier-package sont dans le package.json dans le sous-dossier.
Jelmer Jellema
(en utilisant npm v6.6.0 et node v8.15.0) - Configurez un exemple pour vous-même. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Maintenant, attendez ... vous venez d' installer manuellement les dépendances dans "b", ce n'est pas ce qui se passe lorsque vous clonez un nouveau projet. rm -rf node_modules ; cd .. ; npm install --save ./b. Maintenant listez node_modules, puis listez b.
Cody Allan Taylor
1
Ah vous voulez dire les modules. Oui, les node_modules pour b seront installés dans a / node_modules. Ce qui a du sens, car vous aurez besoin / incluez les modules dans le cadre du code principal, et non comme un module de nœud "réel". Ainsi, un "require ('throug2')" rechercherait through2 dans un / node_modules.
Jelmer Jellema
J'essaie de faire de la génération de code et je veux un sous-dossier-package qui est entièrement prêt à fonctionner, y compris ses propres node_modules. Si je trouve la solution, je m'assurerai de mettre à jour!
ohsully
19

Cas d'utilisation 1 : si vous voulez pouvoir exécuter des commandes npm à partir de chaque sous-répertoire (où se trouve chaque package.json), vous devrez utiliserpostinstall .

Comme je l'utilise souvent de npm-run-alltoute façon, je l'utilise pour le garder court et court (la partie de la post-installation):

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

Cela a l'avantage supplémentaire que je peux installer en une seule fois ou individuellement. Si tu n'en as pas besoin ou que tu ne veux pasnpm-run-all tant que dépendance, consultez la réponse de demisx (en utilisant des sous-shell dans la post-installation).

Cas d'utilisation 2 : Si vous exécutez toutes les commandes npm à partir du répertoire racine (et, par exemple, n'utilisez pas de scripts npm dans les sous-répertoires), vous pouvez simplement installer chaque sous-répertoire comme vous le feriez pour n'importe quelle dépendance:

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

Dans ce dernier cas, ne soyez pas surpris que vous ne trouviez aucun fichier node_modulesou package-lock.jsondans les sous-répertoires - tous les packages seront installés à la racine node_modules, c'est pourquoi vous ne pourrez pas exécuter vos commandes npm (que nécessitent des dépendances) de l'un de vos sous-répertoires.

Si vous n'êtes pas sûr, le cas d'utilisation 1 fonctionne toujours.

Don Vaughn
la source
C'est bien d'avoir chaque sous-module avoir son propre script d'installation, puis de les exécuter tous en post-installation. run-pn'est pas nécessaire, mais c'est alors plus verbeux"postinstall": "npm run install:a && npm run install:b"
Qwerty
Oui, vous pouvez utiliser &&sans run-p. Mais comme vous le dites, c'est moins lisible. Un autre inconvénient (que run-p résout car les installations s'exécutent en parallèle) est que si l'un échoue, aucun autre script n'est affecté
Don Vaughn
3

Ajout de la prise en charge de Windows à la réponse de snozza , ainsi que saut de node_modulesdossier le cas échéant.

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
la source
Vous pouvez certainement. J'ai mis à jour ma solution pour ignorer le dossier node_modules.
Ghostrydr le
2

Inspiré par les scripts fournis ici, j'ai construit un exemple configurable qui:

  • peut être configuré pour utiliser yarn ounpm
  • peut être configuré pour déterminer la commande à utiliser en fonction des fichiers de verrouillage de sorte que si vous le définissez pour utiliser yarnmais qu'un répertoire n'en a qu'un, package-lock.jsonil utiliseranpm pour ce répertoire (la valeur par défaut est true).
  • configurer la journalisation
  • exécute les installations en parallèle en utilisant cp.spawn
  • peut faire des essais à sec pour vous montrer ce qu'il ferait en premier
  • peut être exécuté en tant que fonction ou exécuté automatiquement à l'aide de variables d'environnement
    • lorsqu'il est exécuté en tant que fonction, fournit éventuellement un tableau de répertoires à vérifier
  • renvoie une promesse qui se résout une fois terminée
  • permet de définir la profondeur maximale à regarder si nécessaire
  • sait arrêter de se répéter s'il trouve un dossier avec yarn workspaces (configurable)
  • permet de sauter des répertoires en utilisant un env var séparé par des virgules ou en passant à la configuration un tableau de chaînes à comparer ou une fonction qui reçoit le nom de fichier, le chemin du fichier et l'objet fs.Dirent et attend un résultat booléen.
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;

Et avec son utilisation:

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

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

Si vous avez un findutilitaire sur votre système, vous pouvez essayer d'exécuter la commande suivante dans le répertoire racine de votre application:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

En gros, recherchez tous les package.jsonfichiers et exécutez-les npm installdans ce répertoire, en ignorant tous les node_modulesrépertoires.

Moha le chameau tout-puissant
la source
1
Très bonne réponse. Juste une note que vous pouvez également omettre des chemins supplémentaires avec:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran