Skip to content
This repository was archived by the owner on May 25, 2025. It is now read-only.

Commit 5d684e1

Browse files
fix(exec2): separate spawnAsyncAndReturn and spawnAsync
1 parent bc40ff0 commit 5d684e1

File tree

3 files changed

+150
-122
lines changed

3 files changed

+150
-122
lines changed

scripts/exec2.script.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ runScript(async () => {
1111
await exec2.spawnAsync('node', {
1212
args: ['scripts/dot.script.js', '--error'],
1313
// log: true,
14-
shell: true,
14+
// shell: true,
1515
// forceColor: false,
1616
// passProcessEnv: true,
1717
})

src/util/exec2.test.ts

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,46 @@ import { _expectedErrorString, _stringify, pExpectedError } from '@naturalcycles
22
import { exec2, SpawnError } from './exec2'
33

44
test('spawn ok', () => {
5-
exec2.spawn('git status', {
6-
log: true,
7-
})
5+
exec2.spawn('git status')
86
// no error
97
})
108

119
test('spawn error', () => {
12-
const err = _expectedErrorString(() =>
13-
exec2.spawn('git stat', {
14-
log: true,
15-
}),
16-
)
10+
const err = _expectedErrorString(() => exec2.spawn('git stat'))
1711
expect(err).toMatchInlineSnapshot(`"Error: spawn exited with code 1: git stat"`)
1812
})
1913

2014
test('exec ok', () => {
21-
const s = exec2.exec('git version', {
22-
log: true,
23-
})
15+
const s = exec2.exec('git version')
2416
expect(s.startsWith('git version')).toBe(true)
2517
})
2618

2719
test('exec error', () => {
28-
const err = _expectedErrorString(() =>
29-
exec2.exec('git stat', {
30-
log: true,
31-
}),
32-
)
20+
const err = _expectedErrorString(() => exec2.exec('git stat'))
3321
expect(err).toMatchInlineSnapshot(`"Error: exec exited with code 1: git stat"`)
3422
})
3523

3624
test('spawnAsync ok', async () => {
37-
const s = await exec2.spawnAsync('git version', {
38-
log: true,
39-
})
25+
await exec2.spawnAsync('git version')
26+
// no error
27+
})
28+
29+
test('spawnAsync error', async () => {
30+
const err = await pExpectedError(exec2.spawnAsync('git stat'), Error)
31+
expect(_stringify(err)).toMatchInlineSnapshot(`"Error: spawnAsync exited with code 1: git stat"`)
32+
})
33+
34+
test('spawnAsyncAndReturn ok', async () => {
35+
const s = await exec2.spawnAsyncAndReturn('git version')
4036
expect(s.exitCode).toBe(0)
4137
expect(s.stderr).toBe('')
4238
expect(s.stdout.startsWith('git version')).toBe(true)
4339
})
4440

45-
test('spawnAsync error with throw', async () => {
46-
const err = await pExpectedError(exec2.spawnAsync('git stat'), SpawnError)
41+
test('spawnAsyncAndReturn error with throw', async () => {
42+
const err = await pExpectedError(exec2.spawnAsyncAndReturn('git stat'), SpawnError)
4743
expect(_stringify(err)).toMatchInlineSnapshot(
48-
`"SpawnError: spawnAsync exited with code 1: git stat"`,
44+
`"SpawnError: spawnAsyncAndReturn exited with code 1: git stat"`,
4945
)
5046
expect(err.data.exitCode).toBe(1)
5147
expect(err.data.stdout).toBe('')
@@ -59,9 +55,8 @@ The most similar commands are
5955
`)
6056
})
6157

62-
test('spawnAsync error without throw', async () => {
63-
const { exitCode, stdout, stderr } = await exec2.spawnAsync('git stat', {
64-
log: true,
58+
test('spawnAsyncAndReturn error without throw', async () => {
59+
const { exitCode, stdout, stderr } = await exec2.spawnAsyncAndReturn('git stat', {
6560
throwOnNonZeroCode: false,
6661
})
6762
expect(exitCode).toBe(1)

src/util/exec2.ts

Lines changed: 129 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { dimGrey, dimRed, hasColors, white } from '../colors/colors'
1717
*
1818
* Short-running job, no need to print the output, might want to return the output - use Exec.
1919
*
20-
* Need to both print and return the output - use SpawnAsync.
20+
* Need to both print and return the output - use SpawnAsyncAndReturn.
2121
*
2222
* ***
2323
*
@@ -31,101 +31,13 @@ import { dimGrey, dimRed, hasColors, white } from '../colors/colors'
3131
* Exec always uses the shell (there's no option to disable it).
3232
*/
3333
class Exec2 {
34-
/**
35-
* Advanced/async version of Spawn.
36-
* Consider simpler `spawn` or `exec` first, which are also sync.
37-
*
38-
* spawnAsync features:
39-
*
40-
* 1. Async
41-
* 2. Allows to collect the output AND print it while running.
42-
* 3. Returns SpawnOutput with stdout, stderr and exitCode.
43-
* 4. Allows to not throw on error, but just return SpawnOutput for further inspection.
44-
*
45-
* Defaults:
46-
*
47-
* shell: true
48-
* printWhileRunning: true
49-
* collectOutputWhileRunning: true
50-
* throwOnNonZeroCode: true
51-
* log: true
52-
*/
53-
async spawnAsync(cmd: string, opt: SpawnAsyncOptions = {}): Promise<SpawnOutput> {
54-
const {
55-
shell = true,
56-
printWhileRunning = true,
57-
collectOutputWhileRunning = true,
58-
throwOnNonZeroCode = true,
59-
cwd,
60-
env,
61-
passProcessEnv = true,
62-
forceColor = hasColors,
63-
} = opt
64-
opt.log ??= printWhileRunning // by default log should be true, as we are printing the output
65-
opt.logStart ??= opt.log
66-
opt.logFinish ??= opt.log
67-
const started = Date.now()
68-
this.logStart(cmd, opt)
69-
let stdout = ''
70-
let stderr = ''
71-
72-
// if (printWhileRunning) console.log('') // 1-line padding before the output
73-
74-
return await new Promise<SpawnOutput>((resolve, reject) => {
75-
const p = cp.spawn(cmd, opt.args || [], {
76-
shell,
77-
cwd,
78-
env: {
79-
...(passProcessEnv ? process.env : {}),
80-
...(forceColor ? { FORCE_COLOR: '1' } : {}),
81-
...env,
82-
},
83-
})
84-
85-
p.stdout.on('data', data => {
86-
if (collectOutputWhileRunning) {
87-
stdout += data.toString()
88-
// console.log('stdout:', data.toString())
89-
}
90-
if (printWhileRunning) {
91-
process.stdout.write(data)
92-
// console.log('stderr:', data.toString())
93-
}
94-
})
95-
p.stderr.on('data', data => {
96-
if (collectOutputWhileRunning) {
97-
stderr += data.toString()
98-
}
99-
if (printWhileRunning) {
100-
process.stderr.write(data)
101-
}
102-
})
103-
104-
p.on('close', code => {
105-
// if (printWhileRunning) console.log('') // 1-line padding after the output
106-
const isSuccessful = !code
107-
this.logFinish(cmd, opt, started, isSuccessful)
108-
const exitCode = code || 0
109-
const o: SpawnOutput = {
110-
exitCode,
111-
stdout: stdout.trim(),
112-
stderr: stderr.trim(),
113-
}
114-
if (throwOnNonZeroCode && code) {
115-
return reject(new SpawnError(`spawnAsync exited with code ${code}: ${cmd}`, o))
116-
}
117-
resolve(o)
118-
})
119-
})
120-
}
121-
12234
/**
12335
* Reasons to use it:
12436
* - Sync
12537
* - Need to print output while running
12638
*
12739
* Limitations:
128-
* - Cannot return stdout/stderr (use exec or spawnAsync for that)
40+
* - Cannot return stdout/stderr (use exec, execAsync or spawnAsyncAndReturn for that)
12941
*
13042
* Defaults:
13143
*
@@ -139,7 +51,6 @@ class Exec2 {
13951
opt.logFinish ??= opt.log
14052
const started = Date.now()
14153
this.logStart(cmd, opt)
142-
// console.log('') // 1-line padding before the output
14354

14455
const r = cp.spawnSync(cmd, opt.args, {
14556
encoding: 'utf8',
@@ -153,7 +64,6 @@ class Exec2 {
15364
},
15465
})
15566

156-
// console.log('') // 1-line padding after the output
15767
const isSuccessful = !r.error && !r.status
15868
this.logFinish(cmd, opt, started, isSuccessful)
15969

@@ -218,10 +128,133 @@ class Exec2 {
218128
}
219129
}
220130

221-
throwOnNonZeroExitCode(o: SpawnOutput): void {
222-
if (o.exitCode) {
223-
throw new SpawnError(`spawn exited with code ${o.exitCode}`, o)
224-
}
131+
/**
132+
* Reasons to use it:
133+
* - Async
134+
* - Need to print output while running
135+
*
136+
* Limitations:
137+
* - Cannot return stdout/stderr (use execAsync or spawnAsyncAndReturn for that)
138+
*
139+
* Defaults:
140+
*
141+
* shell: true
142+
* log: true
143+
*/
144+
async spawnAsync(cmd: string, opt: SpawnOptions = {}): Promise<void> {
145+
const { shell = true, cwd, env, passProcessEnv = true, forceColor = hasColors } = opt
146+
opt.log ??= true // by default log should be true, as we are printing the output
147+
opt.logStart ??= opt.log
148+
opt.logFinish ??= opt.log
149+
const started = Date.now()
150+
this.logStart(cmd, opt)
151+
152+
await new Promise<void>((resolve, reject) => {
153+
const p = cp.spawn(cmd, opt.args || [], {
154+
shell,
155+
cwd,
156+
stdio: 'inherit',
157+
env: {
158+
...(passProcessEnv ? process.env : {}),
159+
...(forceColor ? { FORCE_COLOR: '1' } : {}),
160+
...env,
161+
},
162+
})
163+
164+
p.on('close', code => {
165+
const isSuccessful = !code
166+
this.logFinish(cmd, opt, started, isSuccessful)
167+
if (code) {
168+
return reject(new Error(`spawnAsync exited with code ${code}: ${cmd}`))
169+
}
170+
resolve()
171+
})
172+
})
173+
}
174+
175+
/**
176+
* Advanced/async version of Spawn.
177+
* Consider simpler `spawn` or `exec` first, which are also sync.
178+
*
179+
* spawnAsyncAndReturn features:
180+
*
181+
* 1. Async
182+
* 2. Allows to collect the output AND print it while running.
183+
* 3. Returns SpawnOutput with stdout, stderr and exitCode.
184+
* 4. Allows to not throw on error, but just return SpawnOutput for further inspection.
185+
*
186+
* Defaults:
187+
*
188+
* shell: true
189+
* printWhileRunning: true
190+
* collectOutputWhileRunning: true
191+
* throwOnNonZeroCode: true
192+
* log: true
193+
*/
194+
async spawnAsyncAndReturn(cmd: string, opt: SpawnAsyncOptions = {}): Promise<SpawnOutput> {
195+
const {
196+
shell = true,
197+
printWhileRunning = true,
198+
collectOutputWhileRunning = true,
199+
throwOnNonZeroCode = true,
200+
cwd,
201+
env,
202+
passProcessEnv = true,
203+
forceColor = hasColors,
204+
} = opt
205+
opt.log ??= printWhileRunning // by default log should be true, as we are printing the output
206+
opt.logStart ??= opt.log
207+
opt.logFinish ??= opt.log
208+
const started = Date.now()
209+
this.logStart(cmd, opt)
210+
let stdout = ''
211+
let stderr = ''
212+
213+
return await new Promise<SpawnOutput>((resolve, reject) => {
214+
const p = cp.spawn(cmd, opt.args || [], {
215+
shell,
216+
cwd,
217+
env: {
218+
...(passProcessEnv ? process.env : {}),
219+
...(forceColor ? { FORCE_COLOR: '1' } : {}),
220+
...env,
221+
},
222+
})
223+
224+
p.stdout.on('data', data => {
225+
if (collectOutputWhileRunning) {
226+
stdout += data.toString()
227+
// console.log('stdout:', data.toString())
228+
}
229+
if (printWhileRunning) {
230+
process.stdout.write(data)
231+
// console.log('stderr:', data.toString())
232+
}
233+
})
234+
p.stderr.on('data', data => {
235+
if (collectOutputWhileRunning) {
236+
stderr += data.toString()
237+
}
238+
if (printWhileRunning) {
239+
process.stderr.write(data)
240+
}
241+
})
242+
243+
p.on('close', code => {
244+
const isSuccessful = !code
245+
this.logFinish(cmd, opt, started, isSuccessful)
246+
const exitCode = code || 0
247+
const o: SpawnOutput = {
248+
exitCode,
249+
stdout: stdout.trim(),
250+
stderr: stderr.trim(),
251+
}
252+
if (throwOnNonZeroCode && code) {
253+
return reject(new SpawnError(`spawnAsyncAndReturn exited with code ${code}: ${cmd}`, o))
254+
}
255+
resolve(o)
256+
})
257+
})
225258
}
226259

227260
private logStart(cmd: string, opt: SpawnOptions | ExecOptions): void {

0 commit comments

Comments
 (0)