mockaton 11.2.0 → 11.2.2
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 +69 -57
- package/index.d.ts +2 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/Api.js +39 -13
- package/src/ApiConstants.js +2 -1
- package/src/Dashboard.css +1 -1
- package/src/Dashboard.js +14 -12
- package/src/DashboardDevHotReload.js +34 -0
- package/src/DashboardHtml.js +2 -1
- package/src/Filename.js +3 -3
- package/src/Logo.svg +12 -11
- package/src/MockBroker.js +8 -7
- package/src/MockDispatcher.js +12 -7
- package/src/Mockaton.js +14 -5
- package/src/Watcher.js +3 -3
- package/src/WatcherDev.js +19 -0
- package/src/cli.js +1 -1
- package/src/config.js +11 -13
- package/src/mockBrokersCollection.js +3 -7
- package/src/staticCollection.js +1 -1
- package/src/utils/http-response.js +6 -0
- package/Makefile +0 -53
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
<img src="src/Logo.svg" alt="Mockaton Logo" width="210" style="margin-top: 30px"/>
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-

|
|
5
4
|
[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
|
|
6
5
|
[](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql)
|
|
7
6
|
[](https://codecov.io/github/ericfortis/mockaton)
|
|
8
7
|
|
|
9
|
-
An HTTP mock server for simulating APIs with minimal setup
|
|
10
|
-
|
|
8
|
+
An HTTP mock server for simulating APIs with minimal setup — ideal
|
|
9
|
+
for testing difficult to reproduce backend states.
|
|
11
10
|
|
|
12
11
|
## Overview
|
|
13
12
|
With Mockaton, you don’t need to write code for wiring up your
|
|
@@ -17,16 +16,17 @@ following a convention similar to the URLs.
|
|
|
17
16
|
For example, for [/api/company/123](#), the filename could be:
|
|
18
17
|
|
|
19
18
|
<pre>
|
|
20
|
-
<code>my-mocks-dir/<b>api/company</b>/[
|
|
19
|
+
<code>my-mocks-dir/<b>api/company</b>/[id].GET.200.json</code>
|
|
21
20
|
</pre>
|
|
22
21
|
|
|
23
22
|
<br/>
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
## Quick Start (Docker)
|
|
27
|
-
This will spin up Mockaton with the
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
This will spin up Mockaton with the sample directories
|
|
27
|
+
included in this repo mounted on the container.
|
|
28
|
+
|
|
29
|
+
_[mockaton-mocks/](./mockaton-mocks) and [mockaton-static-mocks/](./mockaton-static-mocks)_
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
32
|
git clone https://github.com/ericfortis/mockaton.git --depth 1
|
|
@@ -97,8 +97,8 @@ api/videos.GET.<b>500</b>.txt # Internal Server Error
|
|
|
97
97
|
### Option 1: Browser extension
|
|
98
98
|
The companion Chrome [devtools
|
|
99
99
|
extension](https://github.com/ericfortis/download-http-requests-browser-ext)
|
|
100
|
-
lets you download all the HTTP responses
|
|
101
|
-
|
|
100
|
+
lets you download all the HTTP responses and
|
|
101
|
+
save them following Mockaton’s filename convention.
|
|
102
102
|
|
|
103
103
|
### Option 2: Fallback to your backend
|
|
104
104
|
<details>
|
|
@@ -117,30 +117,17 @@ They will be saved in your `config.mocksDir` following the filename convention.
|
|
|
117
117
|
</details>
|
|
118
118
|
|
|
119
119
|
<br/>
|
|
120
|
-
|
|
121
120
|
<br/>
|
|
122
121
|
|
|
123
122
|
|
|
124
123
|
## Motivation
|
|
125
124
|
|
|
126
|
-
### Testing scenarios that would otherwise be skipped
|
|
127
|
-
- Simulate errors on third-party APIs, or on your project’s backend (if you are a frontend dev, or unfamiliar with that code)
|
|
128
|
-
- Trigger dynamic states on an API. You can do this by using comments on mock filenames, for example, for polled alerts or notifications.
|
|
129
|
-
- Trigger empty (no content) responses
|
|
130
|
-
- Sometimes, the ideal flow you need is just too difficult to reproduce from the actual backend
|
|
131
|
-
|
|
132
|
-
### Works around unstable dev backends while developing UIs
|
|
133
|
-
- Spinning up dev infrastructure
|
|
134
|
-
- Syncing the database
|
|
135
|
-
- Mitigates progress from being blocked by waiting for APIs
|
|
136
|
-
|
|
137
|
-
### Time travel
|
|
138
|
-
If you commit the mocks to your repo, you don’t have to downgrade backends when:
|
|
139
|
-
- Checking out long-lived branches
|
|
140
|
-
- Bisecting bugs
|
|
141
|
-
|
|
142
125
|
### Deterministic and comprehensive states
|
|
143
|
-
|
|
126
|
+
Sometimes the flow you need to test is
|
|
127
|
+
too difficult to reproduce from the actual backend.
|
|
128
|
+
|
|
129
|
+
- Demo edge cases to PMs, Design, and clients
|
|
130
|
+
- Set up screenshot tests, e.g., with [pixaton](https://github.com/ericfortis/pixaton)
|
|
144
131
|
- Spot inadvertent regressions during development. For example, the demo app in
|
|
145
132
|
this repo has a list of colors containing all of their possible states, such as
|
|
146
133
|
permutations for out-of-stock, new-arrival, and discontinued. This way you’ll
|
|
@@ -148,6 +135,10 @@ If you commit the mocks to your repo, you don’t have to downgrade backends whe
|
|
|
148
135
|
|
|
149
136
|
<img src="./demo-app-vite/pixaton-tests/pic-for-readme.vp740x880.light.gold.png" alt="Mockaton Demo App Screenshot" width="740" />
|
|
150
137
|
|
|
138
|
+
<br/>
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## Benefits
|
|
151
142
|
|
|
152
143
|
### Standalone demo server (Docker)
|
|
153
144
|
You can demo your app by compiling the frontend and putting
|
|
@@ -157,27 +148,54 @@ repo includes a demo which builds and runs a docker container.
|
|
|
157
148
|
```sh
|
|
158
149
|
git clone https://github.com/ericfortis/mockaton.git --depth 1
|
|
159
150
|
cd mockaton/demo-app-vite
|
|
160
|
-
make
|
|
151
|
+
make run-standalone-demo
|
|
161
152
|
```
|
|
162
153
|
- App: http://localhost:4040
|
|
163
154
|
- Dashboard: http://localhost:4040/mockaton
|
|
164
155
|
|
|
156
|
+
### Testing scenarios that would otherwise be skipped
|
|
157
|
+
- Trigger dynamic states on an API. You can do this by using comments on mock filenames, for example, for polled alerts or notifications.
|
|
158
|
+
- Testing retries, you can change an endpoint from a 500 to a 200 on the fly.
|
|
159
|
+
- Simulate errors on third-party APIs, or on your project’s backend (if you are a frontend dev, or unfamiliar with that code)
|
|
160
|
+
- Generating dynamic responses. Mockaton lets you use Node’s HTTP handlers (see function mocks) when using function mocks.
|
|
161
|
+
So you can, e.g.:
|
|
162
|
+
- have an in-memory database
|
|
163
|
+
- read from disk
|
|
164
|
+
- read query string
|
|
165
|
+
- pretty much anything you can do with a normal backend request handler
|
|
165
166
|
|
|
166
167
|
### Privacy and security
|
|
167
168
|
- Does not write to disk. Except when you select ✅ **Save Mocks** for scraping mocks from a backend
|
|
168
169
|
- Does not initiate network connections (no logs, no telemetry)
|
|
169
170
|
- Does not hijack your HTTP client
|
|
170
|
-
- Auditable
|
|
171
|
+
- Auditable
|
|
172
|
+
- Organized and small. 4 KLoC (half is UI and tests)
|
|
171
173
|
- Zero dependencies. No runtime and no build packages.
|
|
172
174
|
|
|
173
175
|
<br/>
|
|
174
176
|
|
|
177
|
+
## Benefits of Mocking APIs in General
|
|
178
|
+
The section above highlights benefits specific to Mockaton. There are more, but
|
|
179
|
+
in general here are some benefits which Mockaton has but other tools have as well:
|
|
180
|
+
|
|
181
|
+
### Works around unstable dev backends while developing UIs
|
|
182
|
+
- Syncing the database and spinning up dev infrastructure can be complex
|
|
183
|
+
- Mitigates progress from being blocked by waiting for APIs
|
|
184
|
+
|
|
185
|
+
### Time travel
|
|
186
|
+
If you commit the mocks to your repo, you don’t have to downgrade
|
|
187
|
+
backends when checking out long-lived branches or bisecting bugs.
|
|
188
|
+
|
|
189
|
+
<br/>
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## Usage (without Docker)
|
|
175
193
|
|
|
176
|
-
|
|
194
|
+
_For Docker, see the Quick-Start section above._
|
|
177
195
|
|
|
178
|
-
Requires Node.js
|
|
196
|
+
Requires Node.js **v22.18+**, which supports TypeScript mocks.
|
|
179
197
|
|
|
180
|
-
1. Create a mock in the default
|
|
198
|
+
1. Create a mock in the default directory (`./mockaton-mocks`)
|
|
181
199
|
```sh
|
|
182
200
|
mkdir -p mockaton-mocks/api
|
|
183
201
|
echo "[1,2,3]" > mockaton-mocks/api/foo.GET.200.json
|
|
@@ -220,7 +238,7 @@ The CLI options override their counterparts in `mockaton.config.js`
|
|
|
220
238
|
-p, --port <port> (default: 0) which means auto-assigned
|
|
221
239
|
|
|
222
240
|
-q, --quiet Errors only
|
|
223
|
-
--no-open Don’t open dashboard in a browser
|
|
241
|
+
--no-open Don’t open dashboard in a browser
|
|
224
242
|
|
|
225
243
|
-h, --help
|
|
226
244
|
-v, --version
|
|
@@ -230,16 +248,11 @@ The CLI options override their counterparts in `mockaton.config.js`
|
|
|
230
248
|
## mockaton.config.js (Optional)
|
|
231
249
|
Mockaton looks for a file `mockaton.config.js` in its current working directory.
|
|
232
250
|
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
### Defaults Overview
|
|
252
|
+
The next section has the documentation, but here's an overview of the defaults:
|
|
235
253
|
|
|
236
254
|
```js
|
|
237
|
-
import {
|
|
238
|
-
defineConfig,
|
|
239
|
-
jsToJsonPlugin,
|
|
240
|
-
openInBrowser,
|
|
241
|
-
SUPPORTED_METHODS
|
|
242
|
-
} from 'mockaton'
|
|
255
|
+
import { defineConfig, jsToJsonPlugin, openInBrowser, SUPPORTED_METHODS } from 'mockaton'
|
|
243
256
|
|
|
244
257
|
export default defineConfig({
|
|
245
258
|
mocksDir: 'mockaton-mocks',
|
|
@@ -248,11 +261,11 @@ export default defineConfig({
|
|
|
248
261
|
watcherEnabled: true,
|
|
249
262
|
|
|
250
263
|
host: '127.0.0.1',
|
|
251
|
-
port: 0,
|
|
264
|
+
port: 0, // auto-assigned
|
|
252
265
|
|
|
253
266
|
logLevel: 'normal',
|
|
254
267
|
|
|
255
|
-
delay: 1200,
|
|
268
|
+
delay: 1200, // ms. Applies to routes with the Delay Checkbox "ON"
|
|
256
269
|
delayJitter: 0,
|
|
257
270
|
|
|
258
271
|
proxyFallback: '',
|
|
@@ -271,7 +284,6 @@ export default defineConfig({
|
|
|
271
284
|
corsCredentials: true,
|
|
272
285
|
corsMaxAge: 0,
|
|
273
286
|
|
|
274
|
-
|
|
275
287
|
plugins: [
|
|
276
288
|
[/\.(js|ts)$/, jsToJsonPlugin]
|
|
277
289
|
],
|
|
@@ -280,8 +292,7 @@ export default defineConfig({
|
|
|
280
292
|
})
|
|
281
293
|
```
|
|
282
294
|
|
|
283
|
-
|
|
284
|
-
|
|
295
|
+
<br/>
|
|
285
296
|
<details>
|
|
286
297
|
<summary><b>Config Documentation…</b></summary>
|
|
287
298
|
|
|
@@ -361,7 +372,7 @@ the filename will have `[id]` in their place. For example:
|
|
|
361
372
|
my-mocks-dir<b>/api/user/</b>[id]<b>/likes</b>.GET.200.json
|
|
362
373
|
</pre>
|
|
363
374
|
|
|
364
|
-
Your existing mocks won’t be overwritten.
|
|
375
|
+
Your existing mocks won’t be overwritten. Responses of routes with
|
|
365
376
|
the ☁️ **Cloud Checkbox** selected will be saved with unique filename-comments.
|
|
366
377
|
|
|
367
378
|
|
|
@@ -411,7 +422,7 @@ If you need to send more than one cookie, you can inject them globally
|
|
|
411
422
|
in `config.extraHeaders`, or individually in a function `.js` or `.ts` mock.
|
|
412
423
|
|
|
413
424
|
By the way, the `jwtCookie` helper has a hardcoded header and signature.
|
|
414
|
-
|
|
425
|
+
So it’s useful only if you care about its payload.
|
|
415
426
|
|
|
416
427
|
<br/>
|
|
417
428
|
|
|
@@ -562,7 +573,7 @@ const server = await Mockaton(
|
|
|
562
573
|
|
|
563
574
|
|
|
564
575
|
## You can write JSON mocks in JavaScript or TypeScript
|
|
565
|
-
_TypeScript
|
|
576
|
+
_TypeScript needs **Node 22.18+ or 23.6+**_
|
|
566
577
|
|
|
567
578
|
For example, `api/foo.GET.200.js`
|
|
568
579
|
|
|
@@ -572,7 +583,7 @@ For example, `api/foo.GET.200.js`
|
|
|
572
583
|
export default { foo: 'bar' }
|
|
573
584
|
```
|
|
574
585
|
|
|
575
|
-
### Option B: Function (async or sync)
|
|
586
|
+
### Option B: Function Mocks (async or sync)
|
|
576
587
|
|
|
577
588
|
**Return** a `string | Buffer | Uint8Array`, but **don’t call** `response.end()`
|
|
578
589
|
|
|
@@ -632,7 +643,7 @@ export default function listColors() {
|
|
|
632
643
|
**What if I need to serve a static .js or .ts?**
|
|
633
644
|
|
|
634
645
|
**Option A:** Put it in your `config.staticDir` without the `.GET.200.js` extension.
|
|
635
|
-
|
|
646
|
+
Mocks in `staticDir` take precedence over `mocksDir/*`.
|
|
636
647
|
|
|
637
648
|
**Option B:** Read it and return it. For example:
|
|
638
649
|
```js
|
|
@@ -662,7 +673,6 @@ want a `Content-Type` header in the response.
|
|
|
662
673
|
|
|
663
674
|
<details>
|
|
664
675
|
<summary>Supported Methods</summary>
|
|
665
|
-
<p>From <code>require('node:http').METHODS</code></p>
|
|
666
676
|
<p>
|
|
667
677
|
ACL, BIND, CHECKOUT,
|
|
668
678
|
CONNECT, COPY, DELETE,
|
|
@@ -702,8 +712,6 @@ api/foo.GET.200.json
|
|
|
702
712
|
|
|
703
713
|
A filename can have many comments.
|
|
704
714
|
|
|
705
|
-
<br/>
|
|
706
|
-
|
|
707
715
|
### Default mock for a route
|
|
708
716
|
You can add the comment: `(default)`.
|
|
709
717
|
Otherwise, the first file in **alphabetical order** wins.
|
|
@@ -715,7 +723,7 @@ api/user<b>(default)</b>.GET.200.json
|
|
|
715
723
|
<br/>
|
|
716
724
|
|
|
717
725
|
### Query string params
|
|
718
|
-
The query string is ignored for routing purposes.
|
|
726
|
+
The query string is ignored for routing purposes. It’s only used for
|
|
719
727
|
documenting the URL contract.
|
|
720
728
|
<pre>
|
|
721
729
|
api/video<b>?limit=[limit]</b>.GET.200.json
|
|
@@ -723,7 +731,7 @@ api/video<b>?limit=[limit]</b>.GET.200.json
|
|
|
723
731
|
|
|
724
732
|
On Windows, filenames containing "?" are [not
|
|
725
733
|
permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query
|
|
726
|
-
string it’s ignored anyway.
|
|
734
|
+
string, it’s ignored anyway.
|
|
727
735
|
|
|
728
736
|
<br/>
|
|
729
737
|
|
|
@@ -752,7 +760,6 @@ All of its methods return their `fetch` response promise.
|
|
|
752
760
|
```js
|
|
753
761
|
import { Commander } from 'mockaton'
|
|
754
762
|
|
|
755
|
-
|
|
756
763
|
const myMockatonAddr = 'http://localhost:4040'
|
|
757
764
|
const mockaton = new Commander(myMockatonAddr)
|
|
758
765
|
```
|
|
@@ -852,6 +859,9 @@ await mockaton.reset()
|
|
|
852
859
|
|
|
853
860
|
## Alternatives worth learning as well
|
|
854
861
|
|
|
862
|
+
<details>
|
|
863
|
+
<summary>Learn more…</summary>
|
|
864
|
+
|
|
855
865
|
### Proxy-like
|
|
856
866
|
These are similar to Mockaton in the sense that you can modify the
|
|
857
867
|
mock response without loosing or risking your frontend code state. For
|
|
@@ -875,7 +885,9 @@ programs hijack your browser’s HTTP client (and Node’s).
|
|
|
875
885
|
- [Wire Mock](https://github.com/wiremock/wiremock)
|
|
876
886
|
- [Mock](https://github.com/dhuan/mock)
|
|
877
887
|
- [Swagger](https://swagger.io/)
|
|
888
|
+
- [Mockoon](https://mockoon.com)
|
|
878
889
|
|
|
890
|
+
</details>
|
|
879
891
|
|
|
880
892
|
<br/>
|
|
881
893
|
<br/>
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -3,6 +3,6 @@ export { Commander } from './src/ApiCommander.js'
|
|
|
3
3
|
|
|
4
4
|
export { jwtCookie } from './src/utils/jwt.js'
|
|
5
5
|
export { jsToJsonPlugin } from './src/MockDispatcher.js'
|
|
6
|
-
export { parseJSON } from './src/utils/http-request.js'
|
|
6
|
+
export { parseJSON, BodyReaderError } from './src/utils/http-request.js'
|
|
7
7
|
|
|
8
8
|
export const defineConfig = opts => opts
|
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
|
|
8
8
|
import { cookie } from './cookie.js'
|
|
9
|
+
import { devWatcher } from './WatcherDev.js'
|
|
9
10
|
import { parseJSON } from './utils/http-request.js'
|
|
10
11
|
import { uiSyncVersion } from './Watcher.js'
|
|
11
12
|
import * as staticCollection from './staticCollection.js'
|
|
@@ -16,23 +17,31 @@ import { sendOK, sendJSON, sendUnprocessable, sendFile, sendHTML } from './utils
|
|
|
16
17
|
import { API, LONG_POLL_SERVER_TIMEOUT, HEADER_SYNC_VERSION } from './ApiConstants.js'
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
const DEV = process.env.NODE_ENV === 'development'
|
|
21
|
+
|
|
22
|
+
export const DASHBOARD_ASSETS = [
|
|
23
|
+
'Dashboard.css',
|
|
24
|
+
'Dashboard.js',
|
|
25
|
+
'DashboardDom.js',
|
|
26
|
+
'DashboardStore.js',
|
|
27
|
+
'DashboardDevHotReload.js',
|
|
28
|
+
'ApiCommander.js',
|
|
29
|
+
'Logo.svg',
|
|
30
|
+
'Filename.js', // used on server too
|
|
31
|
+
'ApiConstants.js', // used on server too
|
|
32
|
+
]
|
|
33
|
+
|
|
19
34
|
export const apiGetRequests = new Map([
|
|
20
35
|
[API.dashboard, serveDashboard],
|
|
21
|
-
...[
|
|
22
|
-
|
|
23
|
-
'Dashboard.css',
|
|
24
|
-
'ApiCommander.js',
|
|
25
|
-
'ApiConstants.js',
|
|
26
|
-
'Dashboard.js',
|
|
27
|
-
'DashboardDom.js',
|
|
28
|
-
'DashboardStore.js',
|
|
29
|
-
'Filename.js'
|
|
30
|
-
].map(f => [API.dashboard + '/' + f, serveStatic(f)]),
|
|
31
|
-
|
|
36
|
+
...DASHBOARD_ASSETS.map(f => [API.dashboard + '/' + f, serveStatic(f)]),
|
|
37
|
+
|
|
32
38
|
[API.state, getState],
|
|
33
39
|
[API.syncVersion, longPollClientSyncVersion],
|
|
34
|
-
[API.throws, () => { throw new Error('Test500') }]
|
|
35
40
|
])
|
|
41
|
+
if (DEV) {
|
|
42
|
+
apiGetRequests.set(API.throws, () => { throw new Error('Test500') })
|
|
43
|
+
apiGetRequests.set(API.watchHotReload, longPollDevHotReload)
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
export const apiPatchRequests = new Map([
|
|
38
47
|
[API.cors, setCorsAllowed],
|
|
@@ -54,7 +63,7 @@ export const apiPatchRequests = new Map([
|
|
|
54
63
|
/** # GET */
|
|
55
64
|
|
|
56
65
|
function serveDashboard(_, response) {
|
|
57
|
-
sendHTML(response, DashboardHtml, CSP)
|
|
66
|
+
sendHTML(response, DashboardHtml(config.hotReload), CSP)
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
function serveStatic(f) {
|
|
@@ -101,6 +110,23 @@ function longPollClientSyncVersion(req, response) {
|
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
|
|
113
|
+
function longPollDevHotReload(req, response) {
|
|
114
|
+
function onDevChange(file) {
|
|
115
|
+
devWatcher.unsubscribe(onDevChange)
|
|
116
|
+
sendJSON(response, file)
|
|
117
|
+
}
|
|
118
|
+
response.setTimeout(LONG_POLL_SERVER_TIMEOUT, () => {
|
|
119
|
+
devWatcher.unsubscribe(onDevChange)
|
|
120
|
+
sendJSON(response, '')
|
|
121
|
+
})
|
|
122
|
+
req.on('error', () => {
|
|
123
|
+
devWatcher.unsubscribe(onDevChange)
|
|
124
|
+
response.destroy()
|
|
125
|
+
})
|
|
126
|
+
devWatcher.subscribe(onDevChange)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
104
130
|
|
|
105
131
|
/** # PATCH */
|
|
106
132
|
|
package/src/ApiConstants.js
CHANGED
|
@@ -17,7 +17,8 @@ export const API = {
|
|
|
17
17
|
staticStatus: MOUNT + '/static-status',
|
|
18
18
|
syncVersion: MOUNT + '/sync-version',
|
|
19
19
|
throws: MOUNT + '/throws',
|
|
20
|
-
toggle500: MOUNT + '/toggle500'
|
|
20
|
+
toggle500: MOUNT + '/toggle500',
|
|
21
|
+
watchHotReload: MOUNT + '/watch-hot-reload',
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export const HEADER_502 = 'Mockaton502'
|
package/src/Dashboard.css
CHANGED
package/src/Dashboard.js
CHANGED
|
@@ -96,9 +96,9 @@ function App() {
|
|
|
96
96
|
function Header() {
|
|
97
97
|
return (
|
|
98
98
|
r('header', null,
|
|
99
|
-
r('
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
r('object', {
|
|
100
|
+
data: 'Logo.svg',
|
|
101
|
+
type: 'image/svg+xml',
|
|
102
102
|
width: 120,
|
|
103
103
|
height: 22
|
|
104
104
|
}),
|
|
@@ -737,7 +737,7 @@ function SettingsIcon() {
|
|
|
737
737
|
* The version increments when a mock file is added, removed, or renamed.
|
|
738
738
|
*/
|
|
739
739
|
function initRealTimeUpdates() {
|
|
740
|
-
let oldVersion = undefined // undefined waits until next event or timeout
|
|
740
|
+
let oldVersion = undefined // undefined so it waits until next event or timeout
|
|
741
741
|
let controller = new AbortController()
|
|
742
742
|
|
|
743
743
|
longPoll()
|
|
@@ -823,8 +823,12 @@ function initKeyboardNavigation() {
|
|
|
823
823
|
|
|
824
824
|
|
|
825
825
|
function SyntaxJSON(json) {
|
|
826
|
+
// Capture groups: [string, optional colon, punc]
|
|
827
|
+
const regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s]+)|\S+/g
|
|
828
|
+
|
|
826
829
|
const MAX_NODES = 50_000
|
|
827
830
|
let nNodes = 0
|
|
831
|
+
|
|
828
832
|
const frag = new DocumentFragment()
|
|
829
833
|
|
|
830
834
|
function span(className, textContent) {
|
|
@@ -842,8 +846,7 @@ function SyntaxJSON(json) {
|
|
|
842
846
|
|
|
843
847
|
let match
|
|
844
848
|
let lastIndex = 0
|
|
845
|
-
|
|
846
|
-
while ((match = SyntaxJSON.regex.exec(json)) !== null) {
|
|
849
|
+
while ((match = regex.exec(json)) !== null) {
|
|
847
850
|
if (nNodes > MAX_NODES)
|
|
848
851
|
break
|
|
849
852
|
|
|
@@ -865,13 +868,15 @@ function SyntaxJSON(json) {
|
|
|
865
868
|
text(json.slice(lastIndex))
|
|
866
869
|
return frag
|
|
867
870
|
}
|
|
868
|
-
SyntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s]+)|\S+/g
|
|
869
|
-
// Capture group order: [string, optional colon, punc]
|
|
870
871
|
|
|
871
872
|
|
|
872
873
|
function SyntaxXML(xml) {
|
|
874
|
+
// Capture groups: [tagPunc, tagName, attrName, attrVal]
|
|
875
|
+
const regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
|
|
876
|
+
|
|
873
877
|
const MAX_NODES = 50_000
|
|
874
878
|
let nNodes = 0
|
|
879
|
+
|
|
875
880
|
const frag = new DocumentFragment()
|
|
876
881
|
|
|
877
882
|
function span(className, textContent) {
|
|
@@ -889,8 +894,7 @@ function SyntaxXML(xml) {
|
|
|
889
894
|
|
|
890
895
|
let match
|
|
891
896
|
let lastIndex = 0
|
|
892
|
-
|
|
893
|
-
while ((match = SyntaxXML.regex.exec(xml)) !== null) {
|
|
897
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
894
898
|
if (nNodes > MAX_NODES)
|
|
895
899
|
break
|
|
896
900
|
|
|
@@ -908,6 +912,4 @@ function SyntaxXML(xml) {
|
|
|
908
912
|
frag.normalize()
|
|
909
913
|
return frag
|
|
910
914
|
}
|
|
911
|
-
SyntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
|
|
912
|
-
// Capture groups order: [tagPunc, tagName, attrName, attrVal]
|
|
913
915
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { API } from './ApiConstants.js'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
longPoll()
|
|
5
|
+
async function longPoll() {
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch(API.watchHotReload)
|
|
8
|
+
if (response.ok) {
|
|
9
|
+
const editedFile = await response.json() || ''
|
|
10
|
+
if (editedFile.endsWith('.css')) {
|
|
11
|
+
hotReloadCSS(editedFile)
|
|
12
|
+
longPoll()
|
|
13
|
+
}
|
|
14
|
+
else if (editedFile)
|
|
15
|
+
location.reload()
|
|
16
|
+
else
|
|
17
|
+
longPoll()
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
throw response.statusText
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error('hot reload', error?.message || error)
|
|
24
|
+
setTimeout(longPoll, 3000)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hotReloadCSS(editedFile) {
|
|
29
|
+
const link = document.querySelector(`link[href*="${editedFile}"]`)
|
|
30
|
+
if (link) {
|
|
31
|
+
const href = link.href.split('?')[0]
|
|
32
|
+
link.href = href + '?t=' + Date.now()
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/DashboardHtml.js
CHANGED
|
@@ -6,7 +6,7 @@ export const CSP = [
|
|
|
6
6
|
].join(';')
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
export const DashboardHtml = `<!DOCTYPE html>
|
|
9
|
+
export const DashboardHtml = hotReloadEnabled => `<!DOCTYPE html>
|
|
10
10
|
<html lang="en-US">
|
|
11
11
|
<head>
|
|
12
12
|
<meta charset="UTF-8">
|
|
@@ -30,6 +30,7 @@ export const DashboardHtml = `<!DOCTYPE html>
|
|
|
30
30
|
<title>Mockaton</title>
|
|
31
31
|
</head>
|
|
32
32
|
<body>
|
|
33
|
+
${hotReloadEnabled ? `<script type="module" src="DashboardDevHotReload.js"></script>` : '' }
|
|
33
34
|
</body>
|
|
34
35
|
</html>
|
|
35
36
|
`
|
package/src/Filename.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const httpMethods = [ // @KeepSync
|
|
1
|
+
const httpMethods = [ // @KeepSync node:http.METHODS (this file is used on the client too)
|
|
2
2
|
'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE',
|
|
3
3
|
'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE',
|
|
4
4
|
'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS',
|
|
@@ -28,7 +28,7 @@ export function validateFilename(file) {
|
|
|
28
28
|
if (!httpMethods.includes(method))
|
|
29
29
|
return `Unrecognized HTTP Method: "${method}"`
|
|
30
30
|
}
|
|
31
|
-
// TODO ThinkAbout 206 (reject, handle, or send in full?)
|
|
31
|
+
// TODO @ThinkAbout 206 (reject, handle, or send in full?)
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
export function parseFilename(file) {
|
|
@@ -53,7 +53,7 @@ function responseStatusIsValid(status) {
|
|
|
53
53
|
&& status >= 100
|
|
54
54
|
&& status <= 599
|
|
55
55
|
}
|
|
56
|
-
// TODO ThinkAbout allowing custom status codes
|
|
56
|
+
// TODO @ThinkAbout allowing custom status codes
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
export function makeMockFilename(url, method, status, ext) {
|
package/src/Logo.svg
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<svg width="556" height="100" version="1.1" viewBox="0 0 556 100" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
-
<style
|
|
3
|
+
<style>
|
|
4
4
|
@media (prefers-color-scheme: light) { :root { --color: #333 } }
|
|
5
5
|
@media (prefers-color-scheme: dark) { :root { --color: #eee } }
|
|
6
|
-
path {
|
|
6
|
+
path {
|
|
7
|
+
fill: #777;
|
|
8
|
+
fill: var(--color);
|
|
9
|
+
}
|
|
7
10
|
</style>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
d="m392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z"/>
|
|
16
|
-
</g>
|
|
11
|
+
<path
|
|
12
|
+
d="m13.75 1.8789c-5.9487 0.19352-10.865 4.5652-11.082 11.686v81.445c-1e-7 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-64.982c0.02794-3.4488 3.0988-3.5551 4.2031-1.1562l16.615 59.059c1.4393 5.3711 5.1083 7.9633 8.7656 7.9473 3.6573 0.01603 7.3263-2.5762 8.7656-7.9473l16.615-59.059c1.1043-2.3989 4.1752-2.2925 4.2031 1.1562v64.982c0 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-81.445c-0.17732-7.0807-5.1334-11.492-11.082-11.686-5.9487-0.19352-12.652 3.8309-15.609 13.619l-15.686 57.334-15.686-57.334c-2.9569-9.7882-9.6607-13.813-15.609-13.619zm239.19 0.074219c-2.216 0-4 1.784-4 4v89.057c0 2.216 1.784 4 4 4h4.793c2.216 0 3.9868-1.784 4-4l0.10644-17.94c0.0734-0.07237 12.175-13.75 12.175-13.75 5.6772 11.091 11.404 22.158 17.113 33.232 1.0168 1.9689 3.4217 2.7356 5.3906 1.7188l4.2578-2.1992c1.9689-1.0168 2.7356-3.4217 1.7188-5.3906-6.4691-12.585-12.958-25.16-19.442-37.738l17.223-19.771c1.4555-1.671 1.2803-4.189-0.39062-5.6445l-3.6133-3.1465c-0.73105-0.63679-1.6224-0.96212-2.5176-0.98633-1.151-0.03113-2.3063 0.43508-3.125 1.375l-28.896 33.174v-51.99c0-2.216-1.784-4-4-4zm-58.255 23.316c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312l-0.125-7.8457c0-2.216-1.784-4-4-4h-4.6524c-2.216 0-4 1.784-4 4l3e-3 6.7888c3e-3 3.8063-1.5601 9.3694-8.4716 9.3694h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.6937 0 8.3697 5.2207 8.4687 11.828v2.2207c0 2.216 1.784 4 4 4h4.6524c2.216 0 4-1.784 4-4l0.125-5.7363c0-10.699-8.6117-19.312-19.311-19.312zm-72.182 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z"/>
|
|
13
|
+
<path
|
|
14
|
+
d="m331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z"
|
|
15
|
+
opacity="0.75"/>
|
|
16
|
+
<path
|
|
17
|
+
d="m392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z"/>
|
|
17
18
|
</svg>
|
package/src/MockBroker.js
CHANGED
|
@@ -22,7 +22,7 @@ export class MockBroker {
|
|
|
22
22
|
|
|
23
23
|
#sortMocks() {
|
|
24
24
|
this.mocks.sort()
|
|
25
|
-
const defaults = this.mocks.filter(
|
|
25
|
+
const defaults = this.mocks.filter(f => includesComment(f, DEFAULT_MOCK_COMMENT))
|
|
26
26
|
this.mocks = Array.from(new Set(defaults).union(new Set(this.mocks)))
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -55,18 +55,19 @@ export class MockBroker {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
toggle500() {
|
|
58
|
-
this.
|
|
59
|
-
if (
|
|
58
|
+
const shouldUnset = this.auto500 || this.status === 500
|
|
59
|
+
if (shouldUnset)
|
|
60
60
|
this.selectDefaultFile()
|
|
61
61
|
else {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
this.selectFile(
|
|
62
|
+
const f500 = this.mocks.find(this.#is500)
|
|
63
|
+
if (f500)
|
|
64
|
+
this.selectFile(f500)
|
|
65
65
|
else {
|
|
66
66
|
this.auto500 = true
|
|
67
|
-
this.status = 500
|
|
67
|
+
this.status = 500
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
this.proxied = false
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
setDelayed(delayed) {
|
package/src/MockDispatcher.js
CHANGED
|
@@ -31,17 +31,23 @@ export async function dispatchMock(req, response) {
|
|
|
31
31
|
if (cookie.getCurrent())
|
|
32
32
|
response.setHeader('Set-Cookie', cookie.getCurrent())
|
|
33
33
|
|
|
34
|
-
response.statusCode = broker.auto500
|
|
34
|
+
response.statusCode = broker.auto500
|
|
35
|
+
? 500
|
|
36
|
+
: broker.status
|
|
35
37
|
const { mime, body } = broker.auto500
|
|
36
38
|
? { mime: '', body: '' }
|
|
37
39
|
: await applyPlugins(join(config.mocksDir, broker.file), req, response)
|
|
38
40
|
|
|
39
|
-
logger.accessMock(req.url, broker.file)
|
|
40
41
|
response.setHeader('Content-Type', mime)
|
|
41
42
|
response.setHeader('Content-Length', length(body))
|
|
42
|
-
|
|
43
|
-
setTimeout(() =>
|
|
44
|
-
|
|
43
|
+
|
|
44
|
+
setTimeout(() =>
|
|
45
|
+
response.end(isHead
|
|
46
|
+
? null
|
|
47
|
+
: body
|
|
48
|
+
), Number(broker.delayed && calcDelay()))
|
|
49
|
+
|
|
50
|
+
logger.accessMock(req.url, broker.file)
|
|
45
51
|
}
|
|
46
52
|
catch (error) { // TESTME
|
|
47
53
|
if (error?.code === 'ENOENT') // mock-file has been deleted
|
|
@@ -74,7 +80,6 @@ export async function jsToJsonPlugin(filePath, req, response) {
|
|
|
74
80
|
|
|
75
81
|
function length(body) {
|
|
76
82
|
if (typeof body === 'string') return Buffer.byteLength(body)
|
|
77
|
-
if (
|
|
78
|
-
if (body instanceof Uint8Array) return body.byteLength
|
|
83
|
+
if (body instanceof Uint8Array) return body.byteLength // Buffers are u8
|
|
79
84
|
return 0
|
|
80
85
|
}
|
package/src/Mockaton.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http'
|
|
2
|
+
import pkgJSON from '../package.json' with { type: 'json' }
|
|
2
3
|
|
|
3
4
|
import { API } from './ApiConstants.js'
|
|
4
5
|
import { logger } from './utils/logger.js'
|
|
@@ -8,12 +9,18 @@ import { dispatchStatic } from './StaticDispatcher.js'
|
|
|
8
9
|
import * as staticCollection from './staticCollection.js'
|
|
9
10
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
10
11
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
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 {
|
|
14
|
+
import {
|
|
15
|
+
setHeaders, sendNoContent, sendInternalServerError,
|
|
16
|
+
sendUnprocessable, sendTooLongURI, sendBadRequest
|
|
17
|
+
} from './utils/http-response.js'
|
|
18
|
+
import { watchDevSPA } from './WatcherDev.js'
|
|
19
|
+
import { watchMocksDir, watchStaticDir } from './Watcher.js'
|
|
15
20
|
|
|
16
21
|
|
|
22
|
+
const DEV = process.env.NODE_ENV === 'development'
|
|
23
|
+
|
|
17
24
|
export function Mockaton(options) {
|
|
18
25
|
return new Promise((resolve, reject) => {
|
|
19
26
|
setup(options)
|
|
@@ -25,6 +32,8 @@ export function Mockaton(options) {
|
|
|
25
32
|
watchMocksDir()
|
|
26
33
|
watchStaticDir()
|
|
27
34
|
}
|
|
35
|
+
if (DEV && config.hotReload)
|
|
36
|
+
watchDevSPA()
|
|
28
37
|
|
|
29
38
|
const server = createServer(onRequest)
|
|
30
39
|
server.on('error', reject)
|
|
@@ -41,9 +50,9 @@ export function Mockaton(options) {
|
|
|
41
50
|
|
|
42
51
|
async function onRequest(req, response) {
|
|
43
52
|
response.on('error', logger.warn)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
|
|
54
|
+
setHeaders(response, ['Server', `Mockaton ${pkgJSON.version}`])
|
|
55
|
+
setHeaders(response, config.extraHeaders)
|
|
47
56
|
|
|
48
57
|
const url = req.url || ''
|
|
49
58
|
|
package/src/Watcher.js
CHANGED
|
@@ -30,7 +30,7 @@ export const uiSyncVersion = new class extends EventEmitter {
|
|
|
30
30
|
this.removeListener('ARR', listener)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
#debounce(fn) {
|
|
33
|
+
#debounce(fn) { // TESTME
|
|
34
34
|
let timer
|
|
35
35
|
return () => {
|
|
36
36
|
clearTimeout(timer)
|
|
@@ -45,7 +45,7 @@ export function watchMocksDir() {
|
|
|
45
45
|
if (!file)
|
|
46
46
|
return
|
|
47
47
|
|
|
48
|
-
if (isDirectory(join(dir, file))) {
|
|
48
|
+
if (isDirectory(join(dir, file))) {
|
|
49
49
|
mockBrokerCollection.init()
|
|
50
50
|
uiSyncVersion.increment()
|
|
51
51
|
}
|
|
@@ -61,7 +61,7 @@ export function watchMocksDir() {
|
|
|
61
61
|
})
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function watchStaticDir() {
|
|
64
|
+
export function watchStaticDir() {
|
|
65
65
|
const dir = config.staticDir
|
|
66
66
|
if (!dir)
|
|
67
67
|
return
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { watch } from 'node:fs'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
import { DASHBOARD_ASSETS } from './Api.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export const devWatcher = new class extends EventEmitter {
|
|
7
|
+
emit(file) { super.emit('RELOAD', file) }
|
|
8
|
+
subscribe(listener) { this.once('RELOAD', listener) }
|
|
9
|
+
unsubscribe(listener) { this.removeListener('RELOAD', listener) }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// DashboardHtml.js is not watched.
|
|
13
|
+
// It would need dynamic import + cache busting
|
|
14
|
+
export function watchDevSPA() {
|
|
15
|
+
watch('src', (_, file) => {
|
|
16
|
+
if (DASHBOARD_ASSETS.includes(file))
|
|
17
|
+
devWatcher.emit(file)
|
|
18
|
+
})
|
|
19
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -53,7 +53,7 @@ Options:
|
|
|
53
53
|
-p, --port <port> (default: 0) which means auto-assigned
|
|
54
54
|
|
|
55
55
|
-q, --quiet Errors only
|
|
56
|
-
--no-open Don’t open dashboard in a browser
|
|
56
|
+
--no-open Don’t open dashboard in a browser
|
|
57
57
|
|
|
58
58
|
-h, --help Show this help
|
|
59
59
|
-v, --version Show version
|
package/src/config.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
|
-
import pkgJSON from '../package.json' with { type: 'json' }
|
|
3
2
|
|
|
4
3
|
import { logger } from './utils/logger.js'
|
|
5
4
|
import { isDirectory } from './utils/fs.js'
|
|
@@ -51,7 +50,9 @@ const schema = {
|
|
|
51
50
|
[/\.(js|ts)$/, jsToJsonPlugin]
|
|
52
51
|
], Array.isArray],
|
|
53
52
|
|
|
54
|
-
onReady: [await openInBrowser, is(Function)]
|
|
53
|
+
onReady: [await openInBrowser, is(Function)],
|
|
54
|
+
|
|
55
|
+
hotReload: [false, is(Boolean)]
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
|
|
@@ -69,26 +70,23 @@ export const config = Object.seal(defaults)
|
|
|
69
70
|
export const ConfigValidator = Object.freeze(validators)
|
|
70
71
|
|
|
71
72
|
|
|
72
|
-
/** @param {Partial<Config>}
|
|
73
|
-
export function setup(
|
|
74
|
-
if (
|
|
75
|
-
|
|
73
|
+
/** @param {Partial<Config>} opts */
|
|
74
|
+
export function setup(opts) {
|
|
75
|
+
if (opts.mocksDir)
|
|
76
|
+
opts.mocksDir = resolve(opts.mocksDir)
|
|
76
77
|
|
|
77
|
-
if (
|
|
78
|
-
|
|
78
|
+
if (opts.staticDir)
|
|
79
|
+
opts.staticDir = resolve(opts.staticDir)
|
|
79
80
|
else if (!isDirectory(defaults.staticDir))
|
|
80
|
-
|
|
81
|
+
opts.staticDir = ''
|
|
81
82
|
|
|
82
|
-
Object.assign(config,
|
|
83
|
+
Object.assign(config, opts)
|
|
83
84
|
validate(config, ConfigValidator)
|
|
84
85
|
logger.setLevel(config.logLevel)
|
|
85
|
-
config.extraHeaders.push('Server', `Mockaton ${pkgJSON.version}`)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
89
88
|
export const isFileAllowed = f => !config.ignore.test(f)
|
|
90
89
|
|
|
91
90
|
export const calcDelay = () => config.delayJitter
|
|
92
91
|
? config.delay * (1 + Math.random() * config.delayJitter)
|
|
93
92
|
: config.delay
|
|
94
|
-
|
|
@@ -70,10 +70,8 @@ function filenameIsValid(file) {
|
|
|
70
70
|
|
|
71
71
|
export function unregisterMock(file) {
|
|
72
72
|
const broker = brokerByFilename(file)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const isEmpty = broker.unregister(file)
|
|
76
|
-
if (isEmpty) {
|
|
73
|
+
const hasNoMoreMocks = broker?.unregister(file)
|
|
74
|
+
if (hasNoMoreMocks) {
|
|
77
75
|
const { method, urlMask } = parseFilename(file)
|
|
78
76
|
delete collection[method][urlMask]
|
|
79
77
|
if (!Object.keys(collection[method]).length)
|
|
@@ -96,9 +94,7 @@ export function brokerByFilename(file) {
|
|
|
96
94
|
* worry about the primacy of array-like keys when iterating.
|
|
97
95
|
@returns {MockBroker | undefined} */
|
|
98
96
|
export function brokerByRoute(method, url) {
|
|
99
|
-
|
|
100
|
-
return
|
|
101
|
-
const brokers = Object.values(collection[method])
|
|
97
|
+
const brokers = Object.values(collection[method] || {})
|
|
102
98
|
for (let i = brokers.length - 1; i >= 0; i--)
|
|
103
99
|
if (brokers[i].urlMaskMatches(url))
|
|
104
100
|
return brokers[i]
|
package/src/staticCollection.js
CHANGED
|
@@ -29,7 +29,7 @@ export function init() {
|
|
|
29
29
|
|
|
30
30
|
/** @returns {boolean} registered */
|
|
31
31
|
export function registerMock(relativeFile) {
|
|
32
|
-
if (!isFileAllowed(basename(relativeFile)))
|
|
32
|
+
if (!isFileAllowed(basename(relativeFile)))
|
|
33
33
|
return false
|
|
34
34
|
|
|
35
35
|
const route = '/' + relativeFile
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import fs, { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
2
3
|
import { logger } from './logger.js'
|
|
3
4
|
import { mimeFor } from './mime.js'
|
|
4
5
|
import { HEADER_502 } from '../ApiConstants.js'
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
export function setHeaders(response, headers) {
|
|
9
|
+
for (let i = 0; i < headers.length; i += 2)
|
|
10
|
+
response.setHeader(headers[i], headers[i + 1])
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
export function sendOK(response) {
|
|
8
14
|
logger.access(response)
|
|
9
15
|
response.end()
|
package/Makefile
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
docker: docker-build docker-run
|
|
2
|
-
|
|
3
|
-
docker-build:
|
|
4
|
-
@docker build --no-cache --tag mockaton $(PWD)
|
|
5
|
-
|
|
6
|
-
docker-run: docker-stop
|
|
7
|
-
@docker run --name mockaton \
|
|
8
|
-
--publish 127.0.0.1:2020:2020 \
|
|
9
|
-
--volume $(PWD)/mockaton.config.js:/app/mockaton.config.js \
|
|
10
|
-
--volume $(PWD)/mockaton-mocks:/app/mockaton-mocks \
|
|
11
|
-
--volume $(PWD)/mockaton-static-mocks:/app/mockaton-static-mocks \
|
|
12
|
-
mockaton
|
|
13
|
-
|
|
14
|
-
docker-stop:
|
|
15
|
-
@docker stop mockaton >/dev/null 2>&1 || true
|
|
16
|
-
@docker rm mockaton >/dev/null 2>&1 || true
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
start:
|
|
20
|
-
@node src/cli.js
|
|
21
|
-
|
|
22
|
-
watch:
|
|
23
|
-
@node --watch src/cli.js
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
test:
|
|
27
|
-
@MOCKATON_WATCHER_DEBOUNCE_MS=0 node --test 'src/**/*.test.js'
|
|
28
|
-
|
|
29
|
-
test-docker:
|
|
30
|
-
@docker run --rm --interactive --tty \
|
|
31
|
-
--volume $(PWD):/app \
|
|
32
|
-
--workdir /app \
|
|
33
|
-
node:24 \
|
|
34
|
-
make test
|
|
35
|
-
|
|
36
|
-
coverage:
|
|
37
|
-
@MOCKATON_WATCHER_DEBOUNCE_MS=0 node --test --experimental-test-coverage \
|
|
38
|
-
--test-reporter=spec --test-reporter-destination=stdout \
|
|
39
|
-
--test-reporter=lcov --test-reporter-destination=lcov.info \
|
|
40
|
-
'src/**/*.test.js'
|
|
41
|
-
|
|
42
|
-
pixaton:
|
|
43
|
-
@node --test --experimental-test-isolation=none \
|
|
44
|
-
--import=./pixaton-tests/_setup.js \
|
|
45
|
-
'pixaton-tests/**/*.test.js'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
outdated:
|
|
49
|
-
@npm outdated --parseable |\
|
|
50
|
-
awk -F: '{ printf "npm i %-30s ;# %s\n", $$4, $$2 }'
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.PHONY: *
|