mockaton 13.11.1 → 13.11.3

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 (35) hide show
  1. package/README.md +11 -8
  2. package/package.json +1 -1
  3. package/skills/mockaton/SKILL.md +8 -5
  4. package/src/client/ApiConstants.js +1 -1
  5. package/src/client/IndexHtml.js +1 -1
  6. package/src/client/app-header.css +231 -0
  7. package/src/client/app-header.js +13 -6
  8. package/src/client/app-mock-list.css +340 -0
  9. package/src/client/app-mock-list.js +370 -0
  10. package/src/client/app-payload-viewer.css +90 -0
  11. package/src/client/app-payload-viewer.js +4 -4
  12. package/src/client/app-store.js +1 -0
  13. package/src/client/app.css +0 -654
  14. package/src/client/app.js +18 -360
  15. package/src/client/utils/css.js +7 -0
  16. package/src/client/utils/watcherDev.js +4 -1
  17. package/src/server/Api.js +54 -53
  18. package/src/server/MockDispatcher.js +6 -5
  19. package/src/server/Mockaton.js +11 -12
  20. package/src/server/Mockaton.test.js +65 -51
  21. package/src/server/ProxyRelay.js +3 -2
  22. package/src/server/UrlParsers.js +1 -1
  23. package/src/server/UrlParsers.test.js +1 -1
  24. package/src/server/cli.js +37 -33
  25. package/src/server/cli.test.js +2 -4
  26. package/src/server/{Watcher.js → stores/Watcher.js} +18 -9
  27. package/src/server/{mockBrokersCollection.js → stores/brokers.js} +3 -3
  28. package/src/server/{config.js → stores/config.js} +28 -14
  29. package/src/server/utils/HttpServerResponse.test.js +5 -7
  30. package/src/server/utils/WatcherDevClient.js +1 -1
  31. package/src/server/utils/openInBrowser.js +15 -10
  32. package/www/src/assets/openapi.json +1 -1
  33. /package/src/server/{resolverBypassImportCache.js → ResolverBypassImportCache.js} +0 -0
  34. /package/src/server/{resolverResolveExtensionless.js → ResolverResolveExtensionless.js} +0 -0
  35. /package/src/server/{cookie.js → stores/cookies.js} +0 -0
package/src/server/Api.js CHANGED
@@ -3,36 +3,36 @@
3
3
  * selecting a specific mock-file for a particular route.
4
4
  */
5
5
 
6
- import { join } from 'node:path'
7
- import { write, rm, isFile, resolveIn, listFilesRecursively } from './utils/fs.js'
8
-
9
- import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
10
- import pkgJSON from '../../package.json' with { type: 'json' }
6
+ import { join, relative } from 'node:path'
11
7
 
8
+ import { write, rm, isFile, resolveIn } from './utils/fs.js'
9
+ import { removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
12
10
  import { sseClientHotReload } from './utils/WatcherDevClient.js'
13
- import { stopMocksDirWatcher, sseClientSyncVersion, uiSyncVersion, watchMocksDir } from './Watcher.js'
11
+
12
+ import pkgJSON from '../../package.json' with { type: 'json' }
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')
25
25
 
26
- const getReqs = new Map([
27
- [API.dashboard, serveDashboard],
26
+ const headReqs = new Map([
27
+ [API.health, (_, response) => response.ok()]
28
+ ])
28
29
 
29
- ...listFilesRecursively(CLIENT_ASSETS).map(f => [
30
- API.dashboard + '/' + f,
31
- serveDashboardAsset(f)
32
- ]),
30
+ const getReqs = new Map([
31
+ ...headReqs.entries(),
33
32
 
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)],
@@ -62,20 +62,25 @@ const patchReqs = new Map([
62
62
  ])
63
63
 
64
64
  export async function handleApiRequest(req, response) {
65
- const url = removeQueryStringAndFragment(req.url)
65
+ if (!req.url.startsWith(API.root))
66
+ return false
66
67
 
67
- if ((req.method === 'GET' || req.method === 'HEAD') && url === API.health) {
68
- response.ok()
69
- return
70
- }
68
+ const url = removeQueryStringAndFragment(req.url)
71
69
 
72
70
  const handler = (
73
71
  req.method === 'GET' && getReqs.get(url) ||
72
+ req.method === 'HEAD' && headReqs.get(url) ||
74
73
  req.method === 'PATCH' && patchReqs.get(url))
75
74
  if (handler) {
76
75
  await handler(req, response)
77
76
  return true
78
77
  }
78
+
79
+ if (req.method === 'GET') { // serve static dashboard assets dir
80
+ const f = await resolveIn(CLIENT_ASSETS, relative(API.root, url))
81
+ await response.file(f)
82
+ return true
83
+ }
79
84
  }
80
85
 
81
86
 
@@ -85,18 +90,13 @@ function serveDashboard(_, response) {
85
90
  response.html(IndexHtml(config.hotReload, pkgJSON.version), CSP)
86
91
  }
87
92
 
88
- function serveDashboardAsset(f) {
89
- return async (_, response) => {
90
- await response.file(join(CLIENT_ASSETS, f))
91
- }
92
- }
93
93
 
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,7 +263,7 @@ 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
 
@@ -282,8 +283,8 @@ async function writeMock(req, response) {
282
283
  await write(path, content)
283
284
 
284
285
  if (!config.watcherEnabled) {
285
- mockBrokersCollection.registerMock(file, true)
286
- uiSyncVersion.increment()
286
+ brokers.registerMock(file, true)
287
+ Watcher.emitChange()
287
288
  }
288
289
  response.ok()
289
290
  }
@@ -305,8 +306,8 @@ async function deleteMock(req, response) {
305
306
  await rm(path)
306
307
 
307
308
  if (!config.watcherEnabled) {
308
- mockBrokersCollection.unregisterMock(file)
309
- uiSyncVersion.increment()
309
+ brokers.unregisterMock(file)
310
+ Watcher.emitChange()
310
311
  }
311
312
  response.ok()
312
313
  }
@@ -319,9 +320,9 @@ async function setWatchMocks(req, response) {
319
320
  response.unprocessable(`Expected boolean for "watchMocks"`)
320
321
  else {
321
322
  if (enabled)
322
- watchMocksDir()
323
+ Watcher.watchMocksDir()
323
324
  else
324
- stopMocksDirWatcher()
325
+ Watcher.unwatchMocksDir()
325
326
  response.ok()
326
327
  }
327
328
  }
@@ -1,12 +1,13 @@
1
1
  import { join } from 'node:path'
2
2
 
3
- import { logger } from './utils/logger.js'
4
3
  import { proxy } from './ProxyRelay.js'
5
- import { cookie } from './cookie.js'
6
- import { parseFilename } from '../client/Filename.js'
4
+ import { cookie } from './stores/cookies.js'
7
5
  import { echoFilePlugin } from './MockDispatcherPlugins.js'
8
- import { brokerByRoute } from './mockBrokersCollection.js'
9
- import { config, calcDelay } from './config.js'
6
+ import { brokerByRoute } from './stores/brokers.js'
7
+ import { config, calcDelay } from './stores/config.js'
8
+
9
+ import { logger } from './utils/logger.js'
10
+ import { parseFilename } from '../client/Filename.js'
10
11
 
11
12
 
12
13
  export async function dispatchMock(req, response) {
@@ -7,29 +7,28 @@ import { logger } from './utils/logger.js'
7
7
  import { ServerResponse } from './utils/HttpServerResponse.js'
8
8
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
9
9
  import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
10
+ import { watchDevSPA } from './utils/WatcherDevClient.js'
10
11
 
11
12
  import { API } from '../client/ApiConstants.js'
12
- import { cookie } from './cookie.js'
13
- import { config, setup } from './config.js'
14
- import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
15
13
 
14
+ import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
15
+ import { cookie } from './stores/cookies.js'
16
+ import { config, initConfig } from './stores/config.js'
17
+ import * as brokers from './stores/brokers.js'
16
18
  import { dispatchMock } from './MockDispatcher.js'
17
- import * as mockBrokerCollection from './mockBrokersCollection.js'
18
-
19
- import { watchDevSPA } from './utils/WatcherDevClient.js'
20
- import { watchMocksDir } from './Watcher.js'
19
+ import { watchMocksDir } from './stores/Watcher.js'
21
20
 
22
21
 
23
22
  export function Mockaton(options) {
24
23
  return new Promise((resolve, reject) => {
25
- setup(options)
24
+ initConfig(options)
26
25
  cookie.init(config.cookies)
27
- mockBrokerCollection.init()
26
+ brokers.init()
28
27
 
29
- register('./resolverResolveExtensionless.js', import.meta.url)
28
+ register('./ResolverResolveExtensionless.js', import.meta.url)
30
29
 
31
30
  if (config.bypassImportCache)
32
- register('./resolverBypassImportCache.js', import.meta.url)
31
+ register('./ResolverBypassImportCache.js', import.meta.url)
33
32
 
34
33
  if (config.watcherEnabled)
35
34
  watchMocksDir()
@@ -41,7 +40,7 @@ export function Mockaton(options) {
41
40
  server.on('error', reject)
42
41
  server.listen(config.port, config.host, () => {
43
42
  const url = `http://${server.address().address}:${server.address().port}`
44
- const dashboardUrl = url + API.dashboard
43
+ const dashboardUrl = url + API.root
45
44
  logger.info('Listening', url)
46
45
  logger.info('Dashboard', dashboardUrl)
47
46
  config.onReady(dashboardUrl)
@@ -1,3 +1,4 @@
1
+ import fs from 'node:fs/promises'
1
2
  import { join } from 'node:path'
2
3
  import { spawn } from 'node:child_process'
3
4
  import { tmpdir } from 'node:os'
@@ -7,22 +8,32 @@ import { mkdtempSync } from 'node:fs'
7
8
  import { randomUUID } from 'node:crypto'
8
9
  import { equal, deepEqual, match } from 'node:assert/strict'
9
10
  import { describe, test, before, beforeEach, after } from 'node:test'
10
- import { unlink, mkdir, readFile, rename, readdir, writeFile, rm } from 'node:fs/promises'
11
11
 
12
+ import CONFIG from './Mockaton.test.config.js'
13
+ import { config } from './stores/config.js'
12
14
  import { mimeFor } from './utils/mime.js'
15
+ import { API } from '../client/ApiConstants.js'
16
+ import { Commander } from '../client/ApiCommander.js'
13
17
  import { parseFilename } from '../client/Filename.js'
14
- import { API, Commander } from '../../index.js'
15
18
 
16
- import CONFIG from './Mockaton.test.config.js'
17
- import { config } from './config.js'
18
19
 
20
+ const mocksDir = new class {
21
+ value = mkdtempSync(join(tmpdir(), 'mocks'))
22
+
23
+ rm = f => fs.unlink(join(this.value, f))
24
+ read = f => fs.readFile(join(this.value, f), 'utf8')
25
+ write = (f, data) => fs.writeFile(join(this.value, f), data)
26
+ rename = (src, target) => fs.rename(join(this.value, src), join(this.value, target))
19
27
 
20
- const mocksDir = mkdtempSync(join(tmpdir(), 'mocks'))
28
+ list = d => fs.readdir(join(this.value, d))
29
+ rmdir = d => fs.rm(join(this.value, d), { recursive: true })
30
+ mkdir = d => fs.mkdir(join(this.value, d), { recursive: true })
31
+ }
21
32
 
22
33
  const stdout = []
23
34
  const stderr = []
24
35
  const proc = spawn(join(import.meta.dirname, 'cli.js'), [
25
- mocksDir,
36
+ mocksDir.value,
26
37
  '--config', join(import.meta.dirname, 'Mockaton.test.config.js'),
27
38
  '--no-open'
28
39
  ])
@@ -47,17 +58,6 @@ const serverAddr = await new Promise((resolve, reject) => {
47
58
 
48
59
  after(() => proc.kill('SIGUSR2'))
49
60
 
50
-
51
- const rmFromMocksDir = f => unlink(join(mocksDir, f))
52
- const readFromMocksDir = f => readFile(join(mocksDir, f), 'utf8')
53
- const writeInMocksDir = (f, data) => writeFile(join(mocksDir, f), data)
54
- const renameInMocksDir = (src, target) => rename(join(mocksDir, src), join(mocksDir, target))
55
-
56
- const listFromMocksDir = d => readdir(join(mocksDir, d))
57
- const rmDirFromMocks = d => rm(join(mocksDir, d), { recursive: true })
58
- const makeDirInMocks = dir => mkdir(join(mocksDir, dir), { recursive: true })
59
-
60
-
61
61
  const api = new Commander(serverAddr)
62
62
 
63
63
 
@@ -224,17 +224,17 @@ describe('CORS', () => {
224
224
 
225
225
  describe('Dashboard', () => {
226
226
  test('renders', async () => {
227
- const r = await request(API.dashboard)
227
+ const r = await request(API.root)
228
228
  match(await r.text(), new RegExp('<!DOCTYPE html>'))
229
229
  })
230
230
 
231
231
  test('query string is accepted', async () => {
232
- const r = await request(API.dashboard + '?foo=bar')
232
+ const r = await request(API.root + '?foo=bar')
233
233
  match(await r.text(), new RegExp('<!DOCTYPE html>'))
234
234
  })
235
235
 
236
236
  test('serves assets', async () => {
237
- const r = await request(API.dashboard + '/app.css')
237
+ const r = await request(API.root + '/app.css')
238
238
  match(await r.text(), new RegExp(':root {'))
239
239
  })
240
240
  })
@@ -303,6 +303,7 @@ describe('Delay', () => {
303
303
  const r = await api.setGlobalDelayJitter(0.1)
304
304
  equal(r.status, 200)
305
305
  equal((await fetchState()).delayJitter, 0.1)
306
+ await api.setGlobalDelayJitter(0)
306
307
  })
307
308
  })
308
309
 
@@ -388,14 +389,14 @@ describe('Proxy Fallback', () => {
388
389
  deepEqual(await r2.json(), BODY_PAYLOAD)
389
390
  deepEqual(await r1.json(), BODY_PAYLOAD)
390
391
 
391
- const savedMocks = await listFromMocksDir('non-existing-mock')
392
+ const savedMocks = await mocksDir.list('non-existing-mock')
392
393
  equal(savedMocks.length, 2)
393
394
 
394
- equal(await readFromMocksDir('non-existing-mock/[id].POST.423.json'), expectedBody)
395
+ equal(await mocksDir.read('non-existing-mock/[id].POST.423.json'), expectedBody)
395
396
  for (const m of savedMocks) {
396
397
  const f = join('non-existing-mock', m)
397
- equal(await readFromMocksDir(f), expectedBody)
398
- await rmFromMocksDir(f)
398
+ equal(await mocksDir.read(f), expectedBody)
399
+ await mocksDir.rm(f)
399
400
  }
400
401
  })
401
402
  })
@@ -919,7 +920,7 @@ describe('Dynamic Params', () => {
919
920
  const fx2 = new Fixture('dynamic-params/[id]/suffix/[id].GET.200.txt')
920
921
  const fx3 = new Fixture('dynamic-params/exact-route.GET.200.txt')
921
922
  before(async () => {
922
- await makeDirInMocks('dynamic-params/[id]/suffix/[id]')
923
+ await mocksDir.mkdir('dynamic-params/[id]/suffix/[id]')
923
924
  await fx0.write()
924
925
  await fx1.write()
925
926
  await fx2.write()
@@ -955,7 +956,7 @@ describe('Dynamic Params', () => {
955
956
 
956
957
  test('Dynamic Params on partial segments', async () => {
957
958
  const fx = new Fixture('dynamic-params-partial-[id]/foo.GET.200.txt')
958
- await makeDirInMocks('dynamic-params-partial-[id]')
959
+ await mocksDir.mkdir('dynamic-params-partial-[id]')
959
960
  await fx.write()
960
961
  const r = await request('/dynamic-params-partial-999/foo')
961
962
  equal(await r.text(), fx.body)
@@ -967,7 +968,7 @@ describe('Query String', () => {
967
968
  const fx0 = new Fixture('query-string?foo=[foo]&bar=[bar].GET.200.json')
968
969
  const fx1 = new Fixture('query-string/[id]?limit=[limit].GET.200.json')
969
970
  before(async () => {
970
- await makeDirInMocks('query-string')
971
+ await mocksDir.mkdir('query-string')
971
972
  await fx0.write()
972
973
  await fx1.write()
973
974
  await api.reset()
@@ -1126,10 +1127,10 @@ describe('Registering Mocks', () => {
1126
1127
  test('when watcher is off, newly added mocks do not get registered', async () => {
1127
1128
  await api.setWatchMocks(false)
1128
1129
  const fx = new FixtureExternal('non-auto-registered-file.GET.200.json')
1129
- await writeInMocksDir(fx.file, fx.body)
1130
+ await mocksDir.write(fx.file, fx.body)
1130
1131
  await sleep(100)
1131
1132
  equal(await fx.fetchBroker(), undefined)
1132
- await rmFromMocksDir(fx.file)
1133
+ await mocksDir.rm(fx.file)
1133
1134
  })
1134
1135
 
1135
1136
  test('register', async () => {
@@ -1175,27 +1176,27 @@ describe('Registering Mocks', () => {
1175
1176
  const fx0 = new FixtureExternal('reg0/runtime0.GET.200.txt')
1176
1177
  let version
1177
1178
  before(async () => {
1178
- await makeDirInMocks('reg0')
1179
- await writeInMocksDir(fx0.file, fx0.body)
1179
+ await mocksDir.mkdir('reg0')
1180
+ await mocksDir.write(fx0.file, fx0.body)
1180
1181
  version = await resolveOnNextSyncVersion(-1)
1181
1182
  })
1182
1183
 
1183
1184
  const fx = new FixtureExternal('runtime1.GET.200.txt')
1184
1185
  test('responds when a file is added', async () => {
1185
1186
  const prom = resolveOnNextSyncVersion(version)
1186
- await writeInMocksDir(fx.file, fx.body)
1187
+ await mocksDir.write(fx.file, fx.body)
1187
1188
  equal(await prom, version + 1)
1188
1189
  })
1189
1190
 
1190
1191
  test('responds when a file is deleted', async () => {
1191
1192
  const prom = resolveOnNextSyncVersion(version + 1)
1192
- await rmFromMocksDir(fx.file)
1193
+ await mocksDir.rm(fx.file)
1193
1194
  equal(await prom, version + 2)
1194
1195
  })
1195
1196
 
1196
1197
  test('responds when dir is renamed', async () => {
1197
1198
  const prom = resolveOnNextSyncVersion(version + 2)
1198
- await renameInMocksDir('reg0', 'reg1')
1199
+ await mocksDir.rename('reg0', 'reg1')
1199
1200
  equal(await prom, version + 3)
1200
1201
 
1201
1202
  const s = await fetchState()
@@ -1208,7 +1209,7 @@ describe('Registering Mocks', () => {
1208
1209
  await fx.writeExternally()
1209
1210
  config.watcherDebounceMs = 100 // Because on macOS rmdir triggers a few events
1210
1211
  const nextVerPromise = resolveOnNextSyncVersion()
1211
- await rmDirFromMocks('api/bulk-delete')
1212
+ await mocksDir.rmdir('api/bulk-delete')
1212
1213
  await nextVerPromise
1213
1214
  equal(await fx.fetchBroker(), undefined)
1214
1215
  await sleep(50) // Only for Docker, not sure why we need to delay the server teardown
@@ -1220,24 +1221,37 @@ describe('Registering Mocks', () => {
1220
1221
  * This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed. */
1221
1222
  async function resolveOnNextSyncVersion(currSyncVer = undefined) {
1222
1223
  let skipFirst = currSyncVer === undefined
1223
- const response = await api.getSyncVersion()
1224
- const stream = response.body.pipeThrough(new TextDecoderStream())
1224
+ const reader = (await api.getSyncVersion())
1225
+ .body.pipeThrough(new TextDecoderStream())
1226
+ .getReader()
1225
1227
  let buffer = ''
1226
1228
 
1227
- for await (const chunk of stream) {
1228
- buffer += chunk
1229
- const parts = buffer.split('\n\n')
1230
- buffer = parts.pop() || ''
1231
-
1232
- for (const event of parts)
1233
- for (const line of event.split(/\r?\n/))
1234
- if (line.startsWith('data:')) {
1235
- const v = Number(line.slice(5).trim())
1236
- if (skipFirst || v === currSyncVer)
1237
- skipFirst = false
1238
- else
1239
- return v
1240
- }
1229
+ try {
1230
+ while (true) {
1231
+ try {
1232
+ const { done, value } = await reader.read()
1233
+ if (done) break
1234
+ buffer += value
1235
+ }
1236
+ catch {
1237
+ break
1238
+ }
1239
+ const parts = buffer.split('\n\n')
1240
+ buffer = parts.pop() || ''
1241
+
1242
+ for (const event of parts)
1243
+ for (const line of event.split(/\r?\n/))
1244
+ if (line.startsWith('data:')) {
1245
+ const v = Number(line.slice(5).trim())
1246
+ if (skipFirst || v === currSyncVer)
1247
+ skipFirst = false
1248
+ else
1249
+ return v
1250
+ }
1251
+ }
1252
+ }
1253
+ finally {
1254
+ reader.cancel().catch(() => {})
1241
1255
  }
1242
1256
  }
1243
1257
 
@@ -4,12 +4,13 @@ import { randomUUID } from 'node:crypto'
4
4
  import { extFor } from './utils/mime.js'
5
5
  import { write, isFile, resolveIn } from './utils/fs.js'
6
6
  import { BodyReaderError } from './utils/HttpIncomingMessage.js'
7
-
8
- import { config } from './config.js'
9
7
  import { logger } from './utils/logger.js'
8
+
10
9
  import { makeMockFilename } from '../client/Filename.js'
11
10
  import { EXT_EMPTY, EXT_UNKNOWN_MIME } from '../client/ApiConstants.js'
12
11
 
12
+ import { config } from './stores/config.js'
13
+
13
14
 
14
15
  export async function proxy(req, response, delay) {
15
16
  let proxyResponse
@@ -1,5 +1,5 @@
1
1
  import { relative } from 'node:path'
2
- import { config } from './config.js'
2
+ import { config } from './stores/config.js'
3
3
  import { decode, removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
4
4
  import { parseFilename, removeTrailingSlash } from '../client/Filename.js'
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { test, describe } from 'node:test'
2
2
  import { deepEqual, equal } from 'node:assert/strict'
3
3
  import { parseSegments, parseQueryParams } from './UrlParsers.js'
4
- import { config } from './config.js'
4
+ import { config } from './stores/config.js'
5
5
 
6
6
  test('parseQueryParams', () => {
7
7
  const searchParams = parseQueryParams('/api/foo?limit=123')