Лучший способ запустить npm install для вложенных папок?


129

Каков наиболее правильный способ установки npm packagesво вложенные подпапки?

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

Что такое лучший способ быть packagesв /my-sub-moduleустанавливается автоматически при npm installзапуске в my-app?


Я думаю, что самая идиоматичная вещь - иметь один файл package.json в начале вашего проекта.
Роберт Москаль

Одна из идей - использовать сценарий npm, который запускает файл bash.
Дэвин Трайон

Нельзя ли это сделать с помощью модификации того, как работают локальные пути ?: stackoverflow.com/questions/14381898/…
Evanss 09

Ответы:


26

Если вы хотите запустить одну команду для установки пакетов npm во вложенных подпапках, вы можете запустить сценарий через npmи main package.jsonв корневом каталоге. Скрипт посетит все подкаталоги и запустится npm install.

Ниже представлен .jsскрипт, который позволит добиться желаемого результата:

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

Обратите внимание, что это пример из статьи StrongLoop, в которой конкретно рассматривается модульная node.jsструктура проекта (включая вложенные компоненты и package.jsonфайлы).

Как было предложено, вы также можете добиться того же с помощью сценария bash.

РЕДАКТИРОВАТЬ: заставил код работать в Windows


1
Хотя, чтобы сложнее, спасибо за ссылку на статью.
WHITECOLOR

Хотя структура, основанная на «компонентах», является довольно удобным способом настройки приложения узла, на ранних этапах приложения, вероятно, будет излишним разбивать отдельные файлы package.json и т. Д. Идея имеет тенденцию реализовываться, когда приложение растет и вам законно нужны отдельные модули / службы. Но да, определенно слишком сложно, если не нужно.
snozza

3
Хотя да, подойдет сценарий bash, но я предпочитаю делать это с помощью nodejs для максимальной переносимости между Windows с оболочкой DOS и Linux / Mac с оболочкой Unix.
truthadjustr 03

270

Я предпочитаю использовать пост-установку, если вы знаете имена вложенных подкаталогов. В package.json:

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

10
как насчет нескольких папок? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre

1
@Emre да, вот и все.
Guy

2
@Scott, не могли бы вы просто поместить следующую папку во внутренний package.json, как "postinstall": "cd nested_dir2 && npm install"для каждой папки?
Арон

1
@Aron Что делать, если вам нужны два подкаталога в родительском каталоге имени?
Alec

29
@Emre Это должно сработать, подоболочки могут быть немного чище: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Алек

49

Согласно ответу @Scott, сценарий install | postinstall - самый простой способ, если известны имена подкаталогов. Вот как я запускаю его для нескольких поддиректорий. Например, делать вид , что есть api/, web/и shared/суб-проекты в корне monorepo:

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

1
Идеальное решение. Спасибо, что поделились :-)
Рахул Сони

1
Спасибо за ответ. Работает на меня.
AMIC MING

5
Хорошее использование ( )для создания подоболочек и избегания cd api && npm install && cd ...
Кэмерон Хадсон,

4
Это должен быть выбранный ответ!
tmos

3
Я получаю эту ошибку при запуске npm installна верхнем уровне:"(cd was unexpected at this time."
Mr. Polywhirl

22

Мое решение очень похоже. Чистый Node.js

Следующий сценарий проверяет все подпапки (рекурсивно), пока они есть, package.jsonи запускается npm installв каждой из них. К нему можно добавить исключения: разрешенные папки не иметь package.json. В приведенном ниже примере одна из таких папок - «пакеты». Его можно запустить как «предустановочный» скрипт.

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
твой сценарий хорош. Однако для моих личных целей я предпочитаю удалить первое условие if, чтобы получить глубоко вложенную установку npm!
Guilherme

21

Просто для справки, если люди сталкиваются с этим вопросом. Ты можешь сейчас:

  • Добавьте package.json в подпапку
  • Установите эту подпапку как ссылку-ссылку в основном package.json:

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


2
Обратите внимание, что зависимости устанавливаются в корневую папку. Я подозреваю, что если вы даже рассматриваете этот шаблон, вам нужны зависимости подкаталога package.json в подкаталоге.
Коди Аллан Тейлор

Что вы имеете в виду? Зависимости для подпапки-пакета находятся в package.json в подпапке.
Jelmer Jellema

(с использованием npm v6.6.0 и node v8.15.0) - Создайте пример для себя. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;А теперь подождите ... вы просто вручную установили зависимости в "b", это не то, что происходит, когда вы клонируете новый проект. rm -rf node_modules ; cd .. ; npm install --save ./b, Теперь перечислите node_modules, затем укажите b.
Коди Аллан Тейлор

1
Вы имеете в виду модули. Да, node_modules для b будет установлен в a / node_modules. Это имеет смысл, потому что вам потребуется / включить модули как часть основного кода, а не как «настоящий» модуль узла. Таким образом, "require ('throug2')" будет искать через 2 в / node_modules.
Jelmer Jellema

Я пытаюсь выполнить генерацию кода и хочу, чтобы вложенный пакет был полностью готов к запуску, включая его собственные node_modules. Если найду решение, обязательно обновлюсь!
ohsully

20

Пример использования 1. Если вы хотите иметь возможность запускать команды npm из каждого подкаталога (где находится каждый package.json), вам нужно будет использовать postinstall.

Как я и так часто использую npm-run-all, я использую его, чтобы он был красивым и коротким (часть после установки):

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

Это дает дополнительное преимущество: я могу установить все сразу или по отдельности. Если вам это не нужно или вы не хотите использовать его npm-run-allкак зависимость, посмотрите ответ demisx (используя подоболочки в postinstall).

Вариант использования 2 : если вы будете запускать все команды npm из корневого каталога (и, например, не будете использовать сценарии npm в подкаталогах), вы можете просто установить каждый подкаталог, как и любую зависимость:

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

В последнем случае не удивляйтесь, что вы не найдете ни одного файла node_modulesили package-lock.jsonв подкаталогах - все пакеты будут установлены в корень node_modules, поэтому вы не сможете запускать свои команды npm (которые require dependencies) из любого из ваших подкаталогов.

Если вы не уверены, вариант использования 1 всегда работает.


Приятно иметь у каждого подмодуля свой собственный сценарий установки, а затем выполнять их все после установки. run-pне обязательно, но тогда он более подробный"postinstall": "npm run install:a && npm run install:b"
Qwerty

Да, можно использовать и &&без run-p. Но, как вы говорите, это менее читабельно. Другой недостаток (который решает run-p, потому что установки выполняются параллельно) заключается в том, что при сбое одного скрипта не пострадает ни один другой скрипт
Дон Вон,

3

Добавление поддержки Windows в ответ snozza , а также пропуск node_modulesпапки, если она есть.

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

Вы конечно можете. Я обновил свое решение, чтобы пропустить папку node_modules.
Ghostrydr,

2

Вдохновленный приведенными здесь сценариями, я создал настраиваемый пример, который:

  • можно настроить для использования yarnилиnpm
  • может быть настроен для определения команды для использования на основе файлов блокировки, так что если вы установите ее для использования, yarnно каталог package-lock.jsonбудет использовать только npmдля этого каталога (по умолчанию true).
  • настроить ведение журнала
  • запускает установки параллельно, используя cp.spawn
  • может сделать пробный прогон, чтобы вы сначала увидели, что он сделает
  • может запускаться как функция или автоматически с использованием env vars
    • при запуске в качестве функции необязательно предоставить массив каталогов для проверки
  • возвращает обещание, которое разрешается после завершения
  • позволяет установить максимальную глубину для просмотра при необходимости
  • знает, что нужно остановить рекурсию, если найдет папку с yarn workspaces(настраиваемым)
  • позволяет пропускать каталоги, используя разделенные запятыми env var или передавая config массив строк для сопоставления, или функцию, которая получает имя файла, путь к файлу и объект fs.Dirent и ожидает логический результат.
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;

И при этом используются:

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

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

1

Если у вас есть findутилита в вашей системе, вы можете попробовать запустить следующую команду в корневом каталоге вашего приложения:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

По сути, найдите все package.jsonфайлы и запустите npm installв этом каталоге, пропуская все node_modulesкаталоги.


1
Отличный ответ. Обратите внимание, что вы также можете опустить дополнительные пути:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Эван Моран,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.