The best way to run npm install for nested folders?

前端 未结 9 1762
北恋
北恋 2020-11-30 17:38

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

my-app
  /my-sub-module
  package.json
package.json
<
相关标签:
9条回答
  • 2020-11-30 18:11

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

    • can be setup to use yarn or npm
    • can be setup to determine the command to use based on lock files so that if you set it to use yarn but a directory only has a package-lock.json it will use npm for 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.
    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 })
    
    0 讨论(0)
  • 2020-11-30 18:13

    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:

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

    0 讨论(0)
  • 2020-11-30 18:16

    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:

    // In monorepo root package.json
    {
    ...
     "scripts": {
        "postinstall": "(cd api && npm install); (cd web && npm install); (cd shared && npm install)"
      },
    }
    
    0 讨论(0)
  • 2020-11-30 18:21

    If you have find utility on your system, you could try running the following command in your application root directory:
    find . ! -path "*/node_modules/*" -name "package.json" -execdir npm install \;

    Basically, find all package.json files and run npm install in that directory, skipping all node_modules directories.

    0 讨论(0)
  • 2020-11-30 18:22

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

    "scripts": {
      "postinstall": "cd nested_dir && npm install",
      ...
    }
    
    0 讨论(0)
  • 2020-11-30 18:24

    My solution is very similar. Pure Node.js

    The following script examines all subfolders (recursively) as long as they have package.json and runs npm install in 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.

    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))
    }
    
    0 讨论(0)
提交回复
热议问题