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.
Files changed (38) hide show
  1. package/README.md +28 -16
  2. package/index.d.ts +1 -1
  3. package/index.js +1 -1
  4. package/package.json +1 -1
  5. package/src/client/ApiCommander.js +1 -1
  6. package/src/client/ApiConstants.js +2 -2
  7. package/src/client/IndexHtml.js +18 -18
  8. package/src/client/app-header.js +3 -3
  9. package/src/client/app-payload-viewer.js +4 -8
  10. package/src/client/app-store.js +3 -34
  11. package/src/client/app.js +3 -2
  12. package/src/client/dir/dittoSplitPaths.js +25 -0
  13. package/src/client/dir/dittoSplitPaths.test.js +28 -0
  14. package/src/client/{dir-tree.js → dir/groupByFolder.js} +2 -33
  15. package/src/client/{dir-tree.test.js → dir/groupByFolder.test.js} +2 -26
  16. package/src/client/graphics.js +2 -2
  17. package/src/client/utils/LocalStorage.js +69 -0
  18. package/src/client/utils/css.js +16 -0
  19. package/src/client/{dom-utils-test.js → utils/css.test.js} +1 -1
  20. package/src/client/utils/dom.js +68 -0
  21. package/src/client/utils/watcherDev.js +46 -0
  22. package/src/server/Api.js +16 -3
  23. package/src/server/MockDispatcher.js +11 -8
  24. package/src/server/Mockaton.js +16 -8
  25. package/src/server/Mockaton.test.js +14 -9
  26. package/src/server/ProxyRelay.js +9 -3
  27. package/src/server/{utils/UrlParsers.js → UrlParsers.js} +10 -4
  28. package/src/server/{utils/UrlParsers.test.js → UrlParsers.test.js} +14 -14
  29. package/src/server/config.js +2 -0
  30. package/src/server/utils/HttpServerResponse.js +18 -39
  31. package/src/server/{WatcherDevClient.js → utils/WatcherDevClient.js} +3 -17
  32. package/src/server/utils/fs.js +1 -1
  33. package/src/server/utils/logger.js +4 -4
  34. package/src/server/utils/mime.js +11 -11
  35. package/src/server/utils/mime.test.js +15 -11
  36. package/www/src/assets/openapi.json +147 -147
  37. package/src/client/dom-utils.js +0 -154
  38. 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, DASHBOARD_ASSETS, CLIENT_DIR } from './WatcherDevClient.js'
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
- import { write, rm, isFile, resolveIn } from './utils/fs.js'
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, sseClientHotReload],
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.mockNotFound()
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.mockNotFound()
64
- else
65
- response.internalServerError(error)
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
 
@@ -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.setHeader('Server', `Mockaton ${pkgJSON.version}`)
55
- response.setHeaderList(config.extraHeaders)
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(stdout.at(-1))
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
- match(stdout.at(-1), /BodyReaderError: Could not parse/)
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 are included', async () => {
774
- const { headers } = await api.getState()
775
- equal(headers.get(CONFIG.extraHeaders[0]), CONFIG.extraHeaders[1])
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 Fixture('api/bulk-delete/bar.GET.200.json')
1135
- await fx.write()
1136
- await sleep(0)
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
 
@@ -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(error)
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 ext = extFor(proxyResponse.headers.get('content-type'))
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 '../config.js'
3
- import { decode } from './HttpIncomingMessage.js'
4
- import { parseFilename, removeTrailingSlash, removeQueryStringAndFragment } from '../../client/Filename.js'
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 { parseSplats, parseQueryParams } from './UrlParsers.js'
4
- import { config } from '../config.js'
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('parseSplats', () => {
13
- test('one splat', () => {
14
- const splats = parseSplats(
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(splats, {
18
+ deepEqual(segments, {
19
19
  companyId: '123'
20
20
  })
21
21
  })
22
22
 
23
- test('one splat with trailing slash', () => {
24
- const splats = parseSplats(
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(splats, {
28
+ deepEqual(segments, {
29
29
  companyId: '123'
30
30
  })
31
31
  })
32
32
 
33
- test('two splats and comment', () => {
34
- const splats = parseSplats(
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(splats, {
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 splats = parseSplats(
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(splats, {
49
+ deepEqual(segments, {
50
50
  companyId: '123'
51
51
  })
52
52
  })
@@ -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, { readFileSync } from 'node: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(readFileSync(file, 'utf8'))
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(error) {
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(error) {
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
- else {
109
- this.statusCode = 206 // Partial Content
110
- this.setHeader('Accept-Ranges', 'bytes')
111
- this.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
112
- this.setHeader('Content-Type', mimeFor(file))
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
- const response = this
115
- reader.on('open', function () {
116
- this.pipe(response)
117
- })
118
- reader.on('error', error => {
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 { join } from 'node:path'
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(CLIENT_DIR, (_, file) => {
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(() => {
@@ -1,5 +1,5 @@
1
- import { join, dirname, sep, posix, resolve } from 'node:path'
2
1
  import { lstatSync, readdirSync } from 'node:fs'
2
+ import { join, dirname, sep, posix, resolve } from 'node:path'
3
3
  import { mkdir, writeFile, unlink, realpath } from 'node:fs/promises'
4
4
 
5
5
 
@@ -13,15 +13,15 @@ export const logger = new class {
13
13
  console.info(this.#msg('INFO', ...msg))
14
14
  }
15
15
 
16
- accessMock(url, ...msg) {
16
+ normal(tag = 'NORMAL', url, ...msg) {
17
17
  if (this.#level !== 'quiet')
18
- console.log(this.#msg('MOCK', url, ...msg))
18
+ console.log(this.#msg(tag, url, ...msg))
19
19
  }
20
20
 
21
- access(response, error = '') {
21
+ verbose(tag = 'VERBOSE', response, error = '') {
22
22
  if (this.#level === 'verbose')
23
23
  console.log(this.#msg(
24
- 'ACCESS',
24
+ tag,
25
25
  response.req.method,
26
26
  response.statusCode,
27
27
  response.req.url,
@@ -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 = mapMimeToExt(extToMime)
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 config.extraMimes[ext] || extToMime[ext] || ''
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
- ? findExt(mime)
146
- : EXT_EMPTY
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
- 'text/html',
8
- 'Text/html',
9
- 'text/Html; charset=UTF-16'
10
- ].map(input =>
11
- equal(extFor(input), 'html')))
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
- 'file.html',
15
- 'file.HTmL'
16
- ].map(input =>
17
- equal(mimeFor(input), 'text/html')))
15
+ test('mimeFor', () => {
16
+ [
17
+ 'file.html',
18
+ 'file.HTmL'
19
+ ].map(input =>
20
+ equal(mimeFor(input), 'text/html'))
21
+ })