mockaton 13.3.5 → 13.4.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 +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.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 +2 -2
- 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
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
|
@@ -13,15 +13,15 @@ export const logger = new class {
|
|
|
13
13
|
console.info(this.#msg('INFO', ...msg))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
normal(tag = 'NORMAL', url, ...msg) {
|
|
17
17
|
if (this.#level !== 'quiet')
|
|
18
|
-
console.log(this.#msg(
|
|
18
|
+
console.log(this.#msg(tag, url, ...msg))
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
verbose(tag = 'VERBOSE', response, error = '') {
|
|
22
22
|
if (this.#level === 'verbose')
|
|
23
23
|
console.log(this.#msg(
|
|
24
|
-
|
|
24
|
+
tag,
|
|
25
25
|
response.req.method,
|
|
26
26
|
response.statusCode,
|
|
27
27
|
response.req.url,
|
package/src/server/utils/mime.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { MIMEType } from 'node:util'
|
|
2
|
-
import { config } from '../config.js'
|
|
3
|
-
import { EXT_UNKNOWN_MIME, EXT_EMPTY } from '../../client/ApiConstants.js'
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
// Generated with:
|
|
@@ -119,7 +117,14 @@ const extToMime = {
|
|
|
119
117
|
zst: 'application/zstd'
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
const mimeToExt =
|
|
120
|
+
const mimeToExt = {
|
|
121
|
+
data: mapMimeToExt(extToMime)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function registerMimes(obj) {
|
|
125
|
+
Object.assign(extToMime, obj)
|
|
126
|
+
mimeToExt.data = mapMimeToExt(extToMime)
|
|
127
|
+
}
|
|
123
128
|
|
|
124
129
|
function mapMimeToExt(e2m) {
|
|
125
130
|
const m = {}
|
|
@@ -130,7 +135,7 @@ function mapMimeToExt(e2m) {
|
|
|
130
135
|
|
|
131
136
|
export function mimeFor(filename) {
|
|
132
137
|
const ext = extname(filename).toLowerCase()
|
|
133
|
-
return
|
|
138
|
+
return extToMime[ext] || ''
|
|
134
139
|
}
|
|
135
140
|
function extname(filename) {
|
|
136
141
|
const i = filename.lastIndexOf('.')
|
|
@@ -142,12 +147,7 @@ function extname(filename) {
|
|
|
142
147
|
|
|
143
148
|
export function extFor(mime) {
|
|
144
149
|
return mime
|
|
145
|
-
?
|
|
146
|
-
:
|
|
147
|
-
}
|
|
148
|
-
function findExt(rawMime) {
|
|
149
|
-
const m = new MIMEType(rawMime).essence
|
|
150
|
-
const extraMimeToExt = mapMimeToExt(config.extraMimes)
|
|
151
|
-
return extraMimeToExt[m] || mimeToExt[m] || EXT_UNKNOWN_MIME
|
|
150
|
+
? mimeToExt.data[new MIMEType(mime).essence]
|
|
151
|
+
: ''
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -3,15 +3,19 @@ import { equal } from 'node:assert/strict'
|
|
|
3
3
|
import { extFor, mimeFor } from './mime.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
test('extFor', () =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
test('extFor', () => {
|
|
7
|
+
[
|
|
8
|
+
'text/html',
|
|
9
|
+
'Text/html',
|
|
10
|
+
'text/Html; charset=UTF-16'
|
|
11
|
+
].map(input =>
|
|
12
|
+
equal(extFor(input), 'html'))
|
|
13
|
+
})
|
|
12
14
|
|
|
13
|
-
test('mimeFor', () =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
test('mimeFor', () => {
|
|
16
|
+
[
|
|
17
|
+
'file.html',
|
|
18
|
+
'file.HTmL'
|
|
19
|
+
].map(input =>
|
|
20
|
+
equal(mimeFor(input), 'text/html'))
|
|
21
|
+
})
|