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 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,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
- ### `proxyFallback`
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
- They are applied last, right before ending the response.
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
- ### `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
+ ```
295
298
  ```js
296
- 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
+
297
326
 
298
- // Defaults when `corsAllowed === true`
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” - Albert Einstein</p>
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?: [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.0",
5
+ "version": "7.8.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import scores from './scores.GET.200'
1
+ import scores from './typescript-scores.GET.200'
2
2
 
3
3
  export default [
4
4
  ...scores,
@@ -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