123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593 |
- "use strict";
- const dedent = require("dedent");
- const getPort = require("get-port");
- const npa = require("npm-package-arg");
- const path = require("path");
- 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, npmInstallDependencies } = require("@lerna/npm-install");
- const { createRunner } = require("@lerna/run-lifecycle");
- const { runTopologically } = require("@lerna/run-topologically");
- 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(() => {
- return getFilteredPackages(this.targetGraph, this.execOpts, this.options);
- });
- chain = chain.then((filteredPackages) => {
- this.filteredPackages = filteredPackages;
- if (this.options.contents) {
- // globally override directory to link
- for (const pkg of filteredPackages) {
- pkg.contents = this.options.contents;
- }
- }
- 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(() => {
- 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);
- const runner = this.toposort
- ? runTopologically(this.filteredPackages, mapPackageWithScript, {
- concurrency: this.concurrency,
- rejectCycles: this.options.rejectCycles,
- })
- : pMap(this.filteredPackages, mapPackageWithScript, { concurrency: this.concurrency });
- return runner.finally(() => 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 = [];
- // We don't want to exit on the first hoist issue, but log them all and then exit
- let strictExitOnWarning = false;
- // 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}.`
- );
- if (this.options.strict) {
- strictExitOnWarning = true;
- }
- }
- 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}.`
- );
- if (this.options.strict) {
- strictExitOnWarning = true;
- }
- }
- 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,
- });
- })
- );
- }
- }
- }
- if (this.options.strict && strictExitOnWarning) {
- throw new ValidationError(
- "EHOISTSTRICT",
- "Package version inconsistencies found while hoisting. Fix the above warnings and retry."
- );
- }
- 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 = npmInstallDependencies(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 = npmInstallDependencies(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 pMap(actions, (act) => act(), { concurrency: this.concurrency }).finally(() => 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;
|