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.
- package/README.md +42 -27
- package/package.json +1 -1
- package/skills/mockaton/SKILL.md +21 -19
- package/src/client/ApiCommander.js +20 -19
- package/src/client/{app.js → App.js} +6 -6
- package/src/client/{app-header.js → Header.js} +4 -4
- package/src/client/IndexHtml.js +1 -1
- package/src/client/{app-mock-list.js → MockList.js} +7 -8
- package/src/client/{app-payload-viewer.js → PayloadViewer.js} +3 -3
- package/src/client/{app-store.test.js → store.test.js} +1 -1
- package/src/server/Api.js +57 -54
- package/src/server/MockBroker.js +2 -8
- package/src/server/MockDispatcher.js +6 -5
- package/src/server/Mockaton.js +14 -17
- package/src/server/Mockaton.test.js +61 -48
- package/src/server/ProxyRelay.js +5 -4
- package/src/server/UrlParsers.js +1 -1
- package/src/server/UrlParsers.test.js +1 -1
- package/src/server/cli.js +6 -6
- package/src/server/{Watcher.js → stores/Watcher.js} +18 -9
- package/src/server/{mockBrokersCollection.js → stores/brokers.js} +4 -7
- package/src/server/{config.js → stores/config.js} +30 -16
- package/src/server/utils/HttpIncomingMessage.js +17 -16
- package/src/server/utils/HttpServerResponse.test.js +2 -2
- package/src/server/utils/logger.js +7 -1
- package/src/server/utils/mime.js +2 -0
- package/www/src/assets/openapi.json +1 -1
- /package/src/client/{app.css → App.css} +0 -0
- /package/src/client/{app-header.css → Header.css} +0 -0
- /package/src/client/{app-mock-list.css → MockList.css} +0 -0
- /package/src/client/{app-payload-viewer.css → PayloadViewer.css} +0 -0
- /package/src/client/{graphics.js → icons.js} +0 -0
- /package/src/client/{app-store.js → store.js} +0 -0
- /package/src/server/{resolverBypassImportCache.js → ResolverBypassImportCache.js} +0 -0
- /package/src/server/{resolverResolveExtensionless.js → ResolverResolveExtensionless.js} +0 -0
- /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 './
|
|
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 './
|
|
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) {
|
package/src/server/Mockaton.js
CHANGED
|
@@ -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
|
|
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
|
|
14
|
+
import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
|
|
18
15
|
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
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
|
-
|
|
24
|
+
initConfig(options)
|
|
26
25
|
cookie.init(config.cookies)
|
|
27
|
-
|
|
26
|
+
brokers.init()
|
|
28
27
|
|
|
29
|
-
register('./
|
|
28
|
+
register('./ResolverResolveExtensionless.js', import.meta.url)
|
|
30
29
|
|
|
31
30
|
if (config.bypassImportCache)
|
|
32
|
-
register('./
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
if (url.length > 2048) {
|
|
63
|
+
if (req.url?.length > 2048) {
|
|
67
64
|
response.uriTooLong()
|
|
68
65
|
return
|
|
69
66
|
}
|
|
70
|
-
if (
|
|
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
|
-
|
|
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 + '/
|
|
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
|
|
392
|
+
const savedMocks = await mocksDir.list('non-existing-mock')
|
|
393
393
|
equal(savedMocks.length, 2)
|
|
394
394
|
|
|
395
|
-
equal(await
|
|
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
|
|
399
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1130
|
+
await mocksDir.write(fx.file, fx.body)
|
|
1131
1131
|
await sleep(100)
|
|
1132
1132
|
equal(await fx.fetchBroker(), undefined)
|
|
1133
|
-
await
|
|
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
|
|
1180
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1225
|
-
|
|
1224
|
+
const reader = (await api.getSyncVersion())
|
|
1225
|
+
.body.pipeThrough(new TextDecoderStream())
|
|
1226
|
+
.getReader()
|
|
1226
1227
|
let buffer = ''
|
|
1227
1228
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -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 {
|
|
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()
|
package/src/server/UrlParsers.js
CHANGED
|
@@ -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:
|
|
24
|
-
-p, --port <port> (default:
|
|
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
|
|
7
|
-
import
|
|
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
|
-
|
|
53
|
+
brokers.init()
|
|
50
54
|
uiSyncVersion.increment()
|
|
51
55
|
}
|
|
52
56
|
else if (!isFile(join(dir, file))) { // file deleted
|
|
53
|
-
|
|
57
|
+
brokers.unregisterMock(file)
|
|
54
58
|
uiSyncVersion.increment()
|
|
55
59
|
}
|
|
56
|
-
else if (
|
|
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
|
|
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 '
|
|
6
|
-
import { registerMimes } from '
|
|
7
|
-
import { openInBrowser } from '
|
|
8
|
-
import { is, validate, isInt, isFloat, isOneOf, optionalURL } from '
|
|
9
|
-
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from '
|
|
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 '
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
34
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
|