A melhor maneira de executar npm install para pastas aninhadas?


128

Qual é a maneira mais correta de instalar npm packagesem subpastas aninhadas?

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

Qual é a melhor maneira de fazer packagescom que /my-sub-moduleseja instalado automaticamente ao ser npm installexecutado my-app?


Acho que a coisa mais idiomática é ter um único arquivo package.json no final do seu projeto.
Robert Moskal

Uma ideia seria usar um script npm que executa um arquivo bash.
Davin Tryon

Isso não poderia ser feito com uma modificação em como os caminhos locais funcionam ?: stackoverflow.com/questions/14381898/…
Evanss

Respostas:


26

Se você deseja executar um único comando para instalar pacotes npm em subpastas aninhadas, você pode executar um script via npme principal package.jsonem seu diretório raiz. O script visitará cada subdiretório e será executado npm install.

Abaixo está um .jsscript que alcançará o resultado desejado:

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

Observe que este é um exemplo tirado de um artigo do StrongLoop que aborda especificamente uma estrutura de node.jsprojeto modular (incluindo componentes e package.jsonarquivos aninhados ).

Conforme sugerido, você também pode conseguir o mesmo com um script bash.

EDIT: Fez o código funcionar no Windows


1
Para complicar ainda mais, obrigado pelo link do artigo.
WHITECOLOR

Embora a estrutura baseada em 'componente' seja uma maneira bastante útil de configurar um aplicativo de nó, provavelmente é um exagero nos estágios iniciais do aplicativo quebrar arquivos package.json separados etc. A ideia tende a se concretizar quando o aplicativo cresce e você legitimamente deseja módulos / serviços separados. Mas sim, definitivamente muito complicado se não for necessário.
snozza

3
Embora sim um script bash servirá, mas eu prefiro a maneira nodejs de fazê-lo para máxima portabilidade entre o Windows que tem um shell DOS e Linux / Mac que tem o shell Unix.
truthadjustr

270

Eu prefiro usar pós-instalação, se você souber os nomes do subdiretório aninhado. Em package.json:

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

10
que tal várias pastas? "cd nested_dir && npm install && cd .. & cd nested_dir2 && npm install" ??
Emre

1
@Emre sim - é isso.
Guy

2
@Scott você não pode simplesmente colocar a próxima pasta dentro do package.json como "postinstall": "cd nested_dir2 && npm install"para cada pasta?
Aron

1
@Aron E se você quiser dois subdiretórios dentro do diretório pai do nome?
Alec

29
@Emre Isso deve funcionar, os subshells podem ser ligeiramente mais limpos: "(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."
Alec

49

De acordo com a resposta de @ Scott, o script install | postinstall é a maneira mais simples, desde que os nomes dos subdiretórios sejam conhecidos. É assim que eu o executo para vários sub dirs. Por exemplo, fingir que temos api/, web/e shared/subprojetos em uma raiz monorepo:

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

1
Solução perfeita. Obrigado por compartilhar :-)
Rahul Soni

1
Obrigado pela resposta. Trabalhando para mim.
AMIC MING

5
Bom uso de ( )para criar subshells e evitar cd api && npm install && cd ...
Cameron Hudson

4
Essa deve ser a resposta selecionada!
tmos

3
Recebo este erro ao executar npm installno nível superior:"(cd was unexpected at this time."
Sr. Polywhirl

22

Minha solução é muito semelhante. Pure Node.js

O script a seguir examina todas as subpastas (recursivamente), desde que existam package.jsone sejam executadas npm installem cada uma delas. Pode-se adicionar exceções a ele: pastas permitidas não tendo package.json. No exemplo abaixo, uma dessas pastas é "pacotes". Pode-se executá-lo como um script de "pré-instalação".

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
seu script é bom. No entanto, para meus propósitos pessoais, prefiro remover a primeira 'condição if' para obter uma 'instalação npm' profundamente aninhada!
Guilherme Caraciolo

21

Apenas para referência, caso as pessoas se deparem com esta pergunta. Agora você pode:

  • Adicione um package.json a uma subpasta
  • Instale esta subpasta como link de referência no package.json principal:

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


2
Observe que as dependências são instaladas na pasta raiz. Suspeito que, se você está considerando esse padrão, deseja as dependências do subdiretório package.json no subdiretório.
Cody Allan Taylor

O que você quer dizer? As dependências da subpasta-pacote estão em package.json na subpasta.
Jelmer Jellema

(usando npm v6.6.0 e node v8.15.0) - Configure um exemplo para você. mkdir -p a/b ; cd a ; npm init ; cd b ; npm init ; npm install --save through2 ;Agora espere ... você acabou de instalar manualmente as dependências em "b", não é isso que acontece quando você clona um novo projeto. rm -rf node_modules ; cd .. ; npm install --save ./b. Agora liste node_modules e, em seguida, liste b.
Cody Allan Taylor

1
Ah, você quer dizer os módulos. Sim, o node_modules para b será instalado em a / node_modules. O que faz sentido, porque você exigirá / incluirá os módulos como parte do código principal, não como um módulo de nó "real". Portanto, um "require ('throug2')" pesquisaria através de 2 em um / node_modules.
Jelmer Jellema

Estou tentando fazer a geração de código e quero um pacote de subpasta totalmente preparado para ser executado, incluindo seus próprios node_modules. Se eu encontrar a solução, farei a atualização!
ohsully

19

Caso de uso 1 : se você quiser executar comandos npm de dentro de cada subdiretório (onde cada package.json está), você precisará usar postinstall.

Como costumo usar de npm-run-allqualquer maneira, eu o uso para mantê-lo bonito e curto (a parte na pós-instalação):

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

Isso tem a vantagem de poder instalar tudo de uma vez ou individualmente. Se você não precisa disso ou não quer npm-run-allcomo uma dependência, verifique a resposta do demisx (usando subshells no postinstall).

Caso de uso 2 : se você for executar todos os comandos npm do diretório raiz (e, por exemplo, não usar scripts npm em subdiretórios), poderá simplesmente instalar cada subdiretório como faria com qualquer dependência:

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

No último caso, não se surpreenda se não encontrar nenhum arquivo node_modulesou package-lock.jsonnos subdiretórios - todos os pacotes serão instalados na raiz node_modules, por isso você não será capaz de executar seus comandos npm (que requerem dependências) de qualquer um de seus subdiretórios.

Se você não tiver certeza, use o caso 1 sempre funciona.


É bom ter cada submódulo com seu próprio script de instalação e executá-los todos na pós-instalação. run-pnão é necessário, mas é mais detalhado"postinstall": "npm run install:a && npm run install:b"
Qwerty

Sim, você pode usar &&sem run-p. Mas, como você disse, isso é menos legível. Outra desvantagem (que o run-p resolve porque as instalações são executadas em paralelo) é que, se um falhar, nenhum outro script será afetado
Don Vaughn

3

Adicionando suporte do Windows à resposta do snozza , bem como pular a node_modulespasta, se houver.

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

Com certeza você pode. Atualizei minha solução para ignorar a pasta node_modules.
Ghostrydr

2

Inspirado pelos scripts fornecidos aqui, construí um exemplo configurável que:

  • pode ser configurado para usar yarnounpm
  • pode ser configurado para determinar o comando a ser usado com base nos arquivos de bloqueio, de modo que se você configurá-lo para uso, yarnmas um diretório tiver apenas um, package-lock.jsonele será usado npmpara esse diretório (o padrão é verdadeiro).
  • configurar o registro
  • executa instalações em paralelo usando cp.spawn
  • pode fazer testes para permitir que você veja o que faria primeiro
  • pode ser executado como uma função ou execução automática usando env vars
    • quando executado como uma função, opcionalmente fornece uma variedade de diretórios para verificar
  • retorna uma promessa que resolve quando concluída
  • permite definir a profundidade máxima para olhar, se necessário
  • sabe como parar de repetir se encontrar uma pasta com yarn workspaces(configurável)
  • permite pular diretórios usando um env var separado por vírgulas ou passando ao config um array de strings para corresponder ou uma função que recebe o nome do arquivo, o caminho do arquivo e o fs.Dirent obj e espera um resultado 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 com ele sendo usado:

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

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

1

Se você tiver um findutilitário no sistema, poderá tentar executar o seguinte comando no diretório raiz do aplicativo:
find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

Basicamente, encontre todos os package.jsonarquivos e execute npm installnaquele diretório, pulando todos os node_modulesdiretórios.


1
Ótima resposta. Apenas uma observação de que você também pode omitir caminhos adicionais com:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;
Evan Moran
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.