【问题标题】:The best way to run npm install for nested folders?为嵌套文件夹运行 npm install 的最佳方式?
【发布时间】: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 脚本。
  • 不能通过修改本地路径的工作方式来完成吗?:stackoverflow.com/questions/14381898/…

标签: node.js npm


【解决方案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

    1. 我建议使用yarn workspaces:

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

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

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

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

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

    【讨论】:

    【解决方案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 **/,它只是安装了父目录中所有子目录中的模块。这非常不直观,但不用说这不是您需要的解决方案。

              【讨论】:

                猜你喜欢
                • 2019-08-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2018-11-01
                • 2019-02-07
                • 1970-01-01
                • 2022-07-20
                • 1970-01-01
                相关资源
                最近更新 更多