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.
- 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 -2
- package/src/client/app-payload-viewer.js +4 -7
- package/src/client/app-store.js +20 -78
- package/src/client/app-store.test.js +1 -25
- package/src/client/app.css +9 -5
- package/src/client/app.js +7 -7
- package/src/client/dir/dittoSplitPaths.js +25 -0
- package/src/client/dir/dittoSplitPaths.test.js +28 -0
- package/src/client/{dirStructure.js → dir/groupByFolder.js} +17 -13
- package/src/client/dir/groupByFolder.test.js +82 -0
- 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/utils/css.test.js +74 -0
- package/src/client/{dom-utils.js → utils/dom.js} +2 -21
- 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 +18 -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/dirStructure.test.js +0 -81
- package/src/client/dom-utils.test.js +0 -76
- package/src/client/watcherDev.js +0 -39
package/src/client/graphics.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createSvgElement as s } from './dom
|
|
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', {
|
|
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
|
|
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,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.
|
|
53
|
-
|
|
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(
|
|
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)
|