The best way to run npm install for nested folders?

前端 未结 9 1844
北恋
北恋 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 })
    

提交回复
热议问题