mockaton 13.3.4 → 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 (41) 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 -2
  9. package/src/client/app-payload-viewer.js +4 -7
  10. package/src/client/app-store.js +20 -78
  11. package/src/client/app-store.test.js +1 -25
  12. package/src/client/app.css +9 -5
  13. package/src/client/app.js +7 -7
  14. package/src/client/dir/dittoSplitPaths.js +25 -0
  15. package/src/client/dir/dittoSplitPaths.test.js +28 -0
  16. package/src/client/{dirStructure.js → dir/groupByFolder.js} +17 -13
  17. package/src/client/dir/groupByFolder.test.js +82 -0
  18. package/src/client/graphics.js +2 -2
  19. package/src/client/utils/LocalStorage.js +69 -0
  20. package/src/client/utils/css.js +16 -0
  21. package/src/client/utils/css.test.js +74 -0
  22. package/src/client/{dom-utils.js → utils/dom.js} +2 -21
  23. package/src/client/utils/watcherDev.js +46 -0
  24. package/src/server/Api.js +16 -3
  25. package/src/server/MockDispatcher.js +11 -8
  26. package/src/server/Mockaton.js +18 -8
  27. package/src/server/Mockaton.test.js +14 -9
  28. package/src/server/ProxyRelay.js +9 -3
  29. package/src/server/{utils/UrlParsers.js → UrlParsers.js} +10 -4
  30. package/src/server/{utils/UrlParsers.test.js → UrlParsers.test.js} +14 -14
  31. package/src/server/config.js +2 -0
  32. package/src/server/utils/HttpServerResponse.js +18 -39
  33. package/src/server/{WatcherDevClient.js → utils/WatcherDevClient.js} +3 -17
  34. package/src/server/utils/fs.js +1 -1
  35. package/src/server/utils/logger.js +4 -4
  36. package/src/server/utils/mime.js +11 -11
  37. package/src/server/utils/mime.test.js +15 -11
  38. package/www/src/assets/openapi.json +147 -147
  39. package/src/client/dirStructure.test.js +0 -81
  40. package/src/client/dom-utils.test.js +0 -76
  41. package/src/client/watcherDev.js +0 -39
@@ -1,10 +1,10 @@
1
- import { createSvgElement as s } from './dom-utils.js'
1
+ import { createSvgElement as s } from './utils/dom.js'
2
2
 
3
3
 
4
4
  export const Logo = () =>
5
5
  s('svg', { viewBox: '0 0 556 100' },
6
6
  s('path', { d: 'm13.75 1.8789c-5.9487 0.19352-10.865 4.5652-11.082 11.686v81.445c-1e-7 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-64.982c0.02794-3.4488 3.0988-3.5551 4.2031-1.1562l16.615 59.059c1.4393 5.3711 5.1083 7.9633 8.7656 7.9473 3.6573 0.01603 7.3263-2.5762 8.7656-7.9473l16.615-59.059c1.1043-2.3989 4.1752-2.2925 4.2031 1.1562v64.982c0 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-81.445c-0.17732-7.0807-5.1334-11.492-11.082-11.686-5.9487-0.19352-12.652 3.8309-15.609 13.619l-15.686 57.334-15.686-57.334c-2.9569-9.7882-9.6607-13.813-15.609-13.619zm239.19 0.074219c-2.216 0-4 1.784-4 4v89.057c0 2.216 1.784 4 4 4h4.793c2.216 0 3.9868-1.784 4-4l0.10644-17.94c0.0734-0.07237 12.175-13.75 12.175-13.75 5.6772 11.091 11.404 22.158 17.113 33.232 1.0168 1.9689 3.4217 2.7356 5.3906 1.7188l4.2578-2.1992c1.9689-1.0168 2.7356-3.4217 1.7188-5.3906-6.4691-12.585-12.958-25.16-19.442-37.738l17.223-19.771c1.4555-1.671 1.2803-4.189-0.39062-5.6445l-3.6133-3.1465c-0.73105-0.63679-1.6224-0.96212-2.5176-0.98633-1.151-0.03113-2.3063 0.43508-3.125 1.375l-28.896 33.174v-51.99c0-2.216-1.784-4-4-4zm-58.255 23.316c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312l-0.125-7.8457c0-2.216-1.784-4-4-4h-4.6524c-2.216 0-4 1.784-4 4l3e-3 6.7888c3e-3 3.8063-1.5601 9.3694-8.4716 9.3694h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.6937 0 8.3697 5.2207 8.4687 11.828v2.2207c0 2.216 1.784 4 4 4h4.6524c2.216 0 4-1.784 4-4l0.125-5.7363c0-10.699-8.6117-19.312-19.311-19.312zm-72.182 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }),
7
- s('path', { opacity: 1, fill: 'currentColor', d: 'm331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z' }),
7
+ s('path', { fill: 'currentColor', d: 'm331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z' }),
8
8
  s('path', { d: 'm392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }))
9
9
 
10
10
  export const TimerIcon = () =>
@@ -0,0 +1,69 @@
1
+ export class QueryParamBool {
2
+ constructor(param) {
3
+ this.param = param
4
+ this.value = this.#init()
5
+ }
6
+
7
+ #init() {
8
+ const qs = new URLSearchParams(globalThis.location?.search)
9
+ if (qs.has(this.param))
10
+ return qs.get(this.param) !== '0'
11
+ const stored = globalThis.localStorage?.getItem(this.param) !== '0'
12
+ if (!stored)
13
+ this.#applyToUrl(false)
14
+ return stored
15
+ }
16
+
17
+ toggle() {
18
+ this.value = !this.value
19
+ if (this.value)
20
+ globalThis.localStorage?.removeItem(this.param)
21
+ else
22
+ globalThis.localStorage?.setItem(this.param, '0')
23
+ this.#applyToUrl(this.value)
24
+ }
25
+
26
+ #applyToUrl(nextVal) {
27
+ const url = new URL(globalThis.location?.href)
28
+ if (nextVal)
29
+ url.searchParams.delete(this.param)
30
+ else
31
+ url.searchParams.set(this.param, '0')
32
+ history.replaceState(null, '', url)
33
+ }
34
+ }
35
+
36
+
37
+ export class LocalStorageSet {
38
+ constructor(key) {
39
+ this.key = key
40
+ this.value = this.#parse()
41
+ }
42
+
43
+ add(item) {
44
+ this.value.add(item)
45
+ this.#persist()
46
+ }
47
+
48
+ delete(item) {
49
+ this.value.delete(item)
50
+ this.#persist()
51
+ }
52
+
53
+ has(item) {
54
+ return this.value.has(item)
55
+ }
56
+
57
+ #parse() {
58
+ try {
59
+ return new Set(JSON.parse(globalThis.localStorage?.getItem(this.key) || '[]'))
60
+ }
61
+ catch {
62
+ return new Set()
63
+ }
64
+ }
65
+
66
+ #persist() {
67
+ globalThis.localStorage?.setItem(this.key, JSON.stringify([...this.value]))
68
+ }
69
+ }
@@ -0,0 +1,16 @@
1
+ export function classNames(...args) {
2
+ return args.filter(Boolean).join(' ')
3
+ }
4
+
5
+
6
+ export function extractClassNames({ cssRules }) {
7
+ // Class names must begin with _ or a letter, then it can have numbers and hyphens
8
+ // TODO think about tag.className selectors
9
+ const reClassName = /(?:^|[\s,{>])&?\s*\.([a-zA-Z_][\w-]*)/g
10
+ const cNames = {}
11
+ let match
12
+ for (const rule of cssRules)
13
+ while (match = reClassName.exec(rule.cssText))
14
+ cNames[match[1]] = match[1]
15
+ return cNames
16
+ }
@@ -0,0 +1,74 @@
1
+ import { test } from 'node:test'
2
+ import { deepEqual, equal } from 'node:assert/strict'
3
+ import { extractClassNames, classNames } from './css.js'
4
+
5
+
6
+ test('classNames', () => equal(classNames('a', false && 'b'), 'a'))
7
+
8
+
9
+ test('extractClassNames', () => {
10
+ const cssRules = [
11
+ { cssText: '.TopLevelPascal { color: red; }' },
12
+ { cssText: '.topLevelCamel { color: blue; }' },
13
+ { cssText: '.top_level_snake { color: green; }' },
14
+ { cssText: '.top-level-kebab { color: yellow; }' },
15
+ { cssText: '.Level2Parent {\n & .level2ChildCamel { color: purple; }\n}' },
16
+ { cssText: '.level2Base {\n &.level2ModifierCamel { font-weight: bold; }\n}' },
17
+ { cssText: '.Level3Parent {\n & .level3ChildCamel {\n & .level3_grand_child_snake { color: orange; }\n}\n}' },
18
+ { cssText: '.pseudoParent {\n &:hover { background: red; }\n & .pseudoNestedChild { color: pink; }\n}' },
19
+ { cssText: '.multiClass1, .multi_class_2 { padding: 10px; }' },
20
+ { cssText: '.combParent {\n & > .combChildDirect { margin: 5px; }\n}' },
21
+ { cssText: '.siblingBase {\n & + .siblingAdjacent { border: 1px solid; }\n}' },
22
+ { cssText: '@media (max-width: 768px) {\n .mediaQueryClass {\n & .mqNestedChild { display: none; }\n}\n}' },
23
+ { cssText: '.class_with_123_numbers { color: cyan; }' },
24
+ { cssText: '._privateStyleClass { opacity: 0.5; }' },
25
+ { cssText: '.stringTest { content: ".shouldNotBeExtracted"; background: url(".alsoIgnored"); }' },
26
+ { cssText: '.ComplexRoot {\n & .level2-kebab {\n &.level2ModCamel { color: red; }\n & .level3_snake {\n & .level4PascalChild { color: blue; }\n}\n}\n}' }
27
+ ]
28
+
29
+ const expected = {
30
+ TopLevelPascal: null,
31
+ topLevelCamel: null,
32
+ top_level_snake: null,
33
+ 'top-level-kebab': null,
34
+
35
+ Level2Parent: null,
36
+ level2ChildCamel: null,
37
+ level2Base: null,
38
+ level2ModifierCamel: null,
39
+
40
+ Level3Parent: null,
41
+ level3ChildCamel: null,
42
+ level3_grand_child_snake: null,
43
+
44
+ pseudoParent: null,
45
+ pseudoNestedChild: null,
46
+
47
+ multiClass1: null,
48
+ multi_class_2: null,
49
+
50
+ combParent: null,
51
+ combChildDirect: null,
52
+
53
+ siblingBase: null,
54
+ siblingAdjacent: null,
55
+
56
+ mediaQueryClass: null,
57
+ mqNestedChild: null,
58
+
59
+ class_with_123_numbers: null,
60
+ _privateStyleClass: null,
61
+
62
+ stringTest: null,
63
+
64
+ ComplexRoot: null,
65
+ 'level2-kebab': null,
66
+ level2ModCamel: null,
67
+ level3_snake: null,
68
+ level4PascalChild: null
69
+ }
70
+ for (const k of Object.keys(expected))
71
+ expected[k] = k
72
+
73
+ deepEqual(extractClassNames({ cssRules }), expected)
74
+ })
@@ -2,9 +2,6 @@ export function t(translation) {
2
2
  return translation[0]
3
3
  }
4
4
 
5
- export function classNames(...args) {
6
- return args.filter(Boolean).join(' ')
7
- }
8
5
 
9
6
  export function createElement(tag, props, ...children) {
10
7
  const elem = document.createElement(tag)
@@ -20,6 +17,7 @@ export function createElement(tag, props, ...children) {
20
17
  return elem
21
18
  }
22
19
 
20
+
23
21
  export function createSvgElement(tag, props, ...children) {
24
22
  const elem = document.createElementNS('http://www.w3.org/2000/svg', tag)
25
23
  for (const [k, v] of Object.entries(props))
@@ -28,6 +26,7 @@ export function createSvgElement(tag, props, ...children) {
28
26
  return elem
29
27
  }
30
28
 
29
+
31
30
  export function Fragment(...args) {
32
31
  const frag = new DocumentFragment()
33
32
  for (const arg of args)
@@ -45,7 +44,6 @@ export function restoreFocus(cb) {
45
44
  if (focusQuery)
46
45
  document.querySelector(focusQuery)?.focus()
47
46
  }
48
-
49
47
  function selectorFor(elem) {
50
48
  if (!(elem instanceof Element))
51
49
  return
@@ -68,20 +66,3 @@ function selectorFor(elem) {
68
66
  }
69
67
  return path.reverse().join('>')
70
68
  }
71
-
72
-
73
- export function defineClassNames(sheet) {
74
- Object.assign(sheet, extractClassNames(sheet))
75
- }
76
-
77
- export function extractClassNames({ cssRules }) {
78
- // Class names must begin with _ or a letter, then it can have numbers and hyphens
79
- const reClassName = /(?:^|[\s,{>])&?\s*\.([a-zA-Z_][\w-]*)/g
80
- const cNames = {}
81
- let match
82
- for (const rule of cssRules)
83
- while (match = reClassName.exec(rule.cssText))
84
- cNames[match[1]] = match[1]
85
- return cNames
86
- }
87
-
@@ -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, 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,22 +8,23 @@ 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
+ import { cookie } from './cookie.js'
13
14
  import { config, setup } from './config.js'
14
- import { apiPatchReqs, apiGetReqs } from './Api.js'
15
+ import { apiPatchReqs, apiGetReqs, CLIENT_DIR } from './Api.js'
15
16
 
16
17
  import { dispatchMock } from './MockDispatcher.js'
17
-
18
18
  import * as mockBrokerCollection from './mockBrokersCollection.js'
19
19
 
20
- import { watchDevSPA } from './WatcherDevClient.js'
20
+ import { watchDevSPA } from './utils/WatcherDevClient.js'
21
21
  import { watchMocksDir } from './Watcher.js'
22
22
 
23
23
 
24
24
  export function Mockaton(options) {
25
25
  return new Promise((resolve, reject) => {
26
26
  setup(options)
27
+ cookie.init(config.cookies)
27
28
  mockBrokerCollection.init()
28
29
 
29
30
  if (config.watcherEnabled) {
@@ -31,7 +32,7 @@ export function Mockaton(options) {
31
32
  watchMocksDir()
32
33
  }
33
34
  if (config.hotReload)
34
- watchDevSPA()
35
+ watchDevSPA(CLIENT_DIR)
35
36
 
36
37
  const server = createServer({ IncomingMessage, ServerResponse }, onRequest)
37
38
  server.on('error', reject)
@@ -47,10 +48,17 @@ export function Mockaton(options) {
47
48
  }
48
49
 
49
50
  async function onRequest(req, response) {
51
+ response.setHeader('Server', `Mockaton ${pkgJSON.version}`)
52
+
50
53
  response.on('error', logger.warn)
51
54
 
52
- response.setHeader('Server', `Mockaton ${pkgJSON.version}`)
53
- 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
+ })
54
62
 
55
63
  const url = req.url || ''
56
64
 
@@ -85,7 +93,9 @@ async function onRequest(req, response) {
85
93
  catch (error) {
86
94
  if (error instanceof BodyReaderError)
87
95
  response.unprocessable(`${error.name}: ${error.message}`)
88
- else
96
+ else {
97
+ logger.error(500, req.url, error?.message || error, error?.stack || '')
89
98
  response.internalServerError(error)
99
+ }
90
100
  }
91
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)