index.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. "use strict";
  2. const dedent = require("dedent");
  3. const npa = require("npm-package-arg");
  4. const pMap = require("p-map");
  5. const path = require("path");
  6. const pacote = require("pacote");
  7. const semver = require("semver");
  8. const { Command } = require("@lerna/command");
  9. const npmConf = require("@lerna/npm-conf");
  10. const bootstrap = require("@lerna/bootstrap");
  11. const { ValidationError } = require("@lerna/validation-error");
  12. const { getFilteredPackages } = require("@lerna/filter-options");
  13. const { getRangeToReference } = require("./lib/get-range-to-reference");
  14. module.exports = factory;
  15. function factory(argv) {
  16. return new AddCommand(argv);
  17. }
  18. class AddCommand extends Command {
  19. get requiresGit() {
  20. return false;
  21. }
  22. get dependencyType() {
  23. if (this.options.dev) {
  24. return "devDependencies";
  25. }
  26. if (this.options.peer) {
  27. return "peerDependencies";
  28. }
  29. return "dependencies";
  30. }
  31. initialize() {
  32. this.spec = npa(this.options.pkg);
  33. this.dirs = new Set(this.options.globs.map((fp) => path.resolve(this.project.rootPath, fp)));
  34. this.selfSatisfied = this.packageSatisfied();
  35. // https://docs.npmjs.com/misc/config#save-prefix
  36. this.savePrefix = this.options.exact ? "" : "^";
  37. if (this.packageGraph.has(this.spec.name) && !this.selfSatisfied) {
  38. const available = this.packageGraph.get(this.spec.name).version;
  39. throw new ValidationError(
  40. "ENOTSATISFIED",
  41. dedent`
  42. Requested range not satisfiable:
  43. ${this.spec.name}@${this.spec.fetchSpec} (available: ${available})
  44. `
  45. );
  46. }
  47. let chain = Promise.resolve();
  48. chain = chain.then(() => this.getPackageVersion());
  49. chain = chain.then((version) => {
  50. if (version == null) {
  51. throw new ValidationError(
  52. "ENOTSATISFIED",
  53. dedent`
  54. Requested package has no version: ${this.spec.name}
  55. `
  56. );
  57. }
  58. this.spec.version = version;
  59. });
  60. chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
  61. chain = chain.then((filteredPackages) => {
  62. this.filteredPackages = filteredPackages;
  63. });
  64. chain = chain.then(() => this.collectPackagesToChange());
  65. chain = chain.then((packagesToChange) => {
  66. this.packagesToChange = packagesToChange;
  67. });
  68. return chain.then(() => {
  69. const proceed = this.packagesToChange.length > 0;
  70. if (!proceed) {
  71. this.logger.warn(`No packages found where ${this.spec.name} can be added.`);
  72. }
  73. return proceed;
  74. });
  75. }
  76. execute() {
  77. const numberOfPackages = `${this.packagesToChange.length} package${
  78. this.packagesToChange.length > 1 ? "s" : ""
  79. }`;
  80. this.logger.info("", `Adding ${this.spec.name} in ${numberOfPackages}`);
  81. let chain = Promise.resolve();
  82. chain = chain.then(() => this.makeChanges());
  83. if (this.options.bootstrap !== false) {
  84. chain = chain.then(() => {
  85. const argv = Object.assign({}, this.options, {
  86. args: [],
  87. cwd: this.project.rootPath,
  88. // silence initial cli version logging, etc
  89. composed: "add",
  90. // NEVER pass filter-options, it is very bad
  91. scope: undefined,
  92. ignore: undefined,
  93. private: undefined,
  94. since: undefined,
  95. excludeDependents: undefined,
  96. includeDependents: undefined,
  97. includeDependencies: undefined,
  98. });
  99. return bootstrap(argv);
  100. });
  101. }
  102. return chain;
  103. }
  104. collectPackagesToChange() {
  105. const { name: targetName } = this.spec;
  106. let result = this.filteredPackages;
  107. // Skip packages that only would install themselves
  108. if (this.packageGraph.has(targetName)) {
  109. result = result.filter((pkg) => pkg.name !== targetName);
  110. }
  111. // Skip packages that are not selected by dir globs
  112. if (this.dirs.size) {
  113. result = result.filter((pkg) => this.dirs.has(pkg.location));
  114. }
  115. // Skip packages without actual changes to manifest
  116. result = result.filter((pkg) => {
  117. const deps = this.getPackageDeps(pkg);
  118. // Check if one of the packages to install necessitates a change to pkg's manifest
  119. if (!(targetName in deps)) {
  120. return true;
  121. }
  122. return getRangeToReference(this.spec, deps, pkg.location, this.savePrefix) !== deps[targetName];
  123. });
  124. return result;
  125. }
  126. makeChanges() {
  127. const { name: targetName } = this.spec;
  128. return pMap(this.packagesToChange, (pkg) => {
  129. const deps = this.getPackageDeps(pkg);
  130. const range = getRangeToReference(this.spec, deps, pkg.location, this.savePrefix);
  131. this.logger.verbose("add", `${targetName}@${range} to ${this.dependencyType} in ${pkg.name}`);
  132. deps[targetName] = range;
  133. return pkg.serialize();
  134. });
  135. }
  136. getPackageDeps(pkg) {
  137. let deps = pkg.get(this.dependencyType);
  138. if (!deps) {
  139. deps = {};
  140. pkg.set(this.dependencyType, deps);
  141. }
  142. return deps;
  143. }
  144. getPackageVersion() {
  145. if (this.selfSatisfied) {
  146. const node = this.packageGraph.get(this.spec.name);
  147. return Promise.resolve(this.spec.saveRelativeFileSpec ? node.location : node.version);
  148. }
  149. // @see https://github.com/zkat/pacote/blob/latest/lib/util/opt-check.js
  150. const opts = npmConf({
  151. includeDeprecated: false,
  152. // we can't pass everything, as our --scope conflicts with pacote's --scope
  153. registry: this.options.registry,
  154. });
  155. return pacote.manifest(this.spec, opts.snapshot).then((pkg) => pkg.version);
  156. }
  157. packageSatisfied() {
  158. const { name, fetchSpec } = this.spec;
  159. const pkg = this.packageGraph.get(name);
  160. if (!pkg) {
  161. return false;
  162. }
  163. // an explicit "file:packages/foo" always saves as a relative "file:../foo"
  164. if (this.spec.type === "directory" && fetchSpec === pkg.location) {
  165. this.spec.saveRelativeFileSpec = true;
  166. return true;
  167. }
  168. // existing relative file spec means local dep should be added the same way
  169. this.spec.saveRelativeFileSpec = Array.from(this.packageGraph.values()).some(
  170. (node) =>
  171. node.localDependencies.size &&
  172. Array.from(node.localDependencies.values()).some((resolved) => resolved.type === "directory")
  173. );
  174. if (fetchSpec === "latest") {
  175. return true;
  176. }
  177. return semver.intersects(pkg.version, fetchSpec);
  178. }
  179. }
  180. module.exports.AddCommand = AddCommand;