Skip to content

Commit 2e30271

Browse files
committed
feat(child-process-utils): a small set of utilities for child process
1 parent 00d011c commit 2e30271

File tree

9 files changed

+375
-0
lines changed

9 files changed

+375
-0
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ A set of packages with simple utilities.
2121
|------|-------------|---------|--------------|
2222
| [`@simple-libs/hosted-git-info`](packages/hosted-git-info#readme) | A small library to parse hosted git info. | [![NPM version][hosted-git-info-npm]][hosted-git-info-npm-url] | [![Dependencies status][hosted-git-info-deps]][hosted-git-info-deps-url] |
2323
| [`@simple-libs/stream-utils`](packages/stream-utils#readme) | A small set of utilities for streams. | [![NPM version][stream-utils-npm]][stream-utils-npm-url] | [![Dependencies status][stream-utils-deps]][stream-utils-deps-url] |
24+
| [`@simple-libs/child-process-utils`](packages/child-process-utils#readme) | A small set of utilities for child process. | [![NPM version][child-process-utils-npm]][child-process-utils-npm-url] | [![Dependencies status][child-process-utils-deps]][child-process-utils-deps-url] |
2425

2526
<!-- hosted-git-info -->
2627

@@ -37,3 +38,11 @@ A set of packages with simple utilities.
3738

3839
[stream-utils-deps]: https://img.shields.io/librariesio/release/npm/@simple-libs/stream-utils
3940
[stream-utils-deps-url]: https://libraries.io/npm/@simple-libs%2Fstream-utils/tree
41+
42+
<!-- child-process-utils -->
43+
44+
[child-process-utils-npm]: https://img.shields.io/npm/v/@simple-libs/child-process-utils.svg
45+
[child-process-utils-npm-url]: https://www.npmjs.com/package/@simple-libs/child-process-utils
46+
47+
[child-process-utils-deps]: https://img.shields.io/librariesio/release/npm/@simple-libs/child-process-utils
48+
[child-process-utils-deps-url]: https://libraries.io/npm/@simple-libs%2Fchild-process-utils/tree
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": [
3+
"@trigen/eslint-config/typescript",
4+
"@trigen/eslint-config/typescript-requiring-type-checking",
5+
"@trigen/eslint-config/jest"
6+
],
7+
"parserOptions": {
8+
"tsconfigRootDir": "./packages/child-process-utils",
9+
"project": ["./tsconfig.json"]
10+
}
11+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# @simple-libs/child-process-utils
2+
3+
[![ESM-only package][package]][package-url]
4+
[![NPM version][npm]][npm-url]
5+
[![Node version][node]][node-url]
6+
[![Dependencies status][deps]][deps-url]
7+
[![Install size][size]][size-url]
8+
[![Build status][build]][build-url]
9+
[![Coverage status][coverage]][coverage-url]
10+
11+
[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
12+
[package-url]: https://nodejs.org/api/esm.html
13+
14+
[npm]: https://img.shields.io/npm/v/@simple-libs/child-process-utils.svg
15+
[npm-url]: https://www.npmjs.com/package/@simple-libs/child-process-utils
16+
17+
[node]: https://img.shields.io/node/v/@simple-libs/child-process-utils.svg
18+
[node-url]: https://nodejs.org
19+
20+
[deps]: https://img.shields.io/librariesio/release/npm/@simple-libs/child-process-utils
21+
[deps-url]: https://libraries.io/npm/@simple-libs%2Fchild-process-utils/tree
22+
23+
[size]: https://packagephobia.com/badge?p=@simple-libs/child-process-utils
24+
[size-url]: https://packagephobia.com/result?p=@simple-libs/child-process-utils
25+
26+
[build]: https://img.shields.io/github/actions/workflow/status/TrigenSoftware/simple-libs/tests.yml?branch=main
27+
[build-url]: https://github.com/TrigenSoftware/simple-libs/actions
28+
29+
[coverage]: https://img.shields.io/codecov/c/github/TrigenSoftware/simple-libs.svg?flag=@simple-libs/child-process-utils
30+
[coverage-url]: https://app.codecov.io/gh/TrigenSoftware/simple-libs/tree/main/packages%2Fchild-process-utils
31+
32+
A small set of utilities for child process.
33+
34+
## Install
35+
36+
```bash
37+
# pnpm
38+
pnpm add @simple-libs/child-process-utils
39+
# yarn
40+
yarn add @simple-libs/child-process-utils
41+
# npm
42+
npm i @simple-libs/child-process-utils
43+
```
44+
45+
## Usage
46+
47+
```ts
48+
import {
49+
exitCode,
50+
catchProcessError,
51+
throwProcessError,
52+
outputStream,
53+
output
54+
} from '@simple-libs/child-process-utils'
55+
56+
// Wait for a child process to exit and return its exit code
57+
await exitCode(spawn())
58+
// Returns 0 if the process exited successfully, or the exit code if it failed
59+
60+
// Catch error from a child process
61+
await catchProcessError(spawn())
62+
// Returns the error if the process failed, or null if it succeeded
63+
64+
// Throws an error if the child process exits with a non-zero code.
65+
await throwProcessError(spawn())
66+
67+
// Yields the stdout of a child process.
68+
// It will throw an error if the process exits with a non-zero code.
69+
for await (chunk of outputStream(spawn())) {
70+
console.log(chunk.toString())
71+
}
72+
73+
// Collects the stdout of a child process into a single Buffer.
74+
// It will throw an error if the process exits with a non-zero code.
75+
await output(spawn())
76+
// Returns a Buffer with the stdout of the process
77+
```
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "@simple-libs/child-process-utils",
3+
"type": "module",
4+
"version": "1.0.0",
5+
"description": "A small set of utilities for child process.",
6+
"author": {
7+
"name": "Dan Onoshko",
8+
"email": "[email protected]",
9+
"url": "https://github.com/dangreen"
10+
},
11+
"license": "MIT",
12+
"homepage": "https://github.com/TrigenSoftware/simple-libs/tree/master/packages/child-process-utils#readme",
13+
"funding": "https://ko-fi.com/dangreen",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/TrigenSoftware/simple-libs.git",
17+
"directory": "packages/child-process-utils"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/TrigenSoftware/simple-libs/issues"
21+
},
22+
"keywords": [
23+
"child_process",
24+
"child",
25+
"process",
26+
"utilities",
27+
"utils"
28+
],
29+
"engines": {
30+
"node": ">=18"
31+
},
32+
"exports": "./src/index.ts",
33+
"publishConfig": {
34+
"exports": {
35+
"types": "./dist/index.d.ts",
36+
"import": "./dist/index.js"
37+
},
38+
"directory": "package",
39+
"linkDirectory": false
40+
},
41+
"files": [
42+
"dist"
43+
],
44+
"scripts": {
45+
"clear:package": "del ./package",
46+
"clear:dist": "del ./dist",
47+
"clear": "del ./package ./dist ./coverage",
48+
"prepublishOnly": "run build clear:package clean-publish",
49+
"postpublish": "pnpm clear:package",
50+
"build": "tsc -p tsconfig.build.json",
51+
"lint": "eslint --parser-options tsconfigRootDir:. '**/*.{js,ts}'",
52+
"test:unit": "vitest run --coverage",
53+
"test:types": "tsc --noEmit",
54+
"test": "run -p lint test:unit test:types"
55+
},
56+
"dependencies": {
57+
"@simple-libs/stream-utils": "workspace:^",
58+
"@types/node": "^22.0.0"
59+
}
60+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { spawn } from 'child_process'
2+
import {
3+
describe,
4+
it,
5+
expect
6+
} from 'vitest'
7+
import {
8+
exitCode,
9+
catchProcessError,
10+
outputStream,
11+
output
12+
} from './index.js'
13+
14+
function child(code: string) {
15+
return spawn('node', ['-e', code])
16+
}
17+
18+
describe('child-process-utils', () => {
19+
describe('exitCode', () => {
20+
it('should return the exit code of a child process', async () => {
21+
const childProcess = child('process.exit(0)')
22+
const code = await exitCode(childProcess)
23+
24+
expect(code).toBe(0)
25+
})
26+
27+
it('should return the exit code of a child process with an error', async () => {
28+
const childProcess = child('process.exit(1)')
29+
const code = await exitCode(childProcess)
30+
31+
expect(code).toBe(1)
32+
})
33+
})
34+
35+
describe('catchProcessError', () => {
36+
it('should catch an error from a child process', async () => {
37+
const childProcess = child('throw new Error("Test error")')
38+
const error = await catchProcessError(childProcess)
39+
40+
expect(error).toBeInstanceOf(Error)
41+
expect(error?.message).toContain('Test error')
42+
})
43+
44+
it('should return null if the process exits successfully', async () => {
45+
const childProcess = child('process.exit(0)')
46+
const error = await catchProcessError(childProcess)
47+
48+
expect(error).toBeNull()
49+
})
50+
})
51+
52+
describe('outputStream', () => {
53+
it('should yield the stdout of a child process', async () => {
54+
const childProcess = child(`
55+
(async () => {
56+
for (let i = 0; i < 5; i++) {
57+
console.log('line ' + i)
58+
await new Promise(resolve => setTimeout(resolve, 10))
59+
}
60+
})()
61+
`)
62+
const output = outputStream(childProcess)
63+
const result: string[] = []
64+
65+
for await (const line of output) {
66+
result.push(line.toString())
67+
}
68+
69+
expect(result).toHaveLength(5)
70+
expect(result[0]).toBe('line 0\n')
71+
})
72+
73+
it('should throw an error if the process exits with a non-zero code', async () => {
74+
const childProcess = child('process.exit(1)')
75+
76+
await expect(async () => {
77+
for await (const _ of outputStream(childProcess)) {
78+
void _
79+
}
80+
}).rejects.toThrow('Process exited with non-zero code')
81+
})
82+
})
83+
84+
describe('output', () => {
85+
it('should return the stdout of a child process as a string', async () => {
86+
const childProcess = child(`
87+
(async () => {
88+
for (let i = 0; i < 3; i++) {
89+
console.log('output ' + i)
90+
await new Promise(resolve => setTimeout(resolve, 10))
91+
}
92+
})()
93+
`)
94+
const result = await output(childProcess)
95+
96+
expect(result.toString()).toBe('output 0\noutput 1\noutput 2\n')
97+
})
98+
99+
it('should throw an error if the process exits with a non-zero code', async () => {
100+
const childProcess = child('process.exit(1)')
101+
102+
await expect(output(childProcess)).rejects.toThrow('Process exited with non-zero code')
103+
})
104+
})
105+
})
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { type ChildProcess } from 'child_process'
2+
import { concatBufferStream } from '@simple-libs/stream-utils'
3+
4+
/**
5+
* Wait for a child process to exit and return its exit code.
6+
* @param process
7+
* @returns A promise that resolves to the exit code of the process.
8+
*/
9+
export async function exitCode(process: ChildProcess) {
10+
if (process.exitCode !== null) {
11+
return process.exitCode
12+
}
13+
14+
return new Promise<number>(resolve => process.once('close', resolve))
15+
}
16+
17+
/**
18+
* Catch error from a child process.
19+
* Also captures stderr output.
20+
* @param process
21+
* @returns A promise that resolves to an Error if the process exited with a non-zero code, or null if it exited successfully.
22+
*/
23+
export async function catchProcessError(process: ChildProcess) {
24+
let error = new Error('Process exited with non-zero code')
25+
let stderr = ''
26+
27+
process.on('error', (err: Error) => {
28+
error = err
29+
})
30+
31+
if (process.stderr) {
32+
let chunk: Buffer
33+
34+
for await (chunk of process.stderr) {
35+
stderr += chunk.toString()
36+
}
37+
}
38+
39+
const code = await exitCode(process)
40+
41+
if (stderr) {
42+
error = new Error(stderr)
43+
}
44+
45+
return code ? error : null
46+
}
47+
48+
/**
49+
* Throws an error if the child process exits with a non-zero code.
50+
* @param process
51+
*/
52+
export async function throwProcessError(process: ChildProcess) {
53+
const error = await catchProcessError(process)
54+
55+
if (error) {
56+
throw error
57+
}
58+
}
59+
60+
/**
61+
* Yields the stdout of a child process.
62+
* It will throw an error if the process exits with a non-zero code.
63+
* @param process
64+
* @yields The stdout of the process.
65+
*/
66+
export async function* outputStream(process: ChildProcess) {
67+
const error = throwProcessError(process)
68+
69+
if (process.stdout) {
70+
yield* process.stdout as AsyncIterable<Buffer>
71+
}
72+
73+
await error
74+
}
75+
76+
/**
77+
* Collects the stdout of a child process into a single Buffer.
78+
* It will throw an error if the process exits with a non-zero code.
79+
* @param process
80+
* @returns A promise that resolves to a Buffer containing the stdout of the process.
81+
*/
82+
export function output(process: ChildProcess) {
83+
return concatBufferStream(outputStream(process))
84+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist"
5+
},
6+
"include": [
7+
"src"
8+
],
9+
"exclude": [
10+
"**/*.spec.ts"
11+
]
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.build.json",
3+
"include": [
4+
"src"
5+
],
6+
"exclude": []
7+
}

pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)