Skip to content

Commit 1d10a89

Browse files
committed
Add TypeScript annotations and documentation.
- http() can no longer take a URL as first argument as a shorthand to make a GET request. - runProcess() no longer returns a promise. Instead, the object it returns has a waitUntilExit() method BREAKING CHANGE: http() no longer takes a string as shorthand. runProcess() no longer returns a promise.
1 parent 31deddc commit 1d10a89

File tree

12 files changed

+528
-889
lines changed

12 files changed

+528
-889
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/node_modules
2+
/index.d.ts

README.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,9 @@ Returns an object with the following members:
5353
- `outputStream`: A `Readable` stream for both stdout and stderr.
5454
- `waitForOutput(pattern, timeout = 1000)`: Enables waiting for a given
5555
substring or regular expression to be output, for up to a given timeout.
56+
- `waitUntilExit()`: Returns a promise that will resolve with the exit code.
5657
- `childProcess`: The underlying instance of `ChildProcess`
5758

58-
The returned object is also a `Promise` that when the process exits, resolves
59-
with an object containing `output` and the exit `code`.
60-
6159
```js
6260
import test from 'ava'
6361
import {runProcess} from 'ava-patterns'
@@ -102,13 +100,11 @@ test('writing files', async (t) => {
102100
```
103101

104102
### `http()`
105-
Concisely send HTTP requests.
106-
107-
If given a URL string, sends a GET request and returns the response body.
103+
Make an HTTP request.
108104

109-
Otherwise, takes in an object with properties `method`, `url`, `headers`, and an
110-
optional `body` and returns an object with properties `status`, `headers`, and
111-
`body`.
105+
It takes in an object with properties `method`, `url`, optional `headers`, and
106+
an optional `body` and returns an object with properties `status`, `headers`,
107+
and `body`.
112108

113109
```js
114110
import {http} from 'ava-patterns'
@@ -119,7 +115,6 @@ test('sending HTTP requests', async (t) => {
119115
const response = await http({
120116
method: 'POST',
121117
url: 'https://httpbin.org/post',
122-
headers: {},
123118
body: 'Hello World!'
124119
})
125120
t.like(JSON.parse(response.body), {data: 'Hello World!'})

http/index.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
import {sendRequest} from 'passing-notes'
22

3-
export default async function (request) {
4-
if (typeof request === 'string') {
5-
const response = await sendRequest({
6-
method: 'GET',
7-
headers: {},
8-
url: request,
9-
})
10-
return response.body
11-
}
3+
/**
4+
* @typedef {'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'} Method
5+
* @typedef {{ [name: string]: string }} Headers
6+
* @typedef {string | Buffer} Data
7+
* @typedef {Data | ReadableStream<Data> | AsyncIterable<Data>} Body
8+
*
9+
* @typedef {{
10+
* method: Method,
11+
* url: string,
12+
* headers?: Headers,
13+
* body?: Body
14+
* }} Request
15+
*
16+
* @typedef {{
17+
* status: number,
18+
* headers: Headers,
19+
* body: Body
20+
* }} Response
21+
*/
1222

23+
/**
24+
* Make an HTTP request
25+
*
26+
* @param {Request} request - Request `method` and `url`, with optional
27+
* `headers` and `body`
28+
*
29+
* @return {Promise<Response>} - The response `status`, `headers`, and `body`
30+
*/
31+
export default function (request) {
1332
return sendRequest(request)
1433
}

http/index.test.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import test from 'ava'
22
import {http} from '../index.js'
33

4-
test('sending a simple HTTP GET request', async (t) => {
5-
t.regex(await http('http://example.com'), /Example Domain/)
6-
})
7-
84
test('sending an HTTP request', async (t) => {
95
const response = await http({
106
method: 'POST',
117
url: 'https://httpbin.org/post',
128
headers: {},
139
body: 'Hello World!',
1410
})
11+
12+
if (typeof response.body !== 'string') {
13+
return t.fail('Response body was not a string')
14+
}
15+
1516
t.like(JSON.parse(response.body), {data: 'Hello World!'})
1617
})
18+
19+
test('omitting unused fields', async (t) => {
20+
const response = await http({
21+
method: 'GET',
22+
url: 'https://httpbin.org/status/200',
23+
})
24+
t.like(response, {status: 200})
25+
})

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"repository": "vinsonchuong/ava-patterns",
1111
"main": "index.js",
1212
"scripts": {
13-
"test": "xo && ava",
13+
"test": "xo && tsc --noEmit && ava",
14+
"prepublishOnly": "tsc --emitDeclarationOnly --outFile index.d.ts",
1415
"release": "semantic-release"
1516
},
1617
"type": "module",
@@ -21,7 +22,8 @@
2122
"tempy": "^2.0.0"
2223
},
2324
"devDependencies": {
24-
"ava": "^3.15.0",
25+
"@types/node": "^16.11.6",
26+
"ava": "^4.0.0-rc.1",
2527
"got": "^11.8.2",
2628
"semantic-release": "^18.0.0",
2729
"xo": "^0.45.0"

run-process/index.js

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@ import {spawn} from 'node:child_process'
33
import {PassThrough} from 'node:stream'
44
import {wait} from '../index.js'
55

6+
/**
7+
* Create a temporary directory and delete it at the end of the test.
8+
*
9+
* @param {import('ava').ExecutionContext<any>} t - the AVA test context
10+
* @param {Object} options
11+
* @param {Array<string>} options.command - a shell command to spawn a process
12+
* @param {{ [name: string]: string }} [options.env] - Environment variables to
13+
* pass into the process
14+
* @param {string} [options.cwd] - Working directory in which to run the process
15+
*
16+
* @return {{
17+
* childProcess: import('node:child_process').ChildProcess
18+
* output: string,
19+
* outputStream: import('stream').Readable,
20+
* waitForOutput(output: string | RegExp): Promise<void>,
21+
* waitUntilExit(): Promise<number>
22+
* }}
23+
*/
624
export default function (
725
t,
826
{command: [command, ...args], env = {}, cwd = process.cwd()},
@@ -22,66 +40,73 @@ export default function (
2240
}
2341
})
2442

25-
const program = new Promise((resolve) => {
43+
const exitCode = new Promise((resolve) => {
2644
child.on('close', (code) => {
27-
resolve({code, output: program.output})
45+
resolve(code)
2846
})
2947
})
30-
program.childProcess = child
31-
program.output = ''
32-
program.outputStream = new PassThrough()
3348

34-
program.outputStream.setEncoding('utf8')
49+
let output = ''
50+
51+
const outputStream = new PassThrough()
52+
outputStream.setEncoding('utf8')
3553
child.stdout.setEncoding('utf8')
3654
child.stderr.setEncoding('utf8')
3755
Promise.all([
3856
(async () => {
3957
for await (const data of child.stdout) {
40-
program.output += data
41-
program.outputStream.write(data)
58+
output += data
59+
outputStream.write(data)
4260
}
4361
})(),
4462
(async () => {
4563
for await (const data of child.stderr) {
46-
program.output += data
47-
program.outputStream.write(data)
64+
output += data
65+
outputStream.write(data)
4866
}
4967
})(),
5068
]).then(
5169
() => {
52-
program.outputStream.end()
70+
outputStream.end()
5371
},
5472
(error) => {
55-
program.outputStream.emit('error', error)
73+
outputStream.emit('error', error)
5674
},
5775
)
5876

59-
program.waitForOutput = async (pattern, timeout = 1000) => {
60-
const match =
61-
typeof pattern === 'string'
62-
? (string) => string.includes(pattern)
63-
: (string) => Boolean(pattern.test(string))
64-
65-
await Promise.race([
66-
(async () => {
67-
await wait(timeout)
68-
throw new Error(
69-
`Timeout exceeded without seeing expected output:\n${program.output}`,
70-
)
71-
})(),
72-
(async () => {
73-
for await (const data of program.outputStream) {
74-
if (match(data)) {
75-
return
77+
return {
78+
childProcess: child,
79+
get output() {
80+
return output
81+
},
82+
outputStream,
83+
async waitForOutput(pattern, timeout = 1000) {
84+
await Promise.race([
85+
(async () => {
86+
await wait(timeout)
87+
throw new Error(
88+
`Timeout exceeded without seeing expected output:\n${output}`,
89+
)
90+
})(),
91+
(async () => {
92+
for await (const data of outputStream) {
93+
if (
94+
typeof pattern === 'string'
95+
? data.includes(pattern)
96+
: pattern.test(data)
97+
) {
98+
return
99+
}
76100
}
77-
}
78101

79-
throw new Error(
80-
`Process ended without emitting expected output:\n${program.output}`,
81-
)
82-
})(),
83-
])
102+
throw new Error(
103+
`Process ended without emitting expected output:\n${output}`,
104+
)
105+
})(),
106+
])
107+
},
108+
waitUntilExit() {
109+
return exitCode
110+
},
84111
}
85-
86-
return program
87112
}

run-process/index.test.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,9 @@ test('setting the working directory and environment variables', async (t) => {
8181
})
8282

8383
test('running a simple command that terminates', async (t) => {
84-
const {output, code} = await runProcess(t, {
85-
command: ['ls', '/'],
86-
})
84+
const program = runProcess(t, {command: ['ls', '/']})
85+
const code = await program.waitUntilExit()
8786

88-
t.true(output.includes('tmp'))
87+
t.true(program.output.includes('tmp'))
8988
t.is(code, 0)
9089
})

tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "esnext",
5+
"moduleResolution": "node",
6+
"allowSyntheticDefaultImports": true,
7+
"allowJs": true,
8+
"checkJs": true,
9+
"declaration": true
10+
},
11+
"exclude": [
12+
"node_modules"
13+
]
14+
}

use-temporary-directory/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ import fs from 'fs-extra'
33
import tempy from 'tempy'
44
import stripIndent from 'strip-indent'
55

6+
/**
7+
* @typedef {{
8+
* path: string,
9+
* writeFile(filePath: string, fileContents: string): Promise<void>
10+
* }} Directory
11+
*/
12+
13+
/**
14+
* Create a temporary directory and delete it at the end of the test.
15+
*
16+
* @param {import('ava').ExecutionContext<any>} t
17+
*
18+
* @return {Promise<{
19+
* path: string,
20+
* writeFile(filePath: string, fileContents: string): Promise<void>
21+
* }>} an object allowing manipulation of files within the directory.
22+
*/
623
export default async function (t) {
724
const directory = tempy.directory()
825

use-temporary-directory/index.test.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import path from 'node:path'
22
import {promises as fs} from 'node:fs'
3+
import {promisify} from 'node:util'
4+
import childProcess from 'node:child_process'
35
import test from 'ava'
4-
import {useTemporaryDirectory, runProcess} from '../index.js'
6+
import {useTemporaryDirectory} from '../index.js'
7+
8+
const exec = promisify(childProcess.exec)
59

610
test.serial('creating a directory', async (t) => {
711
const directory = await useTemporaryDirectory(t)
@@ -39,10 +43,7 @@ test('automatically setting permissions for executable files', async (t) => {
3943
`,
4044
)
4145

42-
const {output} = await runProcess(t, {
43-
command: ['./bin.js'],
44-
cwd: directory.path,
45-
})
46+
const {stdout: output} = await exec('./bin.js', {cwd: directory.path})
4647
t.is(output, 'Hello World!\n')
4748
})
4849

0 commit comments

Comments
 (0)