mockaton 13.11.2 → 13.12.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.
Files changed (36) hide show
  1. package/README.md +42 -27
  2. package/package.json +1 -1
  3. package/skills/mockaton/SKILL.md +21 -19
  4. package/src/client/ApiCommander.js +20 -19
  5. package/src/client/{app.js → App.js} +6 -6
  6. package/src/client/{app-header.js → Header.js} +4 -4
  7. package/src/client/IndexHtml.js +1 -1
  8. package/src/client/{app-mock-list.js → MockList.js} +7 -8
  9. package/src/client/{app-payload-viewer.js → PayloadViewer.js} +3 -3
  10. package/src/client/{app-store.test.js → store.test.js} +1 -1
  11. package/src/server/Api.js +57 -54
  12. package/src/server/MockBroker.js +2 -8
  13. package/src/server/MockDispatcher.js +6 -5
  14. package/src/server/Mockaton.js +14 -17
  15. package/src/server/Mockaton.test.js +61 -48
  16. package/src/server/ProxyRelay.js +5 -4
  17. package/src/server/UrlParsers.js +1 -1
  18. package/src/server/UrlParsers.test.js +1 -1
  19. package/src/server/cli.js +6 -6
  20. package/src/server/{Watcher.js → stores/Watcher.js} +18 -9
  21. package/src/server/{mockBrokersCollection.js → stores/brokers.js} +4 -7
  22. package/src/server/{config.js → stores/config.js} +30 -16
  23. package/src/server/utils/HttpIncomingMessage.js +17 -16
  24. package/src/server/utils/HttpServerResponse.test.js +2 -2
  25. package/src/server/utils/logger.js +7 -1
  26. package/src/server/utils/mime.js +2 -0
  27. package/www/src/assets/openapi.json +1 -1
  28. /package/src/client/{app.css → App.css} +0 -0
  29. /package/src/client/{app-header.css → Header.css} +0 -0
  30. /package/src/client/{app-mock-list.css → MockList.css} +0 -0
  31. /package/src/client/{app-payload-viewer.css → PayloadViewer.css} +0 -0
  32. /package/src/client/{graphics.js → icons.js} +0 -0
  33. /package/src/client/{app-store.js → store.js} +0 -0
  34. /package/src/server/{resolverBypassImportCache.js → ResolverBypassImportCache.js} +0 -0
  35. /package/src/server/{resolverResolveExtensionless.js → ResolverResolveExtensionless.js} +0 -0
  36. /package/src/server/{cookie.js → stores/cookies.js} +0 -0
package/README.md CHANGED
@@ -1,14 +1,27 @@
1
1
  <!-- SKILLS_IGNORE_BEGIN -->
2
- <img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
3
2
 
4
3
  ![NPM Version](https://img.shields.io/npm/v/mockaton)
5
4
  [![Test](https://github.com/ericfortis/mockaton/actions/workflows/test.yml/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
6
5
  [![codecov](https://codecov.io/github/ericfortis/mockaton/graph/badge.svg?token=90NYLMMG1J)](https://codecov.io/github/ericfortis/mockaton)
7
6
 
7
+ <img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
8
+
9
+ No API state should be too difficult to test.
10
+
8
11
  ## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills](skills/mockaton/SKILL.md)
9
12
 
10
- Mockaton is an HTTP mock server for simulating APIs. With it, you can
11
- test API errors, edge cases, and difficult to reproduce states in general.
13
+
14
+ Simulate API states that are normally ignored. For example,
15
+ click the 500 button to trigger an error on an endpoint. Then
16
+ unclick it to test your retry logic. Similarly, click the clock
17
+ button 🕓 to delay a response so you can test spinners &mdash;
18
+ you‘ve seen them in production at the top left, or restarting
19
+ their animation midway.
20
+
21
+
22
+ ## Dashboard
23
+ Besides the dashboard UI, there’s [an API](https://mockaton.com/api)
24
+ you can use to set up tests.
12
25
 
13
26
  <picture>
14
27
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.light.gold.png">
@@ -35,47 +48,44 @@ curl localhost:2020/api/user
35
48
 
36
49
 
37
50
  # Installation ([more options ↗](https://mockaton.com/installation))
38
- Mockaton has no dependencies.
39
-
40
51
  ```sh
41
52
  npm install -g mockaton
42
53
  ```
54
+ <i>Mockaton is a Node.js app with no dependencies.</i>
55
+
56
+ <!-- SKILLS_IGNORE_BEGIN -->
57
+ ## Skills
58
+ ```sh
59
+ npx skills add ericfortis/mockaton
60
+ ```
61
+ <!-- SKILLS_IGNORE_END -->
62
+
63
+
43
64
 
44
65
  ## Basic Usage
45
66
  ```sh
46
- mockaton --port 2020 my-mocks-dir/
67
+ mockaton --port 2020 my-mocks-dir
47
68
  ```
48
69
 
49
- Mockaton will serve the files on the given directory. It's a file-system based router, so
50
- filenames can have dynamic parameters. Also, filenames can have comments, which are
51
- anything within parentheses, this way each route can have different mock file variants.
52
- Similarly, each route can have different response status code variants.
70
+ Mockaton will serve the files on the given directory. It's a file-system based router in which
71
+ filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
72
+ and for comments use parentheses `()`. Comments are handy because this way each route
73
+ can have different mock file variants. Similarly, each route can have different response status
74
+ code variants.
53
75
 
54
76
 
55
77
  | Route | Filename | Description |
56
78
  | -----| -----| ---|
57
- | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
58
- | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
59
- | /api/login | api/login(invalid attempt).POST.401.ts | **Anything within parenthesis is a comment**, they are ignored when routing |
60
- | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
61
- | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
62
-
63
-
64
- ## Docs
65
- - [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
66
- - [Programmatic API](https://mockaton.com/api), in which
67
- you can delay a route, select a different mock file, such as a 500 error, among other options.
79
+ | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
80
+ | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
81
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
82
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
83
+ | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
68
84
 
69
85
  <!-- SKILLS_IGNORE_BEGIN -->
70
86
  ## How to scrape your backend APIs?
71
87
  Mockaton has a [Browser Extension](https://mockaton.com/scraping) that lets
72
88
  you download in bulk all your API responses following Mockaton's filename convention.
73
-
74
-
75
- ## Skills
76
- ```sh
77
- npx skills add ericfortis/mockaton
78
- ```
79
89
  <!-- SKILLS_IGNORE_END -->
80
90
 
81
91
 
@@ -135,3 +145,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
135
145
  })
136
146
  }
137
147
  ```
148
+
149
+ ## Docs
150
+ - [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
151
+ - [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
152
+
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.11.2",
5
+ "version": "13.12.0",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -4,36 +4,34 @@ description: Generates and serves mock HTTP APIs. Use when creating, editing, or
4
4
  user-invocable: false
5
5
  ---
6
6
  # Installation ([more options ↗](https://mockaton.com/installation))
7
- Mockaton has no dependencies.
8
-
9
7
  ```sh
10
8
  npm install -g mockaton
11
9
  ```
10
+ <i>Mockaton is a Node.js app with no dependencies.</i>
11
+
12
+
13
+
14
+
12
15
 
13
16
  ## Basic Usage
14
17
  ```sh
15
- mockaton --port 2020 my-mocks-dir/
18
+ mockaton --port 2020 my-mocks-dir
16
19
  ```
17
20
 
18
- Mockaton will serve the files on the given directory. It's a file-system based router, so
19
- filenames can have dynamic parameters. Also, filenames can have comments, which are
20
- anything within parentheses, this way each route can have different mock file variants.
21
- Similarly, each route can have different response status code variants.
21
+ Mockaton will serve the files on the given directory. It's a file-system based router in which
22
+ filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
23
+ and for comments use parentheses `()`. Comments are handy because this way each route
24
+ can have different mock file variants. Similarly, each route can have different response status
25
+ code variants.
22
26
 
23
27
 
24
28
  | Route | Filename | Description |
25
29
  | -----| -----| ---|
26
- | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
27
- | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
28
- | /api/login | api/login(invalid attempt).POST.401.ts | **Anything within parenthesis is a comment**, they are ignored when routing |
29
- | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins |
30
- | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
31
-
32
-
33
- ## Docs
34
- - [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
35
- - [Programmatic API](https://mockaton.com/api), in which
36
- you can delay a route, select a different mock file, such as a 500 error, among other options.
30
+ | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
31
+ | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
32
+ | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. |
33
+ | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment; otherwise, the first mock variant in alphabetical order wins. |
34
+ | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
37
35
 
38
36
 
39
37
 
@@ -93,4 +91,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
93
91
  name: 'Acme, Inc.'
94
92
  })
95
93
  }
96
- ```
94
+ ```
95
+
96
+ ## Docs
97
+ - [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
98
+ - [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
@@ -1,11 +1,15 @@
1
1
  import { API } from './ApiConstants.js'
2
2
 
3
3
 
4
- /** Client for controlling Mockaton via its HTTP API */
4
+ /**
5
+ * Client for controlling Mockaton via its HTTP API
6
+ * @see https://mockaton.com/api
7
+ * @example const mockaton = new Commander('http://localhost:2020')
8
+ */
5
9
  export class Commander {
6
- /** @param {string} addr */
7
- constructor(addr) {
8
- this.addr = addr
10
+ /** @param {string} mockatonServerAddr */
11
+ constructor(mockatonServerAddr) {
12
+ this.addr = mockatonServerAddr
9
13
  }
10
14
 
11
15
  /** @returns {Promise<Response>} */
@@ -16,38 +20,35 @@ export class Commander {
16
20
 
17
21
  reset = () => this.#patch(API.reset)
18
22
 
19
- setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
20
- setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
21
- setCorsAllowed = value => this.#patch(API.cors, value)
22
- setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
23
- setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
24
- setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
25
-
26
- /** @returns {JsonPromise<State.cookies>} */
27
- selectCookie = label => this.#patch(API.cookies, label)
28
-
29
-
30
- bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
31
23
 
32
24
  /** @returns {JsonPromise<ClientMockBroker>} */
33
25
  select = file => this.#patch(API.select, file)
34
26
 
27
+ bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
35
28
 
36
29
  /** @returns {JsonPromise<ClientMockBroker>} */
37
30
  toggleStatus = (method, urlMask, status) => this.#patch(API.toggleStatus, [method, urlMask, status])
38
31
 
32
+ /** @returns {JsonPromise<ClientMockBroker>} */
33
+ setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
39
34
 
40
35
  /** @returns {JsonPromise<ClientMockBroker>} */
41
36
  setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
42
37
 
43
- /** @returns {JsonPromise<ClientMockBroker>} */
44
- setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
45
38
 
39
+ /** @returns {JsonPromise<State.cookies>} */
40
+ selectCookie = label => this.#patch(API.cookies, label)
41
+
42
+ setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
43
+ setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
44
+ setCorsAllowed = value => this.#patch(API.cors, value)
45
+ setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
46
+ setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
47
+ setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
46
48
 
47
49
  writeMock = (file, content) => this.#patch(API.writeMock, [file, content])
48
50
  deleteMock = file => this.#patch(API.deleteMock, file)
49
51
 
50
-
51
52
  /** @returns {JsonPromise<State>} */
52
53
  getState = () => fetch(this.addr + API.state)
53
54
 
@@ -1,14 +1,14 @@
1
1
  import { createElement as r, t, restoreFocus, Fragment } from './utils/dom.js'
2
2
 
3
- import { store } from './app-store.js'
3
+ import { store } from './store.js'
4
4
  import { API } from './ApiConstants.js'
5
- import { Header } from './app-header.js'
5
+ import { Header } from './Header.js'
6
6
  import { adoptSheet } from './utils/css.js'
7
- import { PayloadViewer, previewMock } from './app-payload-viewer.js'
8
- import { MockList, initKeyboardNavigation, renderRow } from './app-mock-list.js'
7
+ import { PayloadViewer, previewMock } from './PayloadViewer.js'
8
+ import { MockList, initKeyboardNavigation, renderRow } from './MockList.js'
9
9
 
10
- import CSS from './app.css' with { type: 'css' }
11
- adoptSheet(CSS, './app.css')
10
+ import CSS from './App.css' with { type: 'css' }
11
+ adoptSheet(CSS, './App.css')
12
12
 
13
13
  store.onError = onError
14
14
  store.render = render
@@ -1,10 +1,10 @@
1
1
  import { createElement as r, t } from './utils/dom.js'
2
- import { Logo, HelpIcon } from './graphics.js'
2
+ import { Logo, HelpIcon } from './icons.js'
3
3
  import { adoptSheet } from './utils/css.js'
4
- import { store } from './app-store.js'
4
+ import { store } from './store.js'
5
5
 
6
- import CSS from './app-header.css' with { type: 'css' }
7
- adoptSheet(CSS, './app-header.css')
6
+ import CSS from './Header.css' with { type: 'css' }
7
+ adoptSheet(CSS, './Header.css')
8
8
 
9
9
 
10
10
  export function Header() {
@@ -13,7 +13,7 @@ export const IndexHtml = (hotReloadEnabled, version) => `
13
13
  <meta charset="UTF-8">
14
14
  <base href="${API.root}/">
15
15
 
16
- <script type="module" src="app.js"></script>
16
+ <script type="module" src="App.js"></script>
17
17
  <link rel="preload" href="${API.state}" as="fetch" crossorigin>
18
18
 
19
19
  <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
@@ -1,12 +1,12 @@
1
1
  import { createElement as r, t, Fragment } from './utils/dom.js'
2
2
 
3
- import { store } from './app-store.js'
4
- import { previewMock } from './app-payload-viewer.js'
3
+ import { store } from './store.js'
4
+ import { previewMock } from './PayloadViewer.js'
5
5
  import { classNames, adoptSheet } from './utils/css.js'
6
- import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
6
+ import { TimerIcon, CloudIcon, ChevronDownIcon } from './icons.js'
7
7
 
8
- import CSS from './app-mock-list.css' with { type: 'css' }
9
- adoptSheet(CSS, './app-mock-list.css')
8
+ import CSS from './MockList.css' with { type: 'css' }
9
+ adoptSheet(CSS, './MockList.css')
10
10
 
11
11
 
12
12
  export function MockList() {
@@ -76,9 +76,8 @@ function TableContent() {
76
76
 
77
77
  if (store.groupByMethod)
78
78
  return Object.keys(store.brokersByMethod).map(method => Fragment(
79
- r('div', {
80
- className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
81
- }, method),
79
+ r('div', { className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy) },
80
+ method),
82
81
  FolderGroups(store.folderGroupsByMethod(method))))
83
82
 
84
83
  return FolderGroups(store.folderGroupsByMethod('*'))
@@ -1,10 +1,10 @@
1
1
  import { createElement as r, t } from './utils/dom.js'
2
2
  import { parseFilename } from './Filename.js'
3
- import { store } from './app-store.js'
3
+ import { store } from './store.js'
4
4
  import { adoptSheet } from './utils/css.js'
5
5
 
6
- import CSS from './app-payload-viewer.css' with { type: 'css' }
7
- adoptSheet(CSS, './app-payload-viewer.css')
6
+ import CSS from './PayloadViewer.css' with { type: 'css' }
7
+ adoptSheet(CSS, './PayloadViewer.css')
8
8
 
9
9
 
10
10
  const titleRef = {}
@@ -1,7 +1,7 @@
1
1
  import { test } from 'node:test'
2
2
  import { deepEqual } from 'node:assert/strict'
3
3
 
4
- import { BrokerRowModel, t } from './app-store.js'
4
+ import { BrokerRowModel, t } from './store.js'
5
5
 
6
6
 
7
7
  test('BrokerRowModel', () => {
package/src/server/Api.js CHANGED
@@ -4,21 +4,21 @@
4
4
  */
5
5
 
6
6
  import { join, relative } from 'node:path'
7
+
7
8
  import { write, rm, isFile, resolveIn } from './utils/fs.js'
9
+ import { removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
10
+ import { sseClientHotReload } from './utils/WatcherDevClient.js'
8
11
 
9
- import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
10
12
  import pkgJSON from '../../package.json' with { type: 'json' }
11
-
12
- import { sseClientHotReload } from './utils/WatcherDevClient.js'
13
- import { stopMocksDirWatcher, sseClientSyncVersion, uiSyncVersion, watchMocksDir } from './Watcher.js'
13
+ import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
14
14
 
15
15
  import { API } from '../client/ApiConstants.js'
16
16
  import { IndexHtml, CSP } from '../client/IndexHtml.js'
17
17
 
18
- import { cookie } from './cookie.js'
19
- import { config, ConfigValidator } from './config.js'
20
- import * as mockBrokersCollection from './mockBrokersCollection.js'
21
- import { removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
18
+ import { cookie } from './stores/cookies.js'
19
+ import { config, ConfigValidator, reinitConfig } from './stores/config.js'
20
+ import * as brokers from './stores/brokers.js'
21
+ import * as Watcher from './stores/Watcher.js'
22
22
 
23
23
 
24
24
  export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
@@ -32,7 +32,7 @@ const getReqs = new Map([
32
32
 
33
33
  [API.root, serveDashboard],
34
34
  [API.state, getState],
35
- [API.syncVersion, sseClientSyncVersion],
35
+ [API.syncVersion, Watcher.sseClientSyncVersion],
36
36
 
37
37
  [API.watchHotReload, onDevWatch],
38
38
  [API.openAPI, (_, response) => response.json(openapi)],
@@ -94,9 +94,9 @@ function serveDashboard(_, response) {
94
94
  function getState(_, response) {
95
95
  response.json({
96
96
  cookies: cookie.list(),
97
- comments: mockBrokersCollection.extractAllComments(),
97
+ comments: brokers.extractAllComments(),
98
98
 
99
- brokersByMethod: mockBrokersCollection.all(),
99
+ brokersByMethod: brokers.all(),
100
100
 
101
101
  delay: config.delay,
102
102
  delayJitter: config.delayJitter,
@@ -118,10 +118,11 @@ function onDevWatch(req, response) {
118
118
  /** # PATCH */
119
119
 
120
120
  function reset(_, response) {
121
- mockBrokersCollection.init()
121
+ reinitConfig()
122
+ brokers.init()
122
123
  cookie.init(config.cookies)
123
124
  response.ok()
124
- uiSyncVersion.increment()
125
+ Watcher.emitChange()
125
126
  }
126
127
 
127
128
 
@@ -133,7 +134,7 @@ async function setCorsAllowed(req, response) {
133
134
  else {
134
135
  config.corsAllowed = corsAllowed
135
136
  response.ok()
136
- uiSyncVersion.increment()
137
+ Watcher.emitChange()
137
138
  }
138
139
  }
139
140
 
@@ -146,7 +147,7 @@ async function setGlobalDelay(req, response) {
146
147
  else {
147
148
  config.delay = delay
148
149
  response.ok()
149
- uiSyncVersion.increment()
150
+ Watcher.emitChange()
150
151
  }
151
152
  }
152
153
 
@@ -158,7 +159,7 @@ async function setGlobalDelayJitter(req, response) {
158
159
  else {
159
160
  config.delayJitter = jitter
160
161
  response.ok()
161
- uiSyncVersion.increment()
162
+ Watcher.emitChange()
162
163
  }
163
164
  }
164
165
 
@@ -170,7 +171,7 @@ async function selectCookie(req, response) {
170
171
  response.unprocessable(error?.message || error)
171
172
  else {
172
173
  response.json(cookie.list())
173
- uiSyncVersion.increment()
174
+ Watcher.emitChange()
174
175
  }
175
176
  }
176
177
 
@@ -183,7 +184,7 @@ async function setProxyFallback(req, response) {
183
184
  else {
184
185
  config.proxyFallback = fallback
185
186
  response.ok()
186
- uiSyncVersion.increment()
187
+ Watcher.emitChange()
187
188
  }
188
189
  }
189
190
 
@@ -195,7 +196,7 @@ async function setCollectProxied(req, response) {
195
196
  else {
196
197
  config.collectProxied = collectProxied
197
198
  response.ok()
198
- uiSyncVersion.increment()
199
+ Watcher.emitChange()
199
200
  }
200
201
  }
201
202
 
@@ -203,41 +204,41 @@ async function setCollectProxied(req, response) {
203
204
 
204
205
  async function bulkUpdateBrokersByCommentTag(req, response) {
205
206
  const comment = await req.json()
206
- mockBrokersCollection.setMocksMatchingComment(comment)
207
+ brokers.setMocksMatchingComment(comment)
207
208
  response.ok()
208
- uiSyncVersion.increment()
209
+ Watcher.emitChange()
209
210
  }
210
211
 
211
212
 
212
213
  async function selectMock(req, response) {
213
214
  const file = await req.json()
214
- const broker = mockBrokersCollection.brokerByFilename(file)
215
+ const broker = brokers.brokerByFilename(file)
215
216
  if (!broker || !broker.hasMock(file))
216
217
  response.unprocessable(`Missing Mock: ${file}`)
217
218
  else {
218
219
  broker.selectFile(file)
219
220
  response.json(broker)
220
- uiSyncVersion.increment()
221
+ Watcher.emitChange()
221
222
  }
222
223
  }
223
224
 
224
225
 
225
226
  async function toggleRouteStatus(req, response) {
226
227
  const [method, urlMask, status] = await req.json()
227
- const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
228
+ const broker = brokers.brokerByRoute(method, urlMask)
228
229
  if (!broker)
229
230
  response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
230
231
  else {
231
232
  broker.toggleStatus(status)
232
233
  response.json(broker)
233
- uiSyncVersion.increment()
234
+ Watcher.emitChange()
234
235
  }
235
236
  }
236
237
 
237
238
 
238
239
  async function setRouteIsDelayed(req, response) {
239
240
  const [method, urlMask, delayed] = await req.json()
240
- const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
241
+ const broker = brokers.brokerByRoute(method, urlMask)
241
242
  if (!broker)
242
243
  response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
243
244
  else if (typeof delayed !== 'boolean')
@@ -245,14 +246,14 @@ async function setRouteIsDelayed(req, response) {
245
246
  else {
246
247
  broker.setDelayed(delayed)
247
248
  response.json(broker)
248
- uiSyncVersion.increment()
249
+ Watcher.emitChange()
249
250
  }
250
251
  }
251
252
 
252
253
 
253
254
  async function setRouteIsProxied(req, response) {
254
255
  const [method, urlMask, proxied] = await req.json()
255
- const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
256
+ const broker = brokers.brokerByRoute(method, urlMask)
256
257
  if (!broker)
257
258
  response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
258
259
  else if (typeof proxied !== 'boolean')
@@ -262,51 +263,53 @@ async function setRouteIsProxied(req, response) {
262
263
  else {
263
264
  broker.setProxied(proxied)
264
265
  response.json(broker)
265
- uiSyncVersion.increment()
266
+ Watcher.emitChange()
266
267
  }
267
268
  }
268
269
 
269
270
 
270
271
  async function writeMock(req, response) {
271
- if (config.readOnly)
272
- return response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly, or --no-read-only (CLI)')
273
-
272
+ if (config.readOnly) {
273
+ response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
274
+ return
275
+ }
274
276
  const [file, content] = await req.json()
275
- if (typeof file !== 'string')
276
- return response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
277
-
277
+ if (typeof file !== 'string') {
278
+ response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
279
+ return
280
+ }
278
281
  const path = await resolveIn(config.mocksDir, file)
279
- if (!path)
280
- return response.forbidden('Filename path resolves outside config.mocksDir')
281
-
282
+ if (!path) {
283
+ response.forbidden('Filename path resolves outside config.mocksDir')
284
+ return
285
+ }
282
286
  await write(path, content)
283
-
284
287
  if (!config.watcherEnabled) {
285
- mockBrokersCollection.registerMock(file, true)
286
- uiSyncVersion.increment()
288
+ brokers.registerMock(file, true)
289
+ Watcher.emitChange()
287
290
  }
288
291
  response.ok()
289
292
  }
290
293
 
291
294
 
292
295
  async function deleteMock(req, response) {
293
- if (config.readOnly)
294
- return response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly, or --no-read-only (CLI)')
295
-
296
+ if (config.readOnly) {
297
+ response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
298
+ return
299
+ }
296
300
  const file = await req.json()
297
301
  const path = await resolveIn(config.mocksDir, file)
298
302
 
299
303
  if (!path)
300
- return response.forbidden('Filename path resolves outside config.mocksDir')
301
-
302
- if (!isFile(path))
303
- return response.unprocessable(`Missing Mock: ${file}`)
304
-
305
- await rm(path)
304
+ response.forbidden('Filename path resolves outside config.mocksDir')
305
+ else if (!isFile(path))
306
+ response.unprocessable(`Missing Mock: ${file}`)
307
+ else
308
+ await rm(path)
306
309
 
307
310
  if (!config.watcherEnabled) {
308
- mockBrokersCollection.unregisterMock(file)
309
- uiSyncVersion.increment()
311
+ brokers.unregisterMock(file)
312
+ Watcher.emitChange()
310
313
  }
311
314
  response.ok()
312
315
  }
@@ -319,9 +322,9 @@ async function setWatchMocks(req, response) {
319
322
  response.unprocessable(`Expected boolean for "watchMocks"`)
320
323
  else {
321
324
  if (enabled)
322
- watchMocksDir()
325
+ Watcher.watchMocksDir()
323
326
  else
324
- stopMocksDirWatcher()
327
+ Watcher.unwatchMocksDir()
325
328
  response.ok()
326
329
  }
327
330
  }
@@ -108,21 +108,15 @@ class UrlMatcher {
108
108
  this.#urlRegex = this.#buildUrlRegex(file)
109
109
  }
110
110
 
111
- #escapeRegex(str) {
112
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
113
- }
114
-
115
111
  #buildUrlRegex(file) {
116
112
  let { urlMask } = parseFilename(file)
117
113
  urlMask = removeQueryStringAndFragment(urlMask)
118
- urlMask = this.#escapeRegex(urlMask)
114
+ urlMask = RegExp.escape(urlMask)
119
115
  urlMask = this.#disregardVariables(urlMask)
120
116
  return new RegExp('^' + urlMask + '/*$')
121
117
  }
122
118
 
123
- #disregardVariables(str) {
124
- return str.replace(/\\\[.*?\\]/g, '[^/]+')
125
- }
119
+ #disregardVariables = str => str.replace(/\\\[.*?\\]/g, '[^/]+')
126
120
 
127
121
  // Appending a '/' so URLs ending with variables don't match
128
122
  // URLs that have a path after that variable. For example,