mockaton 11.0.1 → 11.1.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/Makefile CHANGED
@@ -6,7 +6,7 @@ watch:
6
6
 
7
7
 
8
8
  test:
9
- @node --test 'src/**/*.test.js'
9
+ @MOCKATON_WATCHER_DEBOUNCE_MS=0 node --test 'src/**/*.test.js'
10
10
 
11
11
  test-docker:
12
12
  @docker run --rm --interactive --tty \
@@ -16,7 +16,7 @@ test-docker:
16
16
  make test
17
17
 
18
18
  coverage:
19
- @node --test --experimental-test-coverage \
19
+ @MOCKATON_WATCHER_DEBOUNCE_MS=0 node --test --experimental-test-coverage \
20
20
  --test-reporter=spec --test-reporter-destination=stdout \
21
21
  --test-reporter=lcov --test-reporter-destination=lcov.info \
22
22
  'src/**/*.test.js'
package/README.md CHANGED
@@ -77,7 +77,7 @@ get saved following Mockaton’s filename convention.
77
77
 
78
78
  ### Option 2: Fallback to Your Backend
79
79
  <details>
80
- <summary>Details</summary>
80
+ <summary>Learn more…</summary>
81
81
 
82
82
  This option could be a bit elaborate if your backend uses third-party authentication,
83
83
  because you’d have to manually inject cookies or `sessionStorage` tokens.
@@ -93,7 +93,12 @@ They will be saved in your `config.mocksDir` following the filename convention.
93
93
 
94
94
  <br/>
95
95
 
96
+ <br/>
97
+
98
+
96
99
  ## Motivation
100
+ <details>
101
+ <summary>Motivation…</summary>
97
102
 
98
103
  **No API state should be too hard to test.**
99
104
  With Mockaton, developers can achieve correctness and speed.
@@ -116,15 +121,41 @@ With Mockaton, developers can achieve correctness and speed.
116
121
  - checking out long-lived branches
117
122
  - bisecting bugs
118
123
 
124
+ </details>
125
+
126
+
127
+
128
+ ## Use Cases
129
+ <details>
130
+ <summary>Use Cases…</summary>
131
+
132
+ ### Testing Backend or Frontend
133
+ - Empty responses
134
+ - Errors such as _Bad Request_ and _Internal Server Error_
135
+ - Mocking third-party APIs
136
+ - Polled resources (for triggering their different states)
137
+ - alerts
138
+ - notifications
139
+ - slow to build resources
140
+
141
+ ### Testing Frontend
142
+ - Spinners by delaying responses
143
+ - Setting up UI tests
144
+
145
+ ### Demoing complex backend states
146
+ Sometimes, the ideal flow you need is too difficult to reproduce from the actual backend.
147
+ For this, you can **Bulk Select** mocks by comments to simulate the complete states
148
+ you want. For example, by adding `(demo-part1)`, `(demo-part2)` to the filenames.
149
+
150
+ Similarly, you can deploy a **Standalone Demo Server** by compiling the frontend app and
151
+ putting its built assets in `config.staticDir`. And simulate the flow by Bulk Selecting mocks.
152
+ The [aot-fetch-demo repo](https://github.com/ericfortis/aot-fetch-demo) has a working example.
153
+
154
+ </details>
119
155
 
120
156
  <br/>
121
157
 
122
- ## Privacy and Security
123
- - Zero dependencies (no runtime and no build packages).
124
- - Does not write to disk. Except when you select ✅ **Save Mocks** for scraping mocks from a backend.
125
- - Does not initiate network connections (no logs, no telemetry).
126
- - Does not hijack your HTTP client.
127
- - Auditable. Organized and small &mdash; under 4 KLoC (half is UI and tests).
158
+
128
159
 
129
160
  <br/>
130
161
 
@@ -147,6 +178,16 @@ npx mockaton --port 2345
147
178
  curl localhost:2345/api/foo
148
179
  ```
149
180
 
181
+ 5. Optionally, use a `mockaton.config.js`
182
+ ```js
183
+ import { defineConfig } from 'mockaton'
184
+
185
+ export default defineConfig({
186
+ port: 2345,
187
+ })
188
+ ```
189
+
190
+
150
191
  ### Alternative Installations
151
192
  <details>
152
193
  <summary>With NPM (package.json)…</summary>
@@ -185,6 +226,9 @@ ln -s `realpath mockaton/src/cli.js` ~/bin/mockaton # some dir in your $PATH
185
226
  ## CLI Options
186
227
  The CLI options override their counterparts in `mockaton.config.js`
187
228
 
229
+ <details>
230
+ <summary>CLI Options…</summary>
231
+
188
232
  ```txt
189
233
  -c, --config <file> (default: ./mockaton.config.js)
190
234
 
@@ -200,10 +244,17 @@ The CLI options override their counterparts in `mockaton.config.js`
200
244
  -h, --help Show this help
201
245
  -v, --version Show version
202
246
  ```
247
+ </details>
203
248
 
204
249
 
205
250
  ## mockaton.config.js (Optional)
251
+ Mockaton looks for a file `mockaton.config.js` in its current working directory.
252
+
253
+ <details>
254
+ <summary>Defaults Overview… </summary>
255
+
206
256
  As an overview, these are the defaults:
257
+
207
258
  ```js
208
259
  import {
209
260
  defineConfig,
@@ -247,11 +298,14 @@ export default defineConfig({
247
298
  ],
248
299
 
249
300
  onReady: await openInBrowser,
301
+ watcherEnabled: true
250
302
  })
251
303
  ```
252
304
 
305
+ </details>
306
+
253
307
  <details>
254
- <summary><b>Config Documentation</b></summary>
308
+ <summary><b>Config Documentation…</b></summary>
255
309
 
256
310
  ### `mocksDir?: string`
257
311
  Defaults to `'mockaton-mocks'`.
@@ -495,11 +549,18 @@ Defaults to `'normal'`.
495
549
  - `normal`: info, mock access, warnings, and errors
496
550
  - `verbose`: normal + API access
497
551
 
552
+ <br/>
553
+
554
+
555
+ ### `watcherEnabled?: boolean`
556
+ Defaults to `true`. When `true`, newly added mocks get registered,
557
+ or unregistered when deleting them.
558
+
498
559
  </details>
499
560
 
500
561
 
501
562
  <details>
502
- <summary>Programmatic Launch (Optional)</summary>
563
+ <summary>Programmatic Launch (Optional)…</summary>
503
564
 
504
565
 
505
566
  ```js
@@ -536,32 +597,6 @@ permutations for out-of-stock, new-arrival, and discontinued.
536
597
  <br/>
537
598
 
538
599
 
539
- ## Use Cases
540
- ### Testing Backend or Frontend
541
- - Empty responses
542
- - Errors such as _Bad Request_ and _Internal Server Error_
543
- - Mocking third-party APIs
544
- - Polled resources (for triggering their different states)
545
- - alerts
546
- - notifications
547
- - slow to build resources
548
-
549
- ### Testing Frontend
550
- - Spinners by delaying responses
551
- - Setting up UI tests
552
-
553
- ### Demoing complex backend states
554
- Sometimes, the ideal flow you need is too difficult to reproduce from the actual backend.
555
- For this, you can **Bulk Select** mocks by comments to simulate the complete states
556
- you want. For example, by adding `(demo-part1)`, `(demo-part2)` to the filenames.
557
-
558
- Similarly, you can deploy a **Standalone Demo Server** by compiling the frontend app and
559
- putting its built assets in `config.staticDir`. And simulate the flow by Bulk Selecting mocks.
560
- The [aot-fetch-demo repo](https://github.com/ericfortis/aot-fetch-demo) has a working example.
561
-
562
-
563
- <br/>
564
-
565
600
 
566
601
  ## You can write JSON mocks in JavaScript or TypeScript
567
602
  For example, `api/foo.GET.200.js`
@@ -841,6 +876,16 @@ await mockaton.reset()
841
876
  </details>
842
877
 
843
878
 
879
+ <br/>
880
+
881
+ ## Privacy and Security
882
+ - Zero dependencies (no runtime and no build packages).
883
+ - Does not write to disk. Except when you select ✅ **Save Mocks** for scraping mocks from a backend.
884
+ - Does not initiate network connections (no logs, no telemetry).
885
+ - Does not hijack your HTTP client.
886
+ - Auditable. Organized and small &mdash; under 4 KLoC (half is UI and tests).
887
+
888
+
844
889
  <br/>
845
890
 
846
891
  ## Alternatives worth learning as well
@@ -862,6 +907,12 @@ hijack the HTTP client in Node.js and browsers.
862
907
  - [Fetch Mock](https://github.com/wheresrhys/fetch-mock)
863
908
  - [Mentoss](https://github.com/humanwhocodes/mentoss) Has a server side too
864
909
 
910
+ ### Server Side
911
+
912
+ - [Wire Mock](https://github.com/wiremock/wiremock)
913
+ - [Mock](https://github.com/dhuan/mock)
914
+ - [Swagger](https://swagger.io/)
915
+
865
916
  <br/>
866
917
 
867
918
  ---
package/index.d.ts CHANGED
@@ -5,7 +5,7 @@ type Plugin = (
5
5
  request: IncomingMessage,
6
6
  response: OutgoingMessage
7
7
  ) => Promise<{
8
- mime: string,
8
+ mime: string
9
9
  body: string | Uint8Array
10
10
  }>
11
11
 
@@ -30,7 +30,7 @@ interface Config {
30
30
  extraHeaders?: string[]
31
31
  extraMimes?: { [fileExt: string]: string }
32
32
 
33
- corsAllowed?: boolean,
33
+ corsAllowed?: boolean
34
34
  corsOrigins?: string[]
35
35
  corsMethods?: string[]
36
36
  corsHeaders?: string[]
@@ -42,10 +42,13 @@ interface Config {
42
42
  plugins?: [filenameTester: RegExp, plugin: Plugin][]
43
43
 
44
44
  onReady?: (address: string) => void
45
+
46
+ watcherEnabled?: boolean
45
47
  }
46
48
 
47
49
 
48
50
  export function Mockaton(options: Partial<Config>): Promise<Server | undefined>
51
+
49
52
  export function defineConfig(options: Partial<Config>): Partial<Config>
50
53
 
51
54
  export const jsToJsonPlugin: Plugin
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "11.0.1",
5
+ "version": "11.1.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -12,7 +12,7 @@ import * as staticCollection from './staticCollection.js'
12
12
  import * as mockBrokersCollection from './mockBrokersCollection.js'
13
13
  import { config, ConfigValidator } from './config.js'
14
14
  import { DashboardHtml, CSP } from './DashboardHtml.js'
15
- import { sendOK, sendJSON, sendUnprocessableContent, sendFile, sendHTML } from './utils/http-response.js'
15
+ import { sendOK, sendJSON, sendUnprocessable, sendFile, sendHTML } from './utils/http-response.js'
16
16
  import { API, LONG_POLL_SERVER_TIMEOUT, HEADER_SYNC_VERSION } from './ApiConstants.js'
17
17
 
18
18
 
@@ -81,7 +81,8 @@ function getState(_, response) {
81
81
 
82
82
 
83
83
  function longPollClientSyncVersion(req, response) {
84
- if (uiSyncVersion.version !== Number(req.headers[HEADER_SYNC_VERSION])) {
84
+ const clientVersion = req.headers[HEADER_SYNC_VERSION]
85
+ if (clientVersion !== undefined && uiSyncVersion.version !== Number(clientVersion)) {
85
86
  // e.g., tab was hidden while new mocks were added or removed
86
87
  sendJSON(response, uiSyncVersion.version)
87
88
  return
@@ -115,7 +116,7 @@ async function selectCookie(req, response) {
115
116
 
116
117
  const error = cookie.setCurrent(cookieKey)
117
118
  if (error)
118
- sendUnprocessableContent(response, error?.message || error)
119
+ sendUnprocessable(response, error?.message || error)
119
120
  else
120
121
  sendJSON(response, cookie.list())
121
122
  }
@@ -126,7 +127,7 @@ async function selectMock(req, response) {
126
127
 
127
128
  const broker = mockBrokersCollection.brokerByFilename(file)
128
129
  if (!broker || !broker.hasMock(file))
129
- sendUnprocessableContent(response, `Missing Mock: ${file}`)
130
+ sendUnprocessable(response, `Missing Mock: ${file}`)
130
131
  else {
131
132
  broker.selectFile(file)
132
133
  sendJSON(response, broker)
@@ -139,7 +140,7 @@ async function toggle500(req, response) {
139
140
 
140
141
  const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
141
142
  if (!broker)
142
- sendUnprocessableContent(response, `Route does not exist: ${method} ${urlMask}`)
143
+ sendUnprocessable(response, `Route does not exist: ${method} ${urlMask}`)
143
144
  else {
144
145
  broker.toggle500()
145
146
  sendJSON(response, broker)
@@ -152,9 +153,9 @@ async function setRouteIsDelayed(req, response) {
152
153
 
153
154
  const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
154
155
  if (!broker)
155
- sendUnprocessableContent(response, `Route does not exist: ${method} ${urlMask}`)
156
+ sendUnprocessable(response, `Route does not exist: ${method} ${urlMask}`)
156
157
  else if (typeof delayed !== 'boolean')
157
- sendUnprocessableContent(response, `Expected boolean for "delayed"`)
158
+ sendUnprocessable(response, `Expected boolean for "delayed"`)
158
159
  else {
159
160
  broker.setDelayed(delayed)
160
161
  sendJSON(response, broker)
@@ -167,11 +168,11 @@ async function setRouteIsProxied(req, response) {
167
168
 
168
169
  const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
169
170
  if (!broker)
170
- sendUnprocessableContent(response, `Route does not exist: ${method} ${urlMask}`)
171
+ sendUnprocessable(response, `Route does not exist: ${method} ${urlMask}`)
171
172
  else if (typeof proxied !== 'boolean')
172
- sendUnprocessableContent(response, `Expected boolean for "proxied"`)
173
+ sendUnprocessable(response, `Expected boolean for "proxied"`)
173
174
  else if (proxied && !config.proxyFallback)
174
- sendUnprocessableContent(response, `There’s no proxy fallback`)
175
+ sendUnprocessable(response, `There’s no proxy fallback`)
175
176
  else {
176
177
  broker.setProxied(proxied)
177
178
  sendJSON(response, broker)
@@ -183,7 +184,7 @@ async function updateProxyFallback(req, response) {
183
184
  const fallback = await parseJSON(req)
184
185
 
185
186
  if (!ConfigValidator.proxyFallback(fallback))
186
- sendUnprocessableContent(response, `Invalid Proxy Fallback URL`)
187
+ sendUnprocessable(response, `Invalid Proxy Fallback URL`)
187
188
  else {
188
189
  config.proxyFallback = fallback
189
190
  sendOK(response)
@@ -195,7 +196,7 @@ async function setCollectProxied(req, response) {
195
196
  const collectProxied = await parseJSON(req)
196
197
 
197
198
  if (!ConfigValidator.collectProxied(collectProxied))
198
- sendUnprocessableContent(response, `Expected a boolean for "collectProxied"`)
199
+ sendUnprocessable(response, `Expected a boolean for "collectProxied"`)
199
200
  else {
200
201
  config.collectProxied = collectProxied
201
202
  sendOK(response)
@@ -215,7 +216,7 @@ async function setCorsAllowed(req, response) {
215
216
  const corsAllowed = await parseJSON(req)
216
217
 
217
218
  if (!ConfigValidator.corsAllowed(corsAllowed))
218
- sendUnprocessableContent(response, `Expected boolean for "corsAllowed"`)
219
+ sendUnprocessable(response, `Expected boolean for "corsAllowed"`)
219
220
  else {
220
221
  config.corsAllowed = corsAllowed
221
222
  sendOK(response)
@@ -227,7 +228,7 @@ async function setGlobalDelay(req, response) {
227
228
  const delay = await parseJSON(req)
228
229
 
229
230
  if (!ConfigValidator.delay(delay))
230
- sendUnprocessableContent(response, `Expected non-negative integer for "delay"`)
231
+ sendUnprocessable(response, `Expected non-negative integer for "delay"`)
231
232
  else {
232
233
  config.delay = delay
233
234
  sendOK(response)
@@ -241,9 +242,9 @@ async function setStaticRouteStatusCode(req, response) {
241
242
 
242
243
  const broker = staticCollection.brokerByRoute(urlMask)
243
244
  if (!broker)
244
- sendUnprocessableContent(response, `Static route does not exist: ${urlMask}`)
245
+ sendUnprocessable(response, `Static route does not exist: ${urlMask}`)
245
246
  else if (!(status === 200 || status === 404))
246
- sendUnprocessableContent(response, `Expected 200 or 404 status code`)
247
+ sendUnprocessable(response, `Expected 200 or 404 status code`)
247
248
  else {
248
249
  broker.setStatus(status)
249
250
  sendOK(response)
@@ -256,9 +257,9 @@ async function setStaticRouteIsDelayed(req, response) {
256
257
 
257
258
  const broker = staticCollection.brokerByRoute(urlMask)
258
259
  if (!broker)
259
- sendUnprocessableContent(response, `Static route does not exist: ${urlMask}`)
260
+ sendUnprocessable(response, `Static route does not exist: ${urlMask}`)
260
261
  else if (typeof delayed !== 'boolean')
261
- sendUnprocessableContent(response, `Expected boolean for "delayed"`)
262
+ sendUnprocessable(response, `Expected boolean for "delayed"`)
262
263
  else {
263
264
  broker.setDelayed(delayed)
264
265
  sendOK(response)
@@ -9,80 +9,49 @@ export class Commander {
9
9
  this.#addr = addr
10
10
  }
11
11
 
12
- #patch = (api, body) =>
13
- fetch(this.#addr + api, {
14
- method: 'PATCH',
15
- body: JSON.stringify(body)
16
- })
17
-
18
12
  /** @returns {JsonPromise<State>} */
19
13
  getState = () =>
20
14
  fetch(this.#addr + API.state)
21
15
 
22
- /** @returns {JsonPromise<number>} */
23
- getSyncVersion = (currSyncVer, abortSignal = undefined) =>
16
+ /**
17
+ * @param {number?} currSyncVer - On mismatch, it responds immediately. Otherwise, long polls.
18
+ * @param {AbortSignal} abortSignal
19
+ * @returns {JsonPromise<number>}
20
+ */
21
+ getSyncVersion = (currSyncVer = undefined, abortSignal = undefined) =>
24
22
  fetch(this.#addr + API.syncVersion, {
25
23
  signal: AbortSignal.any([
26
24
  abortSignal,
27
25
  AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
28
26
  ].filter(Boolean)),
29
- headers: {
30
- [HEADER_SYNC_VERSION]: currSyncVer
31
- }
27
+ headers: currSyncVer !== undefined
28
+ ? { [HEADER_SYNC_VERSION]: currSyncVer }
29
+ : {}
32
30
  })
33
31
 
34
32
 
35
- reset() {
36
- return this.#patch(API.reset)
37
- }
38
-
39
- setGlobalDelay(delay) {
40
- return this.#patch(API.globalDelay, delay)
41
- }
42
-
43
- bulkSelectByComment(comment) {
44
- return this.#patch(API.bulkSelect, comment)
45
- }
46
-
47
- selectCookie(cookieKey) {
48
- return this.#patch(API.cookies, cookieKey)
49
- }
50
-
51
- setProxyFallback(proxyAddr) {
52
- return this.#patch(API.fallback, proxyAddr)
53
- }
54
-
55
- setCollectProxied(shouldCollect) {
56
- return this.#patch(API.collectProxied, shouldCollect)
57
- }
58
-
59
- setCorsAllowed(value) {
60
- return this.#patch(API.cors, value)
61
- }
62
-
63
-
64
- select(file) {
65
- return this.#patch(API.select, file)
33
+ #patch(api, body) {
34
+ return fetch(this.#addr + api, {
35
+ method: 'PATCH',
36
+ body: JSON.stringify(body)
37
+ })
66
38
  }
67
39
 
68
- toggle500(method, urlMask) {
69
- return this.#patch(API.toggle500, [method, urlMask])
70
- }
40
+ reset = () => this.#patch(API.reset)
71
41
 
72
- setRouteIsDelayed(method, urlMask, delayed) {
73
- return this.#patch(API.delay, [method, urlMask, delayed])
74
- }
42
+ selectCookie = label => this.#patch(API.cookies, label)
43
+ setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
44
+ setCorsAllowed = value => this.#patch(API.cors, value)
45
+ setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
46
+ setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
75
47
 
76
- setRouteIsProxied(method, urlMask, proxied) {
77
- return this.#patch(API.proxied, [method, urlMask, proxied])
78
- }
79
-
48
+ select = file => this.#patch(API.select, file)
49
+ bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
80
50
 
81
- setStaticRouteIsDelayed(urlMask, delayed) {
82
- return this.#patch(API.delayStatic, [urlMask, delayed])
83
- }
51
+ toggle500 = (method, urlMask) => this.#patch(API.toggle500, [method, urlMask])
52
+ setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
53
+ setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
84
54
 
85
- setStaticRouteStatus(urlMask, status) {
86
- return this.#patch(API.staticStatus, [urlMask, status])
87
- }
55
+ setStaticRouteStatus = (urlMask, status) => this.#patch(API.staticStatus, [urlMask, status])
56
+ setStaticRouteIsDelayed = (urlMask, delayed) => this.#patch(API.delayStatic, [urlMask, delayed])
88
57
  }
package/src/Dashboard.js CHANGED
@@ -734,7 +734,7 @@ function SettingsIcon() {
734
734
  * The version increments when a mock file is added, removed, or renamed.
735
735
  */
736
736
  function initRealTimeUpdates() {
737
- let oldVersion = -1
737
+ let oldVersion = undefined // undefined waits until next event or timeout
738
738
  let controller = new AbortController()
739
739
 
740
740
  longPoll()
@@ -755,11 +755,9 @@ function initRealTimeUpdates() {
755
755
  ErrorToast.close()
756
756
 
757
757
  const version = await response.json()
758
- const skipUpdate = oldVersion === -1
759
758
  if (oldVersion !== version) { // because it could be < or >
760
759
  oldVersion = version
761
- if (!skipUpdate)
762
- store.fetchState()
760
+ store.fetchState()
763
761
  }
764
762
  longPoll()
765
763
  }
package/src/MockBroker.js CHANGED
@@ -2,8 +2,8 @@ import { includesComment, extractComments, parseFilename } from './Filename.js'
2
2
  import { DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
3
3
 
4
4
 
5
- /**
6
- * MockBroker is a state for a particular route. It knows the available mock
5
+ /**
6
+ * MockBroker is a state for a particular route. It knows the available mock
7
7
  * files that can be served for the route, the currently selected file, etc.
8
8
  */
9
9
  export class MockBroker {
@@ -27,7 +27,7 @@ export class MockBroker {
27
27
  }
28
28
 
29
29
  register(file) {
30
- if (this.auto500 && this.#is500(file)) // TESTME
30
+ if (this.auto500 && this.#is500(file))
31
31
  this.selectFile(file)
32
32
  this.mocks.push(file)
33
33
  this.#sortMocks()
@@ -114,7 +114,7 @@ class UrlMatcher {
114
114
  }
115
115
 
116
116
  #disregardVariables(str) { // Stars out all parts that are in square brackets
117
- return str.replace(/\[.*?]/g, '[^/]*')
117
+ return str.replace(/\[.*?]/g, '[^/]+')
118
118
  }
119
119
 
120
120
  // Appending a '/' so URLs ending with variables don't match
@@ -14,11 +14,11 @@ import { sendInternalServerError, sendMockNotFound } from './utils/http-response
14
14
  export async function dispatchMock(req, response) {
15
15
  try {
16
16
  const isHead = req.method === 'HEAD'
17
-
17
+
18
18
  let broker = mockBrokerCollection.brokerByRoute(req.method, req.url)
19
19
  if (!broker && isHead)
20
20
  broker = mockBrokerCollection.brokerByRoute('GET', req.url)
21
-
21
+
22
22
  if (config.proxyFallback && (!broker || broker.proxied)) {
23
23
  await proxy(req, response, broker?.delayed ? calcDelay() : 0)
24
24
  return
@@ -42,6 +42,7 @@ export async function dispatchMock(req, response) {
42
42
  logger.accessMock(req.url, broker.file)
43
43
  response.setHeader('Content-Type', mime)
44
44
  response.setHeader('Content-Length', length(body))
45
+
45
46
  setTimeout(() => response.end(isHead ? null : body),
46
47
  Number(broker.delayed && calcDelay()))
47
48
  }
package/src/Mockaton.js CHANGED
@@ -11,7 +11,7 @@ import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
11
11
  import { watchMocksDir, watchStaticDir } from './Watcher.js'
12
12
  import { apiPatchRequests, apiGetRequests } from './Api.js'
13
13
  import { BodyReaderError, hasControlChars } from './utils/http-request.js'
14
- import { sendNoContent, sendInternalServerError, sendUnprocessableContent, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
14
+ import { sendNoContent, sendInternalServerError, sendUnprocessable, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
15
15
 
16
16
 
17
17
  export function Mockaton(options) {
@@ -20,8 +20,11 @@ export function Mockaton(options) {
20
20
 
21
21
  mockBrokerCollection.init()
22
22
  staticCollection.init()
23
- watchMocksDir()
24
- watchStaticDir()
23
+
24
+ if (options.watcherEnabled) {
25
+ watchMocksDir()
26
+ watchStaticDir()
27
+ }
25
28
 
26
29
  const server = createServer(onRequest)
27
30
  server.on('error', reject)
@@ -75,7 +78,7 @@ async function onRequest(req, response) {
75
78
  }
76
79
  catch (error) {
77
80
  if (error instanceof BodyReaderError)
78
- sendUnprocessableContent(response, `${error.name}: ${error.message}`)
81
+ sendUnprocessable(response, `${error.name}: ${error.message}`)
79
82
  else
80
83
  sendInternalServerError(response, error)
81
84
  }
package/src/ProxyRelay.js CHANGED
@@ -6,7 +6,7 @@ import { extFor } from './utils/mime.js'
6
6
  import { write, isFile } from './utils/fs.js'
7
7
  import { makeMockFilename } from './Filename.js'
8
8
  import { readBody, BodyReaderError } from './utils/http-request.js'
9
- import { sendUnprocessableContent, sendBadGateway } from './utils/http-response.js'
9
+ import { sendUnprocessable, sendBadGateway } from './utils/http-response.js'
10
10
 
11
11
 
12
12
  export async function proxy(req, response, delay) {
@@ -22,7 +22,7 @@ export async function proxy(req, response, delay) {
22
22
  }
23
23
  catch (error) { // TESTME
24
24
  if (error instanceof BodyReaderError)
25
- sendUnprocessableContent(response, error.name)
25
+ sendUnprocessable(response, error.name)
26
26
  else
27
27
  sendBadGateway(response, error)
28
28
  return
@@ -1,13 +1,12 @@
1
1
  import { join } from 'node:path'
2
2
  import { readFileSync } from 'node:fs'
3
3
 
4
+ import { isFile } from './utils/fs.js'
4
5
  import { logger } from './utils/logger.js'
5
6
  import { mimeFor } from './utils/mime.js'
6
7
  import { brokerByRoute } from './staticCollection.js'
7
8
  import { config, calcDelay } from './config.js'
8
9
  import { sendMockNotFound, sendPartialContent } from './utils/http-response.js'
9
- import { execFileSync } from 'node:child_process'
10
- import { isFile } from './utils/fs.js'
11
10
 
12
11
 
13
12
  export async function dispatchStatic(req, response) {
package/src/Watcher.js CHANGED
@@ -15,9 +15,10 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
15
15
  * and also renames, which are two events (delete + add).
16
16
  */
17
17
  export const uiSyncVersion = new class extends EventEmitter {
18
+ delay = Number(process.env.MOCKATON_WATCHER_DEBOUNCE_MS ?? 80)
18
19
  version = 0
19
20
 
20
- increment = this.#debounce(() => {
21
+ increment = /** @type {function} */ this.#debounce(() => {
21
22
  this.version++
22
23
  super.emit('ARR')
23
24
  })
@@ -33,7 +34,7 @@ export const uiSyncVersion = new class extends EventEmitter {
33
34
  let timer
34
35
  return () => {
35
36
  clearTimeout(timer)
36
- timer = setTimeout(fn, 80)
37
+ timer = setTimeout(fn, this.delay)
37
38
  }
38
39
  }
39
40
  }
@@ -64,7 +65,7 @@ export function watchStaticDir() {
64
65
  const dir = config.staticDir
65
66
  if (!dir)
66
67
  return
67
-
68
+
68
69
  watch(dir, { recursive: true, persistent: false }, (_, file) => {
69
70
  if (!file)
70
71
  return
package/src/config.js CHANGED
@@ -49,7 +49,9 @@ const schema = {
49
49
  [/\.(js|ts)$/, jsToJsonPlugin]
50
50
  ], Array.isArray],
51
51
 
52
- onReady: [await openInBrowser, is(Function)]
52
+ onReady: [await openInBrowser, is(Function)],
53
+
54
+ watcherEnabled: [true, is(Boolean)],
53
55
  }
54
56
 
55
57
 
@@ -41,20 +41,22 @@ export function init() {
41
41
  /** @returns {boolean} registered */
42
42
  export function registerMock(file, isFromWatcher = false) {
43
43
  if (brokerByFilename(file)?.hasMock(file)
44
- || !isFileAllowed(basename(file)) // TESTME
44
+ || !isFileAllowed(basename(file))
45
45
  || !filenameIsValid(file))
46
46
  return false
47
47
 
48
48
  const { method, urlMask } = parseFilename(file)
49
49
  collection[method] ??= {}
50
50
 
51
- if (!collection[method][urlMask])
52
- collection[method][urlMask] = new MockBroker(file)
51
+ let broker = collection[method][urlMask]
52
+
53
+ if (!broker)
54
+ broker = collection[method][urlMask] = new MockBroker(file)
53
55
  else
54
- collection[method][urlMask].register(file)
56
+ broker.register(file)
55
57
 
56
- if (isFromWatcher && !collection[method][urlMask].file) // TESTME e.g. auto500 is selected and adding a new one
57
- collection[method][urlMask].selectDefaultFile()
58
+ if (isFromWatcher && !broker.file)
59
+ broker.selectDefaultFile()
58
60
 
59
61
  return true
60
62
  }
@@ -83,8 +85,7 @@ export function unregisterMock(file) {
83
85
  /** @returns {MockBroker | undefined} */
84
86
  export function brokerByFilename(file) {
85
87
  const { method, urlMask } = parseFilename(file)
86
- if (collection[method])
87
- return collection[method][urlMask]
88
+ return collection[method]?.[urlMask]
88
89
  }
89
90
 
90
91
  /**
@@ -113,7 +114,7 @@ export function extractAllComments() {
113
114
  }
114
115
 
115
116
  export function setMocksMatchingComment(comment) {
116
- forEachBroker(b =>
117
+ forEachBroker(b =>
117
118
  b.setByMatchingComment(comment))
118
119
  }
119
120
 
@@ -53,7 +53,7 @@ export function sendTooLongURI(response) {
53
53
  response.end()
54
54
  }
55
55
 
56
- export function sendUnprocessableContent(response, error) {
56
+ export function sendUnprocessable(response, error) {
57
57
  logger.access(response, error)
58
58
  response.statusCode = 422
59
59
  response.end(error)