为嵌套文件夹运行 npm install 的最佳方式?

Posted

技术标签:

【中文标题】为嵌套文件夹运行 npm install 的最佳方式?【英文标题】:The best way to run npm install for nested folders? 【发布时间】:2015-10-24 17:58:12 【问题描述】:

在嵌套子文件夹中安装npm packages 最正确的方法是什么?

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

npm installmy-app 中运行时,自动安装/my-sub-module 中的packages 的最佳方法是什么?

【问题讨论】:

我认为最惯用的做法是在项目的末尾有一个 package.json 文件。 一个想法是使用运行 bash 文件的 npm 脚本。 不能通过修改本地路径的工作方式来完成吗?:***.com/questions/14381898/… 【参考方案1】:

如果您知道嵌套子目录的名称,我更喜欢使用安装后。在package.json:

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

【讨论】:

多个文件夹呢? "cd nested_dir && npm install && cd.. & cd nested_dir2 && npm install" ?? @Emre 是的 - 就是这样。 @Scott 你不能把下一个文件夹放在 package.json 里面,就像每个文件夹的 "postinstall": "cd nested_dir2 && npm install" 一样? @Aron 如果您想要名称父目录中的两个子目录怎么办? @Emre 应该可以,子shell 可能会稍微干净一些:"(cd nested_dir && npm install); (cd nested_dir2 && npm install); ..."【参考方案2】:

根据@Scott 的回答,只要知道子目录名称, install|postinstall 脚本就是最简单的方法。这就是我为多个子目录运行它的方式。例如,假设我们在 monorepo 根目录中有 api/web/shared/ 子项目:

// In monorepo root package.json

...
 "scripts": 
    "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
  ,

在 Windows 上,将括号之间的 ; 替换为 &&

// In monorepo root package.json

...
 "scripts": 
    "postinstall": "(cd api && npm install) && (cd web && npm install) && (cd shared && npm install)"
  ,

【讨论】:

这些 npm 安装是一个接一个还是同时发生? 善用( ) 来创建子shell 并避免cd api && npm install && cd .. 在顶层运行npm install 时出现此错误:"(cd was unexpected at this time." 在 Windows 上,将括号之间的 ; 替换为 && 我的目录相对于它们的父目录嵌套在相同的深度,所以在我的情况下,我必须这样做:"(cd api && npm install); (cd ../web && npm install);... 包括 ../ 以确保它改变目录到右边。正如@CameronHudson 所说,我认为括号会在相同的工作目录上下文中独立运行命令,但由于某种原因,它对我不起作用......【参考方案3】:

用例 1:如果您希望能够从每个子目录(每个 package.json 所在的位置)中运行 npm 命令,则需要使用 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 的答案(在安装后使用子shell)。

用例 2:如果您将从根目录运行所有 npm 命令(例如,不会在子目录中使用 npm 脚本),您可以简单地安装每个子目录,例如你会有任何依赖:

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

在后一种情况下,您在子目录中找不到任何node_modulespackage-lock.json 文件不要感到惊讶——所有软件包都将安装在根目录node_modules 中,这就是为什么您将无法从您的任何子目录运行您的 npm 命令(需要依赖项)。

如果您不确定,用例 1 始终有效。

【讨论】:

最好让每个子模块都有自己的安装脚本,然后在安装后执行它们。 run-p 不是必需的,但它更冗长 "postinstall": "npm run install:a && npm run install:b" 是的,您可以使用&& 而不使用run-p。但正如你所说,这不太可读。另一个缺点(run-p 解决了因为安装并行运行)是如果一个失败,其他脚本不会受到影响 仅供参考,如果您不小心将cd 放入了不包含package.json 的错误路径,它将永远循环安装脚本。因此我使用"install:mymodule": "cd src/mymodule && test -f package.json && npm install" @DonVaughn 这就是重点:test -f package.json 将使npm installerror tmp@1.0.0 install:module: cd module && test -f package.json && npm install npm ERR! Exit status 1 退出。如果没有该检查,它将永远循环,而没有任何线索让您找出错误所在。自己试试吧。 @DonVaughn 我想您知道要纠正错误,您需要知道错误在哪里?或者至少有一个错误。这就是我调整的全部目的。没有它,'npm install' 不会向你抛出任何错误。这是我在这个帖子中的最后一条评论,抱歉。【参考方案4】:

如果你想运行一个命令来在嵌套的子文件夹中安装 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 中工作

【讨论】:

虽然复杂,但感谢文章链接。 虽然基于“组件”的结构是设置节点应用程序的一种非常方便的方法,但在应用程序的早期阶段分解单独的 package.json 文件等可能有点矫枉过正。这个想法倾向于当应用程序增长并且您合法地想要单独的模块/服务时,就会实现。但是,是的,如果没有必要,肯定太复杂了。 虽然可以使用 bash 脚本,但我更喜欢使用 nodejs 的方式来实现它在具有 DOS shell 的 Windows 和具有 Unix shell 的 Linux/Mac 之间的最大可移植性。【参考方案5】:

仅供参考,以防人们遇到这个问题。您现在可以:

将 package.json 添加到子文件夹中 将此子文件夹作为参考链接安装在主 package.json 中:

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

【讨论】:

请注意,依赖项安装在根文件夹中。我怀疑如果你甚至在考虑这种模式,你想要子目录中子目录 package.json 的依赖关系。 什么意思?子文件夹包的依赖项在子文件夹的 package.json 中。 (使用 npm v6.6.0 和节点 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。 啊,你的意思是模块。是的,b 的 node_modules 将安装在 a/node_modules 中。这是有道理的,因为您将需要/包含模块作为主代码的一部分,而不是作为“真正的”节点模块。所以“require('throug2')”会在 a/node_modules 中搜索 through2。 我正在尝试进行代码生成并想要一个完全准备好运行的子文件夹包,包括它自己的 node_modules。如果我找到解决方案,我会确保更新!【参考方案6】:

我的解决方案非常相似。 纯 Node.js

以下脚本检查所有子文件夹(递归),只要它们有package.json 并在每个子文件夹中运行npm install。 可以为其添加例外:允许没有package.json 的文件夹。在下面的示例中,一个这样的文件夹是“packages”。 可以将其作为“预安装”脚本运行。

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

【讨论】:

你的脚本很好。但是,出于我个人的目的,我更喜欢删除第一个“if 条件”以获得深层嵌套的“npm install”!【参考方案7】:

如果您的系统上有find 实用程序,您可以尝试在应用程序根目录中运行以下命令:find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

基本上,找到所有package.json 文件并在该目录中运行npm install,跳过所有node_modules 目录。

【讨论】:

很好的答案。请注意,您还可以省略其他路径:find . ! -path "*/node_modules/*" ! -path "*/additional_path/*" -name "package.json" -execdir npm install \;【参考方案8】:

接受的答案有效,但您可以使用 --prefix 在选定位置运行 npm 命令。

"postinstall": "npm --prefix ./nested_dir install"

--prefix 适用于任何 npm 命令,而不仅仅是 install

你也可以用

查看当前前缀
npm prefix

并将您的全局安装 (-g) 文件夹设置为

npm config set prefix "folder_path"

也许是 TMI,但你明白了……

【讨论】:

【参考方案9】:

EDIT 正如 fgblomqvist 在 cmets 中提到的,npm 现在也支持workspaces。


有些答案已经很老了。我认为现在我们有一些新选项可用于设置monorepos。

    我建议使用yarn workspaces:

工作区是一种设置包架构的新方法,从 Yarn 1.0 开始默认可用。它允许您设置多个包,只需运行一次yarn install 即可一次安装所有包。

    如果您喜欢或必须留在 npm,我建议您查看lerna:

Lerna 是一种工具,可优化使用 git 和 npm 管理多包存储库的工作流程。

lerna 也适用于 yarn 工作区 - article。我刚刚建立了一个 monorepo 项目 - example。

这是一个配置为使用 npm + lerna - MDC Web 的多包项目的示例:它们使用 package.json 的 postinstall 运行 lerna bootstrap

【讨论】:

请不要使用 lerna,NPM 现在也原生支持“工作区”:docs.npmjs.com/cli/v7/using-npm/workspaces【参考方案10】:

将 Windows 支持添加到 snozza's 答案,以及跳过 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' )
)

【讨论】:

【参考方案11】:

受此处提供的脚本的启发,我构建了一个可配置的示例:

可以设置为使用yarnnpm 可以设置为根据锁定文件确定要使用的命令,这样如果您将其设置为使用yarn,但一个目录只有package-lock.json,它将对该目录使用npm(默认为true) . 配置日志记录 使用cp.spawn 并行运行安装 可以进行试运行,让您先看看它会做什么 可以作为函数运行或使用环境变量自动运行 作为函数运行时,可选择提供要检查的目录数组 返回一个完成后解决的承诺 如果需要,允许设置查看的最大深度 知道在找到带有yarn workspaces 的文件夹时停止递归(可配置) 允许使用逗号分隔的 env var 或通过向配置传递要匹配的字符串数组或接收文件名、文件路径和 fs.Dirent obj 并期望布尔结果的函数来跳过目录。李>
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 )

【讨论】:

【参考方案12】:
find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c "cd '' && npm install" \;

【讨论】:

【参考方案13】:

要在每个子目录上运行 npm install,您可以执行以下操作:

"scripts": 
  ...
  "install:all": "for D in */; do npm install --cwd \"$D\"; done"

在哪里

install:all 只是脚本的名字,你可以随意命名

D是当前迭代的目录名

*/ 指定要查找子目录的位置。 directory/*/ 将列出directory/ 中的所有目录,directory/*/*/ 将列出所有目录中的两个级别。

npm install -cwd 在给定文件夹中安装所有依赖项

您还可以运行多个命令,例如:

for D in */; do echo \"Installing stuff on $D\" && npm install --cwd \"$D\"; done

将在每次迭代时打印“Installing stuff on your_subfolder/”。

这也适用于yarn

【讨论】:

【参考方案14】:

[对于 macOS、Linux 用户]:

我创建了一个 bash 文件来安装项目和嵌套文件夹中的所有依赖项。

find . -name node_modules -prune -o -name package.json -execdir npm install \;

说明:在根目录中,排除node_modules文件夹(即使在嵌套文件夹中),找到有package.json文件的目录然后运行npm install命令。

如果您只想在指定文件夹(例如:abc123、def456 文件夹)上查找,请按以下方式运行:

find ./abc123/* ./def456/* -name node_modules -prune -o -name package.json -execdir npm install \;

【讨论】:

【参考方案15】:

任何可以获取目录列表并运行 shell 命令的语言都可以为您执行此操作。

我知道这不是 OP 想要的确切答案,但它始终有效。您需要创建一个子目录名称数组,然后遍历它们并运行npm i,或者您需要运行的任何命令。

作为参考,我尝试了npm i **/,它只是安装了父目录中所有子目录中的模块。这非常不直观,但不用说这不是您需要的解决方案。

【讨论】:

以上是关于为嵌套文件夹运行 npm install 的最佳方式?的主要内容,如果未能解决你的问题,请参考以下文章

npm 错误! cb() 从未调用过!尝试运行 npm install 命令时出错

markdown 在包含package.json文件的每个文件夹中运行npm install

ENOENT:运行 npm install 命令时没有这样的文件或目录

在 src 文件夹上运行 npm install 后,它显示没有找到存储库 [重复]

当我运行`npm install`时,它返回`ERR!代码 EINTEGRITY` (npm 5.3.0)

npm install 不起作用,因为某些包不包含 package.json 文件