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 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 and run `npm run demo`, which will open the following
18
- dashboard. Then, explore the [sample-mocks/](./sample-mocks) directory.
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, I can check out long-lived branches that have old API contracts
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
- ### `proxyFallback`
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
- They are applied last, right before ending the response.
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
- ### `corsAllowed`
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
- Config.corsAllowed = true
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
- // Defaults when `corsAllowed === true`
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?: [string, string][]
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
@@ -1,3 +1,4 @@
1
1
  export { Mockaton } from './src/Mockaton.js'
2
2
  export { jwtCookie } from './src/utils/jwt.js'
3
3
  export { Commander } from './src/Commander.js'
4
+ export { jsToJsonPlugin } from './src/MockDispatcherPlugins.js'
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "A deterministic server-side for developing and testing frontend clients",
4
4
  "type": "module",
5
- "version": "7.7.1",
5
+ "version": "7.8.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -0,0 +1,5 @@
1
+ fruits:
2
+ apple
3
+ banana
4
+ orange
5
+
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
@@ -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 { mimeFor } from './utils/mime.js'
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
- const { file, status, delay } = broker
25
- console.log(decodeURIComponent(req.url), ' → ', file)
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 [mime, mockBody] = broker.isTemp500
37
- ? temp500Plugin(filePath, req, response)
38
- : await preprocessPlugins(filePath, req, response)
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(mockBody), delay)
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.log('Looks like you need a TypeScript compiler\n',
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
+ }
@@ -67,5 +67,5 @@ export function sendNotFound(response) {
67
67
  export function sendInternalServerError(response, error) {
68
68
  console.error(error)
69
69
  response.statusCode = 500
70
- response.end()
70
+ response.end(error?.code || '')
71
71
  }
package/src/utils/mime.js CHANGED
@@ -81,6 +81,8 @@ const mimes = {
81
81
  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
82
82
  xml: 'application/xml',
83
83
  xul: 'application/vnd.mozilla.xul+xml',
84
+ yaml: 'application/yaml',
85
+ yml: 'application/yaml',
84
86
  zip: 'application/zip'
85
87
  }
86
88