mockaton 7.7.1 → 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 +82 -50
- package/index.d.ts +13 -2
- package/index.js +1 -0
- package/package.json +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/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,9 +56,6 @@ _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
61
|
`tsx` is only needed if you want to write mocks in TypeScript
|
|
@@ -68,6 +68,7 @@ import { resolve } from 'node:path'
|
|
|
68
68
|
import { Mockaton } from 'mockaton'
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
// The Config options are explained in a section below
|
|
71
72
|
Mockaton({
|
|
72
73
|
mocksDir: resolve('my-mocks-dir'),
|
|
73
74
|
port: 2345
|
|
@@ -78,31 +79,6 @@ Mockaton({
|
|
|
78
79
|
node --import=tsx my-mockaton.js
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
## Config Options
|
|
82
|
-
There’s a Config section below with more details.
|
|
83
|
-
```ts
|
|
84
|
-
interface Config {
|
|
85
|
-
mocksDir: string
|
|
86
|
-
ignore?: RegExp // Defaults to /(\.DS_Store|~)$/
|
|
87
|
-
|
|
88
|
-
staticDir?: string
|
|
89
|
-
|
|
90
|
-
host?: string, // Defaults to 'localhost'
|
|
91
|
-
port?: number // Defaults to 0, which means auto-assigned
|
|
92
|
-
proxyFallback?: string // Target for relaying routes without mocks
|
|
93
|
-
|
|
94
|
-
delay?: number // Defaults to 1200 (ms)
|
|
95
|
-
cookies?: { [label: string]: string }
|
|
96
|
-
extraMimes?: { [fileExt: string]: string }
|
|
97
|
-
extraHeaders?: []
|
|
98
|
-
|
|
99
|
-
corsAllowed?: boolean, // Defaults to false
|
|
100
|
-
// cors* customization options are explained below
|
|
101
|
-
|
|
102
|
-
onReady?: (dashboardUrl: string) => void // Defaults to trying to open macOS and Win default browser.
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
82
|
---
|
|
107
83
|
|
|
108
84
|
## Mock Variants
|
|
@@ -125,12 +101,6 @@ api/user(default).GET.200.json
|
|
|
125
101
|
---
|
|
126
102
|
|
|
127
103
|
## You can write JSON mocks in JavaScript or TypeScript
|
|
128
|
-
For TypeScript mocks, install [tsx](https://www.npmjs.com/package/tsx) and load it.
|
|
129
|
-
```shell
|
|
130
|
-
npm install --save-dev tsx
|
|
131
|
-
node --import=tsx my-mockaton.js
|
|
132
|
-
```
|
|
133
|
-
---
|
|
134
104
|
|
|
135
105
|
An Object, Array, or String is sent as JSON.
|
|
136
106
|
|
|
@@ -160,6 +130,7 @@ export default function optionalName(request, response) {
|
|
|
160
130
|
|
|
161
131
|
If you need to serve a static `.js` file, put it in your `Config.staticDir`.
|
|
162
132
|
|
|
133
|
+
---
|
|
163
134
|
|
|
164
135
|
## File Name Convention
|
|
165
136
|
This convention is only for files within your `Config.mocksDir`.
|
|
@@ -219,20 +190,33 @@ api/foo/.GET.200.json
|
|
|
219
190
|
|
|
220
191
|
---
|
|
221
192
|
## Config
|
|
193
|
+
### `mocksDir: string`
|
|
194
|
+
This is the only required field
|
|
195
|
+
|
|
196
|
+
### `host?: string`
|
|
197
|
+
Defaults to `'localhost'`
|
|
198
|
+
|
|
199
|
+
### `port?: number`
|
|
200
|
+
Defaults to `0`, which means auto-assigned
|
|
222
201
|
|
|
223
|
-
|
|
202
|
+
|
|
203
|
+
### `ignore?: RegExp`
|
|
204
|
+
Defaults to `/(\.DS_Store|~)$/`
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
### `proxyFallback?: string`
|
|
224
208
|
Lets you specify a target server for serving routes you don’t have mocks for.
|
|
225
209
|
For example, `Config.proxyFallback = 'http://example.com:8080'`
|
|
226
210
|
|
|
227
211
|
|
|
228
|
-
### `delay` 🕓
|
|
212
|
+
### `delay?: number` 🕓
|
|
229
213
|
The clock icon next to the mock selector is a checkbox for delaying a
|
|
230
214
|
particular response. They are handy for testing spinners.
|
|
231
215
|
|
|
232
216
|
The delay is globally configurable via `Config.delay = 1200` (milliseconds).
|
|
233
217
|
|
|
234
218
|
|
|
235
|
-
### `staticDir`
|
|
219
|
+
### `staticDir?: string`
|
|
236
220
|
Files under `Config.staticDir` don’t use the filename convention.
|
|
237
221
|
Also, they take precedence over the `GET` mocks in `Config.mockDir`.
|
|
238
222
|
|
|
@@ -248,7 +232,7 @@ Use Case 2: For a standalone demo server. For example,
|
|
|
248
232
|
build your frontend bundle, and serve it from Mockaton.
|
|
249
233
|
|
|
250
234
|
|
|
251
|
-
### `cookies`
|
|
235
|
+
### `cookies?: { [label: string]: string }`
|
|
252
236
|
The selected cookie is sent in every response in the `Set-Cookie` header.
|
|
253
237
|
|
|
254
238
|
The key is just a label used for selecting a particular cookie in the
|
|
@@ -272,10 +256,8 @@ Config.cookies = {
|
|
|
272
256
|
}
|
|
273
257
|
```
|
|
274
258
|
|
|
275
|
-
### `extraHeaders`
|
|
276
|
-
|
|
277
|
-
In other words, they can overwrite the `Content-Type`. Note
|
|
278
|
-
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.
|
|
279
261
|
|
|
280
262
|
```js
|
|
281
263
|
Config.extraHeaders = [
|
|
@@ -285,18 +267,68 @@ Config.extraHeaders = [
|
|
|
285
267
|
]
|
|
286
268
|
```
|
|
287
269
|
|
|
288
|
-
### `extraMimes`
|
|
270
|
+
### `extraMimes?: { [fileExt: string]: string }`
|
|
289
271
|
```js
|
|
290
272
|
Config.extraMimes = {
|
|
291
273
|
jpg: 'application/jpeg'
|
|
292
274
|
}
|
|
293
275
|
```
|
|
294
276
|
|
|
295
|
-
### `
|
|
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
|
+
```
|
|
296
298
|
```js
|
|
297
|
-
|
|
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
|
+
|
|
298
326
|
|
|
299
|
-
|
|
327
|
+
### `corsAllowed?: boolean`
|
|
328
|
+
Defaults to `corsAllowed = false`
|
|
329
|
+
|
|
330
|
+
When `corsAllowed === true`, these are the default options:
|
|
331
|
+
```js
|
|
300
332
|
Config.corsOrigins = ['*']
|
|
301
333
|
Config.corsMethods = ['GET', 'PUT', 'DELETE', 'POST', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']
|
|
302
334
|
Config.corsHeaders = ['content-type']
|
|
@@ -305,7 +337,7 @@ Config.corsMaxAge = 0 // seconds to cache the preflight req
|
|
|
305
337
|
Config.corsExposedHeaders = [] // headers you need to access in client-side JS
|
|
306
338
|
```
|
|
307
339
|
|
|
308
|
-
### `onReady`
|
|
340
|
+
### `onReady?: (dashboardUrl: string) => void`
|
|
309
341
|
This is a callback `(dashboardAddress: string) => void`, which defaults to
|
|
310
342
|
trying to open the dashboard in your default browser in macOS and Windows.
|
|
311
343
|
|
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