mockaton 13.11.2 → 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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.11.2",
5
+ "version": "13.11.3",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
package/src/server/Api.js CHANGED
@@ -4,21 +4,21 @@
4
4
  */
5
5
 
6
6
  import { join, relative } from 'node:path'
7
+
7
8
  import { write, rm, isFile, resolveIn } from './utils/fs.js'
9
+ import { removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
10
+ import { sseClientHotReload } from './utils/WatcherDevClient.js'
8
11
 
9
- import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
10
12
  import pkgJSON from '../../package.json' with { type: 'json' }
11
-
12
- import { sseClientHotReload } from './utils/WatcherDevClient.js'
13
- import { stopMocksDirWatcher, sseClientSyncVersion, uiSyncVersion, watchMocksDir } from './Watcher.js'
13
+ import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
14
14
 
15
15
  import { API } from '../client/ApiConstants.js'
16
16
  import { IndexHtml, CSP } from '../client/IndexHtml.js'
17
17
 
18
- import { cookie } from './cookie.js'
19
- import { config, ConfigValidator } from './config.js'
20
- import * as mockBrokersCollection from './mockBrokersCollection.js'
21
- import { removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
18
+ import { cookie } from './stores/cookies.js'
19
+ import { config, ConfigValidator, reinitConfig } from './stores/config.js'
20
+ import * as brokers from './stores/brokers.js'
21
+ import * as Watcher from './stores/Watcher.js'
22
22
 
23
23
 
24
24
  export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
@@ -32,7 +32,7 @@ const getReqs = new Map([
32
32
 
33
33
  [API.root, serveDashboard],
34
34
  [API.state, getState],
35
- [API.syncVersion, sseClientSyncVersion],
35
+ [API.syncVersion, Watcher.sseClientSyncVersion],
36
36
 
37
37
  [API.watchHotReload, onDevWatch],
38
38
  [API.openAPI, (_, response) => response.json(openapi)],
@@ -94,9 +94,9 @@ function serveDashboard(_, response) {
94
94
  function getState(_, response) {
95
95
  response.json({
96
96
  cookies: cookie.list(),
97
- comments: mockBrokersCollection.extractAllComments(),
97
+ comments: brokers.extractAllComments(),
98
98
 
99
- brokersByMethod: mockBrokersCollection.all(),
99
+ brokersByMethod: brokers.all(),
100
100
 
101
101
  delay: config.delay,
102
102
  delayJitter: config.delayJitter,
@@ -118,10 +118,11 @@ function onDevWatch(req, response) {
118
118
  /** # PATCH */
119
119
 
120
120
  function reset(_, response) {
121
- mockBrokersCollection.init()
121
+ reinitConfig()
122
+ brokers.init()
122
123
  cookie.init(config.cookies)
123
124
  response.ok()
124
- uiSyncVersion.increment()
125
+ Watcher.emitChange()
125
126
  }
126
127
 
127
128
 
@@ -133,7 +134,7 @@ async function setCorsAllowed(req, response) {
133
134
  else {
134
135
  config.corsAllowed = corsAllowed
135
136
  response.ok()
136
- uiSyncVersion.increment()
137
+ Watcher.emitChange()
137
138
  }
138
139
  }
139
140
 
@@ -146,7 +147,7 @@ async function setGlobalDelay(req, response) {
146
147
  else {
147
148
  config.delay = delay
148
149
  response.ok()
149
- uiSyncVersion.increment()
150
+ Watcher.emitChange()
150
151
  }
151
152
  }
152
153
 
@@ -158,7 +159,7 @@ async function setGlobalDelayJitter(req, response) {
158
159
  else {
159
160
  config.delayJitter = jitter
160
161
  response.ok()
161
- uiSyncVersion.increment()
162
+ Watcher.emitChange()
162
163
  }
163
164
  }
164
165
 
@@ -170,7 +171,7 @@ async function selectCookie(req, response) {
170
171
  response.unprocessable(error?.message || error)
171
172
  else {
172
173
  response.json(cookie.list())
173
- uiSyncVersion.increment()
174
+ Watcher.emitChange()
174
175
  }
175
176
  }
176
177
 
@@ -183,7 +184,7 @@ async function setProxyFallback(req, response) {
183
184
  else {
184
185
  config.proxyFallback = fallback
185
186
  response.ok()
186
- uiSyncVersion.increment()
187
+ Watcher.emitChange()
187
188
  }
188
189
  }
189
190
 
@@ -195,7 +196,7 @@ async function setCollectProxied(req, response) {
195
196
  else {
196
197
  config.collectProxied = collectProxied
197
198
  response.ok()
198
- uiSyncVersion.increment()
199
+ Watcher.emitChange()
199
200
  }
200
201
  }
201
202
 
@@ -203,41 +204,41 @@ async function setCollectProxied(req, response) {
203
204
 
204
205
  async function bulkUpdateBrokersByCommentTag(req, response) {
205
206
  const comment = await req.json()
206
- mockBrokersCollection.setMocksMatchingComment(comment)
207
+ brokers.setMocksMatchingComment(comment)
207
208
  response.ok()
208
- uiSyncVersion.increment()
209
+ Watcher.emitChange()
209
210
  }
210
211
 
211
212
 
212
213
  async function selectMock(req, response) {
213
214
  const file = await req.json()
214
- const broker = mockBrokersCollection.brokerByFilename(file)
215
+ const broker = brokers.brokerByFilename(file)
215
216
  if (!broker || !broker.hasMock(file))
216
217
  response.unprocessable(`Missing Mock: ${file}`)
217
218
  else {
218
219
  broker.selectFile(file)
219
220
  response.json(broker)
220
- uiSyncVersion.increment()
221
+ Watcher.emitChange()
221
222
  }
222
223
  }
223
224
 
224
225
 
225
226
  async function toggleRouteStatus(req, response) {
226
227
  const [method, urlMask, status] = await req.json()
227
- const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
228
+ const broker = brokers.brokerByRoute(method, urlMask)
228
229
  if (!broker)
229
230
  response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
230
231
  else {
231
232
  broker.toggleStatus(status)
232
233
  response.json(broker)
233
- uiSyncVersion.increment()
234
+ Watcher.emitChange()
234
235
  }
235
236
  }
236
237
 
237
238
 
238
239
  async function setRouteIsDelayed(req, response) {
239
240
  const [method, urlMask, delayed] = await req.json()
240
- const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
241
+ const broker = brokers.brokerByRoute(method, urlMask)
241
242
  if (!broker)
242
243
  response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
243
244
  else if (typeof delayed !== 'boolean')
@@ -245,14 +246,14 @@ async function setRouteIsDelayed(req, response) {
245
246
  else {
246
247
  broker.setDelayed(delayed)
247
248
  response.json(broker)
248
- uiSyncVersion.increment()
249
+ Watcher.emitChange()
249
250
  }
250
251
  }
251
252
 
252
253
 
253
254
  async function setRouteIsProxied(req, response) {
254
255
  const [method, urlMask, proxied] = await req.json()
255
- const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
256
+ const broker = brokers.brokerByRoute(method, urlMask)
256
257
  if (!broker)
257
258
  response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
258
259
  else if (typeof proxied !== 'boolean')
@@ -262,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()
@@ -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,23 +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'
13
15
  import { API } from '../client/ApiConstants.js'
14
16
  import { Commander } from '../client/ApiCommander.js'
15
17
  import { parseFilename } from '../client/Filename.js'
16
18
 
17
- import CONFIG from './Mockaton.test.config.js'
18
- import { config } from './config.js'
19
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))
20
27
 
21
- 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
+ }
22
32
 
23
33
  const stdout = []
24
34
  const stderr = []
25
35
  const proc = spawn(join(import.meta.dirname, 'cli.js'), [
26
- mocksDir,
36
+ mocksDir.value,
27
37
  '--config', join(import.meta.dirname, 'Mockaton.test.config.js'),
28
38
  '--no-open'
29
39
  ])
@@ -48,17 +58,6 @@ const serverAddr = await new Promise((resolve, reject) => {
48
58
 
49
59
  after(() => proc.kill('SIGUSR2'))
50
60
 
51
-
52
- const rmFromMocksDir = f => unlink(join(mocksDir, f))
53
- const readFromMocksDir = f => readFile(join(mocksDir, f), 'utf8')
54
- const writeInMocksDir = (f, data) => writeFile(join(mocksDir, f), data)
55
- const renameInMocksDir = (src, target) => rename(join(mocksDir, src), join(mocksDir, target))
56
-
57
- const listFromMocksDir = d => readdir(join(mocksDir, d))
58
- const rmDirFromMocks = d => rm(join(mocksDir, d), { recursive: true })
59
- const makeDirInMocks = dir => mkdir(join(mocksDir, dir), { recursive: true })
60
-
61
-
62
61
  const api = new Commander(serverAddr)
63
62
 
64
63
 
@@ -304,6 +303,7 @@ describe('Delay', () => {
304
303
  const r = await api.setGlobalDelayJitter(0.1)
305
304
  equal(r.status, 200)
306
305
  equal((await fetchState()).delayJitter, 0.1)
306
+ await api.setGlobalDelayJitter(0)
307
307
  })
308
308
  })
309
309
 
@@ -389,14 +389,14 @@ describe('Proxy Fallback', () => {
389
389
  deepEqual(await r2.json(), BODY_PAYLOAD)
390
390
  deepEqual(await r1.json(), BODY_PAYLOAD)
391
391
 
392
- const savedMocks = await listFromMocksDir('non-existing-mock')
392
+ const savedMocks = await mocksDir.list('non-existing-mock')
393
393
  equal(savedMocks.length, 2)
394
394
 
395
- equal(await readFromMocksDir('non-existing-mock/[id].POST.423.json'), expectedBody)
395
+ equal(await mocksDir.read('non-existing-mock/[id].POST.423.json'), expectedBody)
396
396
  for (const m of savedMocks) {
397
397
  const f = join('non-existing-mock', m)
398
- equal(await readFromMocksDir(f), expectedBody)
399
- await rmFromMocksDir(f)
398
+ equal(await mocksDir.read(f), expectedBody)
399
+ await mocksDir.rm(f)
400
400
  }
401
401
  })
402
402
  })
@@ -920,7 +920,7 @@ describe('Dynamic Params', () => {
920
920
  const fx2 = new Fixture('dynamic-params/[id]/suffix/[id].GET.200.txt')
921
921
  const fx3 = new Fixture('dynamic-params/exact-route.GET.200.txt')
922
922
  before(async () => {
923
- await makeDirInMocks('dynamic-params/[id]/suffix/[id]')
923
+ await mocksDir.mkdir('dynamic-params/[id]/suffix/[id]')
924
924
  await fx0.write()
925
925
  await fx1.write()
926
926
  await fx2.write()
@@ -956,7 +956,7 @@ describe('Dynamic Params', () => {
956
956
 
957
957
  test('Dynamic Params on partial segments', async () => {
958
958
  const fx = new Fixture('dynamic-params-partial-[id]/foo.GET.200.txt')
959
- await makeDirInMocks('dynamic-params-partial-[id]')
959
+ await mocksDir.mkdir('dynamic-params-partial-[id]')
960
960
  await fx.write()
961
961
  const r = await request('/dynamic-params-partial-999/foo')
962
962
  equal(await r.text(), fx.body)
@@ -968,7 +968,7 @@ describe('Query String', () => {
968
968
  const fx0 = new Fixture('query-string?foo=[foo]&bar=[bar].GET.200.json')
969
969
  const fx1 = new Fixture('query-string/[id]?limit=[limit].GET.200.json')
970
970
  before(async () => {
971
- await makeDirInMocks('query-string')
971
+ await mocksDir.mkdir('query-string')
972
972
  await fx0.write()
973
973
  await fx1.write()
974
974
  await api.reset()
@@ -1127,10 +1127,10 @@ describe('Registering Mocks', () => {
1127
1127
  test('when watcher is off, newly added mocks do not get registered', async () => {
1128
1128
  await api.setWatchMocks(false)
1129
1129
  const fx = new FixtureExternal('non-auto-registered-file.GET.200.json')
1130
- await writeInMocksDir(fx.file, fx.body)
1130
+ await mocksDir.write(fx.file, fx.body)
1131
1131
  await sleep(100)
1132
1132
  equal(await fx.fetchBroker(), undefined)
1133
- await rmFromMocksDir(fx.file)
1133
+ await mocksDir.rm(fx.file)
1134
1134
  })
1135
1135
 
1136
1136
  test('register', async () => {
@@ -1176,27 +1176,27 @@ describe('Registering Mocks', () => {
1176
1176
  const fx0 = new FixtureExternal('reg0/runtime0.GET.200.txt')
1177
1177
  let version
1178
1178
  before(async () => {
1179
- await makeDirInMocks('reg0')
1180
- await writeInMocksDir(fx0.file, fx0.body)
1179
+ await mocksDir.mkdir('reg0')
1180
+ await mocksDir.write(fx0.file, fx0.body)
1181
1181
  version = await resolveOnNextSyncVersion(-1)
1182
1182
  })
1183
1183
 
1184
1184
  const fx = new FixtureExternal('runtime1.GET.200.txt')
1185
1185
  test('responds when a file is added', async () => {
1186
1186
  const prom = resolveOnNextSyncVersion(version)
1187
- await writeInMocksDir(fx.file, fx.body)
1187
+ await mocksDir.write(fx.file, fx.body)
1188
1188
  equal(await prom, version + 1)
1189
1189
  })
1190
1190
 
1191
1191
  test('responds when a file is deleted', async () => {
1192
1192
  const prom = resolveOnNextSyncVersion(version + 1)
1193
- await rmFromMocksDir(fx.file)
1193
+ await mocksDir.rm(fx.file)
1194
1194
  equal(await prom, version + 2)
1195
1195
  })
1196
1196
 
1197
1197
  test('responds when dir is renamed', async () => {
1198
1198
  const prom = resolveOnNextSyncVersion(version + 2)
1199
- await renameInMocksDir('reg0', 'reg1')
1199
+ await mocksDir.rename('reg0', 'reg1')
1200
1200
  equal(await prom, version + 3)
1201
1201
 
1202
1202
  const s = await fetchState()
@@ -1209,7 +1209,7 @@ describe('Registering Mocks', () => {
1209
1209
  await fx.writeExternally()
1210
1210
  config.watcherDebounceMs = 100 // Because on macOS rmdir triggers a few events
1211
1211
  const nextVerPromise = resolveOnNextSyncVersion()
1212
- await rmDirFromMocks('api/bulk-delete')
1212
+ await mocksDir.rmdir('api/bulk-delete')
1213
1213
  await nextVerPromise
1214
1214
  equal(await fx.fetchBroker(), undefined)
1215
1215
  await sleep(50) // Only for Docker, not sure why we need to delay the server teardown
@@ -1221,24 +1221,37 @@ describe('Registering Mocks', () => {
1221
1221
  * This is for listening to real-time updates. It responds when a new mock is added, deleted, or renamed. */
1222
1222
  async function resolveOnNextSyncVersion(currSyncVer = undefined) {
1223
1223
  let skipFirst = currSyncVer === undefined
1224
- const response = await api.getSyncVersion()
1225
- const stream = response.body.pipeThrough(new TextDecoderStream())
1224
+ const reader = (await api.getSyncVersion())
1225
+ .body.pipeThrough(new TextDecoderStream())
1226
+ .getReader()
1226
1227
  let buffer = ''
1227
1228
 
1228
- for await (const chunk of stream) {
1229
- buffer += chunk
1230
- const parts = buffer.split('\n\n')
1231
- buffer = parts.pop() || ''
1232
-
1233
- for (const event of parts)
1234
- for (const line of event.split(/\r?\n/))
1235
- if (line.startsWith('data:')) {
1236
- const v = Number(line.slice(5).trim())
1237
- if (skipFirst || v === currSyncVer)
1238
- skipFirst = false
1239
- else
1240
- return v
1241
- }
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(() => {})
1242
1255
  }
1243
1256
  }
1244
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')
package/src/server/cli.js CHANGED
@@ -4,9 +4,9 @@ import { pathToFileURL } from 'node:url'
4
4
  import { resolve, join } from 'node:path'
5
5
  import { parseArgs } from 'node:util'
6
6
 
7
+ import { config } from './stores/config.js'
7
8
  import { isFile, isDirectory } from './utils/fs.js'
8
9
  import { Mockaton } from '../../index.js'
9
- import { config } from './config.js'
10
10
  import pkgJSON from '../../package.json' with { type: 'json' }
11
11
 
12
12
 
@@ -19,14 +19,14 @@ SYNOPSIS
19
19
 
20
20
  OPTIONS
21
21
  -c, --config <file> (default: ./${DEFAULT_CONFIG_FILE})
22
-
23
- -H, --host <host> (default: 127.0.0.1)
24
- -p, --port <port> (default: 0) which means auto-assigned
25
-
22
+
23
+ -H, --host <host> (default: ${config.host})
24
+ -p, --port <port> (default: ${config.port}) 0 means auto-assigned
25
+
26
26
  -q, --quiet Show errors only
27
27
  --no-open Don't open dashboard in a browser
28
28
  --no-read-only Allow writing and deleting mocks via API
29
-
29
+
30
30
  --skills Show AI agent SKILL.md file path
31
31
  -h, --help
32
32
  -v, --version
@@ -3,8 +3,8 @@ import { watch } from 'node:fs'
3
3
  import { EventEmitter } from 'node:events'
4
4
 
5
5
  import { config } from './config.js'
6
- import { isFile, isDirectory } from './utils/fs.js'
7
- import * as mockBrokerCollection from './mockBrokersCollection.js'
6
+ import * as brokers from './brokers.js'
7
+ import { isFile, isDirectory } from '../utils/fs.js'
8
8
 
9
9
 
10
10
  let mocksWatcher = null
@@ -38,6 +38,10 @@ export const uiSyncVersion = new class extends EventEmitter {
38
38
  }
39
39
  }
40
40
 
41
+ export function emitChange() {
42
+ uiSyncVersion.increment()
43
+ }
44
+
41
45
 
42
46
  export function watchMocksDir() {
43
47
  const dir = config.mocksDir
@@ -46,22 +50,25 @@ export function watchMocksDir() {
46
50
  return
47
51
 
48
52
  if (isDirectory(join(dir, file))) {
49
- mockBrokerCollection.init()
53
+ brokers.init()
50
54
  uiSyncVersion.increment()
51
55
  }
52
56
  else if (!isFile(join(dir, file))) { // file deleted
53
- mockBrokerCollection.unregisterMock(file)
57
+ brokers.unregisterMock(file)
54
58
  uiSyncVersion.increment()
55
59
  }
56
- else if (mockBrokerCollection.registerMock(file, Boolean('isFromWatcher')))
60
+ else if (brokers.registerMock(file, Boolean('isFromWatcher')))
57
61
  uiSyncVersion.increment()
58
62
  else {
59
63
  // ignore file edits
60
64
  }
61
65
  })
66
+ mocksWatcher?.on('error', () => {
67
+ // on linux, subdir deletion can trigger inotify IN_IGNORED
68
+ })
62
69
  }
63
70
 
64
- export function stopMocksDirWatcher() {
71
+ export function unwatchMocksDir() {
65
72
  mocksWatcher?.close()
66
73
  mocksWatcher = null
67
74
  }
@@ -75,19 +82,21 @@ export function sseClientSyncVersion(req, response) {
75
82
  })
76
83
  response.flushHeaders()
77
84
 
78
- function sendVersion() {
79
- response.write(`data: ${uiSyncVersion.version}\n\n`)
80
- }
81
85
 
82
86
  sendVersion()
83
87
  uiSyncVersion.subscribe(sendVersion)
84
88
 
89
+ function sendVersion() {
90
+ response.write(`data: ${uiSyncVersion.version}\n\n`)
91
+ }
92
+
85
93
  const keepAlive = setInterval(() => {
86
94
  response.write(': ping\n\n')
87
95
  }, 10_000)
88
96
 
89
97
  req.on('close', cleanup)
90
98
  req.on('error', cleanup)
99
+ response.on('error', cleanup)
91
100
  function cleanup() {
92
101
  clearInterval(keepAlive)
93
102
  uiSyncVersion.unsubscribe(sendVersion)
@@ -1,9 +1,9 @@
1
1
  import { basename } from 'node:path'
2
2
 
3
- import { MockBroker } from './MockBroker.js'
4
- import { parseFilename } from '../client/Filename.js'
5
- import { listFilesRecursively } from './utils/fs.js'
6
3
  import { config, isFileAllowed } from './config.js'
4
+ import { MockBroker } from '../MockBroker.js'
5
+ import { listFilesRecursively } from '../utils/fs.js'
6
+ import { parseFilename } from '../../client/Filename.js'
7
7
 
8
8
 
9
9
  /**
@@ -2,13 +2,13 @@ import { resolve } from 'node:path'
2
2
  import { lstatSync } from 'node:fs'
3
3
  import { METHODS } from 'node:http'
4
4
 
5
- import { logger } from './utils/logger.js'
6
- import { registerMimes } from './utils/mime.js'
7
- import { openInBrowser } from './utils/openInBrowser.js'
8
- import { is, validate, isInt, isFloat, isOneOf, optionalURL } from './utils/validate.js'
9
- import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
5
+ import { logger } from '../utils/logger.js'
6
+ import { registerMimes } from '../utils/mime.js'
7
+ import { openInBrowser } from '../utils/openInBrowser.js'
8
+ import { is, validate, isInt, isFloat, isOneOf, optionalURL } from '../utils/validate.js'
9
+ import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from '../utils/http-cors.js'
10
10
 
11
- import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
11
+ import { jsToJsonPlugin } from '../MockDispatcherPlugins.js'
12
12
 
13
13
  /** @type {{
14
14
  * [K in keyof Config]-?: [
@@ -47,15 +47,12 @@ const schema = {
47
47
  corsCredentials: [true, is(Boolean)],
48
48
  corsMaxAge: [0, is(Number)],
49
49
 
50
- plugins: [
51
- [
52
- [/\.(js|ts)$/, jsToJsonPlugin]
53
- ], is(Array)],
50
+ hotReload: [false, is(Boolean)],
51
+ bypassImportCache: [true, is(Boolean)],
54
52
 
53
+ // Non-serializable
54
+ plugins: [[[/\.(js|ts)$/, jsToJsonPlugin]], is(Array)],
55
55
  onReady: [await openInBrowser, is(Function)],
56
-
57
- hotReload: [false, is(Boolean)],
58
- bypassImportCache: [true, is(Boolean)]
59
56
  }
60
57
 
61
58
 
@@ -73,19 +70,36 @@ export const config = Object.seal(defaults)
73
70
  export const ConfigValidator = Object.freeze(validators)
74
71
 
75
72
 
73
+ let originalOpts = {}
74
+
76
75
  /** @param {Partial<Config>} opts */
77
- export function setup(opts) {
76
+ export function initConfig(opts) {
78
77
  if (opts.mocksDir)
79
78
  opts.mocksDir = resolve(opts.mocksDir)
80
79
 
81
80
  Object.assign(config, opts)
81
+ originalOpts = deepCloneExcluding(config, 'plugins', 'onReady')
82
+
82
83
  validate(config, ConfigValidator)
83
84
  logger.setLevel(config.logLevel)
84
85
  registerMimes(config.extraMimes)
85
86
  }
86
87
 
88
+ export function reinitConfig() {
89
+ initConfig(originalOpts)
90
+ }
91
+
87
92
  export const isFileAllowed = f => !config.ignore.test(f)
88
93
 
89
94
  export const calcDelay = () => config.delayJitter
90
95
  ? config.delay * (1 + Math.random() * config.delayJitter)
91
96
  : config.delay
97
+
98
+
99
+ function deepCloneExcluding(obj, ...exclude) {
100
+ const res = {}
101
+ for (const [k, v] of Object.entries(obj))
102
+ if (!exclude.includes(k))
103
+ res[k] = structuredClone(v)
104
+ return res
105
+ }
@@ -4,7 +4,7 @@ import { createServer } from 'node:http'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { equal } from 'node:assert/strict'
6
6
  import { join } from 'node:path'
7
- import { rmdir } from 'node:fs/promises'
7
+ import { rm } from 'node:fs/promises'
8
8
 
9
9
  import { ServerResponse } from './HttpServerResponse.js'
10
10
 
@@ -31,7 +31,7 @@ describe('ServerResponse', { concurrency: true }, () => {
31
31
 
32
32
  after(async () => {
33
33
  server?.close()
34
- await rmdir(tmpDir, { recursive: true, force: true })
34
+ await rm(tmpDir, { recursive: true, force: true })
35
35
  })
36
36
 
37
37
 
@@ -116,7 +116,7 @@
116
116
  "/mockaton/reset": {
117
117
  "patch": {
118
118
  "summary": "Re-initialize Mockaton",
119
- "description": "The selected mocks, cookies, and delays go back to their defaults, but `proxyFallback`, `collectProxied`, and `corsAllowed` are not affected.",
119
+ "description": "Initializes the brokers collections and config options",
120
120
  "x-js-client-example": "await mockaton.reset()",
121
121
  "responses": {
122
122
  "200": {
File without changes