1 | import {Buffer} from 'node:buffer';
|
2 | import path from 'node:path';
|
3 | import childProcess from 'node:child_process';
|
4 | import process from 'node:process';
|
5 | import crossSpawn from 'cross-spawn';
|
6 | import stripFinalNewline from 'strip-final-newline';
|
7 | import {npmRunPathEnv} from 'npm-run-path';
|
8 | import onetime from 'onetime';
|
9 | import {makeError} from './lib/error.js';
|
10 | import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js';
|
11 | import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js';
|
12 | import {handleInput, getSpawnedResult, makeAllStream, validateInputSync} from './lib/stream.js';
|
13 | import {mergePromise, getSpawnedPromise} from './lib/promise.js';
|
14 | import {joinCommand, parseCommand, getEscapedCommand} from './lib/command.js';
|
15 |
|
16 | const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100;
|
17 |
|
18 | const 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 |
|
28 | const 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 |
|
56 | args.unshift('/q');
|
57 | }
|
58 |
|
59 | return {file, args, options, parsed};
|
60 | };
|
61 |
|
62 | const handleOutput = (options, value, error) => {
|
63 | if (typeof value !== 'string' && !Buffer.isBuffer(value)) {
|
64 |
|
65 | return error === undefined ? undefined : '';
|
66 | }
|
67 |
|
68 | if (options.stripFinalNewline) {
|
69 | return stripFinalNewline(value);
|
70 | }
|
71 |
|
72 | return value;
|
73 | };
|
74 |
|
75 | export 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 |
|
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 |
|
164 | export 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 |
|
227 | export function execaCommand(command, options) {
|
228 | const [file, ...args] = parseCommand(command);
|
229 | return execa(file, args, options);
|
230 | }
|
231 |
|
232 | export function execaCommandSync(command, options) {
|
233 | const [file, ...args] = parseCommand(command);
|
234 | return execaSync(file, args, options);
|
235 | }
|
236 |
|
237 | export 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 | }
|