mockaton 13.11.1 → 13.11.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.
Files changed (35) hide show
  1. package/README.md +11 -8
  2. package/package.json +1 -1
  3. package/skills/mockaton/SKILL.md +8 -5
  4. package/src/client/ApiConstants.js +1 -1
  5. package/src/client/IndexHtml.js +1 -1
  6. package/src/client/app-header.css +231 -0
  7. package/src/client/app-header.js +13 -6
  8. package/src/client/app-mock-list.css +340 -0
  9. package/src/client/app-mock-list.js +370 -0
  10. package/src/client/app-payload-viewer.css +90 -0
  11. package/src/client/app-payload-viewer.js +4 -4
  12. package/src/client/app-store.js +1 -0
  13. package/src/client/app.css +0 -654
  14. package/src/client/app.js +18 -360
  15. package/src/client/utils/css.js +7 -0
  16. package/src/client/utils/watcherDev.js +4 -1
  17. package/src/server/Api.js +54 -53
  18. package/src/server/MockDispatcher.js +6 -5
  19. package/src/server/Mockaton.js +11 -12
  20. package/src/server/Mockaton.test.js +65 -51
  21. package/src/server/ProxyRelay.js +3 -2
  22. package/src/server/UrlParsers.js +1 -1
  23. package/src/server/UrlParsers.test.js +1 -1
  24. package/src/server/cli.js +37 -33
  25. package/src/server/cli.test.js +2 -4
  26. package/src/server/{Watcher.js → stores/Watcher.js} +18 -9
  27. package/src/server/{mockBrokersCollection.js → stores/brokers.js} +3 -3
  28. package/src/server/{config.js → stores/config.js} +28 -14
  29. package/src/server/utils/HttpServerResponse.test.js +5 -7
  30. package/src/server/utils/WatcherDevClient.js +1 -1
  31. package/src/server/utils/openInBrowser.js +15 -10
  32. package/www/src/assets/openapi.json +1 -1
  33. /package/src/server/{resolverBypassImportCache.js → ResolverBypassImportCache.js} +0 -0
  34. /package/src/server/{resolverResolveExtensionless.js → ResolverResolveExtensionless.js} +0 -0
  35. /package/src/server/{cookie.js → stores/cookies.js} +0 -0
package/src/client/app.js CHANGED
@@ -3,14 +3,12 @@ import { createElement as r, t, restoreFocus, Fragment } from './utils/dom.js'
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 { adoptSheet } from './utils/css.js'
6
7
  import { PayloadViewer, previewMock } from './app-payload-viewer.js'
7
- import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
8
+ import { MockList, initKeyboardNavigation, renderRow } from './app-mock-list.js'
8
9
 
9
10
  import CSS from './app.css' with { type: 'css' }
10
- import { extractClassNames, classNames } from './utils/css.js'
11
- document.adoptedStyleSheets.push(CSS)
12
- Object.assign(CSS, extractClassNames(CSS))
13
-
11
+ adoptSheet(CSS, './app.css')
14
12
 
15
13
  store.onError = onError
16
14
  store.render = render
@@ -19,22 +17,22 @@ store.renderRow = renderRow
19
17
  onRealTimeUpdate(store.fetchState)
20
18
  initKeyboardNavigation()
21
19
 
22
- let mounted = false
23
20
  function render() {
24
21
  restoreFocus(() => document.body.replaceChildren(App()))
25
22
  if (store.hasChosenLink) previewMock()
26
- if (!mounted) LeftSide.$('a')?.focus()
27
- mounted = true
23
+ if (!store.mounted) LeftSide.$('a')?.focus()
24
+ store.mounted = true
28
25
  }
29
26
 
30
27
  function App() {
31
- return Fragment(
32
- Header(),
33
- r('main', null,
34
- LeftSide(),
35
- r('div', { className: CSS.rightSide },
36
- Resizer(LeftSide.ref),
37
- PayloadViewer())))
28
+ return (
29
+ Fragment(
30
+ Header(),
31
+ r('main', null,
32
+ LeftSide(),
33
+ r('div', { className: CSS.rightSide },
34
+ Resizer(),
35
+ PayloadViewer()))))
38
36
  }
39
37
 
40
38
  function LeftSide() {
@@ -44,305 +42,20 @@ function LeftSide() {
44
42
  style: { width: LeftSide.ref.width },
45
43
  className: CSS.leftSide
46
44
  },
47
- r('div', { className: CSS.SubToolbar },
48
- GroupByMethod(),
49
- BulkSelector()),
50
- r('div', { className: CSS.Table }, MockList())))
45
+ MockList()))
51
46
  }
52
47
  LeftSide.ref = { width: undefined }
53
48
  LeftSide.$ = selector => LeftSide.ref.elem.querySelector(selector)
54
- LeftSide.$$ = selector => LeftSide.ref.elem.querySelectorAll(selector)
55
-
56
-
57
- function GroupByMethod() {
58
- return (
59
- r('label', { className: CSS.GroupByMethod },
60
- r('input', {
61
- type: 'checkbox',
62
- checked: store.groupByMethod,
63
- onChange: store.toggleGroupByMethod
64
- }),
65
- r('span', { className: CSS.checkboxBody }, t`Group by Method`)))
66
- }
67
-
68
-
69
- function BulkSelector() {
70
- const { comments } = store
71
- const firstOption = t`Pick Comment…`
72
- function onChange() {
73
- const value = this.value
74
- this.value = firstOption // hack so it’s always selected
75
- store.bulkSelectByComment(value)
76
- }
77
- const disabled = !comments.length
78
- return (
79
- r('label', { className: CSS.BulkSelector },
80
- r('span', null, t`Bulk Select`),
81
- r('select', {
82
- disabled,
83
- autocomplete: 'off',
84
- title: disabled
85
- ? t`No mock files have comments which are anything within parentheses on the filename.`
86
- : undefined,
87
- onChange
88
- },
89
- r('option', { value: firstOption }, firstOption),
90
- r('hr'),
91
- comments.map(value => r('option', { value }, value)))))
92
- }
93
-
94
-
95
- function MockList() {
96
- if (!Object.keys(store.brokersByMethod).length)
97
- return r('div', null, t`No mocks found`)
98
-
99
- if (store.groupByMethod)
100
- return Object.keys(store.brokersByMethod).map(method => Fragment(
101
- r('div', {
102
- className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
103
- }, method),
104
- FolderGroups(store.folderGroupsByMethod(method))))
105
-
106
- return FolderGroups(store.folderGroupsByMethod('*'))
107
- }
108
-
109
- function FolderGroups(brokersTree) {
110
- const res = []
111
- for (const b of brokersTree) {
112
- if (!b.children.length)
113
- res.push(Row(b))
114
- else
115
- res.push(FolderGroup(b))
116
- }
117
- return res
118
- }
119
-
120
- function FolderGroup(broker) {
121
- const folder = broker.urlMask
122
- const children = broker.children
123
- return (
124
- r('details', {
125
- className: CSS.FolderGroup,
126
- open: !store.collapsedFolders.has(folder),
127
- onToggle() {
128
- store.setFolderCollapsed(folder, !this.open)
129
- }
130
- },
131
- r('summary', null,
132
- r('span', { className: CSS.FolderChevron }, ChevronDownIcon()),
133
- r('span', {
134
- className: classNames(
135
- CSS.FolderName,
136
- store.groupByMethod && CSS.groupedByMethod,
137
- store.canProxy && CSS.canProxy)
138
- },
139
- folder + '…')),
140
- Row(broker),
141
- children.map(c => c.children.length
142
- ? FolderGroup(c)
143
- : Row(c))))
144
- }
145
-
146
- /** @param {BrokerRowModel} row */
147
- function Row(row) {
148
- const { method, urlMask } = row
149
- return (
150
- r('div', {
151
- key: row.key,
152
- className: classNames(CSS.TableRow, mounted && row.isNew && CSS.animIn)
153
- },
154
-
155
- store.canProxy && ClickDragToggler({
156
- className: CSS.ProxyToggler,
157
- title: t`Proxy Toggler`,
158
- body: CloudIcon(),
159
- checked: row.proxied,
160
- commit(checked) {
161
- store.setProxied(method, urlMask, checked)
162
- }
163
- }),
164
-
165
- ClickDragToggler({
166
- className: CSS.DelayToggler,
167
- title: t`Delay`,
168
- body: TimerIcon(),
169
- checked: row.delayed,
170
- commit(checked) {
171
- store.setDelayed(method, urlMask, checked)
172
- }
173
- }),
174
-
175
- ClickDragToggler(
176
- row.isStatic
177
- ? {
178
- className: CSS.StatusCodeToggler,
179
- title: t`Not Found`,
180
- body: t`404`,
181
- disabled: row.opts.length === 1 && row.status === 404,
182
- checked: !row.proxied && row.status === 404,
183
- commit() { store.toggleStatus(method, urlMask, 404) }
184
- }
185
- : {
186
- className: CSS.StatusCodeToggler,
187
- title: t`Internal Server Error`,
188
- body: t`500`,
189
- disabled: row.opts.length === 1 && row.status === 500,
190
- checked: !row.proxied && row.status === 500,
191
- commit() { store.toggleStatus(method, urlMask, 500) }
192
- }),
193
-
194
- !store.groupByMethod && r('span', { className: CSS.Method }, method),
195
-
196
- PreviewLink(method, urlMask, row.urlMaskDittoed),
197
-
198
- MockSelector(row)))
199
- }
200
-
201
-
202
- function renderRow(method, urlMask) {
203
- unChooseOld()
204
- const row = store.brokerAsRow(method, urlMask)
205
- const tr = LeftSide.$(`.${CSS.TableRow}[key="${row.key}"]`)
206
- mergeTableRow(tr, Row(row))
207
- previewMock()
208
-
209
- function unChooseOld() {
210
- return LeftSide.$(`a.${CSS.chosen}`)
211
- ?.classList.remove(CSS.chosen)
212
- }
213
-
214
- function mergeTableRow(oldRow, newRow) {
215
- for (let i = 0; i < newRow.children.length; i++) {
216
- const oldEl = oldRow.children[i]
217
- const newEl = newRow.children[i]
218
- switch (newEl.tagName) {
219
- case 'LABEL': {
220
- const oldInput = oldEl.querySelector('[type="checkbox"]')
221
- const newInput = newEl.querySelector('[type="checkbox"]')
222
- oldInput.checked = newInput.checked
223
- oldInput.disabled = newInput.disabled
224
- break
225
- }
226
- case 'A':
227
- oldEl.className = newEl.className
228
- break
229
- case 'SELECT':
230
- oldEl.replaceChildren(...newEl.cloneNode(true).children)
231
- oldEl.className = newEl.className
232
- oldEl.disabled = newEl.disabled
233
- oldEl.value = newEl.value
234
- break
235
- }
236
- }
237
- }
238
- }
239
49
 
240
50
 
241
- function PreviewLink(method, urlMask, urlMaskDittoed) {
242
- function onClick(event) {
243
- event.preventDefault()
244
- store.previewLink(method, urlMask)
245
- }
246
- const isChosen = store.chosenLink.method === method && store.chosenLink.urlMask === urlMask
247
- const [ditto, tail] = urlMaskDittoed
248
- return (
249
- r('a', {
250
- className: classNames(CSS.PreviewLink, isChosen && CSS.chosen),
251
- href: urlMask,
252
- onClick
253
- }, ditto
254
- ? [r('span', { className: CSS.dittoDir }, ditto), tail]
255
- : tail))
256
- }
257
-
258
-
259
- /** @param {BrokerRowModel} row */
260
- function MockSelector(row) {
261
- return (
262
- r('select', {
263
- onChange() {
264
- store.selectFile(this.value)
265
- },
266
- onKeyDown(event) {
267
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft')
268
- event.preventDefault()
269
- // Because in Firefox they change the select.option, and
270
- // we use those keys for spreadsheet-like navigation.
271
- },
272
- 'aria-label': t`Mock Selector`,
273
- disabled: row.opts.length < 2,
274
- className: classNames(
275
- CSS.MockSelector,
276
- row.selectedIdx > 0 && CSS.nonDefault,
277
- row.selectedFileIs4xx && CSS.status4xx)
278
- }, row.opts.map(([value, label, selected]) =>
279
- r('option', { value, selected }, label))))
280
- }
281
-
282
-
283
- function ClickDragToggler({ checked, commit, className, title, body }) {
284
- function onPointerEnter(event) {
285
- if (event.buttons === 1)
286
- onPointerDown.call(this, event)
287
- }
288
-
289
- function onPointerDown(event) {
290
- if (event.altKey) {
291
- onExclusiveClick.call(this)
292
- return
293
- }
294
- this.checked = !this.checked
295
- this.focus()
296
- commit(this.checked)
297
- }
298
-
299
- function onExclusiveClick() {
300
- const selector = selectorForColumnOf(this)
301
- if (!selector)
302
- return
303
-
304
- // Uncheck all other in the column.
305
- for (const elem of LeftSide.$$(selector))
306
- if (elem !== this && elem.checked && !elem.disabled) {
307
- elem.checked = false
308
- elem.dispatchEvent(new Event('change'))
309
- }
310
-
311
- if (!this.checked) {
312
- this.checked = true
313
- this.dispatchEvent(new Event('change'))
314
- }
315
- this.focus()
316
- }
317
-
318
- function onClick(event) {
319
- if (event.pointerType === 'mouse')
320
- event.preventDefault()
321
- }
322
- function onChange() {
323
- commit(this.checked)
324
- }
325
- return (
326
- r('label', { className: classNames(CSS.Toggler, className), title },
327
- r('input', {
328
- type: 'checkbox',
329
- checked,
330
- onPointerEnter,
331
- onPointerDown,
332
- onClick,
333
- onChange
334
- }),
335
- r('span', { className: CSS.checkboxBody }, body)))
336
- }
337
-
338
- function Resizer(ref) {
51
+ function Resizer() {
339
52
  let raf = 0
340
53
  let initialX = 0
341
54
  let initialWidth = 0
342
55
 
343
56
  function onPointerDown(event) {
344
57
  initialX = event.clientX
345
- initialWidth = ref.elem.clientWidth
58
+ initialWidth = LeftSide.ref.elem.clientWidth
346
59
  addEventListener('pointerup', onUp, { once: true })
347
60
  addEventListener('pointermove', onMove)
348
61
  Object.assign(document.body.style, {
@@ -355,8 +68,8 @@ function Resizer(ref) {
355
68
  function onMove(event) {
356
69
  const MIN_LEFT_WIDTH = 350
357
70
  raf = raf || requestAnimationFrame(() => {
358
- ref.width = Math.max(initialWidth - (initialX - event.clientX), MIN_LEFT_WIDTH) + 'px'
359
- ref.elem.style.width = ref.width
71
+ LeftSide.ref.width = Math.max(initialWidth - (initialX - event.clientX), MIN_LEFT_WIDTH) + 'px'
72
+ LeftSide.ref.elem.style.width = LeftSide.ref.width
360
73
  raf = 0
361
74
  })
362
75
  }
@@ -468,58 +181,3 @@ function onRealTimeUpdate(onUpdate) {
468
181
  conn = null
469
182
  }
470
183
  }
471
-
472
-
473
-
474
- function selectorForColumnOf(elem) {
475
- return columnSelectors().find(s => elem?.matches(s))
476
- }
477
-
478
- function columnSelectors() {
479
- return [
480
- `.${CSS.TableRow} .${CSS.ProxyToggler} input`,
481
- `.${CSS.TableRow} .${CSS.DelayToggler} input`,
482
- `.${CSS.TableRow} .${CSS.StatusCodeToggler} input`,
483
- `.${CSS.TableRow} .${CSS.PreviewLink}`,
484
- // No .MockSelector because down/up arrows have native behavior on them
485
- ]
486
- }
487
-
488
- function initKeyboardNavigation() {
489
- const rowSelectors = [
490
- ...columnSelectors(),
491
- `.${CSS.TableRow} .${CSS.MockSelector}:enabled`,
492
- ]
493
-
494
- addEventListener('keydown', function ({ key }) {
495
- switch (key) {
496
- case 'ArrowDown':
497
- case 'ArrowUp': {
498
- const pivot = document.activeElement
499
- const sel = selectorForColumnOf(pivot)
500
- if (sel) {
501
- const offset = key === 'ArrowDown' ? +1 : -1
502
- const siblings = LeftSide.$$(sel)
503
- circularAdjacent(offset, siblings, pivot).focus()
504
- }
505
- break
506
- }
507
- case 'ArrowRight':
508
- case 'ArrowLeft': {
509
- const pivot = document.activeElement
510
- const sel = rowSelectors.find(s => pivot?.matches(s))
511
- if (sel) {
512
- const offset = key === 'ArrowRight' ? +1 : -1
513
- const siblings = pivot.closest(`.${CSS.TableRow}`).querySelectorAll(rowSelectors.join(','))
514
- circularAdjacent(offset, siblings, pivot).focus()
515
- }
516
- break
517
- }
518
- }
519
- })
520
-
521
- function circularAdjacent(step, siblings, pivot) {
522
- const arr = Array.from(siblings)
523
- return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
524
- }
525
- }
@@ -3,6 +3,13 @@ export function classNames(...args) {
3
3
  }
4
4
 
5
5
 
6
+ export function adoptSheet(sheet, url) {
7
+ sheet.__url = url.replace(/^\.\//, '')
8
+ document.adoptedStyleSheets.push(sheet)
9
+ Object.assign(sheet, extractClassNames(sheet))
10
+ }
11
+
12
+
6
13
  export function extractClassNames({ cssRules }) {
7
14
  // Class names must begin with _ or a letter, then it can have numbers and hyphens
8
15
  // TODO think about tag.className selectors
@@ -41,6 +41,9 @@ function init() {
41
41
 
42
42
  async function hotReloadCSS(file) {
43
43
  const mod = await import(`${document.baseURI}${file}?${Date.now()}`, { with: { type: 'css' } })
44
- document.adoptedStyleSheets = [mod.default]
44
+ mod.default.__url = file
45
+ for (let i = 0; i < document.adoptedStyleSheets.length; i++)
46
+ if (document.adoptedStyleSheets[i].__url === file)
47
+ document.adoptedStyleSheets[i] = mod.default
45
48
  }
46
49
  }