Note: If packages depend on different _versions_ of an external dependency,

the most commonly used version will be hoisted, and a warning will be emitted.

Do _not_ install external dependencies matching `glob` at the repo root. This

can be used to opt out of hoisting for certain dependencies.

keep keep keep add keep keep keep keep keep

When used in conjunction with hoist will throw an error and stop bootstrapping after emitting the version warnings. Has no effect if you aren't hoisting, or if there are no version warnings.

"use strict";

const dedent = require("dedent");
const getPort = require("get-port");
const npa = require("npm-package-arg");
const path = require("path");
const pFinally = require("p-finally");
const pMap = require("p-map");
const pMapSeries = require("p-map-series");
const pWaterfall = require("p-waterfall");

const Command = require("@lerna/command");
const rimrafDir = require("@lerna/rimraf-dir");
const hasNpmVersion = require("@lerna/has-npm-version");
const npmInstall = require("@lerna/npm-install");
const { createRunner } = require("@lerna/run-lifecycle");
const batchPackages = require("@lerna/batch-packages");
const runParallelBatches = require("@lerna/run-parallel-batches");
const symlinkBinary = require("@lerna/symlink-binary");
const symlinkDependencies = require("@lerna/symlink-dependencies");
const ValidationError = require("@lerna/validation-error");
const { getFilteredPackages } = require("@lerna/filter-options");
const PackageGraph = require("@lerna/package-graph");
const pulseTillDone = require("@lerna/pulse-till-done");

const hasDependencyInstalled = require("./lib/has-dependency-installed");
const isHoistedPackage = require("./lib/is-hoisted-package");

module.exports = factory;

function factory(argv) {
  return new BootstrapCommand(argv);
}

class BootstrapCommand extends Command {
  get requiresGit() {
    return false;
  }

  initialize() {
    const { registry, npmClient = "npm", npmClientArgs = [], mutex, hoist, nohoist } = this.options;

    if (npmClient === "yarn" && hoist) {
      throw new ValidationError(
        "EWORKSPACES",
        dedent`
            --hoist is not supported with --npm-client=yarn, use yarn workspaces instead
            A guide is available at https://yarnpkg.com/blog/2017/08/02/introducing-workspaces/
          `
      );
    }

    if (
      npmClient === "yarn" &&
      this.project.manifest.get("workspaces") &&
      this.options.useWorkspaces !== true
    ) {
      throw new ValidationError(
        "EWORKSPACES",
        dedent`
            Yarn workspaces are configured in package.json, but not enabled in lerna.json!
            Please choose one: useWorkspaces = true in lerna.json, or remove package.json workspaces config
          `
      );
    }

    // postinstall and prepare are commonly used to call `lerna bootstrap`,
    // but we need to avoid recursive execution when `--hoist` is enabled
    const { LERNA_EXEC_PATH = "leaf", LERNA_ROOT_PATH = "root" } = process.env;

    if (LERNA_EXEC_PATH === LERNA_ROOT_PATH) {
      this.logger.warn("bootstrap", "Skipping recursive execution");

      return false;
    }

    if (hoist) {
      let hoisting;

      if (hoist === true) {
        // lerna.json `hoist: true`
        hoisting = ["**"];
      } else {
        // `--hoist ...` or lerna.json `hoist: [...]`
        hoisting = [].concat(hoist);
      }

      if (nohoist) {
        if (!Array.isArray(nohoist)) {
          // `--nohoist` single
          hoisting = hoisting.concat(`!${nohoist}`);
        } else {
          // `--nohoist` multiple or lerna.json `nohoist: [...]`
          hoisting = hoisting.concat(nohoist.map(str => `!${str}`));
        }
      }

      this.logger.verbose("hoist", "using globs %j", hoisting);
      this.hoisting = hoisting;
    }

    this.runPackageLifecycle = createRunner({ registry });
    this.npmConfig = {
      registry,
      npmClient,
      npmClientArgs,
      mutex,
    };

    if (npmClient === "npm" && this.options.ci && hasNpmVersion(">=5.7.0")) {
      // never `npm ci` when hoisting
      this.npmConfig.subCommand = this.hoisting ? "install" : "ci";

      if (this.hoisting) {
        // don't mutate lockfiles in CI
        this.npmConfig.npmClientArgs.unshift("--no-save");
      }
    }

    // lerna bootstrap ... -- <input>
    const doubleDashArgs = this.options["--"] || [];
    if (doubleDashArgs.length) {
      this.npmConfig.npmClientArgs = [...npmClientArgs, ...doubleDashArgs];
    }

    // do not run any lifecycle scripts (if configured)
    if (this.options.ignoreScripts) {
      this.npmConfig.npmClientArgs.unshift("--ignore-scripts");
    }

    this.targetGraph = this.options.forceLocal
      ? new PackageGraph(this.packageGraph.rawPackageList, "allDependencies", "forceLocal")
      : this.packageGraph;

    let chain = Promise.resolve();

    chain = chain.then(() => {
      if (this.options.scope) {
        this.logger.notice("filter", "including %j", this.options.scope);
      }

      if (this.options.ignore) {
        this.logger.notice("filter", "excluding %j", this.options.ignore);
      }

      if (this.options.since) {
        this.logger.notice("filter", "changed since %j", this.options.since);
      }

      if (this.options.includeFilteredDependents) {
        this.logger.notice("filter", "including filtered dependents");
      }

      if (this.options.includeFilteredDependencies) {
        this.logger.notice("filter", "including filtered dependencies");
      }

      return getFilteredPackages(this.targetGraph, this.execOpts, this.options);
    });

    chain = chain.then(filteredPackages => {
      this.filteredPackages = filteredPackages;

      if (filteredPackages.length !== this.targetGraph.size && !this.options.forceLocal) {
        this.logger.warn("bootstrap", "Installing local packages that do not match filters from registry");

        // an explicit --scope, --ignore, or --since should only symlink the targeted packages, no others
        this.targetGraph = new PackageGraph(filteredPackages, "allDependencies", this.options.forceLocal);

        // never automatically --save or modify lockfiles
        this.npmConfig.npmClientArgs.unshift(npmClient === "yarn" ? "--pure-lockfile" : "--no-save");

        // never attempt `npm ci`, it would always fail
        if (this.npmConfig.subCommand === "ci") {
          this.npmConfig.subCommand = "install";
        }
      }
    });

    chain = chain.then(() => {
      this.batchedPackages = this.toposort
        ? batchPackages(this.filteredPackages, this.options.rejectCycles)
        : [this.filteredPackages];

      if (npmClient === "yarn" && !mutex) {
        return getPort({ port: 42424, host: "0.0.0.0" }).then(port => {
          this.npmConfig.mutex = `network:${port}`;
          this.logger.silly("npmConfig", this.npmConfig);
        });
      }

      this.logger.silly("npmConfig", this.npmConfig);
    });

    return chain;
  }

  execute() {
    if (this.options.useWorkspaces || this.rootHasLocalFileDependencies()) {
      return this.installRootPackageOnly();
    }

    const filteredLength = this.filteredPackages.length;
    const packageCountLabel = `${filteredLength} package${filteredLength > 1 ? "s" : ""}`;
    const scriptsEnabled = this.options.ignoreScripts !== true;

    // root install does not need progress bar
    this.enableProgressBar();
    this.logger.info("", `Bootstrapping ${packageCountLabel}`);

    // conditional task queue
    const tasks = [];

    if (scriptsEnabled) {
      tasks.push(() => this.runLifecycleInPackages("preinstall"));
    }

    tasks.push(
      () => this.getDependenciesToInstall(),
      result => this.installExternalDependencies(result),
      () => this.symlinkPackages()
    );

    if (scriptsEnabled) {
      tasks.push(
        () => this.runLifecycleInPackages("install"),
        () => this.runLifecycleInPackages("postinstall")
      );

      if (!this.options.ignorePrepublish) {
        tasks.push(() => this.runLifecycleInPackages("prepublish"));
      }

      // "run on local npm install without any arguments", AFTER prepublish
      tasks.push(() => this.runLifecycleInPackages("prepare"));
    }

    return pWaterfall(tasks).then(() => {
      this.logger.success("", `Bootstrapped ${packageCountLabel}`);
    });
  }

  installRootPackageOnly() {
    this.logger.info("bootstrap", "root only");

    // don't hide yarn or npm output
    this.npmConfig.stdio = "inherit";

    return npmInstall(this.project.manifest, this.npmConfig);
  }

  /**
   * If the root manifest has local dependencies with `file:` specifiers,
   * all the complicated bootstrap logic should be skipped in favor of
   * npm5's package-locked auto-hoisting.
   * @returns {Boolean}
   */
  rootHasLocalFileDependencies() {
    const rootDependencies = Object.assign({}, this.project.manifest.dependencies);

    return Object.keys(rootDependencies).some(
      name =>
        this.targetGraph.has(name) &&
        npa.resolve(name, rootDependencies[name], this.project.rootPath).type === "directory"
    );
  }

  runLifecycleInPackages(stage) {
    this.logger.verbose("lifecycle", stage);

    if (!this.filteredPackages.length) {
      return;
    }

    const tracker = this.logger.newItem(stage);

    const mapPackageWithScript = pkg =>
      this.runPackageLifecycle(pkg, stage).then(() => {
        tracker.completeWork(1);
      });

    tracker.addWork(this.filteredPackages.length);

    return pFinally(runParallelBatches(this.batchedPackages, this.concurrency, mapPackageWithScript), () =>
      tracker.finish()
    );
  }

  hoistedDirectory(dependency) {
    return path.join(this.project.rootPath, "node_modules", dependency);
  }

  hoistedPackageJson(dependency) {
    try {
      // eslint-disable-next-line import/no-dynamic-require, global-require
      return require(path.join(this.hoistedDirectory(dependency), "package.json"));
    } catch (e) {
      // Pass.
      return {};
    }
  }

  /**
   * Return a object of root and leaf dependencies to install
   * @returns {Object}
   */
  getDependenciesToInstall() {
    // Configuration for what packages to hoist may be in lerna.json or it may
    // come in as command line options.
    const rootPkg = this.project.manifest;

    // This will contain entries for each hoistable dependency.
    const rootSet = new Set();

    // This will map packages to lists of unhoistable dependencies
    const leaves = new Map();

    /**
     * Map of dependencies to install
     *
     * Map {
     *   "<externalName>": Map {
     *     "<versionRange>": Set { "<dependent1>", "<dependent2>", ... }
     *   }
     * }
     *
     * Example:
     *
     * Map {
     *   "react": Map {
     *     "15.x": Set { "my-component1", "my-component2", "my-component3" },
     *     "^0.14.0": Set { "my-component4" },
     *   }
     * }
     */
    const depsToInstall = new Map();
    const filteredNodes = new Map(
      this.filteredPackages.map(pkg => [pkg.name, this.targetGraph.get(pkg.name)])
    );

    // collect root dependency versions
    const mergedRootDeps = Object.assign(
      {},
      rootPkg.devDependencies,
      rootPkg.optionalDependencies,
      rootPkg.dependencies
    );
    const rootExternalVersions = new Map(
      Object.keys(mergedRootDeps).map(externalName => [externalName, mergedRootDeps[externalName]])
    );

    // seed the root dependencies
    rootExternalVersions.forEach((version, externalName) => {
      const externalDependents = new Set();
      const record = new Map();

      record.set(version, externalDependents);
      depsToInstall.set(externalName, record);
    });

    // build a map of external dependencies to install
    for (const [leafName, leafNode] of filteredNodes) {
      for (const [externalName, resolved] of leafNode.externalDependencies) {
        // rawSpec is something like "^1.2.3"
        const version = resolved.rawSpec;
        const record =
          depsToInstall.get(externalName) || depsToInstall.set(externalName, new Map()).get(externalName);
        const externalDependents = record.get(version) || record.set(version, new Set()).get(version);

        externalDependents.add(leafName);
      }
    }

    const rootActions = [];
    const leafActions = [];

    // determine where each dependency will be installed
    for (const [externalName, externalDependents] of depsToInstall) {
      let rootVersion;

      if (this.hoisting && isHoistedPackage(externalName, this.hoisting)) {
        const commonVersion = Array.from(externalDependents.keys()).reduce((a, b) =>
          externalDependents.get(a).size > externalDependents.get(b).size ? a : b
        );

        // Get the version required by the repo root (if any).
        // If the root doesn't have a dependency on this package then we'll
        // install the most common dependency there.
        rootVersion = rootExternalVersions.get(externalName) || commonVersion;

        if (rootVersion !== commonVersion) {
          this.logger.warn(
            "EHOIST_ROOT_VERSION",
            `The repository root depends on ${externalName}@${rootVersion}, ` +
              `which differs from the more common ${externalName}@${commonVersion}.`
          );
        }

        const dependents = Array.from(externalDependents.get(rootVersion)).map(
          leafName => this.targetGraph.get(leafName).pkg
        );

        // remove collection so leaves don't repeat it
        externalDependents.delete(rootVersion);

        // Install the best version we can in the repo root.
        // Even if it's already installed there we still need to make sure any
        // binaries are linked to the packages that depend on them.
        rootActions.push(() =>
          hasDependencyInstalled(rootPkg, externalName, rootVersion).then(isSatisfied => {
            rootSet.add({
              name: externalName,
              dependents,
              dependency: `${externalName}@${rootVersion}`,
              isSatisfied,
            });
          })
        );
      }

      // Add less common versions to package installs.
      for (const [leafVersion, leafDependents] of externalDependents) {
        for (const leafName of leafDependents) {
          if (rootVersion) {
            this.logger.warn(
              "EHOIST_PKG_VERSION",
              `"${leafName}" package depends on ${externalName}@${leafVersion}, ` +
                `which differs from the hoisted ${externalName}@${rootVersion}.`
            );
          }

          const leafNode = this.targetGraph.get(leafName);
          const leafRecord = leaves.get(leafNode) || leaves.set(leafNode, new Set()).get(leafNode);

          // only install dependency if it's not already installed
          leafActions.push(() =>
            hasDependencyInstalled(leafNode.pkg, externalName, leafVersion).then(isSatisfied => {
              leafRecord.add({
                dependency: `${externalName}@${leafVersion}`,
                isSatisfied,
              });
            })
          );
        }
      }
    }

    return pMapSeries([...rootActions, ...leafActions], el => el()).then(() => {
      this.logger.silly("root dependencies", JSON.stringify(rootSet, null, 2));
      this.logger.silly("leaf dependencies", JSON.stringify(leaves, null, 2));

      return { rootSet, leaves };
    });
  }

  /**
   * Install external dependencies for all packages
   * @returns {Promise}
   */
  installExternalDependencies({ leaves, rootSet }) {
    const tracker = this.logger.newItem("install dependencies");
    const rootPkg = this.project.manifest;
    const actions = [];

    // Start root install first, if any, since it's likely to take the longest.
    if (rootSet.size) {
      // If we have anything to install in the root then we'll install
      // _everything_ that needs to go there.  This is important for
      // consistent behavior across npm clients.
      const root = Array.from(rootSet);

      actions.push(() => {
        const depsToInstallInRoot = root.some(({ isSatisfied }) => !isSatisfied)
          ? root.map(({ dependency }) => dependency)
          : [];

        if (depsToInstallInRoot.length) {
          tracker.info("hoist", "Installing hoisted dependencies into root");
        }

        const promise = npmInstall.dependencies(rootPkg, depsToInstallInRoot, this.npmConfig);

        return pulseTillDone(promise)
          .then(() =>
            // Link binaries into dependent packages so npm scripts will
            // have access to them.
            pMapSeries(root, ({ name, dependents }) => {
              const { bin } = this.hoistedPackageJson(name);

              if (bin) {
                return pMap(dependents, pkg => {
                  const src = this.hoistedDirectory(name);

                  return symlinkBinary(src, pkg);
                });
              }
            })
          )
          .then(() => {
            tracker.info("hoist", "Finished bootstrapping root");
            tracker.completeWork(1);
          });
      });

      // Remove any hoisted dependencies that may have previously been
      // installed in package directories.
      actions.push(() => {
        // Compute the list of candidate directories synchronously
        const candidates = root
          .filter(dep => dep.dependents.length)
          .reduce((list, { name, dependents }) => {
            const dirs = dependents
              .filter(pkg => pkg.nodeModulesLocation !== rootPkg.nodeModulesLocation)
              .map(pkg => path.join(pkg.nodeModulesLocation, name));

            return list.concat(dirs);
          }, []);

        if (!candidates.length) {
          tracker.verbose("hoist", "nothing to prune");
          tracker.completeWork(1); // the action "work"

          return;
        }

        tracker.info("hoist", "Pruning hoisted dependencies");
        tracker.silly("prune", candidates);
        tracker.addWork(candidates.length);

        return pMap(
          candidates,
          dirPath =>
            pulseTillDone(rimrafDir(dirPath)).then(() => {
              tracker.verbose("prune", dirPath);
              tracker.completeWork(1);
            }),
          // these are mostly no-ops in the vast majority of cases
          { concurrency: this.concurrency }
        ).then(() => {
          tracker.info("hoist", "Finished pruning hoisted dependencies");
          tracker.completeWork(1); // the action "work"
        });
      });
    }

    const leafNpmConfig = Object.assign({}, this.npmConfig, {
      // Use `npm install --global-style` for leaves when hoisting is enabled
      npmGlobalStyle: !!this.options.hoist,
    });

    // Install anything that needs to go into the leaves.
    leaves.forEach((leafRecord, leafNode) => {
      const deps = Array.from(leafRecord);

      // If we have any unsatisfied deps then we need to install everything.
      // This is important for consistent behavior across npm clients.
      if (deps.some(({ isSatisfied }) => !isSatisfied)) {
        actions.push(() => {
          const dependencies = deps.map(({ dependency }) => dependency);
          const promise = npmInstall.dependencies(leafNode.pkg, dependencies, leafNpmConfig);

          return pulseTillDone(promise).then(() => {
            tracker.verbose("installed leaf", leafNode.name);
            tracker.completeWork(1);
          });
        });
      }
    });

    if (actions.length) {
      tracker.info("", "Installing external dependencies");
      tracker.verbose("actions", "%d actions, concurrency %d", actions.length, this.concurrency);
      tracker.addWork(actions.length);
    }

    return pFinally(pMap(actions, act => act(), { concurrency: this.concurrency }), () => tracker.finish());
  }

  /**
   * Symlink all packages to the packages/node_modules directory
   * Symlink package binaries to dependent packages' node_modules/.bin directory
   * @returns {Promise}
   */
  symlinkPackages() {
    return symlinkDependencies(
      this.filteredPackages,
      this.targetGraph,
      this.logger.newItem("bootstrap dependencies")
    );
  }
}

module.exports.BootstrapCommand = BootstrapCommand;


"use strict";

const cliRunner = require("@lerna-test/cli-runner");
const initFixture = require("@lerna-test/init-fixture")(__dirname);

test("lerna link symlinks generated binaries of sibling packages", async () => {
  const cwd = await initFixture("lerna-generated-build-tool");
  const lerna = cliRunner(cwd);

  // First bootstrap, I expect this to succeed but don't are about the output
  await lerna("bootstrap");

  const { stdout } = await lerna("run", "build");

  expect(stdout).toMatch("build tool executed");
});


describe("with --hoist and --strict", () => {

it("should throw if there's a hoist warning", async () => {

expect(err.message).toMatch("Package version inconsistencies found");

"use strict";

jest.mock("@lerna/rimraf-dir");
jest.mock("@lerna/npm-install");
jest.mock("@lerna/run-lifecycle");
jest.mock("@lerna/create-symlink");

const fs = require("fs-extra");
const path = require("path");

// mocked or stubbed modules
const rimrafDir = require("@lerna/rimraf-dir");
const npmInstall = require("@lerna/npm-install");
const runLifecycle = require("@lerna/run-lifecycle");
const createSymlink = require("@lerna/create-symlink");
const hasNpmVersion = require("@lerna/has-npm-version");

// helpers
const initFixture = require("@lerna-test/init-fixture")(__dirname);
const normalizeRelativeDir = require("@lerna-test/normalize-relative-dir");
const updateLernaConfig = require("@lerna-test/update-lerna-config");

// file under test
const lernaBootstrap = require("@lerna-test/command-runner")(require("../command"));

// assertion helpers
const installedPackagesInDirectories = testDir =>
  npmInstall.dependencies.mock.calls.reduce((obj, [pkg, dependencies]) => {
    const relative = normalizeRelativeDir(testDir, pkg.location);
    obj[relative || "ROOT"] = dependencies;
    return obj;
  }, {});

const removedDirectories = testDir =>
  rimrafDir.mock.calls.map(([directory]) => normalizeRelativeDir(testDir, directory));

const symlinkedDirectories = testDir =>
  createSymlink.mock.calls
    .slice()
    // ensure sort is always consistent, despite promise variability
    .sort((a, b) => {
      // two-dimensional path sort
      if (b[0] === a[0]) {
        if (b[1] === a[1]) {
          // ignore third field
          return 0;
        }

        return b[1] < a[1] ? 1 : -1;
      }

      return b[0] < a[0] ? 1 : -1;
    })
    .map(([src, dest, type]) => ({
      _src: normalizeRelativeDir(testDir, src),
      dest: normalizeRelativeDir(testDir, dest),
      type,
    }));

describe("BootstrapCommand", () => {
  // stub rimraf because we trust isaacs
  rimrafDir.mockResolvedValue();

  // we stub npmInstall in most tests because
  // we already have enough tests of npmInstall
  npmInstall.mockResolvedValue();
  npmInstall.dependencies.mockResolvedValue();

  // stub runLifecycle because it is a huge source
  // of slowness when running tests for no good reason
  runLifecycle.mockResolvedValue();

  // the underlying implementation of symlinkBinary and symlinkDependencies
  createSymlink.mockResolvedValue();

  describe("lifecycle scripts", () => {
    const npmLifecycleEvent = process.env.npm_lifecycle_event;

    afterEach(() => {
      process.env.npm_lifecycle_event = npmLifecycleEvent;
      delete process.env.LERNA_EXEC_PATH;
      delete process.env.LERNA_ROOT_PATH;
    });

    it("should run preinstall, postinstall and prepublish scripts", async () => {
      const testDir = await initFixture("lifecycle-scripts");

      await lernaBootstrap(testDir)();

      expect(runLifecycle.getOrderedCalls()).toEqual([
        ["package-preinstall", "preinstall"],
        ["package-postinstall", "postinstall"],
        ["package-prepublish", "prepublish"],
      ]);
    });

    it("does not run prepublish scripts with --ignore-prepublish", async () => {
      const testDir = await initFixture("lifecycle-scripts");

      await lernaBootstrap(testDir)("--ignore-prepublish");

      expect(runLifecycle.getOrderedCalls()).toEqual([
        ["package-preinstall", "preinstall"],
        ["package-postinstall", "postinstall"],
      ]);
    });

    it("shouldn't run lifecycle scripts with --ignore-scripts", async () => {
      const testDir = await initFixture("ignored-scripts");

      await lernaBootstrap(testDir)("--ignore-scripts");

      expect(runLifecycle).not.toHaveBeenCalled();
      expect(npmInstall.dependencies).toHaveBeenCalledWith(
        expect.objectContaining({
          name: "package-prepare",
        }),
        ["tiny-tarball@^1.0.0"],
        expect.objectContaining({
          npmClientArgs: ["--ignore-scripts"],
        })
      );
    });

    it("should not recurse from hoisted root lifecycle", async () => {
      const testDir = await initFixture("lifecycle-scripts");

      process.env.LERNA_EXEC_PATH = testDir;
      process.env.LERNA_ROOT_PATH = testDir;

      await lernaBootstrap(testDir)();

      expect(runLifecycle).not.toHaveBeenCalled();
    });
  });

  describe("with hoisting", () => {
    it("should hoist", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--hoist");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(removedDirectories(testDir)).toMatchSnapshot();

      // root includes explicit dependencies and hoisted from leaves
      expect(npmInstall.dependencies).toHaveBeenCalledWith(
        expect.objectContaining({
          name: "basic",
        }),
        ["bar@^2.0.0", "foo@^1.0.0", "@test/package-1@^0.0.0"],
        {
          registry: undefined,
          npmClient: "npm",
          npmClientArgs: [],
          mutex: undefined,
          // npmGlobalStyle is not included at all
        }
      );

      // foo@0.1.2 differs from the more common foo@^1.0.0
      expect(npmInstall.dependencies).toHaveBeenLastCalledWith(
        expect.objectContaining({
          name: "package-3",
        }),
        ["foo@0.1.12"],
        expect.objectContaining({
          npmGlobalStyle: true,
        })
      );
    });

    it("should not hoist when disallowed", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--hoist", "--nohoist", "@test/package-1");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(removedDirectories(testDir)).toMatchSnapshot();
    });

    it("should not hoist when disallowed from lerna.json", async () => {
      const testDir = await initFixture("basic");

      await updateLernaConfig(testDir, {
        hoist: true,
        nohoist: ["@test/package-1"],
      });
      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
    });
  });

  describe("with --npm-client and --hoist", () => {
    it("should throw", async () => {
      expect.assertions(1);

      const testDir = await initFixture("yarn-hoist");

      try {
        await lernaBootstrap(testDir)();
      } catch (err) {
        expect(err.message).toMatch(
          "--hoist is not supported with --npm-client=yarn, use yarn workspaces instead"
        );
      }
    });
  });

  describe("with --ci", () => {
    it("should call npmInstall with ci subCommand if on npm 5.7.0 or later", async () => {
      const testDir = await initFixture("ci");

      await lernaBootstrap(testDir)("--ci");

      expect(hasNpmVersion).toHaveBeenLastCalledWith(">=5.7.0");
      expect(npmInstall.dependencies.mock.calls[0][2]).toEqual({
        subCommand: "ci",
        registry: undefined,
        npmClient: "npm",
        npmClientArgs: [],
        npmGlobalStyle: false,
        mutex: undefined,
      });
    });

    it("should not pass subCommand to npmInstall if on npm version earlier than 5.7.0", async () => {
      const testDir = await initFixture("ci");

      hasNpmVersion.mockReturnValueOnce(false);

      await lernaBootstrap(testDir)("--ci");

      expect(npmInstall.dependencies.mock.calls[0][2]).toEqual({
        registry: undefined,
        npmClient: "npm",
        npmClientArgs: [],
        npmGlobalStyle: false,
        mutex: undefined,
      });
    });

    it("should not run npm ci when hoisting", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--hoist", "package-*", "--ci");

      expect(npmInstall.dependencies.mock.calls[0][2]).toMatchObject({
        subCommand: "install", // not "ci"
        npmClient: "npm",
        npmClientArgs: ["--no-save"],
      });
    });

    it("respects config file { ci: false }", async () => {
      const testDir = await initFixture("ci");

      await updateLernaConfig(testDir, {
        command: {
          bootstrap: {
            ci: false,
          },
        },
      });
      await lernaBootstrap(testDir)();

      expect(hasNpmVersion).not.toHaveBeenCalled();
      expect(npmInstall.dependencies.mock.calls[0][2]).toEqual({
        registry: undefined,
        npmClient: "npm",
        npmClientArgs: [],
        npmGlobalStyle: false,
        mutex: undefined,
      });
    });
  });

  describe("with local package dependencies", () => {
    it("should bootstrap packages", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });

    it("should not bootstrap ignored packages", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--ignore", "{@test/package-2,package-4}");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });

    it("should only bootstrap scoped packages", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--scope", "package-@(3|4)");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });

    it("should respect --force-local", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--scope", "@test/package-1", "--scope", "package-4", "--force-local");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });

    it("should respect --force-local when a single package is in scope", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--scope", "package-4", "--force-local");

      // no packages were installed from the registry
      const installed = installedPackagesInDirectories(testDir);
      expect(installed["packages/package-4"] || []).toEqual([]);

      // package-3 was resolved as a local symlink
      const symlinked = symlinkedDirectories(testDir);
      expect(symlinked).toContainEqual({
        _src: "packages/package-3",
        dest: "packages/package-4/node_modules/package-3",
        type: "junction",
      });
    });

    it("should not update package.json when filtering", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--scope", "@test/package-2", "--ci");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
      expect(npmInstall.dependencies.mock.calls[0][2]).toMatchObject({
        subCommand: "install", // not "ci"
        npmClient: "npm",
        npmClientArgs: ["--no-save"],
      });
    });

    it("should not update yarn.lock when filtering", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)("--scope", "@test/package-2", "--npm-client", "yarn", "--ci");

      expect(npmInstall.dependencies.mock.calls[0][2]).toMatchObject({
        npmClient: "yarn",
        npmClientArgs: ["--pure-lockfile"],
      });
    });

    it("never installs with global style", async () => {
      const testDir = await initFixture("basic");

      await lernaBootstrap(testDir)();

      expect(npmInstall.dependencies).toHaveBeenCalledWith(
        expect.objectContaining({
          name: "@test/package-2",
        }),
        expect.arrayContaining(["foo@^1.0.0"]),
        expect.objectContaining({
          npmGlobalStyle: false,
        })
      );
    });
  });

  describe("with multiple package locations", () => {
    it("should bootstrap packages", async () => {
      const testDir = await initFixture("extra");

      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
      expect(runLifecycle.getOrderedCalls()).toEqual([["@test/package-1", "prepublish"]]);
    });

    it("should not bootstrap ignored packages", async () => {
      const testDir = await initFixture("extra");

      await lernaBootstrap(testDir)("--ignore", "package-@(3|4)");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
    });

    it("bootstraps dependencies not included by --scope with --include-filtered-dependencies", async () => {
      const testDir = await initFixture("extra");

      // we scope to package-2 only but should still install package-1 as it is a dependency of package-2
      await lernaBootstrap(testDir)("--scope", "package-2", "--include-filtered-dependencies");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
    });

    it("bootstraps dependencies excluded by --ignore with --include-filtered-dependencies", async () => {
      const testDir = await initFixture("extra");

      // we ignore package 1 but it should still be installed because it is a dependency of package-2
      await lernaBootstrap(testDir)(
        "--ignore",
        "{@test/package-1,package-@(3|4)}",
        "--include-filtered-dependencies"
      );

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
    });

    it("hoists appropriately", async () => {
      const testDir = await initFixture("extra");

      await lernaBootstrap(testDir)("--hoist");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });
  });

  describe("with external dependencies that haven't been installed", () => {
    it("should get installed", async () => {
      const testDir = await initFixture("cold");

      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
    });

    it("gets network mutex when --npm-client=yarn", async () => {
      const testDir = await initFixture("cold");

      await lernaBootstrap(testDir)("--npm-client", "yarn");

      expect(npmInstall.dependencies.mock.calls[0][2]).toMatchObject({
        npmClient: "yarn",
        mutex: expect.stringMatching(/^network:\d+$/),
      });
    });

    it("gets user defined mutex when --npm-client=yarn", async () => {
      const testDir = await initFixture("cold");

      await lernaBootstrap(testDir)("--npm-client", "yarn", "--mutex", "file:/test/this/path");

      expect(npmInstall.dependencies.mock.calls[0][2]).toMatchObject({
        npmClient: "yarn",
        mutex: "file:/test/this/path",
      });
    });

    it("hoists appropriately", async () => {
      const testDir = await initFixture("cold");

      await updateLernaConfig(testDir, {
        hoist: true,
      });
      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });
  });

  describe("with external dependencies that have already been installed", () => {
    it("should not get re-installed", async () => {
      const testDir = await initFixture("warm");

      await lernaBootstrap(testDir)();

      expect(npmInstall.dependencies).not.toHaveBeenCalled();
    });

    it("hoists appropriately", async () => {
      const testDir = await initFixture("warm");

      await updateLernaConfig(testDir, {
        command: {
          bootstrap: {
            hoist: true,
          },
        },
      });
      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });
  });

  describe("with at least one external dependency to install", () => {
    it("should install all dependencies", async () => {
      const testDir = await initFixture("tepid");

      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
    });

    it("hoists appropriately", async () => {
      const testDir = await initFixture("tepid");

      await lernaBootstrap(testDir)("--hoist");

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });
  });

  describe("with package peerDependencies", () => {
    it("does not bootstrap peerDependencies", async () => {
      const testDir = await initFixture("peer");

      await lernaBootstrap(testDir)();

      expect(npmInstall.dependencies).not.toHaveBeenCalled();
    });
  });

  describe("with registry config", () => {
    it("should install packages from registry", async () => {
      const testDir = await initFixture("registries");

      await lernaBootstrap(testDir)();

      expect(installedPackagesInDirectories(testDir)).toMatchSnapshot();
      expect(npmInstall.dependencies).toHaveBeenLastCalledWith(
        expect.objectContaining({
          name: "@test/package-1",
        }),
        expect.arrayContaining(["foo@^1.0.0"]),
        expect.objectContaining({
          registry: "https://my-secure-registry/npm",
          npmClient: "npm",
          npmGlobalStyle: false,
        })
      );
    });
  });

  describe("with remaining arguments", () => {
    describe("by default", () => {
      it("should turn it into npmClientArgs", async () => {
        const testDir = await initFixture("npm-client-args-1");

        await lernaBootstrap(testDir)("--", "--no-optional", "--production");

        expect(npmInstall.dependencies.mock.calls[0][2]).toMatchObject({
          npmClientArgs: ["--no-optional", "--production"],
        });
      });
    });

    describe("and configured npmClientArgs option", () => {
      it("should merge both together", async () => {
        const testDir = await initFixture("npm-client-args-2");

        await lernaBootstrap(testDir)("--", "--no-optional");

        expect(npmInstall.dependencies.mock.calls[0][2]).toMatchObject({
          npmClientArgs: ["--production", "--no-optional"],
        });
      });
    });
  });

  describe("with yarn workspaces", () => {
    it("should use workspaces feature when installing", async () => {
      const testDir = await initFixture("yarn-workspaces");

      await lernaBootstrap(testDir)();

      expect(npmInstall.dependencies).not.toHaveBeenCalled();
      expect(npmInstall).toHaveBeenLastCalledWith(
        expect.objectContaining({ name: "root" }),
        expect.objectContaining({
          npmClient: "yarn",
          mutex: expect.stringMatching(/^network:\d+$/),
        })
      );
    });

    it("errors when package.json workspaces exists but --use-workspaces is not enabled", async () => {
      expect.assertions(1);

      const testDir = await initFixture("yarn-workspaces");

      try {
        await lernaBootstrap(testDir)("--no-use-workspaces");
      } catch (err) {
        expect(err.message).toMatch(
          "Yarn workspaces are configured in package.json, but not enabled in lerna.json!"
        );
      }
    });
  });

  describe("with relative file: specifiers in root dependencies", () => {
    it("only installs in the root", async () => {
      const testDir = await initFixture("relative-file-specs");

      await lernaBootstrap(testDir)();

      expect(npmInstall.dependencies).not.toHaveBeenCalled();
      expect(npmInstall).toHaveBeenLastCalledWith(
        expect.objectContaining({ name: "relative-file-specs" }),
        expect.objectContaining({
          npmClient: "npm",
          stdio: "inherit",
        })
      );
    });
  });

  describe("with duplicate package names", () => {
    it("throws an error", async () => {
      expect.assertions(1);

      const testDir = await initFixture("duplicate-package-names");

      try {
        await lernaBootstrap(testDir)();
      } catch (err) {
        expect(err.message).toMatch(`Package name "package-1" used in multiple packages`);
      }
    });
  });

  describe("in a cyclical repo", () => {
    it("should throw an error with --reject-cycles", async () => {
      expect.assertions(1);

      const testDir = await initFixture("toposort");

      try {
        await lernaBootstrap(testDir)("--reject-cycles");
      } catch (err) {
        expect(err.message).toMatch("Dependency cycles detected, you should fix these!");
      }
    });
  });

  it("succeeds in repositories with zero packages", async () => {
    const testDir = await initFixture("zero-pkgs");

    const result = await lernaBootstrap(testDir)();

    // cheesy workaround for jest's expectation of assertions
    expect(result).toBeDefined();
  });

  it("does not require an initialized git repo", async () => {
    const testDir = await initFixture("zero-pkgs");

    await fs.remove(path.join(testDir, ".git"));

    const result = await lernaBootstrap(testDir)();

    // cheesy workaround for jest's expectation of assertions
    expect(result).toBeDefined();
  });

  it("requires a git repo when using --since", async () => {
    expect.assertions(1);
    const testDir = await initFixture("zero-pkgs");

    await fs.remove(path.join(testDir, ".git"));

    try {
      await lernaBootstrap(testDir)("--since", "some-branch");
    } catch (err) {
      expect(err.message).toMatch("this is not a git repository");
    }
  });

  describe("with force-local", () => {
    it("links all packages", async () => {
      const testDir = await initFixture("force-local");

      await lernaBootstrap(testDir)("--force-local");

      expect(symlinkedDirectories(testDir)).toMatchSnapshot();
    });
  });
});


"use strict";

const path = require("path");

const cliRunner = require("@lerna-test/cli-runner");
const showCommit = require("@lerna-test/show-commit");
const cloneFixture = require("@lerna-test/clone-fixture")(
  path.resolve(__dirname, "../commands/publish/__tests__")
);

// stabilize changelog commit SHA and datestamp
expect.addSnapshotSerializer(require("@lerna-test/serialize-changelog"));

const env = {
  // never actually upload when calling `npm publish`
  npm_config_dry_run: true,
  // skip npm package validation, none of the stubs are real
  LERNA_INTEGRATION: "SKIP",
};

test("lerna publish updates fixed versions", async () => {
  const { cwd } = await cloneFixture("normal");
  const args = ["publish", "patch", "--yes"];

  const { stdout } = await cliRunner(cwd, env)(...args);
  expect(stdout).toMatchInlineSnapshot(`

Changes:
 - package-1: 1.0.0 => 1.0.1
 - package-2: 1.0.0 => 1.0.1
 - package-3: 1.0.0 => 1.0.1
 - package-4: 1.0.0 => 1.0.1
 - package-5: 1.0.0 => 1.0.1 (private)

Successfully published:
 - package-1@1.0.1
 - package-2@1.0.1
 - package-3@1.0.1
 - package-4@1.0.1
`);

  const patch = await showCommit(cwd);
  expect(patch).toMatchInlineSnapshot(`
v1.0.1

HEAD -> master, tag: v1.0.1, origin/master

diff --git a/lerna.json b/lerna.json
index SHA..SHA 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2 +2 @@
-  "version": "1.0.0"
+  "version": "1.0.1"
diff --git a/packages/package-1/package.json b/packages/package-1/package.json
index SHA..SHA 100644
--- a/packages/package-1/package.json
+++ b/packages/package-1/package.json
@@ -3 +3 @@
-  "version": "1.0.0"
+  "version": "1.0.1"
diff --git a/packages/package-2/package.json b/packages/package-2/package.json
index SHA..SHA 100644
--- a/packages/package-2/package.json
+++ b/packages/package-2/package.json
@@ -3 +3 @@
-  "version": "1.0.0",
+  "version": "1.0.1",
@@ -5 +5 @@
-    "package-1": "^1.0.0"
+    "package-1": "^1.0.1"
diff --git a/packages/package-3/package.json b/packages/package-3/package.json
index SHA..SHA 100644
--- a/packages/package-3/package.json
+++ b/packages/package-3/package.json
@@ -3 +3 @@
-  "version": "1.0.0",
+  "version": "1.0.1",
@@ -8 +8 @@
-    "package-2": "^1.0.0"
+    "package-2": "^1.0.1"
diff --git a/packages/package-4/package.json b/packages/package-4/package.json
index SHA..SHA 100644
--- a/packages/package-4/package.json
+++ b/packages/package-4/package.json
@@ -3 +3 @@
-  "version": "1.0.0",
+  "version": "1.0.1",
diff --git a/packages/package-5/package.json b/packages/package-5/package.json
index SHA..SHA 100644
--- a/packages/package-5/package.json
+++ b/packages/package-5/package.json
@@ -4 +4 @@
-    "package-1": "^1.0.0"
+    "package-1": "^1.0.1"
@@ -7 +7 @@
-    "package-3": "^1.0.0"
+    "package-3": "^1.0.1"
@@ -10 +10 @@
-  "version": "1.0.0"
+  "version": "1.0.1"
`);
});


describe: "Use the specified registry for all npm client operations.",

describe: "Don't allow warnings when hoisting as it causes longer bootstrap times and other issues.",

# `@lerna/bootstrap`

> Link local packages together and install remaining package dependencies

Install [lerna](https://www.npmjs.com/package/lerna) for access to the `lerna` CLI.

## Usage

```sh
$ lerna bootstrap
```

Bootstrap the packages in the current Lerna repo.
Installs all of their dependencies and links any cross-dependencies.

When run, this command will:

1. `npm install` all external dependencies of each package.
2. Symlink together all Lerna `packages` that are dependencies of each other.
3. `npm run prepublish` in all bootstrapped packages (unless `--ignore-prepublish` is passed).
4. `npm run prepare` in all bootstrapped packages.

`lerna bootstrap` respects the `--ignore`, `--scope` and `--include-filtered-dependencies` flags (see [Filter Flags](https://www.npmjs.com/package/@lerna/filter-options)).

Pass extra arguments to npm client by placing them after `--`:

```sh
$ lerna bootstrap -- --production --no-optional
```

May also be configured in `lerna.json`:

```js
{
  ...
  "npmClient": "yarn",
  "npmClientArgs": ["--production", "--no-optional"]
}
```

### --hoist [glob]

Install external dependencies matching `glob` at the repo root so they're
available to all packages. Any binaries from these dependencies will be
linked into dependent package `node_modules/.bin/` directories so they're
available for npm scripts. If the option is present but no `glob` is given
the default is `**` (hoist everything). This option only affects the
`bootstrap` command.

```sh
$ lerna bootstrap --hoist
```

For background on `--hoist`, see the [hoist documentation](https://github.com/lerna/lerna/blob/master/doc/hoist.md).

Note: If packages depend on different _versions_ of an external dependency,
the most commonly used version will be hoisted, and a warning will be emitted.

### --nohoist [glob]

Do _not_ install external dependencies matching `glob` at the repo root. This
can be used to opt out of hoisting for certain dependencies.

```sh
$ lerna bootstrap --hoist --nohoist=babel-*
```

### --ignore

```sh
$ lerna bootstrap --ignore component-*
```

The `--ignore` flag, when used with the `bootstrap` command, can also be set in `lerna.json` under the `command.bootstrap.ignore` key. The command-line flag will take precedence over this option.

**Example**

```json
{
  "version": "0.0.0",
  "command": {
    "bootstrap": {
      "ignore": "component-*"
    }
  }
}
```

> Hint: The glob is matched against the package name defined in `package.json`,
> not the directory name the package lives in.

## Options

### `--ignore-prepublish`

Skip prepublish lifecycle scripts run by default in bootstrapped packages.
Note, this lifecycle is [deprecated](https://docs.npmjs.com/misc/scripts#deprecation-note),
and will likely be removed in the next major version of Lerna.

```sh
$ lerna bootstrap --ignore-prepublish
```

### `--ignore-scripts`

Skip any lifecycle scripts normally run (`prepare`, etc) in bootstrapped packages.

```sh
$ lerna bootstrap --ignore-scripts
```

### `--registry <url>`

When run with this flag, forwarded npm commands will use the specified registry for your package(s).

This is useful if you do not want to explicitly set up your registry
configuration in all of your package.json files individually when e.g. using
private registries.

### `--npm-client <client>`

Must be an executable that knows how to install npm package dependencies.
The default `--npm-client` is `npm`.

```sh
$ lerna bootstrap --npm-client=yarn
```

May also be configured in `lerna.json`:

```js
{
  ...
  "npmClient": "yarn"
}
```

### `--use-workspaces`

Enables integration with [Yarn Workspaces](https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-workspaces-install-phase-1.md) (available since yarn@0.27+).
The values in the array are the commands in which Lerna will delegate operation to Yarn (currently only bootstrapping).
If `--use-workspaces` is true then `packages` will be overridden by the value from `package.json/workspaces.`
May also be configured in `lerna.json`:

```js
{
  ...
  "npmClient": "yarn",
  "useWorkspaces": true
}
```

The root-level package.json must also include a `workspaces` array:

```json
{
  "private": true,
  "devDependencies": {
    "lerna": "^2.2.0"
  },
  "workspaces": ["packages/*"]
}
```

This list is broadly similar to lerna's `packages` config (a list of globs matching directories with a package.json),
except it does not support recursive globs (`"**"`, a.k.a. "globstars").

### `--no-ci`

When using the default `--npm-client`, `lerna bootstrap` will call [`npm ci`](https://docs.npmjs.com/cli/ci) instead of `npm install` in CI environments.
To disable this behavior, pass `--no-ci`:

```sh
$ lerna bootstrap --no-ci
```

To _force_ it during a local install (where it is not automatically enabled), pass `--ci`:

```sh
$ lerna bootstrap --ci
```

This can be useful for "clean" re-installs, or initial installations after fresh cloning.

### `--force-local`

```sh
$ lerna bootstrap --force-local
```

When passed, this flag causes the `bootstrap` command to always symlink local dependencies regardless of matching version range.

## How It Works

Let's use `babel` as an example.

- `babel-generator` and `source-map` (among others) are dependencies of `babel-core`.
- `babel-core`'s [`package.json`](https://github.com/babel/babel/blob/13c961d29d76ccd38b1fc61333a874072e9a8d6a/packages/babel-core/package.json#L28-L47) lists both these packages as keys in `dependencies`, as shown below.

```js
// babel-core package.json
{
  "name": "babel-core",
  ...
  "dependencies": {
    ...
    "babel-generator": "^6.9.0",
    ...
    "source-map": "^0.5.0"
  }
}
```

- Lerna checks if each dependency is also part of the Lerna repo.
  - In this example, `babel-generator` can be an internal dependency, while `source-map` is always an external dependency.
  - The version of `babel-generator` in the `package.json` of `babel-core` is satisfied by `packages/babel-generator`, passing for an internal dependency.
  - `source-map` is `npm install`ed (or `yarn`ed) like normal.
- `packages/babel-core/node_modules/babel-generator` symlinks to `packages/babel-generator`
- This allows nested directory imports

## Notes

- When a dependency version in a package is not satisfied by a package of the same name in the repo, it will be `npm install`ed (or `yarn`ed) like normal.
- Dist-tags, like `latest`, do not satisfy [semver](https://semver.npmjs.com/) ranges.
- Circular dependencies result in circular symlinks which _may_ impact your editor/IDE.

[Webstorm](https://www.jetbrains.com/webstorm/) locks up when circular symlinks are present. To prevent this, add `node_modules` to the list of ignored files and folders in `Preferences | Editor | File Types | Ignored files and folders`.


"use strict";

jest.mock("../lib/get-profile-data");

const loggingOutput = require("@lerna-test/logging-output");
const getProfileData = require("../lib/get-profile-data");
const getTwoFactorAuthRequired = require("../lib/get-two-factor-auth-required");

getProfileData.mockImplementation(() => Promise.resolve({ tfa: {} }));

expect.extend(require("@lerna-test/figgy-pudding-matchers"));

describe("getTwoFactorAuthRequired", () => {
  const origConsoleError = console.error;

  beforeEach(() => {
    console.error = jest.fn();
  });

  afterEach(() => {
    console.error = origConsoleError;
  });

  it("resolves true if tfa.mode === 'auth-and-writes'", async () => {
    getProfileData.mockResolvedValueOnce({
      tfa: {
        mode: "auth-and-writes",
      },
    });

    const result = await getTwoFactorAuthRequired();
    expect(result).toBe(true);
    expect(getProfileData).toHaveBeenLastCalledWith(expect.figgyPudding({ "fetch-retries": 0 }));
  });

  it("resolves false if tfa.mode !== 'auth-and-writes'", async () => {
    getProfileData.mockResolvedValueOnce({
      tfa: {
        mode: "auth-only",
      },
    });

    const result = await getTwoFactorAuthRequired();
    expect(result).toBe(false);
  });

  it("resolves false if tfa.pending === true", async () => {
    getProfileData.mockResolvedValueOnce({
      tfa: {
        pending: true,
        mode: "ignored",
      },
    });

    const result = await getTwoFactorAuthRequired();
    expect(result).toBe(false);
  });

  it("resolves false after profile 404", async () => {
    getProfileData.mockImplementationOnce(() => {
      const err = new Error("third-party profile fail");

      err.code = "E404";

      return Promise.reject(err);
    });

    const result = await getTwoFactorAuthRequired();

    expect(result).toBe(false);
    expect(console.error).not.toHaveBeenCalled();
  });

  it("resolves false after profile 500", async () => {
    getProfileData.mockImplementationOnce(() => {
      const err = new Error("legacy npm Enterprise profile fail");

      err.code = "E500";

      return Promise.reject(err);
    });

    const opts = new Map([["registry", "such-registry-wow"]]);
    const result = await getTwoFactorAuthRequired(opts);

    expect(result).toBe(false);
    expect(loggingOutput("warn")).toContain(
      `Registry "${opts.get(
        "registry"
      )}" does not support 'npm profile get', skipping two-factor auth check...`
    );
  });

  it("logs unexpected failure message before throwing validation error", async () => {
    getProfileData.mockImplementationOnce(() => {
      const err = new Error("zomg explosions");

      err.code = "E401";

      return Promise.reject(err);
    });

    try {
      await getTwoFactorAuthRequired();
    } catch (err) {
      expect(err.prefix).toBe("ETWOFACTOR");
      expect(err.message).toBe("Unable to obtain two-factor auth mode");
      expect(console.error).toHaveBeenCalledWith("zomg explosions");
    }

    expect.assertions(3);
  });
});


# never EVER use CRLF, Windows
* text eol=lf


"use strict";

// local modules _must_ be explicitly mocked
jest.mock("../lib/git-push");
jest.mock("../lib/is-anything-committed");
jest.mock("../lib/is-behind-upstream");
jest.mock("../lib/remote-branch-exists");

const path = require("path");

// mocked module(s)
const writePkg = require("write-pkg");

// helpers
const initFixture = require("@lerna-test/init-fixture")(path.resolve(__dirname, "../../publish/__tests__"));

// test command
const lernaVersion = require("@lerna-test/command-runner")(require("../command"));

describe("git-hosted sibling specifiers", () => {
  test("gitCommittish", async () => {
    const cwd = await initFixture("git-hosted-sibling-committish");

    await lernaVersion(cwd)("minor");

    expect(writePkg.updatedVersions()).toEqual({
      "package-1": "1.1.0",
      "package-2": "1.1.0",
      "package-3": "1.1.0",
      "package-4": "1.1.0",
      "package-5": "1.1.0",
    });

    // package-1 doesn't have any dependencies
    expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
      "package-1": "github:user/package-1#v1.1.0",
    });
    expect(writePkg.updatedManifest("package-3").devDependencies).toMatchObject({
      "package-2": "git+ssh://git@github.com/user/package-2.git#v1.1.0",
    });
    expect(writePkg.updatedManifest("package-4").dependencies).toMatchObject({
      "package-1": "github:user/package-1#v0.0.0", // non-matching semver
    });
    expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
      "package-1": "git+ssh://git@github.com/user/package-1.git#v1.1.0",
    });
  });

  test("gitRange", async () => {
    const cwd = await initFixture("git-hosted-sibling-semver");

    await lernaVersion(cwd)("prerelease", "--preid", "beta");

    expect(writePkg.updatedVersions()).toEqual({
      "package-1": "1.0.1-beta.0",
      "package-2": "1.0.1-beta.0",
      "package-3": "1.0.1-beta.0",
      "package-4": "1.0.1-beta.0",
      "package-5": "1.0.1-beta.0",
    });

    // package-1 doesn't have any dependencies
    expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
      "package-1": "github:user/package-1#semver:^1.0.1-beta.0",
    });
    expect(writePkg.updatedManifest("package-3").devDependencies).toMatchObject({
      "package-2": "git+ssh://git@github.com/user/package-2.git#semver:^1.0.1-beta.0",
    });
    expect(writePkg.updatedManifest("package-4").dependencies).toMatchObject({
      "package-1": "github:user/package-1#semver:^0.1.0", // non-matching semver
    });
    expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
      "package-1": "git+ssh://git@github.com/user/package-1.git#semver:^1.0.1-beta.0",
    });
  });

  test("gitlab", async () => {
    const cwd = await initFixture("git-hosted-sibling-gitlab");

    await lernaVersion(cwd)("premajor", "--preid", "rc");

    expect(writePkg.updatedVersions()).toEqual({
      "package-1": "2.0.0-rc.0",
      "package-2": "2.0.0-rc.0",
      "package-3": "2.0.0-rc.0",
      "package-4": "2.0.0-rc.0",
      "package-5": "2.0.0-rc.0",
    });

    // package-1 doesn't have any dependencies
    expect(writePkg.updatedManifest("package-2").dependencies).toMatchObject({
      "package-1": "gitlab:user/package-1#v2.0.0-rc.0",
    });
    expect(writePkg.updatedManifest("package-3").devDependencies).toMatchObject({
      "package-2": "git+ssh://git@gitlab.com/user/package-2.git#v2.0.0-rc.0",
    });
    expect(writePkg.updatedManifest("package-4").dependencies).toMatchObject({
      "package-1": "git+https://user:token@gitlab.com/user/package-1.git#v2.0.0-rc.0",
    });
    expect(writePkg.updatedManifest("package-5").dependencies).toMatchObject({
      "package-1": "git+ssh://git@gitlab.com/user/package-1.git#v2.0.0-rc.0",
    });
  });
});


// determine where each dependency will be installed

for (const [externalName, externalDependents] of depsToInstall) {

keep keep keep keep add keep keep keep keep keep

// We don't want to exit on the first hoist issue, but log them all and then exit

"use strict";

const filterable = require("@lerna/filter-options");

/**
 * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
 */
exports.command = "bootstrap";

exports.describe = "Link local packages together and install remaining package dependencies";

exports.builder = yargs => {
  yargs
    .example(
      "$0 bootstrap -- --no-optional",
      "# execute `npm install --no-optional` in bootstrapped packages"
    )
    .options({
      hoist: {
        group: "Command Options:",
        describe: "Install external dependencies matching [glob] to the repo root",
        defaultDescription: "'**'",
      },
      nohoist: {
        group: "Command Options:",
        describe: "Don't hoist external dependencies matching [glob] to the repo root",
        type: "string",
        requiresArg: true,
      },
      mutex: {
        hidden: true,
        // untyped and hidden on purpose
      },
      "ignore-prepublish": {
        group: "Command Options:",
        describe: "Don't run prepublish lifecycle scripts in bootstrapped packages.",
        type: "boolean",
      },
      "ignore-scripts": {
        group: "Command Options:",
        describe: "Don't run _any_ lifecycle scripts in bootstrapped packages",
        type: "boolean",
      },
      "npm-client": {
        group: "Command Options:",
        describe: "Executable used to install dependencies (npm, yarn, pnpm, ...)",
        type: "string",
        requiresArg: true,
      },
      registry: {
        group: "Command Options:",
        describe: "Use the specified registry for all npm client operations.",
        type: "string",
        requiresArg: true,
      },
    });

  return filterable(yargs);
};

exports.handler = function handler(argv) {
  return require(".")(argv);
};


"use strict";

const log = require("npmlog");
const childProcess = require("@lerna/child-process");

module.exports = gitPush;

function gitPush(remote, branch, opts) {
  log.silly("gitPush", remote, branch);

  return childProcess.exec("git", ["push", "--follow-tags", "--no-verify", remote, branch], opts);
}


`The repository root depends on ${externalName}@${rootVersion}, ` +

`which differs from the more common ${externalName}@${commonVersion}.`

const dependents = Array.from(externalDependents.get(rootVersion)).map(


# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [3.15.0](https://github.com/lerna/lerna/compare/v3.14.2...v3.15.0) (2019-06-09)

**Note:** Version bump only for package @lerna/exec

## [3.14.2](https://github.com/lerna/lerna/compare/v3.14.1...v3.14.2) (2019-06-09)

**Note:** Version bump only for package @lerna/exec

# [3.14.0](https://github.com/lerna/lerna/compare/v3.13.4...v3.14.0) (2019-05-14)

### Features

* **exec:** Add just-in-time queue management ([23736e5](https://github.com/lerna/lerna/commit/23736e5))

## [3.13.3](https://github.com/lerna/lerna/compare/v3.13.2...v3.13.3) (2019-04-17)

### Bug Fixes

* **child-process:** Centralize `exitCode` translation from string codes into numbers ([09c0103](https://github.com/lerna/lerna/commit/09c0103)), closes [#2031](https://github.com/lerna/lerna/issues/2031)
* **exec:** Handle node string error codes before setting process.exitCode ([#2031](https://github.com/lerna/lerna/issues/2031)) ([c599c64](https://github.com/lerna/lerna/commit/c599c64))

## [3.13.1](https://github.com/lerna/lerna/compare/v3.13.0...v3.13.1) (2019-02-26)

**Note:** Version bump only for package @lerna/exec

# [3.13.0](https://github.com/lerna/lerna/compare/v3.12.1...v3.13.0) (2019-02-15)

### Features

* **meta:** Add `repository.directory` field to package.json ([aec5023](https://github.com/lerna/lerna/commit/aec5023))
* **meta:** Normalize package.json `homepage` field ([abeb4dc](https://github.com/lerna/lerna/commit/abeb4dc))

## [3.12.1](https://github.com/lerna/lerna/compare/v3.12.0...v3.12.1) (2019-02-14)

**Note:** Version bump only for package @lerna/exec

# [3.12.0](https://github.com/lerna/lerna/compare/v3.11.1...v3.12.0) (2019-02-14)

**Note:** Version bump only for package @lerna/exec

# [3.11.0](https://github.com/lerna/lerna/compare/v3.10.8...v3.11.0) (2019-02-08)

**Note:** Version bump only for package @lerna/exec

## [3.10.6](https://github.com/lerna/lerna/compare/v3.10.5...v3.10.6) (2019-01-19)

### Bug Fixes

* **exec:** Tweak description ([68a0685](https://github.com/lerna/lerna/commit/68a0685))
* **options:** Document negated boolean options explicitly ([8bc9669](https://github.com/lerna/lerna/commit/8bc9669))

## [3.10.1](https://github.com/lerna/lerna/compare/v3.10.0...v3.10.1) (2019-01-09)

**Note:** Version bump only for package @lerna/exec

# [3.10.0](https://github.com/lerna/lerna/compare/v3.9.1...v3.10.0) (2019-01-08)

**Note:** Version bump only for package @lerna/exec

# [3.9.0](https://github.com/lerna/lerna/compare/v3.8.5...v3.9.0) (2019-01-08)

**Note:** Version bump only for package @lerna/exec

## [3.8.5](https://github.com/lerna/lerna/compare/v3.8.4...v3.8.5) (2019-01-05)

**Note:** Version bump only for package @lerna/exec

## [3.8.1](https://github.com/lerna/lerna/compare/v3.8.0...v3.8.1) (2018-12-31)

**Note:** Version bump only for package @lerna/exec

## [3.7.2](https://github.com/lerna/lerna/compare/v3.7.1...v3.7.2) (2018-12-21)

**Note:** Version bump only for package @lerna/exec

## [3.7.1](https://github.com/lerna/lerna/compare/v3.7.0...v3.7.1) (2018-12-20)

**Note:** Version bump only for package @lerna/exec

# [3.7.0](https://github.com/lerna/lerna/compare/v3.6.0...v3.7.0) (2018-12-19)

**Note:** Version bump only for package @lerna/exec

# [3.6.0](https://github.com/lerna/lerna/compare/v3.5.1...v3.6.0) (2018-12-07)

**Note:** Version bump only for package @lerna/exec

# [3.5.0](https://github.com/lerna/lerna/compare/v3.4.3...v3.5.0) (2018-11-27)

**Note:** Version bump only for package @lerna/exec

## [3.3.2](https://github.com/lerna/lerna/compare/v3.3.1...v3.3.2) (2018-09-12)

**Note:** Version bump only for package @lerna/exec

# [3.3.0](https://github.com/lerna/lerna/compare/v3.2.1...v3.3.0) (2018-09-06)

### Bug Fixes

* Propagate exit codes from failed executions ([af9c70b](https://github.com/lerna/lerna/commit/af9c70b)), closes [#1374](https://github.com/lerna/lerna/issues/1374) [#1653](https://github.com/lerna/lerna/issues/1653)

## [3.1.3](https://github.com/lerna/lerna/compare/v3.1.2...v3.1.3) (2018-08-21)

**Note:** Version bump only for package @lerna/exec

## [3.1.2](https://github.com/lerna/lerna/compare/v3.1.1...v3.1.2) (2018-08-20)

### Bug Fixes

* Setup instance.filteredPackages explicitly ([32357f8](https://github.com/lerna/lerna/commit/32357f8))

# [3.1.0](https://github.com/lerna/lerna/compare/v3.0.6...v3.1.0) (2018-08-17)

**Note:** Version bump only for package @lerna/exec

## [3.0.6](https://github.com/lerna/lerna/compare/v3.0.5...v3.0.6) (2018-08-16)

**Note:** Version bump only for package @lerna/exec

## [3.0.5](https://github.com/lerna/lerna/compare/v3.0.4...v3.0.5) (2018-08-15)

**Note:** Version bump only for package @lerna/exec

## [3.0.2](https://github.com/lerna/lerna/compare/v3.0.1...v3.0.2) (2018-08-11)

### Bug Fixes

* **exec:** Allow config files to override defaults ([2335149](https://github.com/lerna/lerna/commit/2335149))

# [3.0.0](https://github.com/lerna/lerna/compare/v3.0.0-rc.0...v3.0.0) (2018-08-10)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-rc.0](https://github.com/lerna/lerna/compare/v3.0.0-beta.21...v3.0.0-rc.0) (2018-07-27)

### Features

* Add description from --help summary [skip ci] ([9b65d8e](https://github.com/lerna/lerna/commit/9b65d8e))
* **cli:** Upgrade to Yargs 12 ([7899ab8](https://github.com/lerna/lerna/commit/7899ab8))
* **command:** Remove .defaultOptions() from option resolution stack ([2b27a54](https://github.com/lerna/lerna/commit/2b27a54))
* Count packages affected in command summary logging ([5f5e585](https://github.com/lerna/lerna/commit/5f5e585))

# [3.0.0-beta.21](https://github.com/lerna/lerna/compare/v3.0.0-beta.20...v3.0.0-beta.21) (2018-05-12)

### Bug Fixes

* **child-process:** Prevent duplicate logs when any package-oriented execution fails ([d3a8128](https://github.com/lerna/lerna/commit/d3a8128))

# [3.0.0-beta.20](https://github.com/lerna/lerna/compare/v3.0.0-beta.19...v3.0.0-beta.20) (2018-05-07)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.19](https://github.com/lerna/lerna/compare/v3.0.0-beta.18...v3.0.0-beta.19) (2018-05-03)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.18](https://github.com/lerna/lerna/compare/v3.0.0-beta.17...v3.0.0-beta.18) (2018-04-24)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.17](https://github.com/lerna/lerna/compare/v3.0.0-beta.16...v3.0.0-beta.17) (2018-04-13)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.15](https://github.com/lerna/lerna/compare/v3.0.0-beta.14...v3.0.0-beta.15) (2018-04-09)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.14](https://github.com/lerna/lerna/compare/v3.0.0-beta.13...v3.0.0-beta.14) (2018-04-03)

### Bug Fixes

* **logging:** Log failures from package scripts once, not twice ([436cfe1](https://github.com/lerna/lerna/commit/436cfe1))

# [3.0.0-beta.13](https://github.com/lerna/lerna/compare/v3.0.0-beta.12...v3.0.0-beta.13) (2018-03-31)

### Bug Fixes

* Use ValidationError instead of Error ([bea6bc3](https://github.com/lerna/lerna/commit/bea6bc3))

### Features

* Enable progress bars only when necessary ([b766c83](https://github.com/lerna/lerna/commit/b766c83))

# [3.0.0-beta.12](https://github.com/lerna/lerna/compare/v3.0.0-beta.11...v3.0.0-beta.12) (2018-03-30)

### Features

* Add --no-prefix for streaming output ([#1352](https://github.com/lerna/lerna/issues/1352)) ([36c1fad](https://github.com/lerna/lerna/commit/36c1fad))

# [3.0.0-beta.11](https://github.com/lerna/lerna/compare/v3.0.0-beta.10...v3.0.0-beta.11) (2018-03-29)

### Bug Fixes

* **exec:** Clarify --no-bail option ([6e4c6fd](https://github.com/lerna/lerna/commit/6e4c6fd))

# [3.0.0-beta.10](https://github.com/lerna/lerna/compare/v3.0.0-beta.9...v3.0.0-beta.10) (2018-03-27)

### Features

* **commands:** Delay require of command instantiation ([a1284f3](https://github.com/lerna/lerna/commit/a1284f3))

### BREAKING CHANGES

* **commands:** The default export of command packages is now a factory, not the subclass (which is now a named export).

# [3.0.0-beta.9](https://github.com/lerna/lerna/compare/v3.0.0-beta.8...v3.0.0-beta.9) (2018-03-24)

### Features

* **command:** Rename this.repository -> this.project ([43e98a0](https://github.com/lerna/lerna/commit/43e98a0))

# [3.0.0-beta.7](https://github.com/lerna/lerna/compare/v3.0.0-beta.6...v3.0.0-beta.7) (2018-03-20)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.4](https://github.com/lerna/lerna/compare/v3.0.0-beta.3...v3.0.0-beta.4) (2018-03-19)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.3](https://github.com/lerna/lerna/compare/v3.0.0-beta.2...v3.0.0-beta.3) (2018-03-15)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.2](https://github.com/lerna/lerna/compare/v3.0.0-beta.1...v3.0.0-beta.2) (2018-03-10)

**Note:** Version bump only for package @lerna/exec

# [3.0.0-beta.1](https://github.com/lerna/lerna/compare/v3.0.0-beta.0...v3.0.0-beta.1) (2018-03-09)

### Bug Fixes

* **filter-options:** require a git repo when using --since ([d21b66e](https://github.com/lerna/lerna/commit/d21b66e)), closes [#822](https://github.com/lerna/lerna/issues/822)


`"${leafName}" package depends on ${externalName}@${leafVersion}, ` +

`which differs from the hoisted ${externalName}@${rootVersion}.`

const leafNode = this.targetGraph.get(leafName);

const leafRecord = leaves.get(leafNode) || leaves.set(leafNode, new Set()).get(leafNode);


root: true
extends:
  - airbnb-base
  - plugin:jest/recommended
  - plugin:node/recommended
  - plugin:prettier/recommended
plugins:
  - jest
  - node
  - prettier
rules:
  class-methods-use-this:
    - error
    - exceptMethods:
        - defaultOptions
        - otherCommandConfigs
        - requiresGit
  consistent-return: off
  curly:
    - error
    - all
  max-len:
    - error
    - 110 # keep in sync with .prettierrc.yaml printWidth
    - 2
  no-param-reassign:
    - error
    - props: true
      ignorePropertyModificationsFor:
        - err # Error decoration
        - obj # .reduce() object
        - pkg # Package instance
  no-restricted-syntax:
    - error
    - ForInStatement
    - LabeledStatement
    - WithStatement
  no-underscore-dangle:
    - error
    - allowAfterThis: true
  no-use-before-define:
    - error
    - functions: false
      classes: false
  strict: off
overrides:
  - files: "commands/*/command.js"
    rules:
      global-require: off
  - files:
      - "integration/**"
      - "**/__tests__/**"
      - "**/__mocks__/**"
      - "**/__helpers__/**"
    env:
      jest: true
    rules:
      # dev dependencies are hoisted to the root
      import/no-extraneous-dependencies: off
      # mocked modules are imported before helpers
      import/order: off
      node/no-extraneous-require: off
      node/no-unpublished-require: off
      node/no-unsupported-features/es-syntax:
        - error
        - ignores:
            - asyncFunctions
            # jest transpiles fast-async
      no-console: off
  - files:
      - "test/fixtures/**"
      - "**/__fixtures__/**"
    rules:
      global-require: off
      import/no-extraneous-dependencies: off
      import/no-unresolved: off
      node/no-extraneous-require: off
      node/no-missing-require:
        - error
        - allowModules:
            - "@integration/package-1"
            - "@integration/package-2"
      no-console: off
      strict: off
  - files:
      - "utils/map-to-registry/lib/map-to-registry.js"
      - "utils/npm-conf/lib/nerf-dart.js"
    rules:
      # Allow legacy url API
      node/no-deprecated-api: off


"use strict";

const diff = require("jest-diff");
const {
  EXPECTED_COLOR,
  RECEIVED_COLOR,
  ensureNoExpected,
  matcherHint,
  printExpected,
  printReceived,
  printWithType,
} = require("jest-matcher-utils");
const { equals } = require("expect/build/jasmineUtils.js");

/**
 * Jest matchers for figgy-pudding instances
 *
 *    expect.extend(require("@lerna-test/figgy-pudding-matchers"))
 */
exports.toBeFiggyPudding = toBeFiggyPudding;
exports.toHaveFiggyPudding = toHaveFiggyPudding;
exports.figgyPudding = figgyPudding;

// expect(foo).toBeFiggyPudding();
function toBeFiggyPudding(received, expected) {
  ensureNoExpected(expected, ".toBeFiggyPudding");

  return {
    pass: isFiggyInstance(received),
    message: () =>
      `${matcherHint(".toBeFiggyPudding", "received", "", {
        isNot: this.isNot,
      })}\n\nReceived: ${printReceived(received)}`,
  };
}

// expect(foo).toHaveFiggyPudding({ ...spec });
function toHaveFiggyPudding(received, expected) {
  if (!isFiggyInstance(received)) {
    throw new Error(
      `${matcherHint("[.not].toHaveFiggyPudding", "object", "expected")}\n\n` +
        `${RECEIVED_COLOR("received")} value must be a FiggyPudding instance.\n${printWithType(
          "Received",
          received,
          printReceived
        )}`
    );
  }

  if (!isObjectWithKeys(expected)) {
    throw new Error(
      `${matcherHint("[.not].toHaveFiggyPudding", "object", "expected")}\n\n` +
        `${EXPECTED_COLOR("expected")} value must be an object.\n${printWithType(
          "Expected",
          expected,
          printExpected
        )}`
    );
  }

  const pass = hasEveryMatchingProperty(received, expected);
  const message = pass
    ? () =>
        `${matcherHint(".not.toHaveFiggyPudding")}\n\n` +
        `Expected pudding to not contain:\n` +
        `  ${printExpected(expected)}\n` +
        `Received:\n` +
        `  ${printReceived(received.toJSON())}`
    : () => {
        const figgyObj = received.toJSON();
        const diffString = diff(expected, figgyObj, {
          expand: this.expand,
        });

        return (
          `${matcherHint(".toHaveFiggyPudding")}\n\n` +
          `Expected pudding to contain:\n` +
          `  ${printExpected(expected)}\n` +
          `Received:\n` +
          `  ${printReceived(figgyObj)}${diffString ? `\n\nDifference:\n\n${diffString}` : ""}`
        );
      };

  return {
    actual: received,
    expected,
    message,
    pass,
  };
}

// expect.figgyPudding({ ...spec });
function figgyPudding(received, expected) {
  // an asymmetric matcher does not have access to `this`
  const pass = hasEveryMatchingProperty(received, expected);
  // the message is never called for .toHaveBeenCalledWith() et al.
  const message = pass
    ? () =>
        `${matcherHint(".not.figgyPudding", undefined, undefined, {
          isDirectExpectCall: true,
        })}\n\n` +
        `Expected pudding to not contain:\n` +
        `  ${printExpected(expected)}\n` +
        `Received:\n` +
        `  ${printReceived(received.toJSON())}`
    : () => {
        const figgyObj = received.toJSON();
        const diffString = diff(expected, figgyObj, {
          expand: this.expand,
        });

        return (
          `${matcherHint(".figgyPudding", undefined, undefined, {
            isDirectExpectCall: true,
          })}\n\n` +
          `Expected pudding to contain:\n` +
          `  ${printExpected(expected)}\n` +
          `Received:\n` +
          `  ${printReceived(figgyObj)}${diffString ? `\n\nDifference:\n\n${diffString}` : ""}`
        );
      };

  return { message, pass };
}

function isObjectWithKeys(obj) {
  return (
    obj !== null &&
    typeof obj === "object" &&
    !(obj instanceof Error) &&
    !(obj instanceof Array) &&
    !(obj instanceof Date)
  );
}

function hasEveryMatchingProperty(received, expected) {
  return Object.keys(expected).every(property => equals(received[property], expected[property]));
}

function isFiggyInstance(received) {
  return hasEveryMatchingProperty(received, { __isFiggyPudding: true });
}


return pMapSeries([...rootActions, ...leafActions], el => el()).then(() => {

this.logger.silly("root dependencies", JSON.stringify(rootSet, null, 2));

this.logger.silly("leaf dependencies", JSON.stringify(leaves, null, 2));

if (this.options.strict && strictExitOnWarning) {

"Package version inconsistencies found while hoisting. Fix the above warnings and retry."


Copyright (c) 2018 Tester McPerson <test@example.com>

ABC License


"use strict";

const path = require("path");
const yargs = require("yargs/yargs");

// mocked modules
const collectUpdates = require("@lerna/collect-updates");

// helpers
const initFixture = require("@lerna-test/init-fixture")(path.resolve(__dirname, "../../command"));
const PackageGraph = require("@lerna/package-graph");
const { getPackages } = require("@lerna/project");

const getFilteredPackages = require("../lib/get-filtered-packages");
const filterOptions = require("..");

async function buildGraph(cwd) {
  const packages = await getPackages(cwd);
  return new PackageGraph(packages);
}

function parseOptions(...args) {
  return filterOptions(
    yargs()
      .exitProcess(false)
      .showHelpOnFail(false)
  ).parse(args);
}

// working dir is never mutated
let cwd;

beforeAll(async () => {
  cwd = await initFixture("filtering");
});

test.each`
  flag
  ${"--scope"}
  ${"--ignore"}
`("$flag requires an argument", async ({ flag }) => {
  // swallow stderr during yargs execution
  jest.spyOn(console, "error").mockImplementation(() => {});

  try {
    parseOptions(flag);
  } catch (err) {
    expect(err.message).toMatch("Not enough arguments");
  }

  expect.hasAssertions();
});

test.each`
  argv                                                      | matched
  ${["--scope", "package-3"]}                               | ${[3]}
  ${["--scope", "package-@(1|2)"]}                          | ${[1, 2]}
  ${["--scope", "package-{3,4}"]}                           | ${[3, 4]}
  ${["--ignore", "package-3"]}                              | ${[1, 2, 4, 5]}
  ${["--ignore", "package-@(2|3|4)"]}                       | ${[1, 5]}
  ${["--ignore", "package-{1,2,5}"]}                        | ${[3, 4]}
  ${["--scope", "package-1", "--scope", "*-2"]}             | ${[1, 2]}
  ${["--scope", "package-@(1|2)", "--ignore", "package-2"]} | ${[1]}
  ${["--ignore", "package-{1,2}", "--ignore", "package-3"]} | ${[4, 5]}
`("filters $argv", async ({ argv, matched }) => {
  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions(...argv);

  const result = await getFilteredPackages(packageGraph, execOpts, options);
  expect(result.map(node => node.name)).toEqual(matched.map(n => `package-${n}`));
});

test.each`
  argv
  ${["--scope", "not-a-package"]}
  ${["--ignore", "package-*"]}
  ${["--scope", "package-@(1|2)", "--ignore", "package-{1,2}"]}
`("errors $argv", async ({ argv }) => {
  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions(...argv);

  try {
    await getFilteredPackages(packageGraph, execOpts, options);
  } catch (err) {
    expect(err.message).toMatch("No packages remain after filtering");
  }

  expect.hasAssertions();
});

test("--since returns all packages if no tag is found", async () => {
  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions("--since");

  const result = await getFilteredPackages(packageGraph, execOpts, options);

  expect(result).toHaveLength(5);
  expect(collectUpdates).toHaveBeenLastCalledWith(
    expect.any(Array),
    packageGraph,
    execOpts,
    expect.objectContaining({ since: "" })
  );
});

test("--since returns packages updated since the last tag", async () => {
  collectUpdates.setUpdated(cwd, "package-2", "package-3");

  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions("--since");

  const result = await getFilteredPackages(packageGraph, execOpts, options);

  expect(result.map(node => node.name)).toEqual(["package-2", "package-3"]);
});

test("--since <ref> should return packages updated since <ref>", async () => {
  collectUpdates.setUpdated(cwd, "package-1", "package-2", "package-3");

  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions("--since", "deadbeef");

  const result = await getFilteredPackages(packageGraph, execOpts, options);

  expect(result.map(node => node.name)).toEqual(["package-1", "package-2", "package-3"]);
  expect(collectUpdates).toHaveBeenLastCalledWith(
    expect.any(Array),
    packageGraph,
    execOpts,
    expect.objectContaining({ since: "deadbeef" })
  );
});

test("--scope package-{2,3,4} --since master", async () => {
  collectUpdates.setUpdated(cwd, "package-4", "package-1");

  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions("--scope", "package-{2,3,4}", "--since", "master");

  const result = await getFilteredPackages(packageGraph, execOpts, options);

  expect(result.map(node => node.name)).toEqual(["package-4"]);
  expect(collectUpdates).toHaveBeenLastCalledWith(
    // filter-packages before collect-updates
    [2, 3, 4].map(n => packageGraph.get(`package-${n}`).pkg),
    packageGraph,
    execOpts,
    expect.objectContaining({ since: "master" })
  );
});

test("--include-filtered-dependents", async () => {
  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions("--scope", "package-1", "--include-filtered-dependents");

  const result = await getFilteredPackages(packageGraph, execOpts, options);

  expect(result.map(pkg => pkg.name)).toEqual(["package-1", "package-2", "package-5", "package-3"]);
  expect(collectUpdates).not.toHaveBeenCalled();
});

test("--include-filtered-dependencies", async () => {
  const packageGraph = await buildGraph(cwd);
  const execOpts = { cwd };
  const options = parseOptions("--scope", "package-3", "--include-filtered-dependencies");

  const result = await getFilteredPackages(packageGraph, execOpts, options);

  expect(result.map(pkg => pkg.name)).toEqual(["package-3", "package-2", "package-1"]);
  expect(collectUpdates).not.toHaveBeenCalled();
});


{
"name": "react-hook-form",
"version": "3.6.0",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",

import { useRef, useState, useEffect } from 'react';
import getFieldsValues from './logic/getFieldsValues';
import getFieldValue from './logic/getFieldValue';
import validateField from './logic/validateField';
import findRemovedFieldAndRemoveListener from './logic/findRemovedFieldAndRemoveListener';
import attachEventListeners from './logic/attachEventListeners';
import validateWithSchema from './logic/validateWithSchema';
import combineFieldValues from './logic/combineFieldValues';
import shouldUpdateWithError from './logic/shouldUpdateWithError';
import warnMissingRef from './utils/warnMissingRef';
import modeChecker from './utils/validationModeChecker';
import onDomRemove from './utils/onDomRemove';
import isRadioInput from './utils/isRadioInput';
import isEmptyObject from './utils/isEmptyObject';
import filterUndefinedErrors from './utils/filterUndefinedErrors';
import {
  Props,
  Field,
  ErrorMessages,
  Ref,
  SubmitPromiseResult,
  FieldsObject,
  VoidFunction,
  UseFormFunctions,
  RegisterFunction,
  FieldValue,
  RegisterInput,
} from './types';
import isCheckBoxInput from './utils/isCheckBoxInput';

export default function useForm(
  { mode, validationSchema, defaultValues }: Props = {
    mode: 'onSubmit',
    defaultValues: {},
  },
): UseFormFunctions {
  const fieldsRef = useRef<FieldsObject>({});
  const errorsRef = useRef<ErrorMessages>({});
  const submitCountRef = useRef<number>(0);
  const touchedFieldsRef = useRef<string[]>([]);
  const watchFieldsRef = useRef<{ [key: string]: boolean }>({});
  const isWatchAllRef = useRef<boolean>(false);
  const isSubmittingRef = useRef<boolean>(false);
  const isSubmittedRef = useRef<boolean>(false);
  const isDirtyRef = useRef<boolean>(false);
  const reRenderForm = useState({})[1];
  const validateAndStateUpdateRef = useRef<Function>();

  validateAndStateUpdateRef.current = validateAndStateUpdateRef.current
    ? validateAndStateUpdateRef.current
    : async ({ target: { name }, type }: Ref): Promise<void> => {
        const fields = fieldsRef.current;
        const errors = errorsRef.current;
        const ref = fields[name];
        const isBlurType = type === 'blur';
        const { isOnChange, isOnSubmit, isOnBlur } = modeChecker(mode);
        const onSubmitModeNotSubmitted = !isSubmittedRef.current && (isOnSubmit || !mode);
        const isWatchAll = isWatchAllRef.current;
        const shouldUpdateWatchMode = isWatchAll || watchFieldsRef.current[name];
        const shouldUpdateValidateMode = isOnChange || (isOnBlur && isBlurType);
        let shouldUpdateState = isWatchAll;

        if (!isDirtyRef.current) {
          isDirtyRef.current = true;
          shouldUpdateState = true;
        }

        if (!touchedFieldsRef.current.includes(name)) {
          touchedFieldsRef.current.push(name);
          shouldUpdateState = true;
        }

        if (onSubmitModeNotSubmitted && shouldUpdateWatchMode) return reRenderForm({});

        if (validationSchema) {
          const result = getFieldsValues(fields);
          const schemaValidateErrors = (await validateWithSchema(validationSchema, result)) || {};
          const error = schemaValidateErrors[name];
          const shouldUpdate = ((!error && errors[name]) || error) && shouldUpdateValidateMode;

          if (shouldUpdate || shouldUpdateWatchMode) {
            const errorsCopy = { ...errors, ...{ [name]: error } };
            if (!error) delete errorsCopy[name];

            errorsRef.current = errorsCopy;
            return reRenderForm({});
          }
        } else {
          const error = await validateField(ref, fields);
          const shouldUpdate = shouldUpdateWithError({
            errors,
            error,
            onSubmitModeNotSubmitted,
            isOnBlur,
            isBlurType,
            name,
          });

          if (shouldUpdate || shouldUpdateValidateMode || shouldUpdateWatchMode) {
            const errorsCopy = { ...filterUndefinedErrors(errors), ...error };
            if (!error[name]) delete errorsCopy[name];

            errorsRef.current = errorsCopy;
            return reRenderForm({});
          }
        }

        if (shouldUpdateState) reRenderForm({});
      };

  const removeEventListener: Function = findRemovedFieldAndRemoveListener.bind(
    null,
    fieldsRef.current,
    validateAndStateUpdateRef.current,
  );

  function registerIntoFieldsRef(elementRef, data = { required: false, validate: undefined }): void {
    if (elementRef && !elementRef.name) return warnMissingRef(elementRef);

    const { name, type, value } = elementRef;
    const { required, validate } = data;
    const inputData = {
      ...data,
      ref: elementRef,
    };
    const fields = fieldsRef.current;
    const isRadio = isRadioInput(type);
    const field = fields[name];
    const existRadioOptionIndex =
      isRadio && field && Array.isArray(field.options)
        ? field.options.findIndex(({ ref }): boolean => value === ref.value)
        : -1;

    if ((!isRadio && field) || (isRadio && existRadioOptionIndex > -1)) return;
    if (defaultValues && defaultValues[name]) {
      elementRef[isCheckBoxInput(type) ? 'checked' : 'value'] = defaultValues[name];
    }

    if (isRadio) {
      if (!field) fields[name] = { options: [], required, validate, ref: { type: 'radio', name } };
      if (validate) fields[name].validate = validate;

      (fields[name].options || []).push({
        ...inputData,
        mutationWatcher: onDomRemove(elementRef, (): Function => removeEventListener(inputData, true)),
      });
    } else {
      fields[name] = {
        ...inputData,
        mutationWatcher: onDomRemove(elementRef, (): Function => removeEventListener(inputData, true)),
      };
    }

    attachEventListeners({
      field: isRadio ? (fields[name].options || [])[(fields[name].options || []).length - 1] : fields[name],
      isRadio,
      validateAndStateUpdate: validateAndStateUpdateRef.current,
    });
  }

  function watch(
    filedNames?: string | string[] | undefined,
    defaultValue?: string | string[] | undefined,
  ): FieldValue | FieldValue[] | undefined {
    const watchFields = watchFieldsRef.current;

    if (typeof filedNames === 'string') {
      watchFields[filedNames] = true;
    } else if (Array.isArray(filedNames)) {
      filedNames.forEach(
        (name): void => {
          watchFields[name] = true;
        },
      );
    } else {
      isWatchAllRef.current = true;
      watchFieldsRef.current = {};
    }

    const values = getFieldsValues(fieldsRef.current, filedNames);
    const result = values === undefined || isEmptyObject(values) ? undefined : values;
    return result === undefined ? defaultValue : result;
  }

  function register(data: Ref | RegisterInput): RegisterFunction | undefined {
    if (!data) return;
    if (data.type) {
      if (!data.name) {
        warnMissingRef(data);
        return;
      }
      registerIntoFieldsRef(data);
    }

    return (ref): void => ref && registerIntoFieldsRef(ref, data);
  }

  const handleSubmit = (callback: (Object, e) => void): Function => async (e): Promise<void> => {
    if (e) {
      e.preventDefault();
      e.persist();
    }
    let fieldErrors;
    let fieldValues;
    const fields = fieldsRef.current;
    const currentFieldValues = Object.values(fields);
    isSubmittingRef.current = true;
    reRenderForm({});

    if (validationSchema) {
      fieldValues = getFieldsValues(fields);
      fieldErrors = await validateWithSchema(validationSchema, fieldValues);
    } else {
      const { errors, values }: SubmitPromiseResult = await currentFieldValues.reduce(
        async (previous: Promise<SubmitPromiseResult>, field: Field): Promise<SubmitPromiseResult> => {
          const resolvedPrevious = await previous;
          const {
            ref,
            ref: { name },
          } = field;

          if (!fields[name]) return Promise.resolve(resolvedPrevious);

          const fieldError = await validateField(field, fields);

          if (fieldError[name]) {
            resolvedPrevious.errors = { ...(resolvedPrevious.errors || {}), ...fieldError };
            return Promise.resolve(resolvedPrevious);
          }

          resolvedPrevious.values[name] = getFieldValue(fields, ref);
          return Promise.resolve(resolvedPrevious);
        },
        Promise.resolve({
          errors: {},
          values: {},
        }),
      );

      fieldErrors = {
        ...errors,
        ...filterUndefinedErrors(errorsRef.current),
      };
      fieldValues = values;
    }

    isSubmittedRef.current = true;
    submitCountRef.current += 1;
    isSubmittingRef.current = false;

    if (isEmptyObject(fieldErrors)) {
      callback(combineFieldValues(fieldValues), e);
    } else {
      errorsRef.current = fieldErrors;
      reRenderForm({});
    }
  };

  const unSubscribe = (): void => {
    fieldsRef.current &&
      Object.values(fieldsRef.current).forEach(
        (field: Field): void => {
          const { ref, options } = field;
          isRadioInput(ref.type) && Array.isArray(options)
            ? options.forEach((fieldRef): void => removeEventListener(fieldRef, true))
            : removeEventListener(field, true);
        },
      );
    fieldsRef.current = {};
    watchFieldsRef.current = {};
    errorsRef.current = {};
    isWatchAllRef.current = false;
    isSubmittedRef.current = false;
    isDirtyRef.current = false;
    touchedFieldsRef.current = [];
  };

  const reset = (): void => {
    Object.values(fieldsRef.current)[0]
      .ref.closest('form')
      .reset();
    unSubscribe();
    reRenderForm({});
  };

  const setError = (name: string, type: string, message?: string, ref?: Ref): void => {
    errorsRef.current[name] = {
      type,
      message,
      ref,
      isManual: true,
    };
    reRenderForm({});
  };

  useEffect((): VoidFunction => unSubscribe, [mode]);

  return {
    register,
    handleSubmit,
    watch,
    unSubscribe,
    reset,
    setError,
    errors: errorsRef.current,
    formState: {
      dirty: isDirtyRef.current,
      isSubmitted: isSubmittedRef.current,
      submitCount: submitCountRef.current,
      touched: touchedFieldsRef.current,
      isSubmitting: isSubmittingRef.current,
    },
  };
}


import typescript from 'rollup-plugin-typescript2';

export default {
  input: 'src/index.ts',
  plugins: [
    typescript(),
  ],
  external: ['react'],
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs',
    },
    {
      file: 'dist/index.es.js',
      format: 'es',
    },
  ],
};


import React from 'react';
import ReactDOM from 'react-dom';
import useForm from 'react-hook-form';
import * as yup from 'yup'; // you will have to install yup

const SignupSchema = yup.object().shape({
  firstName: yup.string().required(),
  age: yup
    .number()
    .required()
    .positive()
    .integer(),
  website: yup.string().url(),
});

function App() {
  const { register, handleSubmit, errors } = useForm({
    validationSchema: SignupSchema,
  });
  const onSubmit = data => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>

        Develop?
        Yes
        <input type="radio" name="test" value="yes" ref={register} />
        No
        <input type="radio" name="test" value="no" ref={register} />

        First Name
        <input type="text" name="firstName" ref={register} />

        Last Name
        <input type="text" name="lastName" ref={register} />

        Age
        <input type="text" name="age" ref={register} />

        Website
        <input type="text" name="website" ref={register} />

{Object.keys(errors).length > 0 && Errors: {JSON.stringify(errors, null, 2)}}

      <input type="submit" />
    </form>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);


# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
example/node_modules/
example/build/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

.DS_Store

/lib
/dist
.idea/
/coverage
.rpt2_cache
.coveralls.yml


function registerIntoFieldsRef(elementRef, data = { required: false, validate: undefined }): void {

if (elementRef && !elementRef.name) return warnMissingRef(elementRef);

const { name, type, value } = elementRef;

const setValue = (name: string, value: string | number | boolean): void => {

if (elm.type) elm[isCheckBoxInput(elm.type) ? 'checked' : 'value'] = value;