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


128

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

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

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


我认为最惯用的方法是在项目的to位置有一个package.json文件。
罗伯特·莫斯卡尔

一种想法是使用运行bash文件的npm脚本。
戴文·崔顿

难道这不是一个modificaiton做如何本地路径工作?:stackoverflow.com/questions/14381898/...
Evanss

Answers:


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的方法,以在具有DOS外壳的Windows和具有Unix外壳的Linux / Mac之间实现最大的可移植性。
trueadjustr

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然后不能像"postinstall": "cd nested_dir2 && npm install"将每个文件夹一样,将下一个文件夹放入package.json内吗?
阿隆

1
@Aron如果要在名称父目录中有两个子目录怎么办?
亚历克

28
@Emre应该起作用,子外壳可能会稍微干净一些:“(cd nested_dir && npm install);(cd nested_dir2 && npm install); ...”
Alec

48

根据@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
完美的解决方案。感谢您的分享:-)
Rahul Soni

1
感谢你的回答。为我工作。
AMIC MING

4
善用( )创建子壳并避免使用cd api && npm install && cd ..
卡梅隆·哈德森

4
那应该是选择的答案!
tmos

3
npm install在顶层运行时出现此错误:"(cd was unexpected at this time."
Polywhirl先生

22

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

以下脚本(递归)检查所有子文件夹,只要它们具有package.jsonnpm 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))
}

3
您的脚本很好。但是,出于我个人的目的,我更喜欢删除第一个“如果条件”以获得深层嵌套的“ npm安装”!
Guilherme Caraciolo

21

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

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

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


2
请注意,依赖项安装在根文件夹中。我怀疑,即使您正在考虑使用这种模式,也要依赖子目录package.json的子目录。
Cody Allan Taylor

你什么意思?subfolder-package的依赖项位于子文件夹的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
你的意思是模块。是的,b的node_modules将安装在a / node_modules中。这很有意义,因为您将需要/将模块作为主代码的一部分,而不是作为“实际”节点模块。因此,“ require('throug2')”将在a / node_modules中搜索2。
Jelmer Jellema

我正在尝试进行代码生成,并希望一个完全准备运行的子文件夹包,包括其自己的node_modules。如果找到解决方案,请确保进行更新!
ohsully

19

用例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文件-所有软件包都将安装在root目录中node_modules,这就是为什么您将无法运行npm命令的原因(需要依赖项)。

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


最好让每个子模块都有自己的安装脚本,然后在安装后执行它们。run-p不是必需的,但是它变得更加冗长"postinstall": "npm run install:a && npm run install:b"
Qwerty

是的,您可以&&不使用run-p。但是正如您所说,它的可读性较差。另一个缺点(run-p可以解决问题是因为安装是并行运行的)因此,如果一个失败,则不会影响其他脚本
Don Vaughn

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

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

  • 可以设置使用yarnnpm
  • 可以设置为基于锁定文件确定要使用的命令,以便如果将其设置为使用yarn但目录只有一个目录package-lock.json,它将npm用于该目录(默认为true)。
  • 配置日志记录
  • 使用以下命令并行运行安装 cp.spawn
  • 可以进行空运行,让您先了解会做什么
  • 可以作为函数运行,也可以使用env vars自动运行
    • 作为函数运行时,可以选择提供目录数组以进行检查
  • 返回完成时解决的承诺
  • 允许设置最大深度以查看是否需要
  • 知道如果找到具有yarn workspaces(可配置)的文件夹,将停止递归
  • 允许使用逗号分隔的env var或通过向config传递与之匹配的字符串数组或接收文件名,文件路径和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 })

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 \;
Evan Moran
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.