UNPKG

6.55 kBJavaScriptView Raw
1import {Buffer} from 'node:buffer';
2import path from 'node:path';
3import childProcess from 'node:child_process';
4import process from 'node:process';
5import crossSpawn from 'cross-spawn';
6import stripFinalNewline from 'strip-final-newline';
7import {npmRunPathEnv} from 'npm-run-path';
8import onetime from 'onetime';
9import {makeError} from './lib/error.js';
10import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js';
11import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js';
12import {handleInput, getSpawnedResult, makeAllStream, validateInputSync} from './lib/stream.js';
13import {mergePromise, getSpawnedPromise} from './lib/promise.js';
14import {joinCommand, parseCommand, getEscapedCommand} from './lib/command.js';
15
16const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100;
17
18const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => {
19 const env = extendEnv ? {...process.env, ...envOption} : envOption;
20
21 if (preferLocal) {
22 return npmRunPathEnv({env, cwd: localDir, execPath});
23 }
24
25 return env;
26};
27
28const handleArguments = (file, args, options = {}) => {
29 const parsed = crossSpawn._parse(file, args, options);
30 file = parsed.command;
31 args = parsed.args;
32 options = parsed.options;
33
34 options = {
35 maxBuffer: DEFAULT_MAX_BUFFER,
36 buffer: true,
37 stripFinalNewline: true,
38 extendEnv: true,
39 preferLocal: false,
40 localDir: options.cwd || process.cwd(),
41 execPath: process.execPath,
42 encoding: 'utf8',
43 reject: true,
44 cleanup: true,
45 all: false,
46 windowsHide: true,
47 ...options,
48 };
49
50 options.env = getEnv(options);
51
52 options.stdio = normalizeStdio(options);
53
54 if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') {
55 // #116
56 args.unshift('/q');
57 }
58
59 return {file, args, options, parsed};
60};
61
62const handleOutput = (options, value, error) => {
63 if (typeof value !== 'string' && !Buffer.isBuffer(value)) {
64 // When `execaSync()` errors, we normalize it to '' to mimic `execa()`
65 return error === undefined ? undefined : '';
66 }
67
68 if (options.stripFinalNewline) {
69 return stripFinalNewline(value);
70 }
71
72 return value;
73};
74
75export function execa(file, args, options) {
76 const parsed = handleArguments(file, args, options);
77 const command = joinCommand(file, args);
78 const escapedCommand = getEscapedCommand(file, args);
79
80 validateTimeout(parsed.options);
81
82 let spawned;
83 try {
84 spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options);
85 } catch (error) {
86 // Ensure the returned error is always both a promise and a child process
87 const dummySpawned = new childProcess.ChildProcess();
88 const errorPromise = Promise.reject(makeError({
89 error,
90 stdout: '',
91 stderr: '',
92 all: '',
93 command,
94 escapedCommand,
95 parsed,
96 timedOut: false,
97 isCanceled: false,
98 killed: false,
99 }));
100 return mergePromise(dummySpawned, errorPromise);
101 }
102
103 const spawnedPromise = getSpawnedPromise(spawned);
104 const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise);
105 const processDone = setExitHandler(spawned, parsed.options, timedPromise);
106
107 const context = {isCanceled: false};
108
109 spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned));
110 spawned.cancel = spawnedCancel.bind(null, spawned, context);
111
112 const handlePromise = async () => {
113 const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone);
114 const stdout = handleOutput(parsed.options, stdoutResult);
115 const stderr = handleOutput(parsed.options, stderrResult);
116 const all = handleOutput(parsed.options, allResult);
117
118 if (error || exitCode !== 0 || signal !== null) {
119 const returnedError = makeError({
120 error,
121 exitCode,
122 signal,
123 stdout,
124 stderr,
125 all,
126 command,
127 escapedCommand,
128 parsed,
129 timedOut,
130 isCanceled: context.isCanceled,
131 killed: spawned.killed,
132 });
133
134 if (!parsed.options.reject) {
135 return returnedError;
136 }
137
138 throw returnedError;
139 }
140
141 return {
142 command,
143 escapedCommand,
144 exitCode: 0,
145 stdout,
146 stderr,
147 all,
148 failed: false,
149 timedOut: false,
150 isCanceled: false,
151 killed: false,
152 };
153 };
154
155 const handlePromiseOnce = onetime(handlePromise);
156
157 handleInput(spawned, parsed.options.input);
158
159 spawned.all = makeAllStream(spawned, parsed.options);
160
161 return mergePromise(spawned, handlePromiseOnce);
162}
163
164export function execaSync(file, args, options) {
165 const parsed = handleArguments(file, args, options);
166 const command = joinCommand(file, args);
167 const escapedCommand = getEscapedCommand(file, args);
168
169 validateInputSync(parsed.options);
170
171 let result;
172 try {
173 result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options);
174 } catch (error) {
175 throw makeError({
176 error,
177 stdout: '',
178 stderr: '',
179 all: '',
180 command,
181 escapedCommand,
182 parsed,
183 timedOut: false,
184 isCanceled: false,
185 killed: false,
186 });
187 }
188
189 const stdout = handleOutput(parsed.options, result.stdout, result.error);
190 const stderr = handleOutput(parsed.options, result.stderr, result.error);
191
192 if (result.error || result.status !== 0 || result.signal !== null) {
193 const error = makeError({
194 stdout,
195 stderr,
196 error: result.error,
197 signal: result.signal,
198 exitCode: result.status,
199 command,
200 escapedCommand,
201 parsed,
202 timedOut: result.error && result.error.code === 'ETIMEDOUT',
203 isCanceled: false,
204 killed: result.signal !== null,
205 });
206
207 if (!parsed.options.reject) {
208 return error;
209 }
210
211 throw error;
212 }
213
214 return {
215 command,
216 escapedCommand,
217 exitCode: 0,
218 stdout,
219 stderr,
220 failed: false,
221 timedOut: false,
222 isCanceled: false,
223 killed: false,
224 };
225}
226
227export function execaCommand(command, options) {
228 const [file, ...args] = parseCommand(command);
229 return execa(file, args, options);
230}
231
232export function execaCommandSync(command, options) {
233 const [file, ...args] = parseCommand(command);
234 return execaSync(file, args, options);
235}
236
237export function execaNode(scriptPath, args, options = {}) {
238 if (args && !Array.isArray(args) && typeof args === 'object') {
239 options = args;
240 args = [];
241 }
242
243 const stdio = normalizeStdioNode(options);
244 const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect'));
245
246 const {
247 nodePath = process.execPath,
248 nodeOptions = defaultExecArgv,
249 } = options;
250
251 return execa(
252 nodePath,
253 [
254 ...nodeOptions,
255 scriptPath,
256 ...(Array.isArray(args) ? args : []),
257 ],
258 {
259 ...options,
260 stdin: undefined,
261 stdout: undefined,
262 stderr: undefined,
263 stdio,
264 shell: false,
265 },
266 );
267}