index.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. "use strict";
  2. const os = require("os");
  3. const chalk = require("chalk");
  4. const execa = require("execa");
  5. const logTransformer = require("strong-log-transformer");
  6. // bookkeeping for spawned processes
  7. /** @type {Set<import("execa").ExecaChildProcess<string>>} */
  8. const children = new Set();
  9. // when streaming processes are spawned, use this color for prefix
  10. const colorWheel = ["cyan", "magenta", "blue", "yellow", "green", "red"];
  11. const NUM_COLORS = colorWheel.length;
  12. // ever-increasing index ensures colors are always sequential
  13. let currentColor = 0;
  14. /**
  15. * Execute a command asynchronously, piping stdio by default.
  16. * @param {string} command
  17. * @param {string[]} args
  18. * @param {import("execa").Options} [opts]
  19. */
  20. function exec(command, args, opts) {
  21. const options = Object.assign({ stdio: "pipe" }, opts);
  22. const spawned = spawnProcess(command, args, options);
  23. return wrapError(spawned);
  24. }
  25. /**
  26. * Execute a command synchronously.
  27. * @param {string} command
  28. * @param {string[]} args
  29. * @param {import("execa").SyncOptions} [opts]
  30. */
  31. function execSync(command, args, opts) {
  32. return execa.sync(command, args, opts).stdout;
  33. }
  34. /**
  35. * Spawn a command asynchronously, _always_ inheriting stdio.
  36. * @param {string} command
  37. * @param {string[]} args
  38. * @param {import("execa").Options} [opts]
  39. */
  40. function spawn(command, args, opts) {
  41. const options = Object.assign({}, opts, { stdio: "inherit" });
  42. const spawned = spawnProcess(command, args, options);
  43. return wrapError(spawned);
  44. }
  45. /**
  46. * Spawn a command asynchronously, streaming stdio with optional prefix.
  47. * @param {string} command
  48. * @param {string[]} args
  49. * @param {import("execa").Options} [opts]
  50. * @param {string} [prefix]
  51. */
  52. // istanbul ignore next
  53. function spawnStreaming(command, args, opts, prefix) {
  54. const options = Object.assign({}, opts);
  55. options.stdio = ["ignore", "pipe", "pipe"];
  56. const spawned = spawnProcess(command, args, options);
  57. const stdoutOpts = {};
  58. const stderrOpts = {}; // mergeMultiline causes escaped newlines :P
  59. if (prefix) {
  60. const colorName = colorWheel[currentColor % NUM_COLORS];
  61. const color = chalk[colorName];
  62. currentColor += 1;
  63. stdoutOpts.tag = `${color.bold(prefix)}:`;
  64. stderrOpts.tag = `${color(prefix)}:`;
  65. }
  66. // Avoid "Possible EventEmitter memory leak detected" warning due to piped stdio
  67. if (children.size > process.stdout.listenerCount("close")) {
  68. process.stdout.setMaxListeners(children.size);
  69. process.stderr.setMaxListeners(children.size);
  70. }
  71. spawned.stdout.pipe(logTransformer(stdoutOpts)).pipe(process.stdout);
  72. spawned.stderr.pipe(logTransformer(stderrOpts)).pipe(process.stderr);
  73. return wrapError(spawned);
  74. }
  75. function getChildProcessCount() {
  76. return children.size;
  77. }
  78. /**
  79. * @param {import("execa").ExecaError<string>} result
  80. * @returns {number}
  81. */
  82. function getExitCode(result) {
  83. if (result.exitCode) {
  84. return result.exitCode;
  85. }
  86. // https://nodejs.org/docs/latest-v6.x/api/child_process.html#child_process_event_close
  87. if (typeof result.code === "number") {
  88. return result.code;
  89. }
  90. // https://nodejs.org/docs/latest-v6.x/api/errors.html#errors_error_code
  91. if (typeof result.code === "string") {
  92. return os.constants.errno[result.code];
  93. }
  94. // we tried
  95. return process.exitCode;
  96. }
  97. /**
  98. * @param {string} command
  99. * @param {string[]} args
  100. * @param {import("execa").Options} opts
  101. */
  102. function spawnProcess(command, args, opts) {
  103. const child = execa(command, args, opts);
  104. const drain = (exitCode, signal) => {
  105. children.delete(child);
  106. // don't run repeatedly if this is the error event
  107. if (signal === undefined) {
  108. child.removeListener("exit", drain);
  109. }
  110. // propagate exit code, if any
  111. if (exitCode) {
  112. process.exitCode = exitCode;
  113. }
  114. };
  115. child.once("exit", drain);
  116. child.once("error", drain);
  117. if (opts.pkg) {
  118. child.pkg = opts.pkg;
  119. }
  120. children.add(child);
  121. return child;
  122. }
  123. /**
  124. * @param {import("execa").ExecaChildProcess<string> & { pkg?: import("@lerna/package").Package }} spawned
  125. */
  126. function wrapError(spawned) {
  127. if (spawned.pkg) {
  128. return spawned.catch((err) => {
  129. // ensure exit code is always a number
  130. err.exitCode = getExitCode(err);
  131. // log non-lerna error cleanly
  132. err.pkg = spawned.pkg;
  133. throw err;
  134. });
  135. }
  136. return spawned;
  137. }
  138. exports.exec = exec;
  139. exports.execSync = execSync;
  140. exports.spawn = spawn;
  141. exports.spawnStreaming = spawnStreaming;
  142. exports.getChildProcessCount = getChildProcessCount;
  143. exports.getExitCode = getExitCode;
  144. /**
  145. * @typedef {object} ExecOpts Provided to any execa-based call
  146. * @property {string} cwd
  147. * @property {number} [maxBuffer]
  148. */