mockaton 13.3.5 → 13.4.1
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 +28 -16
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/client/ApiCommander.js +1 -1
- package/src/client/ApiConstants.js +2 -2
- package/src/client/IndexHtml.js +18 -18
- package/src/client/app-header.js +3 -3
- package/src/client/app-payload-viewer.js +4 -8
- package/src/client/app-store.js +3 -34
- package/src/client/app.css +5 -3
- package/src/client/app.js +3 -2
- package/src/client/dir/dittoSplitPaths.js +25 -0
- package/src/client/dir/dittoSplitPaths.test.js +28 -0
- package/src/client/{dir-tree.js → dir/groupByFolder.js} +2 -33
- package/src/client/{dir-tree.test.js → dir/groupByFolder.test.js} +2 -26
- package/src/client/graphics.js +5 -5
- package/src/client/utils/LocalStorage.js +69 -0
- package/src/client/utils/css.js +16 -0
- package/src/client/{dom-utils-test.js → utils/css.test.js} +1 -1
- package/src/client/utils/dom.js +68 -0
- package/src/client/utils/watcherDev.js +46 -0
- package/src/server/Api.js +16 -3
- package/src/server/MockDispatcher.js +11 -8
- package/src/server/Mockaton.js +16 -8
- package/src/server/Mockaton.test.js +14 -9
- package/src/server/ProxyRelay.js +9 -3
- package/src/server/{utils/UrlParsers.js → UrlParsers.js} +10 -4
- package/src/server/{utils/UrlParsers.test.js → UrlParsers.test.js} +14 -14
- package/src/server/config.js +2 -0
- package/src/server/utils/HttpServerResponse.js +18 -39
- package/src/server/{WatcherDevClient.js → utils/WatcherDevClient.js} +3 -17
- package/src/server/utils/fs.js +1 -1
- package/src/server/utils/logger.js +4 -4
- package/src/server/utils/mime.js +11 -11
- package/src/server/utils/mime.test.js +15 -11
- package/www/src/assets/openapi.json +147 -147
- package/src/client/dom-utils.js +0 -154
- package/src/client/watcherDev.js +0 -39
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function t(translation) {
|
|
2
|
+
return translation[0]
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function createElement(tag, props, ...children) {
|
|
7
|
+
const elem = document.createElement(tag)
|
|
8
|
+
if (props)
|
|
9
|
+
for (const [k, v] of Object.entries(props))
|
|
10
|
+
if (v === undefined) continue
|
|
11
|
+
else if (k === 'ref') v.elem = elem
|
|
12
|
+
else if (k === 'style') Object.assign(elem.style, v)
|
|
13
|
+
else if (k.startsWith('on')) elem.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
|
|
14
|
+
else if (k in elem) elem[k] = v
|
|
15
|
+
else elem.setAttribute(k, v)
|
|
16
|
+
elem.append(...children.flat().filter(Boolean))
|
|
17
|
+
return elem
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export function createSvgElement(tag, props, ...children) {
|
|
22
|
+
const elem = document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
23
|
+
for (const [k, v] of Object.entries(props))
|
|
24
|
+
elem.setAttribute(k, v)
|
|
25
|
+
elem.append(...children.flat().filter(Boolean))
|
|
26
|
+
return elem
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export function Fragment(...args) {
|
|
31
|
+
const frag = new DocumentFragment()
|
|
32
|
+
for (const arg of args)
|
|
33
|
+
if (Array.isArray(arg))
|
|
34
|
+
frag.append(...arg)
|
|
35
|
+
else
|
|
36
|
+
frag.appendChild(arg)
|
|
37
|
+
return frag
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function restoreFocus(cb) {
|
|
42
|
+
const focusQuery = selectorFor(document.activeElement)
|
|
43
|
+
cb()
|
|
44
|
+
if (focusQuery)
|
|
45
|
+
document.querySelector(focusQuery)?.focus()
|
|
46
|
+
}
|
|
47
|
+
function selectorFor(elem) {
|
|
48
|
+
if (!(elem instanceof Element))
|
|
49
|
+
return
|
|
50
|
+
const path = []
|
|
51
|
+
while (elem) {
|
|
52
|
+
let qualifier = ''
|
|
53
|
+
if (elem.hasAttribute('key'))
|
|
54
|
+
qualifier = `[key="${elem.getAttribute('key')}"]`
|
|
55
|
+
else {
|
|
56
|
+
let i = 0
|
|
57
|
+
let sib = elem
|
|
58
|
+
while ((sib = sib.previousElementSibling))
|
|
59
|
+
if (sib.tagName === elem.tagName)
|
|
60
|
+
i++
|
|
61
|
+
if (i)
|
|
62
|
+
qualifier = `:nth-of-type(${i + 1})`
|
|
63
|
+
}
|
|
64
|
+
path.push(elem.tagName + qualifier)
|
|
65
|
+
elem = elem.parentElement
|
|
66
|
+
}
|
|
67
|
+
return path.reverse().join('>')
|
|
68
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const url = new URL(import.meta.url).searchParams.get('url')
|
|
2
|
+
|
|
3
|
+
if (!url)
|
|
4
|
+
console.warn('Missing ?url=')
|
|
5
|
+
else
|
|
6
|
+
init()
|
|
7
|
+
|
|
8
|
+
function init() {
|
|
9
|
+
let conn = null
|
|
10
|
+
let timer = null
|
|
11
|
+
|
|
12
|
+
connect()
|
|
13
|
+
window.addEventListener('beforeunload', teardown)
|
|
14
|
+
|
|
15
|
+
function connect() {
|
|
16
|
+
if (conn) return
|
|
17
|
+
|
|
18
|
+
clearTimeout(timer)
|
|
19
|
+
conn = new EventSource(url)
|
|
20
|
+
|
|
21
|
+
conn.onmessage = function (event) {
|
|
22
|
+
const file = event.data
|
|
23
|
+
if (file.endsWith('.css'))
|
|
24
|
+
hotReloadCSS(file)
|
|
25
|
+
else if (file)
|
|
26
|
+
location.reload()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
conn.onerror = function () {
|
|
30
|
+
console.error('hot reload')
|
|
31
|
+
teardown()
|
|
32
|
+
timer = setTimeout(connect, 3000)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function teardown() {
|
|
37
|
+
clearTimeout(timer)
|
|
38
|
+
conn?.close()
|
|
39
|
+
conn = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function hotReloadCSS(file) {
|
|
43
|
+
const mod = await import(`${document.baseURI}${file}?${Date.now()}`, { with: { type: 'css' } })
|
|
44
|
+
document.adoptedStyleSheets = [mod.default]
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/server/Api.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
|
+
import { readdirSync } from 'node:fs'
|
|
8
|
+
import { write, rm, isFile, resolveIn } from './utils/fs.js'
|
|
7
9
|
|
|
8
10
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
9
11
|
|
|
10
|
-
import { sseClientHotReload
|
|
12
|
+
import { sseClientHotReload } from './utils/WatcherDevClient.js'
|
|
11
13
|
import { stopMocksDirWatcher, sseClientSyncVersion, uiSyncVersion, watchMocksDir } from './Watcher.js'
|
|
12
14
|
|
|
13
15
|
import { API } from '../client/ApiConstants.js'
|
|
@@ -16,7 +18,11 @@ import { IndexHtml, CSP } from '../client/IndexHtml.js'
|
|
|
16
18
|
import { cookie } from './cookie.js'
|
|
17
19
|
import { config, ConfigValidator } from './config.js'
|
|
18
20
|
import * as mockBrokersCollection from './mockBrokersCollection.js'
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export const CLIENT_DIR = join(import.meta.dirname, '../client')
|
|
24
|
+
const DASHBOARD_ASSETS = readdirSync(CLIENT_DIR, { recursive: true })
|
|
25
|
+
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
export const apiGetReqs = new Map([
|
|
@@ -26,11 +32,12 @@ export const apiGetReqs = new Map([
|
|
|
26
32
|
[API.state, getState],
|
|
27
33
|
[API.syncVersion, sseClientSyncVersion],
|
|
28
34
|
|
|
29
|
-
[API.watchHotReload,
|
|
35
|
+
[API.watchHotReload, onDevWatch],
|
|
30
36
|
[API.throws, () => { throw new Error('Test500') }]
|
|
31
37
|
])
|
|
32
38
|
|
|
33
39
|
|
|
40
|
+
|
|
34
41
|
export const apiPatchReqs = new Map([
|
|
35
42
|
[API.cors, setCorsAllowed],
|
|
36
43
|
[API.reset, reset],
|
|
@@ -83,6 +90,12 @@ function getState(_, response) {
|
|
|
83
90
|
})
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
function onDevWatch(req, response) {
|
|
94
|
+
if (config.hotReload)
|
|
95
|
+
sseClientHotReload(req, response)
|
|
96
|
+
else
|
|
97
|
+
response.notFound()
|
|
98
|
+
}
|
|
86
99
|
|
|
87
100
|
/** # PATCH */
|
|
88
101
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
|
|
3
3
|
import { logger } from './utils/logger.js'
|
|
4
|
-
|
|
5
4
|
import { proxy } from './ProxyRelay.js'
|
|
6
5
|
import { cookie } from './cookie.js'
|
|
7
6
|
import { parseFilename } from '../client/Filename.js'
|
|
8
7
|
import { echoFilePlugin } from './MockDispatcherPlugins.js'
|
|
9
8
|
import { brokerByRoute } from './mockBrokersCollection.js'
|
|
10
9
|
import { config, calcDelay } from './config.js'
|
|
10
|
+
import { FILENAME_HEADER } from '../client/ApiConstants.js'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
export async function dispatchMock(req, response) {
|
|
14
|
+
response.setHeaderList(config.extraHeaders)
|
|
15
|
+
|
|
14
16
|
try {
|
|
15
17
|
const isHead = req.method === 'HEAD'
|
|
16
18
|
|
|
@@ -23,10 +25,12 @@ export async function dispatchMock(req, response) {
|
|
|
23
25
|
return
|
|
24
26
|
}
|
|
25
27
|
if (!broker) {
|
|
26
|
-
response.
|
|
28
|
+
response.notFound()
|
|
27
29
|
return
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
response.setHeader(FILENAME_HEADER, broker.file)
|
|
33
|
+
|
|
30
34
|
if (cookie.getCurrent())
|
|
31
35
|
response.setHeader('Set-Cookie', cookie.getCurrent())
|
|
32
36
|
|
|
@@ -36,7 +40,6 @@ export async function dispatchMock(req, response) {
|
|
|
36
40
|
setTimeout(async () => {
|
|
37
41
|
await response.partialContent(req.headers.range, join(config.mocksDir, broker.file))
|
|
38
42
|
}, Number(broker.delayed && calcDelay()))
|
|
39
|
-
logger.accessMock(req.url, broker.file)
|
|
40
43
|
return
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -55,14 +58,14 @@ export async function dispatchMock(req, response) {
|
|
|
55
58
|
|
|
56
59
|
setTimeout(() => response.end(isHead ? null : body),
|
|
57
60
|
Number(broker.delayed && calcDelay()))
|
|
58
|
-
|
|
59
|
-
logger.accessMock(req.url, broker.file)
|
|
60
61
|
}
|
|
61
62
|
catch (error) { // TESTME
|
|
62
63
|
if (error?.code === 'ENOENT') // mock-file has been deleted
|
|
63
|
-
response.
|
|
64
|
-
else
|
|
65
|
-
response.internalServerError(
|
|
64
|
+
response.notFound()
|
|
65
|
+
else {
|
|
66
|
+
response.internalServerError()
|
|
67
|
+
logger.error(500, req.url, error?.message || error, error?.stack || '')
|
|
68
|
+
}
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
package/src/server/Mockaton.js
CHANGED
|
@@ -8,17 +8,16 @@ 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
10
|
|
|
11
|
-
import { API } from '../client/ApiConstants.js'
|
|
11
|
+
import { API, FILENAME_HEADER } from '../client/ApiConstants.js'
|
|
12
12
|
|
|
13
13
|
import { cookie } from './cookie.js'
|
|
14
14
|
import { config, setup } from './config.js'
|
|
15
|
-
import { apiPatchReqs, apiGetReqs } from './Api.js'
|
|
15
|
+
import { apiPatchReqs, apiGetReqs, CLIENT_DIR } from './Api.js'
|
|
16
16
|
|
|
17
17
|
import { dispatchMock } from './MockDispatcher.js'
|
|
18
|
-
|
|
19
18
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
20
19
|
|
|
21
|
-
import { watchDevSPA } from './WatcherDevClient.js'
|
|
20
|
+
import { watchDevSPA } from './utils/WatcherDevClient.js'
|
|
22
21
|
import { watchMocksDir } from './Watcher.js'
|
|
23
22
|
|
|
24
23
|
|
|
@@ -33,7 +32,7 @@ export function Mockaton(options) {
|
|
|
33
32
|
watchMocksDir()
|
|
34
33
|
}
|
|
35
34
|
if (config.hotReload)
|
|
36
|
-
watchDevSPA()
|
|
35
|
+
watchDevSPA(CLIENT_DIR)
|
|
37
36
|
|
|
38
37
|
const server = createServer({ IncomingMessage, ServerResponse }, onRequest)
|
|
39
38
|
server.on('error', reject)
|
|
@@ -49,10 +48,17 @@ export function Mockaton(options) {
|
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
async function onRequest(req, response) {
|
|
51
|
+
response.setHeader('Server', `Mockaton ${pkgJSON.version}`)
|
|
52
|
+
|
|
52
53
|
response.on('error', logger.warn)
|
|
53
54
|
|
|
54
|
-
response.
|
|
55
|
-
|
|
55
|
+
response.on('finish', () => {
|
|
56
|
+
const f = response.getHeader(FILENAME_HEADER)
|
|
57
|
+
if (f)
|
|
58
|
+
logger.normal('MOCK', req.url, f)
|
|
59
|
+
else
|
|
60
|
+
logger.verbose('API', response)
|
|
61
|
+
})
|
|
56
62
|
|
|
57
63
|
const url = req.url || ''
|
|
58
64
|
|
|
@@ -87,7 +93,9 @@ async function onRequest(req, response) {
|
|
|
87
93
|
catch (error) {
|
|
88
94
|
if (error instanceof BodyReaderError)
|
|
89
95
|
response.unprocessable(`${error.name}: ${error.message}`)
|
|
90
|
-
else
|
|
96
|
+
else {
|
|
97
|
+
logger.error(500, req.url, error?.message || error, error?.stack || '')
|
|
91
98
|
response.internalServerError(error)
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
}
|
|
@@ -14,6 +14,7 @@ import { parseFilename } from '../client/Filename.js'
|
|
|
14
14
|
import { API, Commander } from '../../index.js'
|
|
15
15
|
|
|
16
16
|
import CONFIG from './Mockaton.test.config.js'
|
|
17
|
+
import { config } from './config.js'
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
const mocksDir = mkdtempSync(join(tmpdir(), 'mocks'))
|
|
@@ -33,7 +34,7 @@ proc.stdout.on('data', data => {
|
|
|
33
34
|
})
|
|
34
35
|
proc.stderr.on('data', data => {
|
|
35
36
|
stderr.push(data.toString())
|
|
36
|
-
DEBUG && process.stderr.write(
|
|
37
|
+
DEBUG && process.stderr.write(stderr.at(-1))
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
const serverAddr = await new Promise((resolve, reject) => {
|
|
@@ -149,7 +150,7 @@ describe('Filename Convention', () => {
|
|
|
149
150
|
body: '[invalid_json]'
|
|
150
151
|
})
|
|
151
152
|
equal(r.status, 422)
|
|
152
|
-
|
|
153
|
+
equal(await r.text(), 'BodyReaderError: Could not parse')
|
|
153
154
|
})
|
|
154
155
|
|
|
155
156
|
test('returns 500 when a handler throws', async () => {
|
|
@@ -764,15 +765,18 @@ describe('MIME', () => {
|
|
|
764
765
|
|
|
765
766
|
|
|
766
767
|
describe('Headers', () => {
|
|
767
|
-
test('responses have version in "Server" header', async () => {
|
|
768
|
+
test('api responses have version in "Server" header', async () => {
|
|
768
769
|
const r = await api.getState()
|
|
769
770
|
const val = r.headers.get('server')
|
|
770
771
|
match(val, /^Mockaton \d+\.\d+\.\d+$/)
|
|
771
772
|
})
|
|
772
773
|
|
|
773
|
-
test('custom headers
|
|
774
|
-
const
|
|
775
|
-
|
|
774
|
+
test('mock responses have version in "Server" header and custom headers', async () => {
|
|
775
|
+
const fx = new Fixture('header.GET.200.json')
|
|
776
|
+
await fx.write()
|
|
777
|
+
const r = await fx.request()
|
|
778
|
+
match(r.headers.get('server'), /^Mockaton \d+\.\d+\.\d+$/)
|
|
779
|
+
equal(r.headers.get(CONFIG.extraHeaders[0]), CONFIG.extraHeaders[1])
|
|
776
780
|
})
|
|
777
781
|
})
|
|
778
782
|
|
|
@@ -1131,13 +1135,14 @@ describe('Registering Mocks', () => {
|
|
|
1131
1135
|
})
|
|
1132
1136
|
|
|
1133
1137
|
test('deleting a folder unregisters mocks in it', async () => {
|
|
1134
|
-
const fx = new
|
|
1135
|
-
await fx.
|
|
1136
|
-
|
|
1138
|
+
const fx = new FixtureExternal('api/bulk-delete/bar.GET.200.json')
|
|
1139
|
+
await fx.writeExternally()
|
|
1140
|
+
config.watcherDebounceMs = 100 // Because on macOS rmdir triggers a few events
|
|
1137
1141
|
const nextVerPromise = resolveOnNextSyncVersion()
|
|
1138
1142
|
await rmDirFromMocks('api/bulk-delete')
|
|
1139
1143
|
await nextVerPromise
|
|
1140
1144
|
equal(await fx.fetchBroker(), undefined)
|
|
1145
|
+
await sleep(50) // Only for Docker, not sure why we need to delay the server teardown
|
|
1141
1146
|
})
|
|
1142
1147
|
})
|
|
1143
1148
|
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
|
|
|
8
8
|
import { config } from './config.js'
|
|
9
9
|
import { logger } from './utils/logger.js'
|
|
10
10
|
import { makeMockFilename } from '../client/Filename.js'
|
|
11
|
+
import { EXT_EMPTY, EXT_UNKNOWN_MIME } from '../client/ApiConstants.js'
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
export async function proxy(req, response, delay) {
|
|
@@ -24,8 +25,10 @@ export async function proxy(req, response, delay) {
|
|
|
24
25
|
catch (error) { // TESTME
|
|
25
26
|
if (error instanceof BodyReaderError)
|
|
26
27
|
response.unprocessable(error.name)
|
|
27
|
-
else
|
|
28
|
-
response.badGateway(
|
|
28
|
+
else {
|
|
29
|
+
response.badGateway()
|
|
30
|
+
logger.warn(error.cause.message)
|
|
31
|
+
}
|
|
29
32
|
return
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -37,7 +40,10 @@ export async function proxy(req, response, delay) {
|
|
|
37
40
|
setTimeout(() => response.end(body), delay) // TESTME
|
|
38
41
|
|
|
39
42
|
if (config.collectProxied) {
|
|
40
|
-
const
|
|
43
|
+
const mime = proxyResponse.headers.get('content-type')
|
|
44
|
+
const ext = mime
|
|
45
|
+
? extFor(mime) || EXT_UNKNOWN_MIME
|
|
46
|
+
: EXT_EMPTY
|
|
41
47
|
await saveMockToDisk(req.url, req.method, proxyResponse.status, ext, body)
|
|
42
48
|
}
|
|
43
49
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { relative } from 'node:path'
|
|
2
|
-
import { config } from '
|
|
3
|
-
import { decode } from './HttpIncomingMessage.js'
|
|
4
|
-
import { parseFilename, removeTrailingSlash, removeQueryStringAndFragment } from '
|
|
2
|
+
import { config } from './config.js'
|
|
3
|
+
import { decode } from './utils/HttpIncomingMessage.js'
|
|
4
|
+
import { parseFilename, removeTrailingSlash, removeQueryStringAndFragment } from '../client/Filename.js'
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
export function parseQueryParams(url) {
|
|
8
8
|
return new URL(url, 'http://_').searchParams
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** @deprecated Use parseSegments */
|
|
12
12
|
export function parseSplats(url, filename) {
|
|
13
|
+
console.info('parseSplats is deprecated in favor of parseSegments')
|
|
14
|
+
return parseSegments(url, filename)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export function parseSegments(url, filename) {
|
|
13
19
|
const { urlMask } = parseFilename(relative(config.mocksDir, filename))
|
|
14
20
|
|
|
15
21
|
const splats = []
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, describe } from 'node:test'
|
|
2
2
|
import { deepEqual, equal } from 'node:assert/strict'
|
|
3
|
-
import {
|
|
4
|
-
import { config } from '
|
|
3
|
+
import { parseSegments, parseQueryParams } from './UrlParsers.js'
|
|
4
|
+
import { config } from './config.js'
|
|
5
5
|
|
|
6
6
|
test('parseQueryParams', () => {
|
|
7
7
|
const searchParams = parseQueryParams('/api/foo?limit=123')
|
|
@@ -9,44 +9,44 @@ test('parseQueryParams', () => {
|
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
describe('
|
|
13
|
-
test('one
|
|
14
|
-
const
|
|
12
|
+
describe('parseSegments', () => {
|
|
13
|
+
test('one segment', () => {
|
|
14
|
+
const segments = parseSegments(
|
|
15
15
|
'/api/company/123',
|
|
16
16
|
`${config.mocksDir}/api/company/[companyId].GET.200.js`
|
|
17
17
|
)
|
|
18
|
-
deepEqual(
|
|
18
|
+
deepEqual(segments, {
|
|
19
19
|
companyId: '123'
|
|
20
20
|
})
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
test('one
|
|
24
|
-
const
|
|
23
|
+
test('one segment with trailing slash', () => {
|
|
24
|
+
const segments = parseSegments(
|
|
25
25
|
'/api/company/123/',
|
|
26
26
|
`${config.mocksDir}/api/company/[companyId].GET.200.js`
|
|
27
27
|
)
|
|
28
|
-
deepEqual(
|
|
28
|
+
deepEqual(segments, {
|
|
29
29
|
companyId: '123'
|
|
30
30
|
})
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
test('two
|
|
34
|
-
const
|
|
33
|
+
test('two segments and comment', () => {
|
|
34
|
+
const segments = parseSegments(
|
|
35
35
|
'/api/company/123/user/456',
|
|
36
36
|
`${config.mocksDir}/api/company/[companyId]/user/[userId](comments).GET.200.js`
|
|
37
37
|
)
|
|
38
|
-
deepEqual(
|
|
38
|
+
deepEqual(segments, {
|
|
39
39
|
companyId: '123',
|
|
40
40
|
userId: '456',
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
test('ignores query string', () => {
|
|
45
|
-
const
|
|
45
|
+
const segments = parseSegments(
|
|
46
46
|
'/api/company/123?foo=456',
|
|
47
47
|
`${config.mocksDir}/api/company/[companyId]?foo=[fooId].GET.200.js`
|
|
48
48
|
)
|
|
49
|
-
deepEqual(
|
|
49
|
+
deepEqual(segments, {
|
|
50
50
|
companyId: '123'
|
|
51
51
|
})
|
|
52
52
|
})
|
package/src/server/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { METHODS } from 'node:http'
|
|
|
3
3
|
|
|
4
4
|
import { logger } from './utils/logger.js'
|
|
5
5
|
import { isDirectory } from './utils/fs.js'
|
|
6
|
+
import { registerMimes } from './utils/mime.js'
|
|
6
7
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
7
8
|
import { optional, is, validate } from './utils/validate.js'
|
|
8
9
|
import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
|
|
@@ -80,6 +81,7 @@ export function setup(opts) {
|
|
|
80
81
|
Object.assign(config, opts)
|
|
81
82
|
validate(config, ConfigValidator)
|
|
82
83
|
logger.setLevel(config.logLevel)
|
|
84
|
+
registerMimes(config.extraMimes)
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
export const isFileAllowed = f => !config.ignore.test(f)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
|
-
import fs
|
|
2
|
+
import fs from 'node:fs'
|
|
3
3
|
|
|
4
|
-
import { logger } from './logger.js'
|
|
5
4
|
import { mimeFor } from './mime.js'
|
|
6
|
-
import { HEADER_502 } from '../../client/ApiConstants.js'
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
export class ServerResponse extends http.ServerResponse {
|
|
@@ -13,83 +11,64 @@ export class ServerResponse extends http.ServerResponse {
|
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
ok() {
|
|
16
|
-
logger.access(this)
|
|
17
14
|
this.end()
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
html(html, csp) {
|
|
21
|
-
logger.access(this)
|
|
22
18
|
this.setHeader('Content-Type', mimeFor('.html'))
|
|
23
19
|
this.setHeader('Content-Security-Policy', csp)
|
|
24
20
|
this.end(html)
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
json(payload) {
|
|
28
|
-
logger.access(this)
|
|
29
24
|
this.setHeader('Content-Type', mimeFor('.json'))
|
|
30
25
|
this.end(JSON.stringify(payload))
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
file(file) {
|
|
34
|
-
logger.access(this)
|
|
28
|
+
async file(file) {
|
|
35
29
|
this.setHeader('Content-Type', mimeFor(file))
|
|
36
|
-
this.end(
|
|
30
|
+
this.end(await fs.promises.readFile(file, 'utf8'))
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
noContent() {
|
|
40
34
|
this.statusCode = 204
|
|
41
|
-
logger.access(this)
|
|
42
35
|
this.end()
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
|
|
46
39
|
badRequest() {
|
|
47
40
|
this.statusCode = 400
|
|
48
|
-
logger.access(this)
|
|
49
41
|
this.end()
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
forbidden() {
|
|
53
45
|
this.statusCode = 403
|
|
54
|
-
logger.access(this)
|
|
55
46
|
this.end()
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
notFound() {
|
|
59
50
|
this.statusCode = 404
|
|
60
|
-
logger.access(this)
|
|
61
|
-
this.end()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
mockNotFound() {
|
|
65
|
-
this.statusCode = 404
|
|
66
|
-
logger.accessMock(this.req.url, '404')
|
|
67
51
|
this.end()
|
|
68
52
|
}
|
|
69
53
|
|
|
70
54
|
uriTooLong() {
|
|
71
55
|
this.statusCode = 414
|
|
72
|
-
logger.access(this)
|
|
73
56
|
this.end()
|
|
74
57
|
}
|
|
75
58
|
|
|
76
59
|
unprocessable(error) {
|
|
77
|
-
logger.access(this, error)
|
|
78
60
|
this.statusCode = 422
|
|
79
61
|
this.end(error)
|
|
80
62
|
}
|
|
81
63
|
|
|
82
64
|
|
|
83
|
-
internalServerError(
|
|
84
|
-
logger.error(500, this.req.url, error?.message || error, error?.stack || '')
|
|
65
|
+
internalServerError() {
|
|
85
66
|
this.statusCode = 500
|
|
86
67
|
this.end()
|
|
87
68
|
}
|
|
88
69
|
|
|
89
|
-
badGateway(
|
|
90
|
-
logger.warn('Fallback Proxy Error:', error.cause.message)
|
|
70
|
+
badGateway() {
|
|
91
71
|
this.statusCode = 502
|
|
92
|
-
this.setHeader(HEADER_502, 1)
|
|
93
72
|
this.end()
|
|
94
73
|
}
|
|
95
74
|
|
|
@@ -104,20 +83,20 @@ export class ServerResponse extends http.ServerResponse {
|
|
|
104
83
|
this.statusCode = 416 // Range Not Satisfiable
|
|
105
84
|
this.setHeader('Content-Range', `bytes */${size}`)
|
|
106
85
|
this.end()
|
|
86
|
+
return
|
|
107
87
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
88
|
+
|
|
89
|
+
this.statusCode = 206 // Partial Content
|
|
90
|
+
this.setHeader('Accept-Ranges', 'bytes')
|
|
91
|
+
this.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
92
|
+
this.setHeader('Content-Type', mimeFor(file))
|
|
93
|
+
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
113
95
|
const reader = fs.createReadStream(file, { start, end })
|
|
114
|
-
|
|
115
|
-
reader.on('
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
this.internalServerError(error)
|
|
120
|
-
})
|
|
121
|
-
}
|
|
96
|
+
this.on('error', reject)
|
|
97
|
+
reader.on('error', reject)
|
|
98
|
+
reader.on('end', resolve)
|
|
99
|
+
reader.pipe(this)
|
|
100
|
+
})
|
|
122
101
|
}
|
|
123
102
|
}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { watch } from 'node:fs'
|
|
2
2
|
import { EventEmitter } from 'node:events'
|
|
3
|
-
import { watch, readdirSync } from 'node:fs'
|
|
4
|
-
|
|
5
|
-
import { config } from './config.js'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export const CLIENT_DIR = join(import.meta.dirname, '../client')
|
|
9
|
-
export const DASHBOARD_ASSETS = readdirSync(CLIENT_DIR)
|
|
10
3
|
|
|
11
4
|
|
|
12
5
|
const devClientWatcher = new class extends EventEmitter {
|
|
@@ -18,20 +11,14 @@ const devClientWatcher = new class extends EventEmitter {
|
|
|
18
11
|
|
|
19
12
|
// Although `client/IndexHtml.js` is watched, it returns a stale version.
|
|
20
13
|
// i.e., it would need to be a dynamic import + cache busting.
|
|
21
|
-
export function watchDevSPA() {
|
|
22
|
-
watch(
|
|
14
|
+
export function watchDevSPA(dir) {
|
|
15
|
+
watch(dir, (_, file) => {
|
|
23
16
|
devClientWatcher.emit(file)
|
|
24
17
|
})
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
|
|
28
20
|
/** Realtime notify Dev UI changes */
|
|
29
21
|
export function sseClientHotReload(req, response) {
|
|
30
|
-
if (!config.hotReload) {
|
|
31
|
-
response.notFound()
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
22
|
response.writeHead(200, {
|
|
36
23
|
'Content-Type': 'text/event-stream',
|
|
37
24
|
'Cache-Control': 'no-cache',
|
|
@@ -42,7 +29,6 @@ export function sseClientHotReload(req, response) {
|
|
|
42
29
|
function onDevChange(file = '') {
|
|
43
30
|
response.write(`data: ${file}\n\n`)
|
|
44
31
|
}
|
|
45
|
-
|
|
46
32
|
devClientWatcher.subscribe(onDevChange)
|
|
47
33
|
|
|
48
34
|
const keepAlive = setInterval(() => {
|
package/src/server/utils/fs.js
CHANGED