process.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /**
  2. * Process helpers shared by the release scripts: the release steps drive `git`,
  3. * `pnpm`, `npm`, and `tar`, and each needs one of three failure behaviours.
  4. */
  5. import { spawn, spawnSync } from 'node:child_process'
  6. import { realpathSync } from 'node:fs'
  7. import { fileURLToPath } from 'node:url'
  8. /** Where and with what environment a release step runs a command. */
  9. export interface RunOptions {
  10. readonly cwd?: string
  11. readonly env?: NodeJS.ProcessEnv
  12. }
  13. /** What a command produced, for a caller that decides what a failure means. */
  14. export interface CommandResult {
  15. /** Exit status, or null when a signal ended the process. */
  16. readonly status: number | null
  17. readonly stdout: string
  18. readonly stderr: string
  19. }
  20. /**
  21. * Run a command and capture its output without judging the exit status.
  22. * @param command - executable name.
  23. * @param args - command arguments.
  24. * @param options - working directory and environment.
  25. * @returns The exit status and captured streams.
  26. */
  27. export function attempt(command: string, args: readonly string[], options: RunOptions = {}): CommandResult {
  28. const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, encoding: 'utf8' })
  29. if (result.error !== undefined) throw result.error
  30. return { status: result.status, stdout: result.stdout, stderr: result.stderr }
  31. }
  32. /**
  33. * Run a command, then echo and return its captured output. Output is buffered
  34. * until exit and stdout precedes stderr.
  35. * @param command - executable name.
  36. * @param args - command arguments.
  37. * @param options - working directory and environment.
  38. * @returns The exit status and captured streams.
  39. */
  40. export function attemptEchoed(command: string, args: readonly string[], options: RunOptions = {}): CommandResult {
  41. const result = spawnSync(command, [...args], {
  42. cwd: options.cwd,
  43. env: options.env,
  44. encoding: 'utf8',
  45. stdio: ['inherit', 'pipe', 'pipe'],
  46. })
  47. if (result.error !== undefined) throw result.error
  48. if (result.stdout !== '') process.stdout.write(result.stdout)
  49. if (result.stderr !== '') process.stderr.write(result.stderr)
  50. return { status: result.status, stdout: result.stdout, stderr: result.stderr }
  51. }
  52. /**
  53. * Run a command, capture its standard output, and fail on a non-zero exit.
  54. * @param command - executable name.
  55. * @param args - command arguments.
  56. * @param options - working directory and environment.
  57. * @returns The trimmed standard output.
  58. */
  59. export function capture(command: string, args: readonly string[], options: RunOptions = {}): string {
  60. const result = attempt(command, args, options)
  61. if (result.status !== 0) {
  62. throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}:\n${result.stdout}\n${result.stderr}`)
  63. }
  64. return result.stdout.trim()
  65. }
  66. /**
  67. * Run a command with inherited streams without blocking the event loop, so a
  68. * caller can hold several commands in flight, and fail on a non-zero exit.
  69. * Concurrent children interleave their output at line granularity.
  70. * @param command - executable name.
  71. * @param args - command arguments.
  72. * @param options - working directory and environment.
  73. * @returns Resolves when the command exits with status zero.
  74. */
  75. export function runConcurrent(command: string, args: readonly string[], options: RunOptions = {}): Promise<void> {
  76. return new Promise((resolveRun, rejectRun) => {
  77. const child = spawn(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
  78. child.once('error', rejectRun)
  79. child.once('close', (status, signal) => {
  80. if (status === 0) resolveRun()
  81. else rejectRun(new Error(`${command} ${args.join(' ')} exited with ${String(status ?? signal)}`))
  82. })
  83. })
  84. }
  85. /**
  86. * Return whether Node started the given module as the process entry point.
  87. * @param moduleUrl - the caller's `import.meta.url`.
  88. * @returns True when Node started this module.
  89. */
  90. export function isEntry(moduleUrl: string): boolean {
  91. const invoked = process.argv[1]
  92. if (invoked === undefined) return false
  93. return realpathSync(invoked) === realpathSync(fileURLToPath(moduleUrl))
  94. }