index.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. "use strict";
  2. const cloneDeep = require("clone-deep");
  3. const dedent = require("dedent");
  4. const execa = require("execa");
  5. const log = require("npmlog");
  6. const os = require("os");
  7. const { PackageGraph } = require("@lerna/package-graph");
  8. const { Project } = require("@lerna/project");
  9. const { writeLogFile } = require("@lerna/write-log-file");
  10. const { ValidationError } = require("@lerna/validation-error");
  11. const { cleanStack } = require("./lib/clean-stack");
  12. const { defaultOptions } = require("./lib/default-options");
  13. const { logPackageError } = require("./lib/log-package-error");
  14. const { warnIfHanging } = require("./lib/warn-if-hanging");
  15. const DEFAULT_CONCURRENCY = os.cpus().length;
  16. class Command {
  17. constructor(_argv) {
  18. log.pause();
  19. log.heading = "lerna";
  20. const argv = cloneDeep(_argv);
  21. log.silly("argv", argv);
  22. // "FooCommand" => "foo"
  23. this.name = this.constructor.name.replace(/Command$/, "").toLowerCase();
  24. // composed commands are called from other commands, like publish -> version
  25. this.composed = typeof argv.composed === "string" && argv.composed !== this.name;
  26. if (!this.composed) {
  27. // composed commands have already logged the lerna version
  28. log.notice("cli", `v${argv.lernaVersion}`);
  29. }
  30. // launch the command
  31. let runner = new Promise((resolve, reject) => {
  32. // run everything inside a Promise chain
  33. let chain = Promise.resolve();
  34. chain = chain.then(() => {
  35. this.project = new Project(argv.cwd);
  36. });
  37. chain = chain.then(() => this.configureEnvironment());
  38. chain = chain.then(() => this.configureOptions());
  39. chain = chain.then(() => this.configureProperties());
  40. chain = chain.then(() => this.configureLogging());
  41. chain = chain.then(() => this.runValidations());
  42. chain = chain.then(() => this.runPreparations());
  43. chain = chain.then(() => this.runCommand());
  44. chain.then(
  45. (result) => {
  46. warnIfHanging();
  47. resolve(result);
  48. },
  49. (err) => {
  50. if (err.pkg) {
  51. // Cleanly log specific package error details
  52. logPackageError(err, this.options.stream);
  53. } else if (err.name !== "ValidationError") {
  54. // npmlog does some funny stuff to the stack by default,
  55. // so pass it directly to avoid duplication.
  56. log.error("", cleanStack(err, this.constructor.name));
  57. }
  58. // ValidationError does not trigger a log dump, nor do external package errors
  59. if (err.name !== "ValidationError" && !err.pkg) {
  60. writeLogFile(this.project.rootPath);
  61. }
  62. warnIfHanging();
  63. // error code is handled by cli.fail()
  64. reject(err);
  65. }
  66. );
  67. });
  68. // passed via yargs context in tests, never actual CLI
  69. /* istanbul ignore else */
  70. if (argv.onResolved || argv.onRejected) {
  71. runner = runner.then(argv.onResolved, argv.onRejected);
  72. // when nested, never resolve inner with outer callbacks
  73. delete argv.onResolved; // eslint-disable-line no-param-reassign
  74. delete argv.onRejected; // eslint-disable-line no-param-reassign
  75. }
  76. // "hide" irrelevant argv keys from options
  77. for (const key of ["cwd", "$0"]) {
  78. Object.defineProperty(argv, key, { enumerable: false });
  79. }
  80. Object.defineProperty(this, "argv", {
  81. value: Object.freeze(argv),
  82. });
  83. Object.defineProperty(this, "runner", {
  84. value: runner,
  85. });
  86. }
  87. // proxy "Promise" methods to "private" instance
  88. then(onResolved, onRejected) {
  89. return this.runner.then(onResolved, onRejected);
  90. }
  91. /* istanbul ignore next */
  92. catch(onRejected) {
  93. return this.runner.catch(onRejected);
  94. }
  95. get requiresGit() {
  96. return true;
  97. }
  98. // Override this to inherit config from another command.
  99. // For example `changed` inherits config from `publish`.
  100. get otherCommandConfigs() {
  101. return [];
  102. }
  103. configureEnvironment() {
  104. // eslint-disable-next-line global-require
  105. const ci = require("is-ci");
  106. let loglevel;
  107. let progress;
  108. /* istanbul ignore next */
  109. if (ci || !process.stderr.isTTY) {
  110. log.disableColor();
  111. progress = false;
  112. } else if (!process.stdout.isTTY) {
  113. // stdout is being piped, don't log non-errors or progress bars
  114. progress = false;
  115. loglevel = "error";
  116. } else if (process.stderr.isTTY) {
  117. log.enableColor();
  118. log.enableUnicode();
  119. }
  120. Object.defineProperty(this, "envDefaults", {
  121. value: {
  122. ci,
  123. progress,
  124. loglevel,
  125. },
  126. });
  127. }
  128. configureOptions() {
  129. // Command config object normalized to "command" namespace
  130. const commandConfig = this.project.config.command || {};
  131. // The current command always overrides otherCommandConfigs
  132. const overrides = [this.name, ...this.otherCommandConfigs].map((key) => commandConfig[key]);
  133. this.options = defaultOptions(
  134. // CLI flags, which if defined overrule subsequent values
  135. this.argv,
  136. // Namespaced command options from `lerna.json`
  137. ...overrides,
  138. // Global options from `lerna.json`
  139. this.project.config,
  140. // Environmental defaults prepared in previous step
  141. this.envDefaults
  142. );
  143. }
  144. configureProperties() {
  145. const { concurrency, sort, maxBuffer } = this.options;
  146. this.concurrency = Math.max(1, +concurrency || DEFAULT_CONCURRENCY);
  147. this.toposort = sort === undefined || sort;
  148. /** @type {import("@lerna/child-process").ExecOpts} */
  149. this.execOpts = {
  150. cwd: this.project.rootPath,
  151. maxBuffer,
  152. };
  153. }
  154. configureLogging() {
  155. const { loglevel } = this.options;
  156. if (loglevel) {
  157. log.level = loglevel;
  158. }
  159. // handle log.success()
  160. log.addLevel("success", 3001, { fg: "green", bold: true });
  161. // create logger that subclasses use
  162. Object.defineProperty(this, "logger", {
  163. value: log.newGroup(this.name),
  164. });
  165. // emit all buffered logs at configured level and higher
  166. log.resume();
  167. }
  168. enableProgressBar() {
  169. /* istanbul ignore next */
  170. if (this.options.progress !== false) {
  171. log.enableProgress();
  172. }
  173. }
  174. gitInitialized() {
  175. const opts = {
  176. cwd: this.project.rootPath,
  177. // don't throw, just want boolean
  178. reject: false,
  179. // only return code, no stdio needed
  180. stdio: "ignore",
  181. };
  182. return execa.sync("git", ["rev-parse"], opts).exitCode === 0;
  183. }
  184. runValidations() {
  185. if ((this.options.since !== undefined || this.requiresGit) && !this.gitInitialized()) {
  186. throw new ValidationError("ENOGIT", "The git binary was not found, or this is not a git repository.");
  187. }
  188. if (!this.project.manifest) {
  189. throw new ValidationError("ENOPKG", "`package.json` does not exist, have you run `lerna init`?");
  190. }
  191. if (!this.project.version) {
  192. throw new ValidationError("ENOLERNA", "`lerna.json` does not exist, have you run `lerna init`?");
  193. }
  194. if (this.options.independent && !this.project.isIndependent()) {
  195. throw new ValidationError(
  196. "EVERSIONMODE",
  197. dedent`
  198. You ran lerna with --independent or -i, but the repository is not set to independent mode.
  199. To use independent mode you need to set lerna.json's "version" property to "independent".
  200. Then you won't need to pass the --independent or -i flags.
  201. `
  202. );
  203. }
  204. }
  205. runPreparations() {
  206. if (!this.composed && this.project.isIndependent()) {
  207. // composed commands have already logged the independent status
  208. log.info("versioning", "independent");
  209. }
  210. if (!this.composed && this.options.ci) {
  211. log.info("ci", "enabled");
  212. }
  213. let chain = Promise.resolve();
  214. chain = chain.then(() => this.project.getPackages());
  215. chain = chain.then((packages) => {
  216. this.packageGraph = new PackageGraph(packages);
  217. });
  218. return chain;
  219. }
  220. runCommand() {
  221. return Promise.resolve()
  222. .then(() => this.initialize())
  223. .then((proceed) => {
  224. if (proceed !== false) {
  225. return this.execute();
  226. }
  227. // early exits set their own exitCode (if non-zero)
  228. });
  229. }
  230. initialize() {
  231. throw new ValidationError(this.name, "initialize() needs to be implemented.");
  232. }
  233. execute() {
  234. throw new ValidationError(this.name, "execute() needs to be implemented.");
  235. }
  236. }
  237. module.exports.Command = Command;