index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. "use strict";
  2. const pMap = require("p-map");
  3. const childProcess = require("@lerna/child-process");
  4. const { Command } = require("@lerna/command");
  5. const { Profiler } = require("@lerna/profiler");
  6. const { runTopologically } = require("@lerna/run-topologically");
  7. const { ValidationError } = require("@lerna/validation-error");
  8. const { getFilteredPackages } = require("@lerna/filter-options");
  9. module.exports = factory;
  10. function factory(argv) {
  11. return new ExecCommand(argv);
  12. }
  13. class ExecCommand extends Command {
  14. get requiresGit() {
  15. return false;
  16. }
  17. initialize() {
  18. const dashedArgs = this.options["--"] || [];
  19. this.command = this.options.cmd || dashedArgs.shift();
  20. this.args = (this.options.args || []).concat(dashedArgs);
  21. if (!this.command) {
  22. throw new ValidationError("ENOCOMMAND", "A command to execute is required");
  23. }
  24. // inverted boolean options
  25. this.bail = this.options.bail !== false;
  26. this.prefix = this.options.prefix !== false;
  27. // accessing properties of process.env can be expensive,
  28. // so cache it here to reduce churn during tighter loops
  29. this.env = Object.assign({}, process.env);
  30. let chain = Promise.resolve();
  31. chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
  32. chain = chain.then((filteredPackages) => {
  33. this.filteredPackages = filteredPackages;
  34. });
  35. return chain.then(() => {
  36. this.count = this.filteredPackages.length;
  37. this.packagePlural = this.count === 1 ? "package" : "packages";
  38. this.joinedCommand = [this.command].concat(this.args).join(" ");
  39. });
  40. }
  41. execute() {
  42. this.logger.info(
  43. "",
  44. "Executing command in %d %s: %j",
  45. this.count,
  46. this.packagePlural,
  47. this.joinedCommand
  48. );
  49. let chain = Promise.resolve();
  50. if (this.options.parallel) {
  51. chain = chain.then(() => this.runCommandInPackagesParallel());
  52. } else if (this.toposort) {
  53. chain = chain.then(() => this.runCommandInPackagesTopological());
  54. } else {
  55. chain = chain.then(() => this.runCommandInPackagesLexical());
  56. }
  57. if (this.bail) {
  58. // only the first error is caught
  59. chain = chain.catch((err) => {
  60. process.exitCode = err.exitCode;
  61. // rethrow to halt chain and log properly
  62. throw err;
  63. });
  64. } else {
  65. // detect error (if any) from collected results
  66. chain = chain.then((results) => {
  67. /* istanbul ignore else */
  68. if (results.some((result) => result.failed)) {
  69. // propagate "highest" error code, it's probably the most useful
  70. const codes = results.filter((result) => result.failed).map((result) => result.exitCode);
  71. const exitCode = Math.max(...codes, 1);
  72. this.logger.error("", "Received non-zero exit code %d during execution", exitCode);
  73. process.exitCode = exitCode;
  74. }
  75. });
  76. }
  77. return chain.then(() => {
  78. this.logger.success(
  79. "exec",
  80. "Executed command in %d %s: %j",
  81. this.count,
  82. this.packagePlural,
  83. this.joinedCommand
  84. );
  85. });
  86. }
  87. getOpts(pkg) {
  88. // these options are passed _directly_ to execa
  89. return {
  90. cwd: pkg.location,
  91. shell: true,
  92. extendEnv: false,
  93. env: Object.assign({}, this.env, {
  94. LERNA_PACKAGE_NAME: pkg.name,
  95. LERNA_ROOT_PATH: this.project.rootPath,
  96. }),
  97. reject: this.bail,
  98. pkg,
  99. };
  100. }
  101. getRunner() {
  102. return this.options.stream
  103. ? (pkg) => this.runCommandInPackageStreaming(pkg)
  104. : (pkg) => this.runCommandInPackageCapturing(pkg);
  105. }
  106. runCommandInPackagesTopological() {
  107. let profiler;
  108. let runner;
  109. if (this.options.profile) {
  110. profiler = new Profiler({
  111. concurrency: this.concurrency,
  112. log: this.logger,
  113. outputDirectory: this.options.profileLocation || this.project.rootPath,
  114. });
  115. const callback = this.getRunner();
  116. runner = (pkg) => profiler.run(() => callback(pkg), pkg.name);
  117. } else {
  118. runner = this.getRunner();
  119. }
  120. let chain = runTopologically(this.filteredPackages, runner, {
  121. concurrency: this.concurrency,
  122. rejectCycles: this.options.rejectCycles,
  123. });
  124. if (profiler) {
  125. chain = chain.then((results) => profiler.output().then(() => results));
  126. }
  127. return chain;
  128. }
  129. runCommandInPackagesParallel() {
  130. return pMap(this.filteredPackages, (pkg) => this.runCommandInPackageStreaming(pkg));
  131. }
  132. runCommandInPackagesLexical() {
  133. return pMap(this.filteredPackages, this.getRunner(), { concurrency: this.concurrency });
  134. }
  135. runCommandInPackageStreaming(pkg) {
  136. return childProcess.spawnStreaming(this.command, this.args, this.getOpts(pkg), this.prefix && pkg.name);
  137. }
  138. runCommandInPackageCapturing(pkg) {
  139. return childProcess.spawn(this.command, this.args, this.getOpts(pkg));
  140. }
  141. }
  142. module.exports.ExecCommand = ExecCommand;