Najlepszy sposób na uruchomienie instalacji npm dla zagnieżdżonych folderów?

129

Jaki jest najbardziej poprawny sposób instalacji npm packagesw zagnieżdżonych podfolderach?

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

Jaki jest najlepszy sposób, aby mieć packagesw /my-sub-modulebyć instalowane automatycznie podczas npm installuruchamiania w my-app?

BIAŁY KOLOR
źródło
Myślę, że najbardziej idiomatyczną rzeczą jest posiadanie pojedynczego pliku package.json na końcu projektu.
Robert Moskal
Jednym z pomysłów byłoby użycie skryptu npm, który uruchamia plik bash.
Davin Tryon
Czy nie można tego zrobić, modyfikując sposób działania ścieżek lokalnych ?: stackoverflow.com/questions/14381898/…
Evanss

Odpowiedzi:

26

Jeśli chcesz uruchomić pojedyncze polecenie, aby zainstalować pakiety npm w zagnieżdżonych podfolderach, możesz uruchomić skrypt za pośrednictwem npmi main package.jsonw katalogu głównym. Skrypt odwiedzi każdy podkatalog i uruchomi się npm install.

Poniżej znajduje się .jsskrypt, który osiągnie zamierzony efekt:

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

Zauważ, że jest to przykład zaczerpnięty z artykułu StrongLoop, który dotyczy konkretnie modułowej node.jsstruktury projektu (w tym zagnieżdżonych komponentów i package.jsonplików).

Jak zasugerowano, możesz również osiągnąć to samo za pomocą skryptu bash.

EDYCJA: Kod działał w systemie Windows

snozza
źródło
1
Ale to skomplikowane, dzięki za link do artykułu.
WHITECOLOR
Chociaż struktura oparta na komponentach jest całkiem wygodnym sposobem na skonfigurowanie aplikacji węzła, prawdopodobnie na wczesnych etapach aplikacji jest przesadą, aby wyłamać oddzielne pliki package.json itp. Pomysł ma tendencję do realizacji, gdy aplikacja rośnie i słusznie chcesz mieć oddzielne moduły / usługi. Ale tak, zdecydowanie zbyt skomplikowane, jeśli nie jest to konieczne.
snozza
3
Chociaż tak, skrypt bash zrobi to, ale wolę robić to w sposób nodejs, aby uzyskać maksymalną przenośność między Windows, który ma powłokę DOS, a Linux / Mac, który ma powłokę Unix.
trueadjustr
270

Wolę używać po instalacji, jeśli znasz nazwy zagnieżdżonego podkatalogu. W package.json:

"scripts": {
  "postinstall": "cd nested_dir && npm install",
  ...
}
Scott
źródło
10
a co z wieloma folderami? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre
1
@Emre yes - to wszystko.
Guy
2
@Scott, czy nie możesz więc po prostu umieścić następnego folderu w wewnętrznym pliku package.json, jak "postinstall": "cd nested_dir2 && npm install"dla każdego folderu?
Aron
1
@Aron A co, jeśli chcesz mieć dwa podkatalogi w katalogu nadrzędnym nazwy?
Alec,
29
@Emre To powinno działać, podpowłoki mogą być nieco czystsze: „(cd nested_dir && npm install); (cd nested_dir2 && npm install); ...”
Alec
49

Zgodnie z odpowiedzią @ Scotta, skrypt install | postinstall jest najprostszym sposobem, o ile znane są nazwy podkatalogów. W ten sposób uruchamiam go dla wielu sub-reżerów. Na przykład, mamy udawać api/, web/a shared/podprojekty w korzeniu monorepo:

// In monorepo root package.json
{
...
 "scripts": {
    "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
  },
}
demisx
źródło
1
Idealne rozwiązanie. Dzięki za udostępnienie :-)
Rahul Soni
1
Dziękuję za odpowiedź. Pracuje dla mnie.
AMIC MING
5
Dobre wykorzystanie ( )do tworzenia podpowłok i unikania cd api && npm install && cd ...
Cameron Hudson
4
To powinna być wybrana odpowiedź!
tmos
3
Otrzymuję ten błąd podczas pracy npm installna najwyższym poziomie:"(cd was unexpected at this time."
Pan Polywhirl
22

Moje rozwiązanie jest bardzo podobne. Czysty Node.js

Poniższy skrypt sprawdza wszystkie podfoldery (rekurencyjnie), o ile mają package.jsoni działa npm installw każdym z nich. Można dodać do niego wyjątki: foldery dozwolone, których nie można mieć package.json. W poniższym przykładzie jednym z takich folderów jest „pakiety”. Można go uruchomić jako skrypt "preinstalacyjny".

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))
}
katamfetamina
źródło
3
twój scenariusz jest fajny. Jednak ze względów osobistych wolę usunąć pierwszy warunek „if”, aby uzyskać zagnieżdżoną „instalację npm”!
Guilherme Caraciolo
21

Tylko w celach informacyjnych na wypadek, gdyby ludzie zetknęli się z tym pytaniem. Możesz teraz:

  • Dodaj plik package.json do podfolderu
  • Zainstaluj ten podfolder jako łącze referencyjne w głównym pakiecie.json:

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

Jelmer Jellema
źródło
2
Zauważ, że zależności są instalowane w folderze głównym. Podejrzewam, że jeśli rozważasz nawet ten wzorzec, chcesz, aby zależności z podkatalogu package.json znajdowały się w podkatalogu.
Cody Allan Taylor
Co masz na myśli? Zależności dla pakietu podfolderu znajdują się w pliku package.json w podfolderze.
Jelmer Jellema
(używając npm v6.6.0 i node v8.15.0) - Skonfiguruj przykład dla siebie. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Teraz czekaj ... po prostu ręcznie zainstalowałeś zależności w "b", to nie jest to, co się dzieje, gdy klonujesz nowy projekt. rm -rf node_modules ; cd .. ; npm install --save ./b. Teraz lista node_modules, a następnie lista b.
Cody Allan Taylor
1
Ach, masz na myśli moduły. Tak, moduły node_modules dla b zostaną zainstalowane w a / node_modules. Ma to sens, ponieważ będziesz potrzebować / uwzględnić moduły jako część głównego kodu, a nie jako „prawdziwy” moduł węzłowy. Tak więc „require ('throug2')” przeszukałby 2 w / node_modules.
Jelmer Jellema
Próbuję generować kod i chcę, aby pakiet podfolderu był w pełni przygotowany do uruchomienia, w tym własne moduły node_modules. Jeśli znajdę rozwiązanie, zaktualizuję!
ohsully
20

Przypadek użycia 1 : Jeśli chcesz mieć możliwość uruchamiania poleceń npm z każdego podkatalogu (gdzie znajduje się każdy pakiet package.json), musisz użyćpostinstall .

Ponieważ i tak często używam npm-run-all, używam go, aby był ładny i krótki (część w postinstalacji):

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

Ma to dodatkową zaletę, że mogę zainstalować wszystko naraz lub pojedynczo. Jeśli tego nie potrzebujesz lub nie chcesznpm-run-all jako zależności, sprawdź odpowiedź demisx (używając podpowłok w postinstall).

Przypadek użycia 2 : Jeśli będziesz uruchamiać wszystkie polecenia npm z katalogu głównego (i na przykład nie będziesz używać skryptów npm w podkatalogach), możesz po prostu zainstalować każdy podkatalog tak, jak każdą zależną:

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

W tym drugim przypadku nie zdziw się, że nie znajdziesz żadnego pliku node_modulesani package-lock.jsonw podkatalogach - wszystkie pakiety zostaną zainstalowane w katalogu głównymnode_modules , dlatego nie będziesz mógł uruchamiać poleceń npm (czyli wymagają zależności) z dowolnego podkatalogu.

Jeśli nie masz pewności, przypadek użycia 1 zawsze działa.

Don Vaughn
źródło
Fajnie jest mieć każdy moduł podrzędny z własnym skryptem instalacyjnym, a następnie uruchamiać je wszystkie po instalacji. run-pnie jest konieczne, ale jest wtedy bardziej szczegółowe"postinstall": "npm run install:a && npm run install:b"
Qwerty
Tak, możesz używać &&bez run-p. Ale jak mówisz, jest to mniej czytelne. Inną wadą (to, że run-p rozwiązuje, ponieważ instalacje przebiegają równolegle) jest to, że jeśli jeden się nie powiedzie, żaden inny skrypt nie zostanie dotknięty
Don Vaughn
3

Dodanie obsługi systemu Windows do odpowiedzi snozzy , a także pomijanie node_modulesfolderu, jeśli jest obecny.

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
źródło
Pewnie, że możesz. Zaktualizowałem moje rozwiązanie, aby pominąć folder node_modules.
Ghostrydr
2

Zainspirowany dostarczonymi tutaj skryptami, zbudowałem konfigurowalny przykład, który:

  • można skonfigurować do używania yarnlubnpm
  • można skonfigurować, aby określić polecenie do użycia na podstawie plików blokujących, tak aby jeśli ustawisz go na używanie, yarnale katalog będzie miał tylko nazwę, package-lock.jsonktóra będzie używana npmdla tego katalogu (domyślnie true).
  • skonfigurować rejestrowanie
  • uruchamia instalacje równolegle za pomocą cp.spawn
  • może wykonywać przebiegi na sucho, abyś mógł zobaczyć, co zrobi najpierw
  • można uruchomić jako funkcję lub uruchomić automatycznie przy użyciu zmiennych env
    • gdy jest uruchamiany jako funkcja, opcjonalnie dostarcza tablicę katalogów do sprawdzenia
  • zwraca obietnicę, która zostanie rozpatrzona po wypełnieniu
  • umożliwia ustawienie maksymalnej głębokości, aby spojrzeć w razie potrzeby
  • wie, że ma przestać się powtarzać, jeśli znajdzie folder z yarn workspaces(konfigurowalne)
  • pozwala na pomijanie katalogów przy użyciu rozdzielanej przecinkami zmiennej env lub przez przekazanie config tablicy ciągów do dopasowania lub funkcji, która otrzymuje nazwę pliku, ścieżkę do pliku i obiekt fs.Dirent i oczekuje wyniku boolowskiego.
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;

A gdy jest używany:

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

installRecursively(process.cwd(), { dry: true })
Braden Rockwell Napier
źródło
1

Jeśli masz findnarzędzie w swoim systemie, możesz spróbować uruchomić następujące polecenie w katalogu głównym aplikacji:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Zasadniczo znajdź wszystkie package.jsonpliki i uruchom npm installw tym katalogu, pomijając wszystkie node_moduleskatalogi.

Moha, wszechmocny wielbłąd
źródło
1
Świetna odpowiedź. Tylko uwaga, że ​​możesz również pominąć dodatkowe ścieżki za pomocą:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran