mockaton 13.3.1 → 13.3.3
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/package.json +1 -1
- package/src/client/app-store.js +3 -16
- package/src/client/app.css +29 -15
- package/src/client/app.js +26 -18
- package/src/client/dirStructure.js +83 -0
- package/src/client/dirStructure.test.js +86 -0
- package/src/server/Api.js +1 -0
- package/src/server/Mockaton.js +1 -1
- package/src/server/Watcher.js +0 -1
- package/src/server/mockBrokersCollection.js +2 -6
package/package.json
CHANGED
package/src/client/app-store.js
CHANGED
|
@@ -140,21 +140,7 @@ export const store = {
|
|
|
140
140
|
store.brokersByMethod[method][urlMask] = broker
|
|
141
141
|
},
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
const groups = []
|
|
145
|
-
let g = null
|
|
146
|
-
for (const row of store._brokersAsRowsByMethod(method)) {
|
|
147
|
-
const folder = row.urlMask.substring(0, row.urlMask.lastIndexOf('/') + 1)
|
|
148
|
-
if (!g || g.folder !== folder) {
|
|
149
|
-
g = { folder, children: [] }
|
|
150
|
-
groups.push(g)
|
|
151
|
-
}
|
|
152
|
-
g.children.push(row)
|
|
153
|
-
}
|
|
154
|
-
return groups
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
_brokersAsRowsByMethod(method) {
|
|
143
|
+
brokersAsRowsByMethod(method) {
|
|
158
144
|
const rows = store._brokersAsArray(method)
|
|
159
145
|
.map(b => new BrokerRowModel(b, store.canProxy))
|
|
160
146
|
.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
|
|
@@ -174,7 +160,7 @@ export const store = {
|
|
|
174
160
|
arr.push(...Object.values(brokers))
|
|
175
161
|
return arr
|
|
176
162
|
},
|
|
177
|
-
|
|
163
|
+
|
|
178
164
|
|
|
179
165
|
previewLink(method, urlMask) {
|
|
180
166
|
store.setChosenLink(method, urlMask)
|
|
@@ -285,6 +271,7 @@ export class BrokerRowModel {
|
|
|
285
271
|
method = ''
|
|
286
272
|
urlMask = ''
|
|
287
273
|
urlMaskDittoed = ['', '']
|
|
274
|
+
children = []
|
|
288
275
|
#broker = /** @type ClientMockBroker */ {}
|
|
289
276
|
#canProxy = false
|
|
290
277
|
|
package/src/client/app.css
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
--colorLabel: light-dark(#555, #aaa);
|
|
13
13
|
--colorText: light-dark(#000, #fff);
|
|
14
14
|
|
|
15
|
-
--colorAccent: light-dark(#
|
|
16
|
-
--colorHover: light-dark(#
|
|
15
|
+
--colorAccent: light-dark(#0081ff, #2495ff);
|
|
16
|
+
--colorHover: light-dark(#dbedff, #062d59);
|
|
17
17
|
|
|
18
18
|
--colorRed: light-dark(#da0f00, #f41606);
|
|
19
19
|
--colorPink: light-dark(#ed206a, #f92672);
|
|
@@ -50,6 +50,11 @@ body {
|
|
|
50
50
|
corner-shape: squircle;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
label,
|
|
54
|
+
button {
|
|
55
|
+
user-select: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
a:focus-visible,
|
|
54
59
|
input:focus-visible,
|
|
55
60
|
select:focus-visible,
|
|
@@ -415,6 +420,7 @@ main {
|
|
|
415
420
|
.Table {
|
|
416
421
|
height: 100%;
|
|
417
422
|
padding: 16px;
|
|
423
|
+
padding-bottom: 64px;
|
|
418
424
|
padding-left: 12px;
|
|
419
425
|
user-select: none;
|
|
420
426
|
overflow-y: auto;
|
|
@@ -449,6 +455,7 @@ main {
|
|
|
449
455
|
|
|
450
456
|
.FolderGroup {
|
|
451
457
|
position: relative;
|
|
458
|
+
border-radius: var(--radius);
|
|
452
459
|
|
|
453
460
|
> summary {
|
|
454
461
|
left: 0;
|
|
@@ -456,6 +463,7 @@ main {
|
|
|
456
463
|
overflow: hidden;
|
|
457
464
|
align-items: center;
|
|
458
465
|
padding: 5px 0;
|
|
466
|
+
font-weight: 500;
|
|
459
467
|
font-size: 12px;
|
|
460
468
|
list-style: none;
|
|
461
469
|
cursor: pointer;
|
|
@@ -465,7 +473,6 @@ main {
|
|
|
465
473
|
text-overflow: ellipsis;
|
|
466
474
|
border-radius: var(--radius);
|
|
467
475
|
|
|
468
|
-
|
|
469
476
|
&:active {
|
|
470
477
|
cursor: grabbing;
|
|
471
478
|
}
|
|
@@ -475,7 +482,7 @@ main {
|
|
|
475
482
|
}
|
|
476
483
|
|
|
477
484
|
&:hover {
|
|
478
|
-
|
|
485
|
+
background: var(--colorHover);
|
|
479
486
|
}
|
|
480
487
|
|
|
481
488
|
.FolderName {
|
|
@@ -510,17 +517,23 @@ main {
|
|
|
510
517
|
}
|
|
511
518
|
}
|
|
512
519
|
|
|
513
|
-
&[open]
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
left: 0;
|
|
517
|
-
width: 16px;
|
|
518
|
-
|
|
519
|
-
.FolderChevron {
|
|
520
|
-
transform: rotate(0deg);
|
|
520
|
+
&[open] {
|
|
521
|
+
&:has(summary:hover):not(:has(details:hover)) {
|
|
522
|
+
background: linear-gradient(90deg, var(--colorHover), var(--colorBg));
|
|
521
523
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
+
|
|
525
|
+
> summary {
|
|
526
|
+
position: absolute;
|
|
527
|
+
top: 0;
|
|
528
|
+
left: 0;
|
|
529
|
+
width: 16px;
|
|
530
|
+
|
|
531
|
+
.FolderChevron {
|
|
532
|
+
transform: rotate(0deg);
|
|
533
|
+
}
|
|
534
|
+
.FolderName {
|
|
535
|
+
display: none;
|
|
536
|
+
}
|
|
524
537
|
}
|
|
525
538
|
}
|
|
526
539
|
}
|
|
@@ -556,6 +569,7 @@ main {
|
|
|
556
569
|
padding: 6px 8px;
|
|
557
570
|
margin-right: -2px;
|
|
558
571
|
margin-left: 4px;
|
|
572
|
+
font-weight: 500;
|
|
559
573
|
border-radius: var(--radius);
|
|
560
574
|
word-break: break-word;
|
|
561
575
|
|
|
@@ -567,7 +581,7 @@ main {
|
|
|
567
581
|
background: var(--colorAccent);
|
|
568
582
|
}
|
|
569
583
|
.dittoDir {
|
|
570
|
-
opacity: 0.
|
|
584
|
+
opacity: 0.8;
|
|
571
585
|
filter: saturate(0.1);
|
|
572
586
|
}
|
|
573
587
|
}
|
package/src/client/app.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createElement as r, t, classNames, restoreFocus, Fragment, defineClassN
|
|
|
3
3
|
import { store } from './app-store.js'
|
|
4
4
|
import { API } from './ApiConstants.js'
|
|
5
5
|
import { Header } from './app-header.js'
|
|
6
|
+
import { dirStructure } from './dirStructure.js'
|
|
6
7
|
import { PayloadViewer, previewMock } from './app-payload-viewer.js'
|
|
7
8
|
import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
|
|
8
9
|
|
|
@@ -100,21 +101,30 @@ function MockList() {
|
|
|
100
101
|
r('div', {
|
|
101
102
|
className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
|
|
102
103
|
}, method),
|
|
103
|
-
FolderGroups(store.
|
|
104
|
+
FolderGroups(store.brokersAsRowsByMethod(method))))
|
|
104
105
|
|
|
105
|
-
return FolderGroups(store.
|
|
106
|
+
return FolderGroups(store.brokersAsRowsByMethod('*'))
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
function FolderGroups(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
function FolderGroups(bRows) {
|
|
110
|
+
const res = []
|
|
111
|
+
for (const b of dirStructure(bRows)) {
|
|
112
|
+
if (!b.children.length)
|
|
113
|
+
res.push(Row(b))
|
|
114
|
+
else
|
|
115
|
+
res.push(FolderGroup(b))
|
|
116
|
+
}
|
|
117
|
+
return res
|
|
118
|
+
}
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
function FolderGroup(broker) {
|
|
121
|
+
const folder = broker.urlMask
|
|
122
|
+
const children = broker.children
|
|
123
|
+
return (
|
|
124
|
+
r('details', {
|
|
114
125
|
className: CSS.FolderGroup,
|
|
115
126
|
open: !store.collapsedFolders.has(folder),
|
|
116
127
|
onToggle() {
|
|
117
|
-
// TODO alt+click exclusive open
|
|
118
128
|
store.setFolderCollapsed(folder, !this.open)
|
|
119
129
|
}
|
|
120
130
|
},
|
|
@@ -127,15 +137,14 @@ function FolderGroups(groups) {
|
|
|
127
137
|
store.canProxy && CSS.canProxy)
|
|
128
138
|
},
|
|
129
139
|
folder + '…')),
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
Row(broker),
|
|
141
|
+
children.map(c => c.children.length
|
|
142
|
+
? FolderGroup(c)
|
|
143
|
+
: Row(c))))
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
* @param {number} i
|
|
137
|
-
*/
|
|
138
|
-
function Row(row, i) {
|
|
146
|
+
/** @param {BrokerRowModel} row */
|
|
147
|
+
function Row(row) {
|
|
139
148
|
const { method, urlMask } = row
|
|
140
149
|
return (
|
|
141
150
|
r('div', {
|
|
@@ -163,7 +172,7 @@ function Row(row, i) {
|
|
|
163
172
|
|
|
164
173
|
!store.groupByMethod && r('span', { className: CSS.Method }, method),
|
|
165
174
|
|
|
166
|
-
PreviewLink(method, urlMask, row.urlMaskDittoed
|
|
175
|
+
PreviewLink(method, urlMask, row.urlMaskDittoed),
|
|
167
176
|
|
|
168
177
|
MockSelector(row)))
|
|
169
178
|
}
|
|
@@ -208,7 +217,7 @@ function renderRow(method, urlMask) {
|
|
|
208
217
|
|
|
209
218
|
|
|
210
219
|
|
|
211
|
-
function PreviewLink(method, urlMask, urlMaskDittoed
|
|
220
|
+
function PreviewLink(method, urlMask, urlMaskDittoed) {
|
|
212
221
|
function onClick(event) {
|
|
213
222
|
event.preventDefault()
|
|
214
223
|
store.previewLink(method, urlMask)
|
|
@@ -219,7 +228,6 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
|
|
|
219
228
|
r('a', {
|
|
220
229
|
className: classNames(CSS.PreviewLink, isChosen && CSS.chosen),
|
|
221
230
|
href: urlMask,
|
|
222
|
-
autofocus,
|
|
223
231
|
onClick
|
|
224
232
|
}, ditto
|
|
225
233
|
? [r('span', { className: CSS.dittoDir }, ditto), tail]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
class TrieNode {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.items = []
|
|
4
|
+
this.kids = new Map()
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// TODO it should ignore query string
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{method: string, urlMask:string, children: BrokerLite[]}} BrokerLite
|
|
12
|
+
* @param {BrokerLite[]} brokers
|
|
13
|
+
* @returns {BrokerLite[]}
|
|
14
|
+
*/
|
|
15
|
+
export function dirStructure(brokers) {
|
|
16
|
+
const root = new TrieNode()
|
|
17
|
+
|
|
18
|
+
for (let b of brokers) {
|
|
19
|
+
let curr = root
|
|
20
|
+
for (const seg of b.urlMask.split('/').filter(Boolean)) {
|
|
21
|
+
if (!curr.kids.has(seg))
|
|
22
|
+
curr.kids.set(seg, new TrieNode())
|
|
23
|
+
curr = curr.kids.get(seg)
|
|
24
|
+
}
|
|
25
|
+
curr.items.push(b)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = []
|
|
29
|
+
for (const child of root.kids.values())
|
|
30
|
+
result.push(...convertNode(child))
|
|
31
|
+
|
|
32
|
+
if (root.items.length) {
|
|
33
|
+
const elems = [root.items[0], ...result]
|
|
34
|
+
for (let i = 1; i < root.items.length; i++)
|
|
35
|
+
elems.push(root.items[i])
|
|
36
|
+
|
|
37
|
+
const parentNode = elems[0]
|
|
38
|
+
for (let i = 1; i < elems.length; i++)
|
|
39
|
+
parentNode.children.push(elems[i])
|
|
40
|
+
|
|
41
|
+
return [parentNode]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Recursively converts a TrieNode into a flattened, nested array of objects.
|
|
49
|
+
* Flattens the tree by having the first available route at a given directory level
|
|
50
|
+
* act as the parent wrapper for the remaining items and subdirectories.
|
|
51
|
+
*
|
|
52
|
+
* @param {TrieNode} node
|
|
53
|
+
* @returns {BrokerLite[]}
|
|
54
|
+
*/
|
|
55
|
+
function convertNode(node) {
|
|
56
|
+
const childNodes = []
|
|
57
|
+
for (const child of node.kids.values())
|
|
58
|
+
childNodes.push(...convertNode(child))
|
|
59
|
+
|
|
60
|
+
const elems = []
|
|
61
|
+
if (node.items.length) {
|
|
62
|
+
elems.push(node.items[0])
|
|
63
|
+
elems.push(...childNodes)
|
|
64
|
+
for (let i = 1; i < node.items.length; i++)
|
|
65
|
+
elems.push(node.items[i])
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
elems.push(...childNodes)
|
|
69
|
+
|
|
70
|
+
if (!elems.length)
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
const parentNode = elems[0]
|
|
74
|
+
|
|
75
|
+
if (node.items.length || !parentNode.children.length) {
|
|
76
|
+
for (let i = 1; i < elems.length; i++)
|
|
77
|
+
parentNode.children.push(elems[i])
|
|
78
|
+
return [parentNode]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return elems
|
|
82
|
+
}
|
|
83
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { deepEqual } from 'node:assert/strict'
|
|
3
|
+
import { dirStructure } from './dirStructure.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const input = [
|
|
7
|
+
{ children: [], method: 'GET', urlMask: '/api/user' },
|
|
8
|
+
{ children: [], method: 'GET', urlMask: '/api/user/avatar' },
|
|
9
|
+
{ children: [], method: 'GET', urlMask: '/api/video/[id]' },
|
|
10
|
+
{ children: [], method: 'GET', urlMask: '/index.html' },
|
|
11
|
+
{ children: [], method: 'GET', urlMask: '/media/file-a.txt' },
|
|
12
|
+
{ children: [], method: 'GET', urlMask: '/media/file-b.txt' },
|
|
13
|
+
{ children: [], method: 'GET', urlMask: '/media/sub/file-aa.txt' },
|
|
14
|
+
{ children: [], method: 'GET', urlMask: '/media/sub/file-bb.txt' },
|
|
15
|
+
{ children: [], method: 'POST', urlMask: '/api/user' },
|
|
16
|
+
{ children: [], method: 'POST', urlMask: '/api/user/avatar/foo' },
|
|
17
|
+
{ children: [], method: 'PATCH', urlMask: '/api/user' }
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const expected = [
|
|
22
|
+
{
|
|
23
|
+
urlMask: '/api/user',
|
|
24
|
+
method: 'GET',
|
|
25
|
+
children: [
|
|
26
|
+
{
|
|
27
|
+
urlMask: '/api/user/avatar',
|
|
28
|
+
method: 'GET',
|
|
29
|
+
children: [
|
|
30
|
+
{
|
|
31
|
+
urlMask: '/api/user/avatar/foo',
|
|
32
|
+
method: 'POST',
|
|
33
|
+
children: []
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
urlMask: '/api/user',
|
|
39
|
+
method: 'POST',
|
|
40
|
+
children: []
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
urlMask: '/api/user',
|
|
44
|
+
method: 'PATCH',
|
|
45
|
+
children: []
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
urlMask: '/api/video/[id]',
|
|
51
|
+
method: 'GET',
|
|
52
|
+
children: []
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
urlMask: '/index.html',
|
|
56
|
+
method: 'GET',
|
|
57
|
+
children: []
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
urlMask: '/media/file-a.txt',
|
|
61
|
+
method: 'GET',
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
urlMask: '/media/file-b.txt',
|
|
65
|
+
method: 'GET',
|
|
66
|
+
children: []
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
urlMask: '/media/sub/file-aa.txt',
|
|
70
|
+
method: 'GET',
|
|
71
|
+
children: [
|
|
72
|
+
{
|
|
73
|
+
urlMask: '/media/sub/file-bb.txt',
|
|
74
|
+
method: 'GET',
|
|
75
|
+
children: []
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
test('acceptance', () => {
|
|
84
|
+
deepEqual(dirStructure(input), expected)
|
|
85
|
+
})
|
|
86
|
+
|
package/src/server/Api.js
CHANGED
package/src/server/Mockaton.js
CHANGED
|
@@ -5,8 +5,8 @@ import pkgJSON from '../../package.json' with { type: 'json' }
|
|
|
5
5
|
|
|
6
6
|
import { logger } from './utils/logger.js'
|
|
7
7
|
import { ServerResponse } from './utils/HttpServerResponse.js'
|
|
8
|
-
import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
|
|
9
8
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
9
|
+
import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
|
|
10
10
|
|
|
11
11
|
import { API } from '../client/ApiConstants.js'
|
|
12
12
|
|
package/src/server/Watcher.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { basename } from 'node:path'
|
|
2
2
|
|
|
3
|
-
import { cookie } from './cookie.js'
|
|
4
3
|
import { MockBroker } from './MockBroker.js'
|
|
5
4
|
import { parseFilename } from '../client/Filename.js'
|
|
6
5
|
import { listFilesRecursively } from './utils/fs.js'
|
|
@@ -28,19 +27,16 @@ export const all = () => collection
|
|
|
28
27
|
|
|
29
28
|
export function init() {
|
|
30
29
|
collection = {}
|
|
31
|
-
cookie.init(config.cookies)
|
|
32
|
-
|
|
33
30
|
listFilesRecursively(config.mocksDir)
|
|
34
31
|
.sort()
|
|
35
32
|
.forEach(f => registerMock(f))
|
|
36
|
-
|
|
37
33
|
forEachBroker(b => b.selectDefaultFile())
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
/** @returns {boolean} registered */
|
|
41
37
|
export function registerMock(file, isFromWatcher = false) {
|
|
42
|
-
if (brokerByFilename(file)?.hasMock(file)
|
|
43
|
-
|
|
38
|
+
if (brokerByFilename(file)?.hasMock(file) ||
|
|
39
|
+
!isFileAllowed(basename(file)))
|
|
44
40
|
return false
|
|
45
41
|
|
|
46
42
|
const { method, urlMask } = parseFilename(file)
|