mockaton 13.11.2 → 13.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +42 -27
  2. package/package.json +1 -1
  3. package/skills/mockaton/SKILL.md +21 -19
  4. package/src/client/ApiCommander.js +20 -19
  5. package/src/client/{app.js → App.js} +6 -6
  6. package/src/client/{app-header.js → Header.js} +4 -4
  7. package/src/client/IndexHtml.js +1 -1
  8. package/src/client/{app-mock-list.js → MockList.js} +7 -8
  9. package/src/client/{app-payload-viewer.js → PayloadViewer.js} +3 -3
  10. package/src/client/{app-store.test.js → store.test.js} +1 -1
  11. package/src/server/Api.js +57 -54
  12. package/src/server/MockBroker.js +2 -8
  13. package/src/server/MockDispatcher.js +6 -5
  14. package/src/server/Mockaton.js +14 -17
  15. package/src/server/Mockaton.test.js +61 -48
  16. package/src/server/ProxyRelay.js +5 -4
  17. package/src/server/UrlParsers.js +1 -1
  18. package/src/server/UrlParsers.test.js +1 -1
  19. package/src/server/cli.js +6 -6
  20. package/src/server/{Watcher.js → stores/Watcher.js} +18 -9
  21. package/src/server/{mockBrokersCollection.js → stores/brokers.js} +4 -7
  22. package/src/server/{config.js → stores/config.js} +30 -16
  23. package/src/server/utils/HttpIncomingMessage.js +17 -16
  24. package/src/server/utils/HttpServerResponse.test.js +2 -2
  25. package/src/server/utils/logger.js +7 -1
  26. package/src/server/utils/mime.js +2 -0
  27. package/www/src/assets/openapi.json +1 -1
  28. /package/src/client/{app.css → App.css} +0 -0
  29. /package/src/client/{app-header.css → Header.css} +0 -0
  30. /package/src/client/{app-mock-list.css → MockList.css} +0 -0
  31. /package/src/client/{app-payload-viewer.css → PayloadViewer.css} +0 -0
  32. /package/src/client/{graphics.js → icons.js} +0 -0
  33. /package/src/client/{app-store.js → store.js} +0 -0
  34. /package/src/server/{resolverBypassImportCache.js → ResolverBypassImportCache.js} +0 -0
  35. /package/src/server/{resolverResolveExtensionless.js → ResolverResolveExtensionless.js} +0 -0
  36. /package/src/server/{cookie.js → stores/cookies.js} +0 -0
@@ -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) {
@@ -6,30 +6,29 @@ import pkgJSON from '../../package.json' with { type: 'json' }
6
6
  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
- import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
9
+ import { IncomingMessage } 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
-
16
13
  import { dispatchMock } from './MockDispatcher.js'
17
- import * as mockBrokerCollection from './mockBrokersCollection.js'
14
+ import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
18
15
 
19
- import { watchDevSPA } from './utils/WatcherDevClient.js'
20
- import { watchMocksDir } from './Watcher.js'
16
+ import * as brokers from './stores/brokers.js'
17
+ import { cookie } from './stores/cookies.js'
18
+ import { config, initConfig } from './stores/config.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()
@@ -61,13 +60,11 @@ async function onRequest(req, response) {
61
60
  logger.verbose('APP', response)
62
61
  })
63
62
 
64
- const url = req.url || ''
65
-
66
- if (url.length > 2048) {
63
+ if (req.url?.length > 2048) {
67
64
  response.uriTooLong()
68
65
  return
69
66
  }
70
- if (hasControlChars(url)) {
67
+ if (req.urlHasControlChars()) {
71
68
  response.badRequest()
72
69
  return
73
70
  }
@@ -84,7 +81,7 @@ async function onRequest(req, response) {
84
81
  }
85
82
  }
86
83
  catch (error) {
87
- if (error instanceof BodyReaderError)
84
+ if (error instanceof IncomingMessage.BodyReaderError)
88
85
  response.unprocessable(`${error.name}: ${error.message}`)
89
86
  else {
90
87
  logger.error(500, req.url, error?.message || error, error?.stack || '')
@@ -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
 
@@ -235,7 +234,7 @@ describe('Dashboard', () => {
235
234
  })
236
235
 
237
236
  test('serves assets', async () => {
238
- const r = await request(API.root + '/app.css')
237
+ const r = await request(API.root + '/App.css')
239
238
  match(await r.text(), new RegExp(':root {'))
240
239
  })
241
240
  })
@@ -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
 
@@ -3,13 +3,14 @@ import { randomUUID } from 'node:crypto'
3
3
 
4
4
  import { extFor } from './utils/mime.js'
5
5
  import { write, isFile, resolveIn } from './utils/fs.js'
6
- import { BodyReaderError } from './utils/HttpIncomingMessage.js'
7
-
8
- import { config } from './config.js'
6
+ import { IncomingMessage } from './utils/HttpIncomingMessage.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
@@ -23,7 +24,7 @@ export async function proxy(req, response, delay) {
23
24
  })
24
25
  }
25
26
  catch (error) { // TESTME
26
- if (error instanceof BodyReaderError)
27
+ if (error instanceof IncomingMessage.BodyReaderError)
27
28
  response.unprocessable(error.name)
28
29
  else {
29
30
  response.badGateway()
@@ -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,7 @@
1
- import { basename } from 'node:path'
2
-
3
- import { MockBroker } from './MockBroker.js'
4
- import { parseFilename } from '../client/Filename.js'
5
- import { listFilesRecursively } from './utils/fs.js'
6
1
  import { config, isFileAllowed } from './config.js'
2
+ import { MockBroker } from '../MockBroker.js'
3
+ import { listFilesRecursively } from '../utils/fs.js'
4
+ import { parseFilename } from '../../client/Filename.js'
7
5
 
8
6
 
9
7
  /**
@@ -35,8 +33,7 @@ export function init() {
35
33
 
36
34
  /** @returns {boolean} registered */
37
35
  export function registerMock(file, isFromWatcher = false) {
38
- if (brokerByFilename(file)?.hasMock(file) ||
39
- !isFileAllowed(basename(file)))
36
+ if (brokerByFilename(file)?.hasMock(file) || !isFileAllowed(file))
40
37
  return false
41
38
 
42
39
  const { method, urlMask } = parseFilename(file)
@@ -1,14 +1,14 @@
1
- import { resolve } from 'node:path'
1
+ import { resolve, basename } 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
 
87
- export const isFileAllowed = f => !config.ignore.test(f)
88
+ export function reinitConfig() {
89
+ initConfig(originalOpts)
90
+ }
91
+
92
+ export const isFileAllowed = f => !config.ignore.test(basename(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
+ }
@@ -1,6 +1,8 @@
1
1
  import http, { METHODS } from 'node:http'
2
2
 
3
3
 
4
+ export const reControlAndDelChars = /[\x00-\x1f\x7f]/
5
+
4
6
  export const methodIsSupported = method => METHODS.includes(method)
5
7
 
6
8
  export function removeQueryStringAndFragment(url = '') {
@@ -16,6 +18,8 @@ export class BodyReaderError extends Error {
16
18
  }
17
19
 
18
20
  export class IncomingMessage extends http.IncomingMessage {
21
+ static BodyReaderError = BodyReaderError
22
+
19
23
  json() {
20
24
  return this.body(JSON.parse)
21
25
  }
@@ -29,18 +33,28 @@ export class IncomingMessage extends http.IncomingMessage {
29
33
  for await (const chunk of this) {
30
34
  lengthSoFar += chunk.length
31
35
  if (lengthSoFar > MAX_BODY_SIZE)
32
- throw new BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
36
+ throw new IncomingMessage.BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
33
37
  chunks.push(chunk)
34
38
  }
35
39
 
36
40
  if (lengthSoFar !== expectedLength)
37
- throw new BodyReaderError('Length mismatch')
41
+ throw new IncomingMessage.BodyReaderError('Length mismatch')
38
42
 
39
43
  try {
40
44
  return parser(Buffer.concat(chunks).toString())
41
45
  }
42
46
  catch (_) {
43
- throw new BodyReaderError('Could not parse')
47
+ throw new IncomingMessage.BodyReaderError('Could not parse')
48
+ }
49
+ }
50
+
51
+ urlHasControlChars() {
52
+ try {
53
+ const decoded = decode(this.url || '')
54
+ return !decoded || reControlAndDelChars.test(decoded)
55
+ }
56
+ catch {
57
+ return true
44
58
  }
45
59
  }
46
60
  }
@@ -49,19 +63,6 @@ export const parseJSON = req =>
49
63
  IncomingMessage.prototype.body.call(req, JSON.parse)
50
64
 
51
65
 
52
-
53
- export const reControlAndDelChars = /[\x00-\x1f\x7f]/
54
-
55
- export function hasControlChars(url) {
56
- try {
57
- const decoded = decode(url)
58
- return !decoded || reControlAndDelChars.test(decoded)
59
- }
60
- catch {
61
- return true
62
- }
63
- }
64
-
65
66
  export function decode(url) {
66
67
  const candidate = decodeURIComponent(url)
67
68
  return candidate === decodeURIComponent(candidate)
@@ -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