mockaton 8.2.22 → 8.3.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
@@ -6,20 +6,17 @@
6
6
 
7
7
  _Mockaton_ is a mock server for improving the frontend development and testing experience.
8
8
 
9
- With Mockaton, you don’t need to write code for wiring your mocks. Instead, it
9
+ With Mockaton you don’t need to write code for wiring your mocks. Instead, it
10
10
  scans a given directory for filenames following a convention similar to the
11
11
  URL paths. For example, the following file will be served on `/api/user/1234`
12
12
  ```
13
13
  my-mocks-dir/api/user/[user-id].GET.200.json
14
14
  ```
15
15
 
16
- By the way, [this browser
17
- extension](https://github.com/ericfortis/devtools-ext-tar-http-requests)
18
- can create a TAR of your requests following that convention.
19
-
20
- Nonetheless, you don’t need to mock all your APIs. Mockaton
21
- can request from your backend the routes you don’t have mocks for.
22
- That’s done with `config.proxyFallback = 'http://mybackend'`
16
+ By the way, you don’t need to mock all your APIs. Mockaton can request
17
+ from your backend the routes you don’t have mocks for. That’s done
18
+ with `config.proxyFallback = 'http://mybackend'`. For convenience, you
19
+ can save mocks for those responses with `config.collectProxied = true`
23
20
 
24
21
  ## Multiple Mock Variants
25
22
  Each route can have many mocks, which could either be:
@@ -37,7 +34,7 @@ which is handy for setting up tests (see **Commander API** below).
37
34
  <picture>
38
35
  <source media="(prefers-color-scheme: light)" srcset="./README-dashboard-light.png">
39
36
  <source media="(prefers-color-scheme: dark)" srcset="./README-dashboard-dark.png">
40
- <img alt="Mockaton Dashboard Demo" src="./README-dashboard-light.png" style="max-width: 860px">
37
+ <img alt="Mockaton Dashboard Demo" src="./README-dashboard-light.png">
41
38
  </picture>
42
39
 
43
40
 
@@ -93,7 +90,7 @@ The _Reset_ button is for registering newly added, removed, or renamed mocks.
93
90
  - Polled resources (for triggering their different states)
94
91
  - alerts
95
92
  - notifications
96
- - slow to build assets
93
+ - slow to build resources
97
94
 
98
95
  ### Time Travel
99
96
  If you commit the mocks to your repo, it’s straightforward to bisect bugs and
@@ -103,8 +100,8 @@ backends to old API contracts or databases.
103
100
  ### Deterministic Standalone Demo Server
104
101
  Perhaps you need to demo your app, but the ideal flow is too complex to
105
102
  simulate from the actual backend. In this case, compile your frontend app and
106
- put its built assets in `config.staticDir`. Then, from the Mockaton dashboard
107
- you can "Bulk Select" mocks to simulate the complete states you want to demo.
103
+ put its built assets in `config.staticDir`. Then, on the dashboard
104
+ "Bulk Select" mocks to simulate the complete states you want to demo.
108
105
  For bulk-selecting, you just need to add a comment to the mock
109
106
  filename, such as `(demo-part1)`, `(demo-part2)`.
110
107
 
@@ -113,7 +110,7 @@ filename, such as `(demo-part1)`, `(demo-part2)`.
113
110
  - Avoids spinning up and maintaining hefty backends when developing UIs.
114
111
  - For a deterministic, comprehensive, and consistent backend state. For example, having
115
112
  a collection with all the possible state variants helps for spotting inadvertent bugs.
116
- - Sometimes, frontend progress is blocked waiting for some backend API. Similarly,
113
+ - Sometimes frontend progress is blocked waiting for some backend API. Similarly,
117
114
  it’s often delayed due to missing data or inconvenient contracts. Therefore,
118
115
  many meetings can be saved by prototyping frontend features with mocks, and
119
116
  then showing those contracts to the backend team.
@@ -271,14 +268,40 @@ Defaults to `0`, which means auto-assigned
271
268
  Defaults to `/(\.DS_Store|~)$/`
272
269
 
273
270
 
274
- ### `delay?: number` 🕓
275
- The delay is globally configurable, it defaults to `1200` (milliseconds).
271
+ ### `delay?: number`
272
+ Routes can individually be delayed with the 🕓 checkbox. On the other hand,
273
+ the amount is globally configurable. It defaults to `config.delay=1200` milliseconds.
276
274
 
277
275
 
278
276
  ### `proxyFallback?: string`
279
277
  Lets you specify a target server for serving routes you don’t have mocks for.
280
278
  For example, `config.proxyFallback = 'http://example.com'`
281
279
 
280
+ ### `collectProxied?: boolean`
281
+ Defaults to `false`. With this flag you can save mocks that hit
282
+ your proxy fallback to `config.mocksDir`. If there are UUIDv4 in the
283
+ URL the filename will have `[id]` in their place. For example,
284
+
285
+ ```
286
+ /api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
287
+ my-mocks-dir/api/user/[id]/likes.GET.200.json
288
+ ```
289
+
290
+ Note that newly saved mocks won’t be served until you
291
+ **register them** by reinitializing Mockaton or clicking "Reset".
292
+
293
+ Registered mocks won’t be overwritten (they don’t hit the fallback server).
294
+ On the other hand, newly saved mocks get overwritten while they are unregistered.
295
+
296
+ <details>
297
+ <summary>Extension Details</summary>
298
+ <p>
299
+ If you see an <code>.unknown</code> extension, that’s because the
300
+ <code>Content-Type</code> sent by your backend is either missing or not present
301
+ in the predefined list. For the latter, add it to <code>config.extraMimes</code>
302
+ </p>
303
+ </details>
304
+
282
305
 
283
306
  ### `staticDir?: string`
284
307
  - Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
@@ -349,9 +372,10 @@ type Plugin = (
349
372
  body: string | Uint8Array
350
373
  }>
351
374
  ```
352
- Plugins are for processing mocks before sending them.
375
+ Plugins are for processing mocks before sending them. If no regex matches the filename,
376
+ it fallbacks to reading the file from disk and computing the MIME from the extension.
353
377
 
354
- Note: don’t call `response.end()` on them.
378
+ Note: don’t call `response.end()` on any plugin.
355
379
 
356
380
  <details>
357
381
  <summary><b> See Plugin Examples </b></summary>
@@ -365,7 +389,11 @@ import { readFileSync } from 'node:js'
365
389
  import { jsToJsonPlugin } from 'mockaton'
366
390
 
367
391
  config.plugins = [
368
- [/\.(js|ts)$/, jsToJsonPlugin], // Default but you need to add it to your list if you need it
392
+
393
+ // Although `jsToJsonPlugin` is set by default, you need to add it to your list if you need it.
394
+ // In other words, your plugins array overwrites the default list. This way you can remove it.
395
+ [/\.(js|ts)$/, jsToJsonPlugin],
396
+
369
397
  [/\.yml$/, yamlToJsonPlugin],
370
398
  [/foo\.GET\.200\.txt$/, capitalizePlugin], // e.g. GET /api/foo would be capitalized
371
399
  ]
package/index.d.ts CHANGED
@@ -18,6 +18,7 @@ interface Config {
18
18
  host?: string,
19
19
  port?: number
20
20
  proxyFallback?: string
21
+ collectProxied?: boolean
21
22
 
22
23
  delay?: number
23
24
  cookies?: { [label: string]: string }
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": "8.2.22",
5
+ "version": "8.3.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -39,6 +39,8 @@ export const apiPatchRequests = new Map([
39
39
  [API.cors, setCorsAllowed]
40
40
  ])
41
41
 
42
+ /* GET */
43
+
42
44
  function serveDashboard(_, response) { sendFile(response, join(import.meta.dirname, 'Dashboard.html')) }
43
45
  function serveDashboardAsset(req, response) { sendFile(response, join(import.meta.dirname, req.url)) }
44
46
 
@@ -48,13 +50,26 @@ function listMockBrokers(_, response) { sendJSON(response, mockBrokersCollection
48
50
  function getProxyFallback(_, response) { sendJSON(response, Config.proxyFallback) }
49
51
  function getIsCorsAllowed(_, response) { sendJSON(response, Config.corsAllowed) }
50
52
 
53
+ async function listStaticFiles(req, response) { // TESTME
54
+ try {
55
+ const files = Config.staticDir
56
+ ? listFilesRecursively(Config.staticDir).filter(f => !Config.ignore.test(f))
57
+ : []
58
+ sendJSON(response, files)
59
+ }
60
+ catch (error) {
61
+ sendBadRequest(response, error)
62
+ }
63
+ }
64
+
65
+
66
+ /* PATCH */
51
67
 
52
68
  function reinitialize(_, response) {
53
69
  mockBrokersCollection.init()
54
70
  sendOK(response)
55
71
  }
56
72
 
57
-
58
73
  async function selectCookie(req, response) {
59
74
  try {
60
75
  cookie.setCurrent(await parseJSON(req))
@@ -65,14 +80,12 @@ async function selectCookie(req, response) {
65
80
  }
66
81
  }
67
82
 
68
-
69
83
  async function selectMock(req, response) {
70
84
  try {
71
85
  const file = await parseJSON(req)
72
86
  const broker = mockBrokersCollection.getBrokerByFilename(file)
73
87
  if (!broker || !broker.mockExists(file))
74
88
  throw `Missing Mock: ${file}`
75
-
76
89
  broker.updateFile(file)
77
90
  sendOK(response)
78
91
  }
@@ -81,17 +94,14 @@ async function selectMock(req, response) {
81
94
  }
82
95
  }
83
96
 
84
-
85
97
  async function setRouteIsDelayed(req, response) {
86
98
  try {
87
99
  const body = await parseJSON(req)
88
100
  const broker = mockBrokersCollection.getBrokerForUrl(
89
101
  body[DF.routeMethod],
90
102
  body[DF.routeUrlMask])
91
-
92
103
  if (!broker)
93
104
  throw `Route does not exist: ${body[DF.routeUrlMask]} ${body[DF.routeUrlMask]}`
94
-
95
105
  broker.updateDelay(body[DF.delayed])
96
106
  sendOK(response)
97
107
  }
@@ -100,7 +110,6 @@ async function setRouteIsDelayed(req, response) {
100
110
  }
101
111
  }
102
112
 
103
-
104
113
  async function updateProxyFallback(req, response) {
105
114
  try {
106
115
  const fallback = await parseJSON(req)
@@ -116,7 +125,6 @@ async function updateProxyFallback(req, response) {
116
125
  }
117
126
  }
118
127
 
119
-
120
128
  async function bulkUpdateBrokersByCommentTag(req, response) {
121
129
  try {
122
130
  mockBrokersCollection.setMocksMatchingComment(await parseJSON(req))
@@ -127,7 +135,6 @@ async function bulkUpdateBrokersByCommentTag(req, response) {
127
135
  }
128
136
  }
129
137
 
130
-
131
138
  async function setCorsAllowed(req, response) {
132
139
  try {
133
140
  Config.corsAllowed = await parseJSON(req)
@@ -138,16 +145,3 @@ async function setCorsAllowed(req, response) {
138
145
  }
139
146
  }
140
147
 
141
-
142
- // TESTME
143
- async function listStaticFiles(req, response) {
144
- try {
145
- const files = Config.staticDir
146
- ? listFilesRecursively(Config.staticDir).filter(f => !Config.ignore.test(f))
147
- : []
148
- sendJSON(response, files)
149
- }
150
- catch (error) {
151
- sendBadRequest(response, error)
152
- }
153
- }
@@ -21,3 +21,5 @@ export const DF = { // Dashboard Fields (XHR)
21
21
 
22
22
  export const DEFAULT_500_COMMENT = '(Mockaton 500)'
23
23
  export const DEFAULT_MOCK_COMMENT = '(default)'
24
+
25
+ export const EXT_FOR_UNKNOWN_MIME = 'unknown'
package/src/Commander.js CHANGED
@@ -7,6 +7,16 @@ export class Commander {
7
7
  this.#addr = addr
8
8
  }
9
9
 
10
+ #get(api) {
11
+ return fetch(this.#addr + api)
12
+ }
13
+ #patch(api, body) {
14
+ return fetch(this.#addr + api, {
15
+ method: 'PATCH',
16
+ body: JSON.stringify(body)
17
+ })
18
+ }
19
+
10
20
  listMocks() {
11
21
  return this.#get(API.mocks)
12
22
  }
@@ -58,15 +68,4 @@ export class Commander {
58
68
  listStaticFiles() {
59
69
  return this.#get(API.static)
60
70
  }
61
-
62
-
63
- #get(api) {
64
- return fetch(this.#addr + api)
65
- }
66
- #patch(api, body) {
67
- return fetch(this.#addr + api, {
68
- method: 'PATCH',
69
- body: JSON.stringify(body)
70
- })
71
- }
72
71
  }
package/src/Config.js CHANGED
@@ -14,6 +14,7 @@ export const Config = Object.seal({
14
14
  host: '127.0.0.1',
15
15
  port: 0, // auto-assigned
16
16
  proxyFallback: '', // e.g. http://localhost:9999
17
+ collectProxied: false,
17
18
 
18
19
  delay: 1200, // milliseconds
19
20
  cookies: {}, // defaults to the first kv
@@ -47,6 +48,7 @@ export function setup(options) {
47
48
  host: is(String),
48
49
  port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
49
50
  proxyFallback: optional(URL.canParse),
51
+ collectProxied: is(Boolean),
50
52
 
51
53
  delay: ms => Number.isInteger(ms) && ms > 0,
52
54
  cookies: is(Object),
package/src/Dashboard.js CHANGED
@@ -423,7 +423,7 @@ function createElement(elem, props = null, ...children) {
423
423
  node[key] = value
424
424
  else
425
425
  node.setAttribute(key, value)
426
- node.append(...children.flat())
426
+ node.append(...children.flat().filter(a => a))
427
427
  return node
428
428
  }
429
429
 
@@ -431,7 +431,7 @@ function createSvgElement(tagName, props, ...children) {
431
431
  const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName)
432
432
  for (const [key, value] of Object.entries(props))
433
433
  elem.setAttribute(key, value)
434
- elem.append(...children.flat())
434
+ elem.append(...children.flat().filter(a => a))
435
435
  return elem
436
436
  }
437
437
 
package/src/Filename.js CHANGED
@@ -44,7 +44,6 @@ export function parseFilename(file) {
44
44
  }
45
45
  }
46
46
 
47
-
48
47
  function removeTrailingSlash(url = '') {
49
48
  return url
50
49
  .replace(/\/$/, '')
@@ -59,5 +58,14 @@ function responseStatusIsValid(status) {
59
58
  }
60
59
 
61
60
 
61
+ export function makeMockFilename(url, method, status, ext) {
62
+ const urlMask = replaceIds(removeTrailingSlash(url))
63
+ return [urlMask, method, status, ext].join('.')
64
+ }
65
+
66
+ const reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
67
+ function replaceIds(filename) {
68
+ return filename.replaceAll(reUuidV4, '[id]')
69
+ }
62
70
 
63
71
 
@@ -7,10 +7,6 @@ export async function applyPlugins(filePath, req, response) {
7
7
  for (const [regex, plugin] of Config.plugins) // TESTME capitalizePlugin
8
8
  if (regex.test(filePath))
9
9
  return await plugin(filePath, req, response)
10
- return defaultPlugin(filePath)
11
- }
12
-
13
- export function defaultPlugin(filePath) {
14
10
  return {
15
11
  mime: mimeFor(filePath),
16
12
  body: read(filePath)
package/src/ProxyRelay.js CHANGED
@@ -1,4 +1,8 @@
1
+ import { join } from 'node:path'
2
+ import { write } from './utils/fs.js'
1
3
  import { Config } from './Config.js'
4
+ import { extFor } from './utils/mime.js'
5
+ import { makeMockFilename } from './Filename.js'
2
6
 
3
7
 
4
8
  export async function proxy(req, response) {
@@ -6,6 +10,14 @@ export async function proxy(req, response) {
6
10
  method: req.method,
7
11
  headers: req.headers
8
12
  })
13
+ // TODO investigate how to include many repeated headers such as set-cookie
9
14
  response.writeHead(proxyResponse.status, Object.fromEntries(proxyResponse.headers))
10
- response.end(await proxyResponse.text())
15
+ const body = await proxyResponse.text()
16
+ response.end(body)
17
+
18
+ if (Config.collectProxied) { // TESTME
19
+ const ext = extFor(proxyResponse.headers.get('content-type'))
20
+ const filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
21
+ write(join(Config.mocksDir, filename), body)
22
+ }
11
23
  }
package/src/utils/fs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import path, { join } from 'node:path'
2
- import { lstatSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { lstatSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
3
3
 
4
4
 
5
5
  export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
@@ -8,6 +8,14 @@ export const isDirectory = path => lstatSync(path, { throwIfNoEntry: false })?.i
8
8
  export const read = path => readFileSync(path)
9
9
 
10
10
  /** @returns {Array<string>} paths relative to `dir` */
11
- export const listFilesRecursively = dir => readdirSync(dir, { recursive: true })
12
- .map(f => f.replaceAll(path.sep, path.posix.sep)) // TESTME
13
- .filter(f => isFile(join(dir, f)))
11
+ export const listFilesRecursively = dir => {
12
+ const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
13
+ return process.platform === 'win32'
14
+ ? files.map(f => f.replaceAll(path.sep, path.posix.sep)) // TESTME
15
+ : files
16
+ }
17
+
18
+ export const write = (fPath, body) => {
19
+ mkdirSync(path.dirname(fPath), { recursive: true })
20
+ writeFileSync(fPath, body)
21
+ }
package/src/utils/mime.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Config } from '../Config.js'
2
+ import { EXT_FOR_UNKNOWN_MIME } from '../ApiConstants.js'
2
3
 
3
4
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
4
5
  // m = {};
@@ -93,3 +94,22 @@ export function mimeFor(filename) {
93
94
  console.info(`Missing MIME for ${filename}`)
94
95
  return mime
95
96
  }
97
+
98
+ export function extFor(mime) {
99
+ const ext = findExt(mime)
100
+ if (!ext) {
101
+ console.info(`Missing extension for ${mime}`)
102
+ return EXT_FOR_UNKNOWN_MIME
103
+ }
104
+ return ext
105
+ }
106
+
107
+ function findExt(targetMime) {
108
+ for (const [ext, mime] of Object.entries(Config.extraMimes))
109
+ if (targetMime === mime)
110
+ return ext
111
+ for (const [ext, mime] of Object.entries(mimes))
112
+ if (targetMime === mime)
113
+ return ext
114
+ return ''
115
+ }