node.js 为嵌套文件夹运行 npm install 的最佳方法?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/31773546/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 19:12:00  来源:igfitidea点击:

The best way to run npm install for nested folders?

node.jsnpm

提问by WHITECOLOR

What is the most correct way to install npm packagesin nested sub folders?

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

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

What is the best way to have packagesin /my-sub-modulebe installed automatically when npm installrun in my-app?

什么是有最好的方式packages/my-sub-module,当被自动安装npm install在运行my-app

采纳答案by snozza

If you want to run a single command to install npm packages in nested subfolders, you can run a script via npmand main package.jsonin your root directory. The script will visit every subdirectory and run npm install.

如果你想运行一个命令来在嵌套的子文件夹中安装 npm 包,你可以在你的根目录中通过npm和 main运行一个脚本package.json。该脚本将访问每个子目录并运行npm install.

Below is a .jsscript that will achieve the desired result:

下面是一个.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' })
})

Note that this is an example taken from a StrongLooparticle that specifically addresses a modular node.jsproject structure (including nested components and package.jsonfiles).

请注意,这是一个来自StrongLoop文章的示例,该文章专门针对模块化node.js项目结构(包括嵌套组件和package.json文件)。

As suggested, you could also achieve the same thing with a bash script.

按照建议,您也可以使用 bash 脚本实现相同的目的。

EDIT: Made the code work in Windows

编辑:使代码在 Windows 中工作

回答by Scott

I prefer using post-install, if you know the names of the nested subdir. In package.json:

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

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

回答by demisx

Per @Scott's answer, the install|postinstall script is the simplest way as long as sub-directory names are known. This is how I run it for multiple sub dirs. For example, pretend we have api/, web/and shared/sub-projects in a monorepo root:

根据@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)"
  },
}

回答by catamphetamine

My solution is very similar. Pure Node.js

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

The following script examines all subfolders (recursively) as long as they have package.jsonand runs npm installin each of them. One can add exceptions to it: folders allowed not having package.json. In the example below one such folder is "packages". One can run it as a "preinstall" script.

以下脚本检查所有子文件夹(递归),只要它们在每个子文件夹中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))
}

回答by Jelmer Jellema

Just for reference in case people come across this question. You can now:

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

  • Add a package.json to a subfolder
  • Install this subfolder as reference-link in the main package.json:
  • 将 package.json 添加到子文件夹
  • 将此子文件夹作为参考链接安装在主 package.json 中:

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

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

回答by DonVaughn

Use Case 1: If you want be able to run npm commands from within each subdirectory (where each package.json is), you will need to use postinstall.

用例 1:如果您希望能够从每个子目录(每个 package.json 所在的位置)中运行 npm 命令,您将需要使用postinstall.

As I often use npm-run-allanyway, I use it to keep it nice and short (the part in the 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:*"
}

This has the added benefit that I can install all at once, or individually. If you don't need this or don't want npm-run-allas a dependency, check out demisx's answer (using subshells in postinstall).

这有一个额外的好处,我可以一次安装,也可以单独安装。如果您不需要这个或不想npm-run-all作为依赖项,请查看 demisx 的答案(在安装后使用 subshel​​l)。

Use Case 2: If you will be running all npm commands from the root directory (and, for example, won't be using npm scripts in subdirectories), then simply install each subdirectory like you would any dependecy:

用例 2:如果您将从根目录运行所有 npm 命令(并且,例如,不会在子目录中使用 npm 脚本),那么只需像安装任何依赖项一样安装每个子目录:

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

In the latter case, don't be surprised that you don't find any node_modulesor package-lock.jsonfile in the sub-directories - all packages will be installed in the root node_modules, which is why you won't be able to run your npm commands (that require dependencies) from within a subdirectory.

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

If you're not sure, use case 1 always works.

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

回答by Ghostrydr

Adding Windows support to snozza'sanswer, as well as skipping of node_modulesfolder if present.

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

回答by Braden Rockwell Napier

Inspired by the scripts provided here, I built a configurable example which:

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

  • can be setup to use yarnor npm
  • can be setup to determine the command to use based on lock files so that if you set it to use yarnbut a directory only has a package-lock.jsonit will use npmfor that directory (defaults to true).
  • configure logging
  • runs installations in parallel using cp.spawn
  • can do dry runs to let you see what it would do first
  • can be run as a function or auto run using env vars
    • when run as a function, optionally provide array of directories to check
  • returns a promise that resolves when completed
  • allows setting max depth to look if needed
  • knows to stop recursing if it finds a folder with yarn workspaces(configurable)
  • allows skipping directories using a comma separated env var or by passing the config an array of strings to match against or a function which receives the file name, file path, and the fs.Dirent obj and expects a boolean result.
  • 可以设置为使用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;



And with it being used:

并使用它:

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

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