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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.3.1",
5
+ "version": "13.3.3",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -140,21 +140,7 @@ export const store = {
140
140
  store.brokersByMethod[method][urlMask] = broker
141
141
  },
142
142
 
143
- folderGroupsByMethod(method) {
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
 
@@ -12,8 +12,8 @@
12
12
  --colorLabel: light-dark(#555, #aaa);
13
13
  --colorText: light-dark(#000, #fff);
14
14
 
15
- --colorAccent: light-dark(#0059dd, #2495ff);
16
- --colorHover: light-dark(#bbe0ff, #062d59);
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
- color: var(--colorText);
485
+ background: var(--colorHover);
479
486
  }
480
487
 
481
488
  .FolderName {
@@ -510,17 +517,23 @@ main {
510
517
  }
511
518
  }
512
519
 
513
- &[open] > summary {
514
- position: absolute;
515
- top: 0;
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
- .FolderName {
523
- display: none;
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.9;
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.folderGroupsByMethod(method))))
104
+ FolderGroups(store.brokersAsRowsByMethod(method))))
104
105
 
105
- return FolderGroups(store.folderGroupsByMethod('*'))
106
+ return FolderGroups(store.brokersAsRowsByMethod('*'))
106
107
  }
107
108
 
108
- function FolderGroups(groups) {
109
- return groups.map(({ folder, children }) => {
110
- if (children.length === 1)
111
- return Row(children[0], 0)
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
- return r('details', {
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
- children.map(Row))
131
- })
140
+ Row(broker),
141
+ children.map(c => c.children.length
142
+ ? FolderGroup(c)
143
+ : Row(c))))
132
144
  }
133
145
 
134
- /**
135
- * @param {BrokerRowModel} row
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, i === 0),
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, autofocus) {
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
@@ -88,6 +88,7 @@ function getState(_, response) {
88
88
 
89
89
  function reset(_, response) {
90
90
  mockBrokersCollection.init()
91
+ cookie.init(config.cookies)
91
92
  response.ok()
92
93
  uiSyncVersion.increment()
93
94
  }
@@ -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
 
@@ -4,7 +4,6 @@ import { EventEmitter } from 'node:events'
4
4
 
5
5
  import { config } from './config.js'
6
6
  import { isFile, isDirectory } from './utils/fs.js'
7
-
8
7
  import * as mockBrokerCollection from './mockBrokersCollection.js'
9
8
 
10
9
 
@@ -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
- || !isFileAllowed(basename(file)))
38
+ if (brokerByFilename(file)?.hasMock(file) ||
39
+ !isFileAllowed(basename(file)))
44
40
  return false
45
41
 
46
42
  const { method, urlMask } = parseFilename(file)