mockaton 7.7.0 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -53
- package/index.d.ts +13 -2
- package/index.js +1 -0
- package/package.json +1 -1
- package/sample-mocks/api/user/{scores-full.GET.200.ts → typescript-scores-full.GET.200.ts} +1 -1
- package/sample-mocks/api/user/yaml-fruits.GET.200.yaml +5 -0
- package/src/Config.js +4 -0
- package/src/Dashboard.js +1 -1
- package/src/MockDispatcher.js +9 -40
- package/src/MockDispatcherPlugins.js +34 -0
- package/src/utils/http-response.js +1 -1
- package/src/utils/mime.js +2 -0
- /package/sample-mocks/api/user/{scores.GET.200.ts → typescript-scores.GET.200.ts} +0 -0
package/README.md
CHANGED
|
@@ -14,8 +14,11 @@ can be used for downloading a TAR of your XHR requests following that convention
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
## Getting Started Demo
|
|
17
|
-
Checkout this repo
|
|
18
|
-
|
|
17
|
+
- Checkout this repo
|
|
18
|
+
- `npm install tsx`
|
|
19
|
+
- `npm run demo:ts`
|
|
20
|
+
which will open the following dashboard
|
|
21
|
+
- Explore the [sample-mocks/](./sample-mocks) directory
|
|
19
22
|
|
|
20
23
|
<picture>
|
|
21
24
|
<source media="(prefers-color-scheme: light)" srcset="./README-dashboard-light.png">
|
|
@@ -41,7 +44,7 @@ _Reset_ button is for registering newly added, removed, or renamed mocks.
|
|
|
41
44
|
- As API documentation
|
|
42
45
|
- If you commit the mocks in the repo, when bisecting a bug, you don’t
|
|
43
46
|
have to sync the frontend with many backend repos
|
|
44
|
-
- Similarly,
|
|
47
|
+
- Similarly, it allows for checking out long-lived branches that have old API contracts
|
|
45
48
|
|
|
46
49
|
## Motivation
|
|
47
50
|
- Avoids having to spin up and maintain hefty or complex backends when developing UIs.
|
|
@@ -53,13 +56,11 @@ _Reset_ button is for registering newly added, removed, or renamed mocks.
|
|
|
53
56
|
- Reverse Proxies such as [Burp](https://portswigger.net/burp) are also handy for overriding responses.
|
|
54
57
|
- [Mock Server Worker](https://mswjs.io)
|
|
55
58
|
|
|
56
|
-
### Caveats
|
|
57
|
-
- Syncing the mocks, but the browser extension mentioned above helps.
|
|
58
|
-
|
|
59
59
|
|
|
60
60
|
## Basic Usage
|
|
61
|
+
`tsx` is only needed if you want to write mocks in TypeScript
|
|
61
62
|
```
|
|
62
|
-
npm install mockaton
|
|
63
|
+
npm install mockaton tsx
|
|
63
64
|
```
|
|
64
65
|
Create a `my-mockaton.js` file
|
|
65
66
|
```js
|
|
@@ -67,6 +68,7 @@ import { resolve } from 'node:path'
|
|
|
67
68
|
import { Mockaton } from 'mockaton'
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
// The Config options are explained in a section below
|
|
70
72
|
Mockaton({
|
|
71
73
|
mocksDir: resolve('my-mocks-dir'),
|
|
72
74
|
port: 2345
|
|
@@ -74,32 +76,7 @@ Mockaton({
|
|
|
74
76
|
```
|
|
75
77
|
|
|
76
78
|
```sh
|
|
77
|
-
node my-mockaton.js
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Config Options
|
|
81
|
-
There’s a Config section below with more details.
|
|
82
|
-
```ts
|
|
83
|
-
interface Config {
|
|
84
|
-
mocksDir: string
|
|
85
|
-
ignore?: RegExp // Defaults to /(\.DS_Store|~)$/
|
|
86
|
-
|
|
87
|
-
staticDir?: string
|
|
88
|
-
|
|
89
|
-
host?: string, // Defaults to 'localhost'
|
|
90
|
-
port?: number // Defaults to 0, which means auto-assigned
|
|
91
|
-
proxyFallback?: string // Target for relaying routes without mocks
|
|
92
|
-
|
|
93
|
-
delay?: number // Defaults to 1200 (ms)
|
|
94
|
-
cookies?: { [label: string]: string }
|
|
95
|
-
extraMimes?: { [fileExt: string]: string }
|
|
96
|
-
extraHeaders?: []
|
|
97
|
-
|
|
98
|
-
corsAllowed?: boolean, // Defaults to false
|
|
99
|
-
// cors* customization options are explained below
|
|
100
|
-
|
|
101
|
-
onReady?: (dashboardUrl: string) => void // Defaults to trying to open macOS and Win default browser.
|
|
102
|
-
}
|
|
79
|
+
node --import=tsx my-mockaton.js
|
|
103
80
|
```
|
|
104
81
|
|
|
105
82
|
---
|
|
@@ -124,12 +101,6 @@ api/user(default).GET.200.json
|
|
|
124
101
|
---
|
|
125
102
|
|
|
126
103
|
## You can write JSON mocks in JavaScript or TypeScript
|
|
127
|
-
For TypeScript mocks, install [tsx](https://www.npmjs.com/package/tsx) and load it.
|
|
128
|
-
```shell
|
|
129
|
-
npm install --save-dev tsx
|
|
130
|
-
node --import=tsx my-mockaton.js
|
|
131
|
-
```
|
|
132
|
-
---
|
|
133
104
|
|
|
134
105
|
An Object, Array, or String is sent as JSON.
|
|
135
106
|
|
|
@@ -159,6 +130,7 @@ export default function optionalName(request, response) {
|
|
|
159
130
|
|
|
160
131
|
If you need to serve a static `.js` file, put it in your `Config.staticDir`.
|
|
161
132
|
|
|
133
|
+
---
|
|
162
134
|
|
|
163
135
|
## File Name Convention
|
|
164
136
|
This convention is only for files within your `Config.mocksDir`.
|
|
@@ -218,20 +190,33 @@ api/foo/.GET.200.json
|
|
|
218
190
|
|
|
219
191
|
---
|
|
220
192
|
## Config
|
|
193
|
+
### `mocksDir: string`
|
|
194
|
+
This is the only required field
|
|
195
|
+
|
|
196
|
+
### `host?: string`
|
|
197
|
+
Defaults to `'localhost'`
|
|
221
198
|
|
|
222
|
-
### `
|
|
199
|
+
### `port?: number`
|
|
200
|
+
Defaults to `0`, which means auto-assigned
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
### `ignore?: RegExp`
|
|
204
|
+
Defaults to `/(\.DS_Store|~)$/`
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
### `proxyFallback?: string`
|
|
223
208
|
Lets you specify a target server for serving routes you don’t have mocks for.
|
|
224
209
|
For example, `Config.proxyFallback = 'http://example.com:8080'`
|
|
225
210
|
|
|
226
211
|
|
|
227
|
-
### `delay` 🕓
|
|
212
|
+
### `delay?: number` 🕓
|
|
228
213
|
The clock icon next to the mock selector is a checkbox for delaying a
|
|
229
214
|
particular response. They are handy for testing spinners.
|
|
230
215
|
|
|
231
216
|
The delay is globally configurable via `Config.delay = 1200` (milliseconds).
|
|
232
217
|
|
|
233
218
|
|
|
234
|
-
### `staticDir`
|
|
219
|
+
### `staticDir?: string`
|
|
235
220
|
Files under `Config.staticDir` don’t use the filename convention.
|
|
236
221
|
Also, they take precedence over the `GET` mocks in `Config.mockDir`.
|
|
237
222
|
|
|
@@ -247,7 +232,7 @@ Use Case 2: For a standalone demo server. For example,
|
|
|
247
232
|
build your frontend bundle, and serve it from Mockaton.
|
|
248
233
|
|
|
249
234
|
|
|
250
|
-
### `cookies`
|
|
235
|
+
### `cookies?: { [label: string]: string }`
|
|
251
236
|
The selected cookie is sent in every response in the `Set-Cookie` header.
|
|
252
237
|
|
|
253
238
|
The key is just a label used for selecting a particular cookie in the
|
|
@@ -271,10 +256,8 @@ Config.cookies = {
|
|
|
271
256
|
}
|
|
272
257
|
```
|
|
273
258
|
|
|
274
|
-
### `extraHeaders`
|
|
275
|
-
|
|
276
|
-
In other words, they can overwrite the `Content-Type`. Note
|
|
277
|
-
that it's an array and the header name goes in even indices.
|
|
259
|
+
### `extraHeaders?: string[]`
|
|
260
|
+
Note it’s a unidimensional array. The header name goes at even indices.
|
|
278
261
|
|
|
279
262
|
```js
|
|
280
263
|
Config.extraHeaders = [
|
|
@@ -284,18 +267,68 @@ Config.extraHeaders = [
|
|
|
284
267
|
]
|
|
285
268
|
```
|
|
286
269
|
|
|
287
|
-
### `extraMimes`
|
|
270
|
+
### `extraMimes?: { [fileExt: string]: string }`
|
|
288
271
|
```js
|
|
289
272
|
Config.extraMimes = {
|
|
290
273
|
jpg: 'application/jpeg'
|
|
291
274
|
}
|
|
292
275
|
```
|
|
293
276
|
|
|
294
|
-
### `
|
|
277
|
+
### `plugins?: { [fileEnding: string]: Plugin }`
|
|
278
|
+
```ts
|
|
279
|
+
type Plugin = (
|
|
280
|
+
filePath: string,
|
|
281
|
+
request: IncomingMessage,
|
|
282
|
+
response: OutgoingMessage
|
|
283
|
+
) => {
|
|
284
|
+
mime: string,
|
|
285
|
+
body: string | Uint8Array
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
Plugins are for processing mocks before sending them. The key is the ending
|
|
289
|
+
of a filename. In other words, it’s not limited to the file extension.
|
|
290
|
+
|
|
291
|
+
Node’s `request` and `response` are included but don’t call `response.end()`
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
#### Plugin Examples
|
|
295
|
+
```shell
|
|
296
|
+
npm install yaml
|
|
297
|
+
```
|
|
295
298
|
```js
|
|
296
|
-
|
|
299
|
+
import { readFileSync as read } from 'node:js'
|
|
300
|
+
import { parse } from 'yaml'
|
|
301
|
+
import { jsToJsonPlugin } from 'mockaton'
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
Config.plugins = {
|
|
305
|
+
'.yaml': function yamlToJsonPlugin(filePath) {
|
|
306
|
+
return {
|
|
307
|
+
mime: 'application/json',
|
|
308
|
+
body: JSON.stringify(parse(read(filePath, 'utf8')))
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
// e.g. GET /api/foo would be capitalized
|
|
313
|
+
'foo.GET.200.txt': function capitalizePlugin(filePath) {
|
|
314
|
+
return {
|
|
315
|
+
mime: 'application/text',
|
|
316
|
+
body: read(filePath, 'utf8').toUpperCase()
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
// Default Plugins
|
|
321
|
+
'.js': jsToJsonPlugin,
|
|
322
|
+
'.ts': jsToJsonPlugin // yes, it’s reused
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
297
326
|
|
|
298
|
-
|
|
327
|
+
### `corsAllowed?: boolean`
|
|
328
|
+
Defaults to `corsAllowed = false`
|
|
329
|
+
|
|
330
|
+
When `corsAllowed === true`, these are the default options:
|
|
331
|
+
```js
|
|
299
332
|
Config.corsOrigins = ['*']
|
|
300
333
|
Config.corsMethods = ['GET', 'PUT', 'DELETE', 'POST', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']
|
|
301
334
|
Config.corsHeaders = ['content-type']
|
|
@@ -304,7 +337,7 @@ Config.corsMaxAge = 0 // seconds to cache the preflight req
|
|
|
304
337
|
Config.corsExposedHeaders = [] // headers you need to access in client-side JS
|
|
305
338
|
```
|
|
306
339
|
|
|
307
|
-
### `onReady`
|
|
340
|
+
### `onReady?: (dashboardUrl: string) => void`
|
|
308
341
|
This is a callback `(dashboardAddress: string) => void`, which defaults to
|
|
309
342
|
trying to open the dashboard in your default browser in macOS and Windows.
|
|
310
343
|
|
|
@@ -380,7 +413,7 @@ await mockaton.reset()
|
|
|
380
413
|
|
|
381
414
|
<div style="display: flex; align-items: center; gap: 20px">
|
|
382
415
|
<img src="./sample-mocks/api/user/avatar.GET.200.png" width="170"/>
|
|
383
|
-
<p style="font-size: 18px">“Use Mockaton
|
|
416
|
+
<p style="font-size: 18px">“Use Mockaton”</p>
|
|
384
417
|
</div>
|
|
385
418
|
|
|
386
419
|
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import { Server } from 'node:http';
|
|
1
|
+
import { Server, IncomingMessage, OutgoingMessage } from 'node:http';
|
|
2
|
+
|
|
3
|
+
type Plugin = (
|
|
4
|
+
filePath: string,
|
|
5
|
+
request: IncomingMessage,
|
|
6
|
+
response: OutgoingMessage
|
|
7
|
+
) => {
|
|
8
|
+
mime: string,
|
|
9
|
+
body: string | Uint8Array
|
|
10
|
+
}
|
|
2
11
|
|
|
3
12
|
interface Config {
|
|
4
13
|
mocksDir: string
|
|
@@ -12,9 +21,11 @@ interface Config {
|
|
|
12
21
|
|
|
13
22
|
delay?: number
|
|
14
23
|
cookies?: { [label: string]: string }
|
|
15
|
-
extraHeaders?:
|
|
24
|
+
extraHeaders?: string[]
|
|
16
25
|
extraMimes?: { [fileExt: string]: string }
|
|
17
26
|
|
|
27
|
+
plugins?: { [fileExt: string]: Plugin }
|
|
28
|
+
|
|
18
29
|
corsAllowed?: boolean,
|
|
19
30
|
corsOrigins: string[]
|
|
20
31
|
corsMethods: string[]
|
package/index.js
CHANGED
package/package.json
CHANGED
package/src/Config.js
CHANGED
|
@@ -19,6 +19,8 @@ export const Config = Object.seal({
|
|
|
19
19
|
extraHeaders: [],
|
|
20
20
|
extraMimes: {},
|
|
21
21
|
|
|
22
|
+
plugins: {},
|
|
23
|
+
|
|
22
24
|
corsAllowed: false,
|
|
23
25
|
corsOrigins: ['*'],
|
|
24
26
|
corsMethods: StandardMethods,
|
|
@@ -48,6 +50,8 @@ export function setup(options) {
|
|
|
48
50
|
extraHeaders: val => Array.isArray(val) && val.length % 2 === 0,
|
|
49
51
|
extraMimes: is(Object),
|
|
50
52
|
|
|
53
|
+
plugins: is(Object),
|
|
54
|
+
|
|
51
55
|
corsAllowed: is(Boolean),
|
|
52
56
|
corsOrigins: validateCorsAllowedOrigins,
|
|
53
57
|
corsMethods: validateCorsAllowedMethods,
|
package/src/Dashboard.js
CHANGED
|
@@ -197,7 +197,7 @@ function PreviewLink({ method, urlMask }) {
|
|
|
197
197
|
document.querySelector(`.${CSS.PreviewLink}.${CSS.chosen}`)?.classList.remove(CSS.chosen)
|
|
198
198
|
this.classList.add(CSS.chosen)
|
|
199
199
|
clearTimeout(spinner)
|
|
200
|
-
const mime = res.headers.get('content-type')
|
|
200
|
+
const mime = res.headers.get('content-type') || ''
|
|
201
201
|
if (mime.startsWith('image/')) // naively assumes GET.200
|
|
202
202
|
renderPayloadImage(this.href)
|
|
203
203
|
else
|
package/src/MockDispatcher.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import { readFileSync as read } from 'node:fs'
|
|
3
2
|
|
|
4
3
|
import { proxy } from './ProxyRelay.js'
|
|
5
4
|
import { cookie } from './cookie.js'
|
|
6
5
|
import { Config } from './Config.js'
|
|
7
|
-
import {
|
|
6
|
+
import { preprocessPlugins } from './MockDispatcherPlugins.js'
|
|
8
7
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
9
8
|
import { JsonBodyParserError } from './utils/http-request.js'
|
|
10
9
|
import { sendInternalServerError, sendNotFound, sendBadRequest } from './utils/http-response.js'
|
|
@@ -21,11 +20,8 @@ export async function dispatchMock(req, response) {
|
|
|
21
20
|
return
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const filePath = join(Config.mocksDir, file)
|
|
27
|
-
|
|
28
|
-
response.statusCode = status
|
|
23
|
+
console.log(decodeURIComponent(req.url), ' → ', broker.file)
|
|
24
|
+
response.statusCode = broker.status
|
|
29
25
|
|
|
30
26
|
if (cookie.getCurrent())
|
|
31
27
|
response.setHeader('Set-Cookie', cookie.getCurrent())
|
|
@@ -33,12 +29,12 @@ export async function dispatchMock(req, response) {
|
|
|
33
29
|
for (let i = 0; i < Config.extraHeaders.length; i += 2)
|
|
34
30
|
response.setHeader(Config.extraHeaders[i], Config.extraHeaders[i + 1])
|
|
35
31
|
|
|
36
|
-
const
|
|
37
|
-
?
|
|
38
|
-
: await preprocessPlugins(
|
|
32
|
+
const { mime, body } = broker.isTemp500
|
|
33
|
+
? { mime: '', body: '' }
|
|
34
|
+
: await preprocessPlugins(join(Config.mocksDir, broker.file), req, response)
|
|
39
35
|
|
|
40
36
|
response.setHeader('Content-Type', mime)
|
|
41
|
-
setTimeout(() => response.end(
|
|
37
|
+
setTimeout(() => response.end(body), broker.delay)
|
|
42
38
|
}
|
|
43
39
|
catch (error) {
|
|
44
40
|
if (error instanceof JsonBodyParserError)
|
|
@@ -46,38 +42,11 @@ export async function dispatchMock(req, response) {
|
|
|
46
42
|
else if (error.code === 'ENOENT') // mock-file has been deleted
|
|
47
43
|
sendNotFound(response)
|
|
48
44
|
else if (error.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
|
|
49
|
-
if (error.toString().includes('Unknown file extension ".ts
|
|
50
|
-
console.
|
|
51
|
-
' npm install tsx\n',
|
|
52
|
-
' node --import=tsx my-mockaton.js\n')
|
|
45
|
+
if (error.toString().includes('Unknown file extension ".ts'))
|
|
46
|
+
console.error('Looks like you need a TypeScript compiler')
|
|
53
47
|
sendInternalServerError(response, error)
|
|
54
48
|
}
|
|
55
49
|
else
|
|
56
50
|
sendInternalServerError(response, error)
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
|
-
|
|
60
|
-
// TODO expose to userland for custom plugins such yaml -> json
|
|
61
|
-
async function preprocessPlugins(filePath, req, response) {
|
|
62
|
-
if (filePath.endsWith('.js') || filePath.endsWith('.ts'))
|
|
63
|
-
return await jsPlugin(filePath, req, response)
|
|
64
|
-
return readPlugin(filePath, req, response)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function temp500Plugin(filePath) {
|
|
68
|
-
return [mimeFor(filePath), '']
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function jsPlugin(filePath, req, response) {
|
|
72
|
-
const jsExport = (await import(filePath + '?' + Date.now())).default // date for cache busting
|
|
73
|
-
const mockBody = typeof jsExport === 'function'
|
|
74
|
-
? await jsExport(req, response)
|
|
75
|
-
: JSON.stringify(jsExport, null, 2)
|
|
76
|
-
const mime = response.getHeader('Content-Type') // jsFunc are allowed to set it
|
|
77
|
-
|| mimeFor('.json')
|
|
78
|
-
return [mime, mockBody]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function readPlugin(filePath) {
|
|
82
|
-
return [mimeFor(filePath), read(filePath)]
|
|
83
|
-
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync as read } from 'node:fs'
|
|
2
|
+
import { mimeFor } from './utils/mime.js'
|
|
3
|
+
import { Config } from './Config.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const plugins = {
|
|
7
|
+
'.js': jsToJsonPlugin,
|
|
8
|
+
'.ts': jsToJsonPlugin
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function preprocessPlugins(filePath, req, response) {
|
|
12
|
+
for (const [ext, plugin] of Object.entries({ ...plugins, ...Config.plugins }))
|
|
13
|
+
if (filePath.endsWith(ext))
|
|
14
|
+
return await plugin(filePath, req, response)
|
|
15
|
+
return defaultPlugin(filePath)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function defaultPlugin(filePath) {
|
|
19
|
+
return {
|
|
20
|
+
mime: mimeFor(filePath),
|
|
21
|
+
body: read(filePath)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function jsToJsonPlugin(filePath, req, response) {
|
|
26
|
+
const jsExport = (await import(filePath + '?' + Date.now())).default // date for cache busting
|
|
27
|
+
const body = typeof jsExport === 'function'
|
|
28
|
+
? await jsExport(req, response)
|
|
29
|
+
: JSON.stringify(jsExport, null, 2)
|
|
30
|
+
return {
|
|
31
|
+
mime: response.getHeader('Content-Type') || mimeFor('.json'), // jsFunc are allowed to set it
|
|
32
|
+
body
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/utils/mime.js
CHANGED
|
File without changes
|