index.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. "use strict";
  2. const pMap = require("p-map");
  3. const { Command } = require("@lerna/command");
  4. const { npmRunScript, npmRunScriptStreaming } = require("@lerna/npm-run-script");
  5. const { output } = require("@lerna/output");
  6. const { Profiler } = require("@lerna/profiler");
  7. const { timer } = require("@lerna/timer");
  8. const { runTopologically } = require("@lerna/run-topologically");
  9. const { ValidationError } = require("@lerna/validation-error");
  10. const { getFilteredPackages } = require("@lerna/filter-options");
  11. module.exports = factory;
  12. function factory(argv) {
  13. return new RunCommand(argv);
  14. }
  15. class RunCommand extends Command {
  16. get requiresGit() {
  17. return false;
  18. }
  19. initialize() {
  20. const { script, npmClient = "npm" } = this.options;
  21. this.script = script;
  22. this.args = this.options["--"] || [];
  23. this.npmClient = npmClient;
  24. if (!script) {
  25. throw new ValidationError("ENOSCRIPT", "You must specify a lifecycle script to run");
  26. }
  27. // inverted boolean options
  28. this.bail = this.options.bail !== false;
  29. this.prefix = this.options.prefix !== false;
  30. let chain = Promise.resolve();
  31. chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
  32. chain = chain.then((filteredPackages) => {
  33. this.packagesWithScript =
  34. script === "env"
  35. ? filteredPackages
  36. : filteredPackages.filter((pkg) => pkg.scripts && pkg.scripts[script]);
  37. });
  38. return chain.then(() => {
  39. this.count = this.packagesWithScript.length;
  40. this.packagePlural = this.count === 1 ? "package" : "packages";
  41. this.joinedCommand = [this.npmClient, "run", this.script].concat(this.args).join(" ");
  42. if (!this.count) {
  43. this.logger.success("run", `No packages found with the lifecycle script '${script}'`);
  44. // still exits zero, aka "ok"
  45. return false;
  46. }
  47. });
  48. }
  49. execute() {
  50. this.logger.info(
  51. "",
  52. "Executing command in %d %s: %j",
  53. this.count,
  54. this.packagePlural,
  55. this.joinedCommand
  56. );
  57. let chain = Promise.resolve();
  58. const getElapsed = timer();
  59. if (this.options.parallel) {
  60. chain = chain.then(() => this.runScriptInPackagesParallel());
  61. } else if (this.toposort) {
  62. chain = chain.then(() => this.runScriptInPackagesTopological());
  63. } else {
  64. chain = chain.then(() => this.runScriptInPackagesLexical());
  65. }
  66. if (this.bail) {
  67. // only the first error is caught
  68. chain = chain.catch((err) => {
  69. process.exitCode = err.exitCode;
  70. // rethrow to halt chain and log properly
  71. throw err;
  72. });
  73. } else {
  74. // detect error (if any) from collected results
  75. chain = chain.then((results) => {
  76. /* istanbul ignore else */
  77. if (results.some((result) => result.failed)) {
  78. // propagate "highest" error code, it's probably the most useful
  79. const codes = results.filter((result) => result.failed).map((result) => result.exitCode);
  80. const exitCode = Math.max(...codes, 1);
  81. this.logger.error("", "Received non-zero exit code %d during execution", exitCode);
  82. process.exitCode = exitCode;
  83. }
  84. });
  85. }
  86. return chain.then(() => {
  87. this.logger.success(
  88. "run",
  89. "Ran npm script '%s' in %d %s in %ss:",
  90. this.script,
  91. this.count,
  92. this.packagePlural,
  93. (getElapsed() / 1000).toFixed(1)
  94. );
  95. this.logger.success("", this.packagesWithScript.map((pkg) => `- ${pkg.name}`).join("\n"));
  96. });
  97. }
  98. getOpts(pkg) {
  99. // these options are NOT passed directly to execa, they are composed in npm-run-script
  100. return {
  101. args: this.args,
  102. npmClient: this.npmClient,
  103. prefix: this.prefix,
  104. reject: this.bail,
  105. pkg,
  106. };
  107. }
  108. getRunner() {
  109. return this.options.stream
  110. ? (pkg) => this.runScriptInPackageStreaming(pkg)
  111. : (pkg) => this.runScriptInPackageCapturing(pkg);
  112. }
  113. runScriptInPackagesTopological() {
  114. let profiler;
  115. let runner;
  116. if (this.options.profile) {
  117. profiler = new Profiler({
  118. concurrency: this.concurrency,
  119. log: this.logger,
  120. outputDirectory: this.options.profileLocation,
  121. });
  122. const callback = this.getRunner();
  123. runner = (pkg) => profiler.run(() => callback(pkg), pkg.name);
  124. } else {
  125. runner = this.getRunner();
  126. }
  127. let chain = runTopologically(this.packagesWithScript, runner, {
  128. concurrency: this.concurrency,
  129. rejectCycles: this.options.rejectCycles,
  130. });
  131. if (profiler) {
  132. chain = chain.then((results) => profiler.output().then(() => results));
  133. }
  134. return chain;
  135. }
  136. runScriptInPackagesParallel() {
  137. return pMap(this.packagesWithScript, (pkg) => this.runScriptInPackageStreaming(pkg));
  138. }
  139. runScriptInPackagesLexical() {
  140. return pMap(this.packagesWithScript, this.getRunner(), { concurrency: this.concurrency });
  141. }
  142. runScriptInPackageStreaming(pkg) {
  143. return npmRunScriptStreaming(this.script, this.getOpts(pkg));
  144. }
  145. runScriptInPackageCapturing(pkg) {
  146. const getElapsed = timer();
  147. return npmRunScript(this.script, this.getOpts(pkg)).then((result) => {
  148. this.logger.info(
  149. "run",
  150. "Ran npm script '%s' in '%s' in %ss:",
  151. this.script,
  152. pkg.name,
  153. (getElapsed() / 1000).toFixed(1)
  154. );
  155. output(result.stdout);
  156. return result;
  157. });
  158. }
  159. }
  160. module.exports.RunCommand = RunCommand;