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 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
  ![NPM Version](https://img.shields.io/npm/v/mockaton)
4
- ![NPM Version](https://img.shields.io/npm/l/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
  [![CodeQL](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/ericfortis/mockaton/actions/workflows/github-code-scanning/codeql)
7
6
  [![codecov](https://codecov.io/github/ericfortis/mockaton/graph/badge.svg?token=90NYLMMG1J)](https://codecov.io/github/ericfortis/mockaton)
8
7
 
9
- An HTTP mock server for simulating APIs with minimal setup
10
- &mdash; ideal for testing difficult to reproduce backend states.
8
+ An HTTP mock server for simulating APIs with minimal setup &mdash; 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>/[company-id].GET.200.json</code>
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 mock directories included in this repo:
28
- [mockaton-mocks/](./mockaton-mocks) and [mockaton-static-mocks/](./mockaton-static-mocks)
29
- mounted on the container.
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, and they
101
- get saved following Mockaton’s filename convention.
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
- - Ideal for setting up screenshot tests, e.g., with [pixaton](https://github.com/ericfortis/pixaton)
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 start-standalone-demo
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, organized, and small. 4 KLoC (half is UI and tests)
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
- ## Usage Without Docker
194
+ _For Docker, see the Quick-Start section above._
177
195
 
178
- Requires Node.js. **v22.18+** support writing mocks in TypeScript.
196
+ Requires Node.js **v22.18+**, which supports TypeScript mocks.
179
197
 
180
- 1. Create a mock in the default mocks directory (`./mockaton-mocks`)
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 (noops onReady callback)
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
- <details>
234
- <summary>Defaults Overview… </summary>
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
- </details>
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. In other words, responses of routes with
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
- In other words, it’s useful only if you care about its payload.
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 mocks need **Node 22.18+ or 23.6+**_
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
- In other words, mocks in `staticDir` take precedence over `mocksDir/*`.
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. In other words, it’s only used for
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
@@ -43,6 +43,8 @@ export interface Config {
43
43
  plugins?: [filenameTester: RegExp, plugin: Plugin][]
44
44
 
45
45
  onReady?: (address: string) => void
46
+
47
+ hotReload?: boolean // For UI dev purposes only
46
48
  }
47
49
 
48
50
 
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
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "11.2.0",
5
+ "version": "11.2.2",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
8
8
  ".": {
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
- 'Logo.svg',
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
 
@@ -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
@@ -132,7 +132,7 @@ header {
132
132
  border-bottom: 1px solid var(--colorSecondaryActionBorder);
133
133
  background: var(--colorHeaderBackground);
134
134
 
135
- > img {
135
+ > object {
136
136
  align-self: end;
137
137
  margin-right: 22px;
138
138
  margin-bottom: 5px;
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('img', {
100
- alt: t`Mockaton`,
101
- src: 'Logo.svg',
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
- SyntaxJSON.regex.lastIndex = 0 // resets regex
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
- SyntaxXML.regex.lastIndex = 0
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
+ }
@@ -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 with node:http.METHODS
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>:root { --color: #000000; }
3
+ <style>
4
4
  @media (prefers-color-scheme: light) { :root { --color: #333 } }
5
5
  @media (prefers-color-scheme: dark) { :root { --color: #eee } }
6
- path { fill: var(--color) }
6
+ path {
7
+ fill: #777;
8
+ fill: var(--color);
9
+ }
7
10
  </style>
8
- <g stroke-opacity=".99608">
9
- <path
10
- 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"/>
11
- <path
12
- 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"
13
- opacity="0.75"/>
14
- <path
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(file => includesComment(file, DEFAULT_MOCK_COMMENT))
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.proxied = false
59
- if (this.auto500 || this.status === 500)
58
+ const shouldUnset = this.auto500 || this.status === 500
59
+ if (shouldUnset)
60
60
  this.selectDefaultFile()
61
61
  else {
62
- const f = this.mocks.find(this.#is500) // TESTME
63
- if (f)
64
- this.selectFile(f)
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 // TESTME
67
+ this.status = 500
68
68
  }
69
69
  }
70
+ this.proxied = false
70
71
  }
71
72
 
72
73
  setDelayed(delayed) {
@@ -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 ? 500 : broker.status // TESTME plugins can change it
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(() => response.end(isHead ? null : body),
44
- Number(broker.delayed && calcDelay()))
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 (Buffer.isBuffer(body)) return body.length
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 { sendNoContent, sendInternalServerError, sendUnprocessable, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
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
- for (let i = 0; i < config.extraHeaders.length; i += 2)
46
- response.setHeader(config.extraHeaders[i], config.extraHeaders[i + 1])
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))) { // TESTME
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() { // TESTME
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 (noops onReady callback)
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>} options */
73
- export function setup(options) {
74
- if (options.mocksDir)
75
- options.mocksDir = resolve(options.mocksDir)
73
+ /** @param {Partial<Config>} opts */
74
+ export function setup(opts) {
75
+ if (opts.mocksDir)
76
+ opts.mocksDir = resolve(opts.mocksDir)
76
77
 
77
- if (options.staticDir)
78
- options.staticDir = resolve(options.staticDir)
78
+ if (opts.staticDir)
79
+ opts.staticDir = resolve(opts.staticDir)
79
80
  else if (!isDirectory(defaults.staticDir))
80
- options.staticDir = ''
81
+ opts.staticDir = ''
81
82
 
82
- Object.assign(config, options)
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
- if (!broker) // TESTME
74
- return
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
- if (!collection[method])
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]
@@ -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))) // TESTME
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: *