Der beste Weg, um npm install für verschachtelte Ordner auszuführen?


128

Was ist die korrekteste Methode zur Installation npm packagesin verschachtelten Unterordnern?

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

Was ist der beste Weg zu sein packagesin /my-sub-moduleautomatisch installiert werden , wenn npm installeinlaufen my-app?


Ich denke, das Idiomatischste ist, eine einzige package.json-Datei am Anfang Ihres Projekts zu haben.
Robert Moskal

Eine Idee wäre, ein npm-Skript zu verwenden, das eine Bash-Datei ausführt.
Davin Tryon

Könnte dies nicht mit einer Änderung der Funktionsweise lokaler Pfade geschehen?: Stackoverflow.com/questions/14381898/…
Evanss

Antworten:


26

Wenn Sie einen einzelnen Befehl ausführen möchten, um npm-Pakete in verschachtelten Unterordnern zu installieren, können Sie ein Skript über npmund main package.jsonin Ihrem Stammverzeichnis ausführen . Das Skript besucht jedes Unterverzeichnis und wird ausgeführt npm install.

Unten finden Sie ein .jsSkript, mit dem das gewünschte Ergebnis erzielt wird:

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

Beachten Sie, dass dies ein Beispiel aus einem StrongLoop- Artikel ist, der sich speziell mit einer modularen node.jsProjektstruktur (einschließlich verschachtelter Komponenten und package.jsonDateien) befasst.

Wie vorgeschlagen, können Sie dasselbe auch mit einem Bash-Skript erreichen.

BEARBEITEN: Der Code funktioniert unter Windows


1
Zu kompliziert, danke für den Artikel-Link.
WHITECOLOR

Während die auf Komponenten basierende Struktur eine praktische Möglichkeit ist, eine Knoten-App einzurichten, ist es wahrscheinlich in den frühen Phasen der App übertrieben, separate package.json-Dateien usw. aufzubrechen. Die Idee wird in der Regel verwirklicht, wenn die App wächst und wächst Sie möchten zu Recht separate Module / Dienste. Aber ja, definitiv zu kompliziert, wenn nicht nötig.
Snozza

3
Ja, ein Bash-Skript reicht zwar aus, aber ich bevorzuge die Methode von nodejs, um maximale Portabilität zwischen Windows mit einer DOS-Shell und Linux / Mac mit der Unix-Shell zu erreichen.
Truthadjustr

270

Ich bevorzuge die Nachinstallation, wenn Sie die Namen des verschachtelten Unterverzeichnisses kennen. In package.json:

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

10
Was ist mit mehreren Ordnern? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre

1
@ Emre ja - das war's.
Guy

2
@Scott kannst du dann nicht einfach den nächsten Ordner in die Datei package.json legen, wie "postinstall": "cd nested_dir2 && npm install"für jeden Ordner?
Aron

1
@Aron Was ist, wenn Sie zwei Unterverzeichnisse im übergeordneten Verzeichnis des Namens möchten?
Alec

28
@Emre Das sollte funktionieren, Subshells könnten etwas sauberer sein: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Alec

48

Laut der Antwort von @ Scott ist das Skript install | postinstall der einfachste Weg, solange die Namen der Unterverzeichnisse bekannt sind. So führe ich es für mehrere Unterverzeichnisse aus. Zum Beispiel so tun , als wir haben api/, web/und shared/Teilprojekte in einem monorepo root:

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

1
Perfekte Lösung. Danke fürs Teilen :-)
Rahul Soni

1
Danke für die Antwort. Ich arbeite für mich.
AMIC MING

4
Gute Verwendung ( ), um Unterschalen zu erstellen und zu vermeiden cd api && npm install && cd ...
Cameron Hudson

4
Das sollte die ausgewählte Antwort sein!
tmos

2
Ich erhalte diesen Fehler, wenn ich npm installauf der obersten Ebene "(cd was unexpected at this time."
laufe

22

Meine Lösung ist sehr ähnlich. Pure Node.js

Das folgende Skript untersucht alle Unterordner (rekursiv), solange sie vorhanden sind, package.jsonund wird npm installin jedem von ihnen ausgeführt. Man kann Ausnahmen hinzufügen: Ordner dürfen nicht haben package.json. Im folgenden Beispiel ist ein solcher Ordner "Pakete". Man kann es als "Preinstall" -Skript ausführen.

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
Ihr Skript ist nett. Für meine persönlichen Zwecke ziehe ich es jedoch vor, die erste 'if-Bedingung' zu entfernen, um eine tief verschachtelte 'npm-Installation' zu erhalten!
Guilherme Caraciolo

21

Nur als Referenz für den Fall, dass Menschen auf diese Frage stoßen. Du kannst jetzt:

  • Fügen Sie einem Unterordner eine package.json hinzu
  • Installieren Sie diesen Unterordner als Referenzlink im Hauptpaket.json:

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


2
Beachten Sie, dass Abhängigkeiten im Stammordner installiert sind. Ich vermute, wenn Sie dieses Muster überhaupt in Betracht ziehen, möchten Sie die Abhängigkeiten des Unterverzeichnisses package.json im Unterverzeichnis.
Cody Allan Taylor

Was meinst du? Die Abhängigkeiten für das Unterordner-Paket befinden sich in der package.json im Unterordner.
Jelmer Jellema

(mit npm v6.6.0 & node v8.15.0) - Richten Sie ein Beispiel für sich ein. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Warten Sie ... Sie haben Abhängigkeiten nur manuell in "b" installiert. Dies passiert nicht, wenn Sie ein neues Projekt klonen. rm -rf node_modules ; cd .. ; npm install --save ./b. Listen Sie nun node_modules und dann b auf.
Cody Allan Taylor

1
Ah du meinst die Module. Ja, die Knotenmodule für b werden in a / Knotenmodule installiert. Dies ist sinnvoll, da Sie die Module als Teil des Hauptcodes benötigen / einschließen und nicht als "echtes" Knotenmodul. Ein "require ('throug2')" würde also2 in einem / node_modules durchsuchen.
Jelmer Jellema

Ich versuche, Code zu generieren und möchte ein Unterordner-Paket, das vollständig für die Ausführung vorbereitet ist, einschließlich seiner eigenen node_modules. Wenn ich die Lösung finde, werde ich auf jeden Fall aktualisieren!
Ohsully

19

Anwendungsfall 1 : Wenn Sie npm-Befehle in jedem Unterverzeichnis (wo sich jede package.json befindet) ausführen können, müssen Sie verwenden postinstall.

Wie ich es npm-run-allsowieso oft benutze , benutze ich es, um es schön kurz zu halten (der Teil in der Nachinstallation):

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

Dies hat den zusätzlichen Vorteil, dass ich alle auf einmal oder einzeln installieren kann. Wenn Sie dies nicht benötigen oder nicht npm-run-allals Abhängigkeit benötigen , lesen Sie die Antwort von demisx (unter Verwendung von Subshells in der Nachinstallation).

Anwendungsfall 2 : Wenn Sie alle npm-Befehle aus dem Stammverzeichnis ausführen (und beispielsweise keine npm-Skripte in Unterverzeichnissen verwenden), können Sie einfach jedes Unterverzeichnis wie jede andere Installation installieren:

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

Im letzteren Fall wundern Sie sich nicht, dass Sie keine node_modulesoder keine package-lock.jsonDatei in den Unterverzeichnissen finden - alle Pakete werden im Stammverzeichnis installiertnode_modules , weshalb Sie Ihre npm-Befehle nicht ausführen können Abhängigkeiten erfordern) aus einem Ihrer Unterverzeichnisse.

Wenn Sie sich nicht sicher sind, funktioniert Anwendungsfall 1 immer.


Es ist schön, dass jedes Submodul ein eigenes Installationsskript hat und diese dann alle nach der Installation ausführt. run-pist nicht notwendig, aber es ist dann ausführlicher"postinstall": "npm run install:a && npm run install:b"
Qwerty

Ja, Sie können &&ohne verwenden run-p. Aber wie Sie sagen, das ist weniger lesbar. Ein weiterer Nachteil (der durch run-p behoben wird, weil Installationen parallel ausgeführt werden) ist, dass bei einem
Don Vaughn,

3

Hinzufügen von Windows-Unterstützung zu Snozzas Antwort sowie Überspringen des node_modulesOrdners, falls vorhanden.

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

Sie können sicher. Ich habe meine Lösung aktualisiert, um den Ordner node_modules zu überspringen.
Ghostrydr

2

Inspiriert von den hier bereitgestellten Skripten habe ich ein konfigurierbares Beispiel erstellt, das:

  • kann eingerichtet werden, um zu verwenden yarnodernpm
  • kann eingerichtet werden, um den zu verwendenden Befehl basierend auf Sperrdateien zu bestimmen, sodass er für dieses Verzeichnis verwendet wird , wenn Sie ihn für die Verwendung festlegen, yarnaber nur ein Verzeichnis hat (standardmäßig true).package-lock.jsonnpm
  • Protokollierung konfigurieren
  • führt Installationen parallel mit aus cp.spawn
  • kann Trockenläufe machen, damit Sie sehen, was es zuerst tun würde
  • kann als Funktion oder automatisch mit env vars ausgeführt werden
    • Wenn Sie als Funktion ausgeführt werden, stellen Sie optional ein Array von Verzeichnissen zur Überprüfung bereit
  • gibt ein Versprechen zurück, das nach Abschluss aufgelöst wird
  • Ermöglicht die Einstellung der maximalen Tiefe bei Bedarf
  • kann aufhören zu rekursieren, wenn es einen Ordner mit findet yarn workspaces (konfigurierbar) gefunden wird.
  • Ermöglicht das Überspringen von Verzeichnissen mithilfe eines durch Kommas getrennten env var oder durch Übergeben der Konfiguration eines Arrays von Zeichenfolgen, mit denen abgeglichen werden soll, oder einer Funktion, die den Dateinamen, den Dateipfad und das fs.Dirent-Objekt empfängt und ein boolesches Ergebnis erwartet.
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;

Und damit wird es verwendet:

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

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

1

Wenn Sie ein findDienstprogramm auf Ihrem System haben, können Sie versuchen, den folgenden Befehl in Ihrem Anwendungsstammverzeichnis auszuführen:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Suchen Sie im Grunde alle package.jsonDateien und führen Sie sie npm installin diesem Verzeichnis aus, wobei Sie alle node_modulesVerzeichnisse überspringen .


1
Gute Antwort. Nur eine Anmerkung, dass Sie auch zusätzliche Pfade weglassen können mit:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.