对嵌套文件夹运行npm install的最好方法是什么?

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

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

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

162666 次浏览

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

我的解决方案非常相似。 纯粹的node . js < / p > 下面的脚本检查所有子文件夹(递归),只要它们有package.json,并在每个子文件夹中运行npm install。 可以为其添加例外:允许没有package.json的文件夹。在下面的例子中,一个这样的文件夹是“packages”。 可以将其作为“preinstall”脚本运行
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))
}

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

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

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

只是作为参考,以防人们遇到这个问题。你现在可以:

  • 添加包。Json到子文件夹中
  • 在main package.json中安装这个子文件夹作为reference-link:

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

用例1:如果你想能够从每个子目录(每个包。json is),你将需要使用postinstall

因为我经常使用npm-run-all,我使用它来保持它的美观和简短(在postinstall的部分):

{
"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的答案(在postinstall中使用subshell)。

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

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

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

如果您不确定,用例1总是有效的。

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

  • 可以设置为使用yarnnpm
  • 可以根据锁文件来确定要使用的命令,因此如果你将其设置为使用yarn,但一个目录只有package-lock.json,它将为该目录使用npm(默认为true)。
  • 配置日志记录
  • 使用cp.spawn并行运行安装
  • 可以先做演练,让你看看它会做什么
  • 可以作为函数运行,也可以使用env变量自动运行
    • 当作为函数运行时,可选地提供目录数组进行检查
    • 李< / ul > < / >
    • 返回一个在完成时解决的承诺
    • 允许设置最大深度看,如果需要
    • 知道在找到带有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 })
    

根据@Scott的回答,只要知道子目录名,安装|postinstall脚本是最简单的方法。这就是我如何对多个子dirs运行它。例如,假设在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)"
},
}
如果您的系统上有find实用程序,您可以尝试在应用程序根目录中运行以下命令 find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \; < / p >

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

find . -maxdepth 1 -type d \( ! -name . \) -exec bash -c "cd '{}' && npm install" \;

编辑正如fgblomqvist在评论中提到的,npm现在也支持工作区


有些答案相当古老。我认为现在我们有一些新的选项来设置monorepos

  1. 我建议使用纱工作区:

工作区是一种设置包架构的新方法,从Yarn 1.0开始默认提供。它允许你以这样一种方式设置多个包,你只需要运行yarn install一次就可以在一次传递中安装所有包。

  1. 如果你更喜欢或不得不使用npm,我建议看看lerna:

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

lerna也可以完美地用于yarn工作区- 文章。我刚刚完成设置一个monorepo项目- 例子

下面是一个配置为使用npm + lerna - 争取民主变革运动的网络的多包项目的示例:它们使用package运行lerna bootstrap。json是postinstall

[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 \;

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

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

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

还可以查看当前前缀with

npm prefix

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

npm config set prefix "folder_path"

也许是TMI,但你懂的…

要在每个子目录上运行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

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

我知道这不是OP想要的答案,但这是一个永远有效的答案。你需要创建一个子目录名数组,然后循环遍历它们并运行npm i,或任何你需要运行的命令。

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