mockaton 10.6.7 → 10.7.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Server, IncomingMessage, OutgoingMessage } from 'node:http';
1
+ import { Server, IncomingMessage, OutgoingMessage } from 'node:http'
2
2
 
3
3
  type Plugin = (
4
4
  filePath: string,
@@ -16,7 +16,7 @@ interface Config {
16
16
 
17
17
  host?: string,
18
18
  port?: number
19
-
19
+
20
20
  logLevel?: 'normal' | 'verbose' | 'quiet'
21
21
 
22
22
  delay?: number
@@ -37,8 +37,8 @@ interface Config {
37
37
  corsExposedHeaders?: string[]
38
38
  corsCredentials?: boolean
39
39
  corsMaxAge?: number
40
-
41
-
40
+
41
+
42
42
  plugins?: [filenameTester: RegExp, plugin: Plugin][]
43
43
 
44
44
  onReady?: (address: string) => void
@@ -64,11 +64,11 @@ export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
64
64
 
65
65
  export type ClientMockBroker = {
66
66
  mocks: string[]
67
- currentMock: {
68
- file: string
69
- delayed: boolean
70
- proxied: boolean
71
- }
67
+ file: string
68
+ status: number
69
+ auto500: boolean
70
+ delayed: boolean
71
+ proxied: boolean
72
72
  }
73
73
  export type ClientBrokersByMethod = {
74
74
  [method: string]: {
@@ -89,14 +89,14 @@ export type ClientStaticBrokers = {
89
89
  export interface State {
90
90
  brokersByMethod: ClientBrokersByMethod
91
91
  staticBrokers: ClientStaticBrokers
92
-
93
- cookies: [label:string, selected:boolean][]
92
+
93
+ cookies: [label: string, selected: boolean][]
94
94
  comments: string[]
95
-
95
+
96
96
  delay: number
97
-
97
+
98
98
  collectProxied: boolean
99
99
  proxyFallback: string
100
-
100
+
101
101
  corsAllowed?: boolean
102
102
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "10.6.7",
5
+ "version": "10.7.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "start": "node src/cli.js",
24
24
  "watch": "node --watch src/cli.js",
25
25
  "pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none 'pixaton-tests/**/*.test.js'",
26
- "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %s ;# %s\\n\", $4, $2 }'"
26
+ "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
27
27
  },
28
28
  "devDependencies": {
29
29
  "pixaton": "1.1.3",
package/src/Api.js CHANGED
@@ -21,7 +21,7 @@ export const apiGetRequests = new Map([
21
21
  ...[
22
22
  'Dashboard.css',
23
23
  'Dashboard.js',
24
- 'ApiConstants.js', 'ApiCommander.js', 'Filename.js', 'DashboardStore.js', 'DashboardDom.js',
24
+ 'ApiCommander.js', 'ApiConstants.js', 'DashboardDom.js', 'DashboardStore.js', 'Filename.js',
25
25
  'Logo.svg'
26
26
  ].map(f => [API.dashboard + '/' + f, serveStatic(f)]),
27
27
 
@@ -117,7 +117,7 @@ async function selectMock(req, response) {
117
117
  sendUnprocessableContent(response, `Missing Mock: ${file}`)
118
118
  else {
119
119
  broker.selectFile(file)
120
- sendJSON(response, broker.currentMock)
120
+ sendJSON(response, broker)
121
121
  }
122
122
  }
123
123
 
@@ -130,7 +130,7 @@ async function toggle500(req, response) {
130
130
  sendUnprocessableContent(response, `Route does not exist: ${body[DF.routeMethod]} ${body[DF.routeUrlMask]}`)
131
131
  else {
132
132
  broker.toggle500()
133
- sendJSON(response, broker.currentMock)
133
+ sendJSON(response, broker)
134
134
  }
135
135
  }
136
136
 
@@ -147,7 +147,7 @@ async function setRouteIsDelayed(req, response) {
147
147
  sendUnprocessableContent(response, `Expected boolean for "delayed"`)
148
148
  else {
149
149
  broker.setDelayed(delayed)
150
- sendOK(response)
150
+ sendJSON(response, broker)
151
151
  }
152
152
  }
153
153
 
@@ -166,7 +166,7 @@ async function setRouteIsProxied(req, response) {
166
166
  sendUnprocessableContent(response, `There’s no proxy fallback`)
167
167
  else {
168
168
  broker.setProxied(proxied)
169
- sendOK(response)
169
+ sendJSON(response, broker)
170
170
  }
171
171
  }
172
172
 
@@ -231,7 +231,6 @@ async function setStaticRouteStatusCode(req, response) {
231
231
  }
232
232
  }
233
233
 
234
-
235
234
  async function setStaticRouteIsDelayed(req, response) {
236
235
  const body = await parseJSON(req)
237
236
  const delayed = body[DF.delayed]
@@ -9,27 +9,27 @@ export class Commander {
9
9
  this.#addr = addr
10
10
  }
11
11
 
12
- #patch = (api, body) => {
13
- return fetch(this.#addr + api, {
12
+ #patch = (api, body) =>
13
+ fetch(this.#addr + api, {
14
14
  method: 'PATCH',
15
15
  body: JSON.stringify(body)
16
16
  })
17
- }
18
17
 
19
18
  /** @returns {JsonPromise<State>} */
20
- getState = () => {
21
- return fetch(this.#addr + API.state)
22
- }
19
+ getState = () =>
20
+ fetch(this.#addr + API.state)
23
21
 
24
22
  /** @returns {JsonPromise<number>} */
25
- getSyncVersion = (currentSyncVersion, abortSignal) => {
26
- return fetch(this.#addr + API.syncVersion, {
27
- signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
23
+ getSyncVersion = (currSyncVer, abortSignal) =>
24
+ fetch(this.#addr + API.syncVersion, {
25
+ signal: AbortSignal.any([
26
+ abortSignal,
27
+ AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)
28
+ ]),
28
29
  headers: {
29
- [DF.syncVersion]: currentSyncVersion
30
+ [DF.syncVersion]: currSyncVer
30
31
  }
31
32
  })
32
- }
33
33
 
34
34
 
35
35
  reset() {
@@ -28,12 +28,7 @@ export const DF = { // Dashboard Fields (XHR)
28
28
  syncVersion: 'last_received_sync_version'
29
29
  }
30
30
 
31
- // TODO @ThinkAbout these affecting partial matches when bulk-selecting
32
- // e.g. 'ton' would match
33
- export const AUTO_500_COMMENT = '(Mockaton 500)'
31
+ export const HEADER_FOR_502 = 'Mockaton502'
34
32
  export const DEFAULT_MOCK_COMMENT = '(default)'
35
-
36
33
  export const EXT_FOR_UNKNOWN_MIME = 'unknown'
37
34
  export const LONG_POLL_SERVER_TIMEOUT = 8_000
38
-
39
- export const HEADER_FOR_502 = 'Mockaton502'
package/src/Dashboard.css CHANGED
@@ -78,7 +78,7 @@ body {
78
78
 
79
79
  select, a, input, button {
80
80
  cursor: pointer;
81
-
81
+
82
82
  &:focus-visible {
83
83
  outline: 2px solid var(--colorAccent);
84
84
  }
@@ -369,6 +369,19 @@ table {
369
369
  > tr:first-child > th {
370
370
  border-top: 0;
371
371
  }
372
+
373
+ tr.animIn {
374
+ opacity: 0;
375
+ transform: scaleY(0);
376
+ animation: _kfAnimIn 180ms ease-in-out forwards;
377
+ }
378
+ }
379
+
380
+ @keyframes _kfAnimIn {
381
+ to {
382
+ opacity: 1;
383
+ transform: scaleY(1);
384
+ }
372
385
  }
373
386
 
374
387
  .Method {
@@ -544,11 +557,22 @@ table {
544
557
  }
545
558
  }
546
559
 
560
+ &:disabled + span {
561
+ cursor: not-allowed;
562
+ opacity: 0.7;
563
+ }
547
564
  &:checked + span {
548
565
  border-color: var(--colorRed);
549
566
  color: white;
550
567
  background: var(--colorRed);
551
568
  }
569
+ &:not(:checked):enabled:hover + span {
570
+ border-color: var(--colorRed);
571
+ color: var(--colorRed);
572
+ }
573
+ &:enabled:active + span {
574
+ cursor: grabbing;
575
+ }
552
576
  }
553
577
 
554
578
  > span {
@@ -558,14 +582,6 @@ table {
558
582
  font-weight: bold;
559
583
  color: var(--colorSecondaryAction);
560
584
  border-radius: var(--radius);
561
-
562
- &:hover {
563
- border-color: var(--colorRed);
564
- color: var(--colorRed);
565
- }
566
- &:active {
567
- cursor: grabbing;
568
- }
569
585
  }
570
586
  }
571
587
 
@@ -628,6 +644,8 @@ table {
628
644
  background: var(--colorAccent);
629
645
  animation-name: _kfProgress;
630
646
  animation-timing-function: linear;
647
+ animation-iteration-count: infinite;
648
+ animation-direction: alternate;
631
649
  /* duration in JavaScript */
632
650
  }
633
651
  }
package/src/Dashboard.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createElement as r, createSvgElement as s, className, restoreFocus, Defer, Fragment, useRef } from './DashboardDom.js'
2
- import { AUTO_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
3
- import { store, dittoSplitPaths, BrokerRowModel } from './DashboardStore.js'
2
+ import { store, BrokerRowModel } from './DashboardStore.js'
3
+ import { HEADER_FOR_502 } from './ApiConstants.js'
4
4
  import { parseFilename } from './Filename.js'
5
5
 
6
6
 
@@ -28,6 +28,7 @@ const CSS = {
28
28
  SaveProxiedCheckbox: null,
29
29
  SettingsMenu: null,
30
30
 
31
+ animIn: null,
31
32
  chosen: null,
32
33
  dittoDir: null,
33
34
  leftSide: null,
@@ -63,10 +64,12 @@ initRealTimeUpdates()
63
64
  initKeyboardNavigation()
64
65
 
65
66
  function render() {
67
+ render.count++
66
68
  restoreFocus(() => document.body.replaceChildren(...App()))
67
69
  if (store.hasChosenLink)
68
- previewMock(store.chosenLink.method, store.chosenLink.urlMask)
70
+ previewMock()
69
71
  }
72
+ render.count = 0
70
73
 
71
74
  const t = translation => translation[0]
72
75
 
@@ -156,9 +159,7 @@ function BulkSelector() {
156
159
  },
157
160
  r('option', { value: firstOption }, firstOption),
158
161
  r('hr'),
159
- comments.map(value => r('option', { value }, value)),
160
- r('hr'),
161
- r('option', { value: AUTO_500_COMMENT }, t`Auto500`)
162
+ comments.map(value => r('option', { value }, value))
162
163
  )))
163
164
  }
164
165
 
@@ -278,7 +279,7 @@ function MockList() {
278
279
  return Object.keys(store.brokersByMethod).map(method => Fragment(
279
280
  r('tr', null,
280
281
  r('th', { colspan: 2 + Number(store.canProxy) }),
281
- r('th', null, method)),
282
+ r('th', { colspan: 2 }, method)),
282
283
  store.brokersAsRowsByMethod(method).map(Row)))
283
284
 
284
285
  return store.brokersAsRowsByMethod('*').map(Row)
@@ -289,9 +290,12 @@ function MockList() {
289
290
  * @param {number} i
290
291
  */
291
292
  function Row(row, i) {
292
- const { method, urlMask } = row
293
+ const { key, method, urlMask } = row
293
294
  return (
294
- r('tr', { key: Row.key(method, urlMask) },
295
+ r('tr', {
296
+ key,
297
+ ...className(render.count > 1 && row.isNew && CSS.animIn)
298
+ },
295
299
  store.canProxy && r('td', null,
296
300
  ProxyToggler(method, urlMask, row.proxied)),
297
301
 
@@ -299,7 +303,12 @@ function Row(row, i) {
299
303
  DelayRouteToggler(method, urlMask, row.delayed)),
300
304
 
301
305
  r('td', null,
302
- InternalServerErrorToggler(method, urlMask, !row.proxied && row.selectedFileIs500)),
306
+ InternalServerErrorToggler(
307
+ method,
308
+ urlMask,
309
+ !row.proxied && row.status === 500, // checked
310
+ row.opts.length === 1 && row.status === 500 // disabled
311
+ )),
303
312
 
304
313
  !store.groupByMethod && r('td', className(CSS.Method),
305
314
  method),
@@ -310,14 +319,13 @@ function Row(row, i) {
310
319
  r('td', null,
311
320
  MockSelector(row))))
312
321
  }
313
- Row.key = (method, urlMask) => method + '::' + urlMask
314
322
 
315
323
  function renderRow(method, urlMask) {
316
324
  restoreFocus(() => {
317
325
  unChooseOld()
318
- trFor(Row.key(method, urlMask))
319
- .replaceWith(Row(store.brokerAsRow(method, urlMask)))
320
- previewMock(method, urlMask)
326
+ const row = store.brokerAsRow(method, urlMask)
327
+ trFor(row.key).replaceWith(Row(row))
328
+ previewMock()
321
329
  })
322
330
 
323
331
  function trFor(key) {
@@ -362,7 +370,7 @@ function MockSelector(row) {
362
370
  CSS.MockSelector,
363
371
  row.selectedIdx > 0 && CSS.nonDefault,
364
372
  row.selectedFileIs4xx && CSS.status4xx)
365
- }, row.opts.map(([value, label, selected]) =>
373
+ }, row.opts.map(([value, label, selected]) =>
366
374
  r('option', { value, selected }, label))))
367
375
  }
368
376
 
@@ -375,7 +383,7 @@ function DelayRouteToggler(method, urlMask, checked) {
375
383
  })
376
384
  }
377
385
 
378
- function InternalServerErrorToggler(method, urlMask, checked) {
386
+ function InternalServerErrorToggler(method, urlMask, checked, disabled) {
379
387
  return (
380
388
  r('label', {
381
389
  className: CSS.InternalServerErrorToggler,
@@ -383,6 +391,7 @@ function InternalServerErrorToggler(method, urlMask, checked) {
383
391
  },
384
392
  r('input', {
385
393
  type: 'checkbox',
394
+ disabled,
386
395
  checked,
387
396
  onChange() { store.toggle500(method, urlMask) },
388
397
  'data-focus-group': FocusGroup.StatusToggler
@@ -410,38 +419,45 @@ function ProxyToggler(method, urlMask, checked) {
410
419
  /** # StaticFilesList */
411
420
 
412
421
  function StaticFilesList() {
413
- const { staticBrokers, canProxy, groupByMethod } = store
414
- if (!Object.keys(staticBrokers).length)
415
- return null
416
-
417
- const dp = dittoSplitPaths(Object.keys(staticBrokers)).map(([ditto, tail]) => ditto
418
- ? [r('span', className(CSS.dittoDir), ditto), tail]
419
- : tail)
420
- return (
421
- Fragment(
422
+ const { canProxy, groupByMethod } = store
423
+ const rows = store.staticBrokersAsRows()
424
+ return !rows.length
425
+ ? null
426
+ : Fragment(
422
427
  r('tr', null,
423
428
  r('th', { colspan: (2 + Number(!groupByMethod)) + Number(canProxy) }),
424
- r('th', null, t`Static GET`)),
425
- Object.values(staticBrokers).map(({ route, status, delayed }, i) =>
426
- r('tr', null,
427
- canProxy && r('td'),
428
- r('td', null,
429
- DelayStaticRouteToggler(route, delayed)),
429
+ r('th', { colspan: 2 }, t`Static GET`)),
430
+ rows.map(StaticRow))
431
+ }
430
432
 
431
- r('td', null,
432
- NotFoundToggler(route, status === 404)),
433
+ /** @param {StaticBrokerRowModel} row */
434
+ function StaticRow(row) {
435
+ const { canProxy, groupByMethod } = store
436
+ const [ditto, tail] = row.urlMaskDittoed
437
+ return (
438
+ r('tr', {
439
+ key: row.key,
440
+ ...className(render.count > 1 && row.isNew && CSS.animIn)
441
+ },
442
+ canProxy && r('td'),
443
+ r('td', null,
444
+ DelayStaticRouteToggler(row.urlMask, row.delayed)),
445
+
446
+ r('td', null,
447
+ NotFoundToggler(row.urlMask, row.status === 404)),
433
448
 
434
- !groupByMethod && r('td', className(CSS.Method),
435
- 'GET'),
449
+ !groupByMethod && r('td', className(CSS.Method),
450
+ 'GET'),
436
451
 
437
- r('td', null,
438
- r('a', {
439
- href: route,
440
- target: '_blank',
441
- className: CSS.PreviewLink,
442
- 'data-focus-group': FocusGroup.PreviewLink
443
- }, dp[i]))
444
- ))))
452
+ r('td', null,
453
+ r('a', {
454
+ href: row.urlMask,
455
+ target: '_blank',
456
+ className: CSS.PreviewLink,
457
+ 'data-focus-group': FocusGroup.PreviewLink
458
+ }, ditto
459
+ ? [r('span', className(CSS.dittoDir), ditto), tail]
460
+ : tail))))
445
461
  }
446
462
 
447
463
  function DelayStaticRouteToggler(route, checked) {
@@ -586,7 +602,6 @@ function PayloadViewerTitleWhenProxied({ mime, status, statusText, gatewayIsBad
586
602
  ' ' + mime))
587
603
  }
588
604
 
589
- // TODO indeterminate when there's store.delayJitter
590
605
  const SPINNER_DELAY = 80
591
606
  function PayloadViewerProgressBar() {
592
607
  return (
@@ -594,7 +609,9 @@ function PayloadViewerProgressBar() {
594
609
  r('div', { style: { animationDuration: store.delay - SPINNER_DELAY + 'ms' } })))
595
610
  }
596
611
 
597
- async function previewMock(method, urlMask) {
612
+ async function previewMock() {
613
+ const { method, urlMask } = store.chosenLink
614
+
598
615
  previewMock.controller?.abort()
599
616
  previewMock.controller = new AbortController
600
617
 
@@ -609,7 +626,7 @@ async function previewMock(method, urlMask) {
609
626
  signal: previewMock.controller.signal
610
627
  })
611
628
  clearTimeout(spinnerTimer)
612
- const { proxied, file } = store.brokerFor(method, urlMask).currentMock
629
+ const { proxied, file } = store.brokerFor(method, urlMask)
613
630
  if (proxied || file)
614
631
  await updatePayloadViewer(proxied, file, response)
615
632
  }
@@ -1,22 +1,18 @@
1
1
  import { deferred } from './DashboardDom.js'
2
2
  import { Commander } from './ApiCommander.js'
3
3
  import { parseFilename, extractComments } from './Filename.js'
4
- import { AUTO_500_COMMENT } from './ApiConstants.js'
5
4
 
6
5
 
7
6
  const t = translation => translation[0]
8
- const mockaton = new Commander(location.origin)
7
+ const api = new Commander(location.origin)
9
8
 
10
9
  export const store = {
11
10
  onError(err) {},
12
11
  render() {},
13
12
  renderRow(method, urlMask) {},
14
13
 
15
- /** @type {State.brokersByMethod} */
16
- brokersByMethod: {},
17
-
18
- /** @type {State.staticBrokers} */
19
- staticBrokers: {},
14
+ brokersByMethod: /** @type {State.brokersByMethod} */ {},
15
+ staticBrokers: /** @type {State.staticBrokers} */ {},
20
16
 
21
17
  cookies: [],
22
18
  comments: [],
@@ -28,11 +24,11 @@ export const store = {
28
24
  return Boolean(store.proxyFallback)
29
25
  },
30
26
 
31
- getSyncVersion: mockaton.getSyncVersion,
27
+ getSyncVersion: api.getSyncVersion,
32
28
 
33
29
  async fetchState() {
34
30
  try {
35
- const response = await mockaton.getState()
31
+ const response = await api.getState()
36
32
  if (!response.ok) throw response
37
33
  Object.assign(store, await response.json())
38
34
  store.render()
@@ -63,9 +59,10 @@ export const store = {
63
59
  store.chosenLink = { method, urlMask }
64
60
  },
65
61
 
62
+
66
63
  async reset() {
67
64
  try {
68
- const response = await mockaton.reset()
65
+ const response = await api.reset()
69
66
  if (!response.ok) throw response
70
67
  store.setChosenLink('', '')
71
68
  await store.fetchState()
@@ -75,16 +72,17 @@ export const store = {
75
72
 
76
73
  async bulkSelectByComment(value) {
77
74
  try {
78
- const response = await mockaton.bulkSelectByComment(value)
75
+ const response = await api.bulkSelectByComment(value)
79
76
  if (!response.ok) throw response
80
77
  await store.fetchState()
81
78
  }
82
79
  catch (error) { store.onError(error) }
83
80
  },
84
81
 
82
+
85
83
  async setGlobalDelay(value) {
86
84
  try {
87
- const response = await mockaton.setGlobalDelay(value)
85
+ const response = await api.setGlobalDelay(value)
88
86
  if (!response.ok) throw response
89
87
  store.delay = value
90
88
  }
@@ -93,7 +91,7 @@ export const store = {
93
91
 
94
92
  async selectCookie(name) {
95
93
  try {
96
- const response = await mockaton.selectCookie(name)
94
+ const response = await api.selectCookie(name)
97
95
  if (!response.ok) throw response
98
96
  store.cookies = await response.json()
99
97
  }
@@ -102,7 +100,7 @@ export const store = {
102
100
 
103
101
  async setProxyFallback(value) {
104
102
  try {
105
- const response = await mockaton.setProxyFallback(value)
103
+ const response = await api.setProxyFallback(value)
106
104
  if (!response.ok) throw response
107
105
  store.proxyFallback = value
108
106
  store.render()
@@ -112,7 +110,7 @@ export const store = {
112
110
 
113
111
  async setCollectProxied(checked) {
114
112
  try {
115
- const response = await mockaton.setCollectProxied(checked)
113
+ const response = await api.setCollectProxied(checked)
116
114
  if (!response.ok) throw response
117
115
  store.collectProxied = checked
118
116
  }
@@ -124,36 +122,53 @@ export const store = {
124
122
  return store.brokersByMethod[method]?.[urlMask]
125
123
  },
126
124
 
125
+ setBroker(broker) {
126
+ const { method, urlMask } = parseFilename(broker.file)
127
+ store.brokersByMethod[method] ??= {}
128
+ store.brokersByMethod[method][urlMask] = broker
129
+ },
130
+
127
131
  _dittoCache: new Map(),
128
132
 
129
- dittoedUrlFor(method, urlMask) {
130
- return store._dittoCache.get(method + '::' + urlMask)
133
+ _brokersAsArray(byMethod = '*') {
134
+ const arr = []
135
+ for (const [method, brokers] of Object.entries(store.brokersByMethod))
136
+ if (byMethod === '*' || byMethod === method)
137
+ arr.push(...Object.values(brokers))
138
+ return arr
131
139
  },
132
140
 
133
141
  brokersAsRowsByMethod(method) {
134
142
  const rows = store._brokersAsArray(method)
143
+ .map(b => new BrokerRowModel(b, store.canProxy))
144
+ .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
135
145
  const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
136
- for (let i = 0; i < rows.length; i++) {
137
- const r = rows[i]
146
+ rows.forEach((r, i) => {
138
147
  r.setUrlMaskDittoed(urlMasksDittoed[i])
139
- store._dittoCache.set(r.method + '::' + r.urlMask, r.urlMaskDittoed)
140
- }
148
+ r.setIsNew(!store._dittoCache.has(r.key))
149
+ store._dittoCache.set(r.key, urlMasksDittoed[i])
150
+ })
141
151
  return rows
142
152
  },
143
153
 
144
- _brokersAsArray(byMethod = '*') {
145
- const rows = []
146
- for (const [method, brokers] of Object.entries(store.brokersByMethod))
147
- if (byMethod === '*' || byMethod === method)
148
- for (const broker of Object.values(brokers))
149
- rows.push(new BrokerRowModel(broker, store.canProxy))
150
- return rows.sort((a, b) => a.urlMask.localeCompare(b.urlMask))
151
- },
152
-
153
154
  brokerAsRow(method, urlMask) {
154
- const row = new BrokerRowModel(store.brokerFor(method, urlMask), store.canProxy)
155
- row.setUrlMaskDittoed(store.dittoedUrlFor(method, urlMask))
156
- return row
155
+ const b = store.brokerFor(method, urlMask)
156
+ const r = new BrokerRowModel(b, store.canProxy)
157
+ r.setUrlMaskDittoed(store._dittoCache.get(r.key))
158
+ return r
159
+ },
160
+
161
+ staticBrokersAsRows() {
162
+ const rows = Object.values(store.staticBrokers)
163
+ .map(b => new StaticBrokerRowModel(b))
164
+ .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
165
+ const urlMasksDittoed = dittoSplitPaths(rows.map(r => r.urlMask))
166
+ rows.forEach((r, i) => {
167
+ r.setUrlMaskDittoed(urlMasksDittoed[i])
168
+ r.setIsNew(!store._dittoCache.has(r.key))
169
+ store._dittoCache.set(r.key, urlMasksDittoed[i])
170
+ })
171
+ return rows
157
172
  },
158
173
 
159
174
  previewLink(method, urlMask) {
@@ -163,10 +178,10 @@ export const store = {
163
178
 
164
179
  async selectFile(file) {
165
180
  try {
166
- const response = await mockaton.select(file)
181
+ const response = await api.select(file)
167
182
  if (!response.ok) throw response
168
183
  const { method, urlMask } = parseFilename(file)
169
- store.brokerFor(method, urlMask).currentMock = await response.json()
184
+ store.setBroker(await response.json())
170
185
  store.setChosenLink(method, urlMask)
171
186
  store.renderRow(method, urlMask)
172
187
  }
@@ -175,9 +190,9 @@ export const store = {
175
190
 
176
191
  async toggle500(method, urlMask) {
177
192
  try {
178
- const response = await mockaton.toggle500(method, urlMask)
193
+ const response = await api.toggle500(method, urlMask)
179
194
  if (!response.ok) throw response
180
- store.brokerFor(method, urlMask).currentMock = await response.json()
195
+ store.setBroker(await response.json())
181
196
  store.setChosenLink(method, urlMask)
182
197
  store.renderRow(method, urlMask)
183
198
  }
@@ -186,9 +201,9 @@ export const store = {
186
201
 
187
202
  async setProxied(method, urlMask, checked) {
188
203
  try {
189
- const response = await mockaton.setRouteIsProxied(method, urlMask, checked)
204
+ const response = await api.setRouteIsProxied(method, urlMask, checked)
190
205
  if (!response.ok) throw response
191
- store.brokerFor(method, urlMask).currentMock.proxied = checked
206
+ store.setBroker(await response.json())
192
207
  store.setChosenLink(method, urlMask)
193
208
  store.renderRow(method, urlMask)
194
209
  }
@@ -197,9 +212,9 @@ export const store = {
197
212
 
198
213
  async setDelayed(method, urlMask, checked) {
199
214
  try {
200
- const response = await mockaton.setRouteIsDelayed(method, urlMask, checked)
215
+ const response = await api.setRouteIsDelayed(method, urlMask, checked)
201
216
  if (!response.ok) throw response
202
- store.brokerFor(method, urlMask).currentMock.delayed = checked
217
+ store.setBroker(await response.json())
203
218
  }
204
219
  catch (error) { store.onError(error) }
205
220
  },
@@ -207,7 +222,7 @@ export const store = {
207
222
 
208
223
  async setDelayedStatic(route, checked) {
209
224
  try {
210
- const response = await mockaton.setStaticRouteIsDelayed(route, checked)
225
+ const response = await api.setStaticRouteIsDelayed(route, checked)
211
226
  if (!response.ok) throw response
212
227
  store.staticBrokers[route].delayed = checked
213
228
  }
@@ -216,7 +231,7 @@ export const store = {
216
231
 
217
232
  async setStaticRouteStatus(route, status) {
218
233
  try {
219
- const response = await mockaton.setStaticRouteStatus(route, status)
234
+ const response = await api.setStaticRouteStatus(route, status)
220
235
  if (!response.ok) throw response
221
236
  store.staticBrokers[route].status = status
222
237
  }
@@ -312,9 +327,10 @@ dittoSplitPaths.test = function () {
312
327
  deferred(dittoSplitPaths.test)
313
328
 
314
329
 
315
-
316
330
  export class BrokerRowModel {
317
331
  opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
332
+ isNew = false
333
+ key = ''
318
334
  method = ''
319
335
  urlMask = ''
320
336
  urlMaskDittoed = ['', '']
@@ -328,50 +344,46 @@ export class BrokerRowModel {
328
344
  constructor(broker, canProxy) {
329
345
  this.#broker = broker
330
346
  this.#canProxy = canProxy
331
- const { method, urlMask } = parseFilename(broker.currentMock.file)
347
+ const { method, urlMask } = parseFilename(broker.file)
348
+ this.key = 'brm' + '::' + method + '::' + urlMask
332
349
  this.method = method
333
350
  this.urlMask = urlMask
334
351
  this.opts = this.#makeOptions()
335
352
  }
336
-
353
+
337
354
  setUrlMaskDittoed(urlMaskDittoed) {
338
355
  this.urlMaskDittoed = urlMaskDittoed
339
356
  }
340
-
341
- get delayed() {
342
- return this.#broker.currentMock.delayed
343
- }
344
- get proxied() {
345
- return this.#canProxy && this.#broker.currentMock.proxied
357
+ setIsNew(isNew) {
358
+ this.isNew = isNew
346
359
  }
360
+
361
+ get status() { return this.#broker.status }
362
+ get auto500() { return this.#broker.auto500 }
363
+ get delayed() { return this.#broker.delayed }
364
+ get proxied() { return this.#canProxy && this.#broker.proxied }
365
+ get selectedFile() { return this.#broker.file }
347
366
  get selectedIdx() {
348
367
  return this.opts.findIndex(([, , selected]) => selected)
349
368
  }
350
- get selectedFile() {
351
- return this.#broker.currentMock.file
352
- }
353
369
  get selectedFileIs4xx() {
354
- const { status } = parseFilename(this.selectedFile)
355
- return status >= 400 && status < 500
356
- }
357
- get selectedFileIs500() {
358
- const { status } = parseFilename(this.selectedFile)
359
- return status === 500
370
+ return this.status >= 400 && this.status < 500
360
371
  }
361
372
 
362
373
  #makeOptions() {
363
- const proxied = this.proxied
364
- const selectedIs500 = this.selectedFileIs500
365
-
366
- const opts = this.#broker.mocks
367
- .filter(f => selectedIs500 || !f.includes(AUTO_500_COMMENT))
368
- .map(f => [
369
- f,
370
- this.#optionLabelFor(f),
371
- !proxied && f === this.selectedFile
372
- ])
374
+ const opts = this.#broker.mocks.map(f => [
375
+ f,
376
+ this.#optionLabelFor(f),
377
+ !this.auto500 && !this.proxied && f === this.selectedFile
378
+ ])
373
379
 
374
- if (proxied)
380
+ if (this.auto500)
381
+ opts.push([
382
+ '__AUTO_500__',
383
+ t`Auto500`,
384
+ true
385
+ ])
386
+ else if (this.proxied)
375
387
  opts.push([
376
388
  '__PROXIED__',
377
389
  t`Proxied`,
@@ -383,49 +395,32 @@ export class BrokerRowModel {
383
395
 
384
396
  #optionLabelFor(file) {
385
397
  const { status, ext } = parseFilename(file)
386
- const comments = extractComments(file)
387
- const isAutogen500 = comments.includes(AUTO_500_COMMENT)
388
398
  return [
389
- isAutogen500 ? '' : status,
399
+ status,
390
400
  ext === 'empty' || ext === 'unknown' ? '' : ext,
391
- isAutogen500 ? t`Auto500` : comments.join(' ')
401
+ extractComments(file).join(' ')
392
402
  ].filter(Boolean).join(' ')
393
403
  }
394
404
  }
395
405
 
396
- const TestBrokerRowModelOptions = {
397
- 'ignores autogen500 when unselected'() {
398
- const broker = {
399
- currentMock: { file: 'api/other' },
400
- mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
401
- }
402
- const row = new BrokerRowModel(broker, false)
403
- console.assert(row.opts.length === 0)
404
- },
405
-
406
- 'keeps non-autogen500 when unselected'() {
406
+ const TestBrokerRowModel = {
407
+ 'has Auto500 when is autogenerated 500'() {
407
408
  const broker = {
408
- currentMock: { file: 'api/other' },
409
- mocks: [`api/user.GET.500.txt`]
409
+ auto500: true,
410
+ file: 'api/user.GET.200.json',
411
+ mocks: ['api/user.GET.200.json']
410
412
  }
411
413
  const row = new BrokerRowModel(broker, false)
412
- console.assert(row.opts.length === 1)
413
- console.assert(row.opts[0][1] === t`500 txt`)
414
- },
415
-
416
- 'renames autogen file to Auto500'() {
417
- const broker = {
418
- currentMock: { file: `api/user${AUTO_500_COMMENT}.GET.500.empty` },
419
- mocks: [`api/user${AUTO_500_COMMENT}.GET.500.empty`]
420
- }
421
- const row = new BrokerRowModel(broker, false)
422
- console.assert(row.opts.length === 1)
423
- console.assert(row.opts[0][1] === t`Auto500`)
414
+ const opts = row.opts.map(([, n, selected]) => [n, selected])
415
+ console.assert(deepEqual(opts, [
416
+ ['200 json', false],
417
+ [t`Auto500`, true],
418
+ ]))
424
419
  },
425
420
 
426
421
  'filename has extension except when empty or unknown'() {
427
422
  const broker = {
428
- currentMock: { file: `api/other` },
423
+ file: `api/user0.GET.200.empty`,
429
424
  mocks: [
430
425
  `api/user0.GET.200.empty`,
431
426
  `api/user1.GET.200.unknown`,
@@ -434,22 +429,21 @@ const TestBrokerRowModelOptions = {
434
429
  ]
435
430
  }
436
431
  const row = new BrokerRowModel(broker, false)
437
- // Think about, in cases like this, the only option the user has
438
- // for discerning empty and unknown is on the Previewer Title
439
- console.assert(deepEqual(row.opts.map(([, n]) => n), [
440
- '200',
441
- '200',
442
- '200 json',
443
- '200 json (another json)',
432
+ const opts = row.opts.map(([, n, selected]) => [n, selected])
433
+ console.assert(deepEqual(opts, [
434
+ ['200', true],
435
+ ['200', false],
436
+ ['200 json', false],
437
+ ['200 json (another json)', false]
444
438
  ]))
439
+ // TODO Think about, in cases like this, the only option the user has
440
+ // for discerning empty and unknown is on the Previewer Title
445
441
  },
446
442
 
447
443
  'appends "Proxied" label iff current is proxied'() {
448
444
  const broker = {
449
- currentMock: {
450
- file: 'api/foo',
451
- proxied: true
452
- },
445
+ file: 'api/foo',
446
+ proxied: true,
453
447
  mocks: [`api/foo.GET.200.json`]
454
448
  }
455
449
  const row = new BrokerRowModel(broker, true)
@@ -459,7 +453,31 @@ const TestBrokerRowModelOptions = {
459
453
  ]))
460
454
  }
461
455
  }
462
- deferred(() => Object.values(TestBrokerRowModelOptions).forEach(t => t()))
456
+ deferred(() => Object.values(TestBrokerRowModel).forEach(t => t()))
457
+
458
+
459
+ export class StaticBrokerRowModel {
460
+ isNew = false
461
+ key = ''
462
+ method = 'GET'
463
+ urlMaskDittoed = ['', '']
464
+ #broker = /** @type {ClientStaticBroker} */ {}
465
+
466
+ /** @param {ClientStaticBroker} broker */
467
+ constructor(broker) {
468
+ this.#broker = broker
469
+ this.key = 'sbrm' + '::' + this.method + '::' + broker.route
470
+ }
471
+ setUrlMaskDittoed(urlMaskDittoed) {
472
+ this.urlMaskDittoed = urlMaskDittoed
473
+ }
474
+ setIsNew(isNew) {
475
+ this.isNew = isNew
476
+ }
477
+ get urlMask() { return this.#broker.route }
478
+ get delayed() { return this.#broker.delayed }
479
+ get status() { return this.#broker.status }
480
+ }
463
481
 
464
482
  function deepEqual(a, b) {
465
483
  return JSON.stringify(a) === JSON.stringify(b)
package/src/Filename.js CHANGED
@@ -22,7 +22,6 @@ export const includesComment = (filename, search) =>
22
22
  extractComments(filename).some(comment => comment.includes(search))
23
23
 
24
24
 
25
- // TODO ThinkAbout 206 (reject, handle, or send in full?)
26
25
  export function validateFilename(file) {
27
26
  const tokens = file.replace(reComments, '').split('.')
28
27
  if (tokens.length < 4)
@@ -34,7 +33,8 @@ export function validateFilename(file) {
34
33
 
35
34
  if (!httpMethods.includes(method))
36
35
  return `Unrecognized HTTP Method: "${method}"`
37
- }
36
+ }
37
+ // TODO ThinkAbout 206 (reject, handle, or send in full?)
38
38
 
39
39
 
40
40
  export function parseFilename(file) {
@@ -54,12 +54,12 @@ function removeTrailingSlash(url = '') {
54
54
  .replace('/#', '#')
55
55
  }
56
56
 
57
- // TODO ThinkAbout allowing custom status codes
58
57
  function responseStatusIsValid(status) {
59
58
  return Number.isInteger(status)
60
59
  && status >= 100
61
60
  && status <= 599
62
61
  }
62
+ // TODO ThinkAbout allowing custom status codes
63
63
 
64
64
 
65
65
  export function makeMockFilename(url, method, status, ext) {
package/src/MockBroker.js CHANGED
@@ -1,99 +1,82 @@
1
1
  import { includesComment, extractComments, parseFilename } from './Filename.js'
2
- import { AUTO_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
2
+ import { DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
3
3
 
4
4
 
5
- /** MockBroker is a state for a particular route. It knows the available mock files
6
- * that can be served for the route, the currently selected file, and if it’s delayed. */
5
+ /**
6
+ * MockBroker is a state for a particular route. It knows the available mock
7
+ * files that can be served for the route, and the currently selected file, etc.
8
+ */
7
9
  export class MockBroker {
8
10
  constructor(file) {
11
+ this.file = '' // selected mock filename
12
+ this.mocks = [] // filenames
13
+ this.status = -1
14
+ this.delayed = false
15
+ this.proxied = false
16
+ this.auto500 = false
9
17
  this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
10
- this.mocks = []
11
- this.currentMock = {
12
- file: '',
13
- delayed: false,
14
- proxied: false
15
- }
16
18
  this.register(file)
17
19
  }
18
20
 
19
- get file() { return this.currentMock.file }
20
- get delayed() { return this.currentMock.delayed }
21
- get proxied() { return this.currentMock.proxied }
22
-
23
- get status() { return parseFilename(this.file).status }
24
- get temp500IsSelected() { return this.#isTemp500(this.file) }
25
-
26
- hasMock(file) { return this.mocks.includes(file) }
27
-
28
- register(file) {
29
- if (this.#is500(file)) {
30
- if (this.temp500IsSelected)
31
- this.selectFile(file)
32
- this.#deleteTemp500()
33
- }
34
- this.mocks.push(file)
35
- this.#sortMocks()
36
- }
37
-
38
- #is500(file) {
39
- return parseFilename(file).status === 500
40
- }
41
-
42
- #deleteTemp500() {
43
- this.mocks = this.mocks.filter(file => !this.#isTemp500(file))
44
- }
45
-
46
- #isTemp500(file) {
47
- return includesComment(file, AUTO_500_COMMENT)
48
- }
21
+ #is500 = file => parseFilename(file).status === 500
49
22
 
50
23
  #sortMocks() {
51
24
  this.mocks.sort()
52
25
  const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
53
- const temp500 = this.mocks.filter(this.#isTemp500)
54
- this.mocks = [
55
- ...defaults,
56
- ...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
57
- ...temp500
58
- ]
26
+ this.mocks = Array.from(new Set(defaults).union(new Set(this.mocks)))
59
27
  }
60
28
 
61
- ensureItHas500() {
62
- if (!this.mocks.some(this.#is500))
63
- this.#registerTemp500()
64
- }
65
- #registerTemp500() {
66
- const { urlMask, method } = parseFilename(this.mocks[0])
67
- const file = urlMask.replace(/^\//, '') // Removes leading slash
68
- this.mocks.push(`${file}${AUTO_500_COMMENT}.${method}.500.empty`)
29
+ register(file) {
30
+ if (this.auto500 && this.#is500(file)) // TESTME
31
+ this.selectFile(file)
32
+ this.mocks.push(file)
33
+ this.#sortMocks()
69
34
  }
70
35
 
71
36
  unregister(file) {
72
37
  this.mocks = this.mocks.filter(f => f !== file)
73
38
  const isEmpty = !this.mocks.length
74
- || this.mocks.length === 1 && this.#isTemp500(this.mocks[0])
75
39
  if (!isEmpty && this.file === file)
76
40
  this.selectDefaultFile()
77
41
  return isEmpty
78
42
  }
79
43
 
44
+ hasMock = file => this.mocks.includes(file)
45
+
46
+ selectFile(filename) {
47
+ this.file = filename
48
+ this.proxied = false
49
+ this.auto500 = false
50
+ this.status = parseFilename(filename).status
51
+ }
52
+
80
53
  selectDefaultFile() {
81
54
  this.selectFile(this.mocks[0])
82
55
  }
83
56
 
84
- selectFile(filename) {
85
- this.currentMock.proxied = false
86
- this.currentMock.file = filename
57
+ toggle500() {
58
+ this.proxied = false // TESTME
59
+ if (this.auto500 || this.status === 500)
60
+ this.selectDefaultFile()
61
+ else {
62
+ const f = this.mocks.find(this.#is500) // TESTME
63
+ if (f)
64
+ this.selectFile(f)
65
+ else {
66
+ this.auto500 = true
67
+ this.status = 500 // TESTME
68
+ }
69
+ }
87
70
  }
88
71
 
89
- toggle500() {
90
- this.#is500(this.currentMock.file)
91
- ? this.selectDefaultFile()
92
- : this.selectFile(this.mocks.find(this.#is500))
72
+ setDelayed(delayed) {
73
+ this.delayed = delayed
93
74
  }
94
75
 
95
- setDelayed(delayed) { this.currentMock.delayed = delayed }
96
- setProxied(proxied) { this.currentMock.proxied = proxied }
76
+ setProxied(proxied) {
77
+ this.auto500 = false // TESTME
78
+ this.proxied = proxied
79
+ }
97
80
 
98
81
  setByMatchingComment(comment) {
99
82
  for (const file of this.mocks)
@@ -35,8 +35,8 @@ export async function dispatchMock(req, response) {
35
35
  for (let i = 0; i < config.extraHeaders.length; i += 2)
36
36
  response.setHeader(config.extraHeaders[i], config.extraHeaders[i + 1])
37
37
 
38
- response.statusCode = broker.status // TESTME plugins can change it
39
- const { mime, body } = broker.temp500IsSelected
38
+ response.statusCode = broker.auto500 ? 500 : broker.status // TESTME plugins can change it
39
+ const { mime, body } = broker.auto500
40
40
  ? { mime: '', body: '' }
41
41
  : await applyPlugins(join(config.mocksDir, broker.file), req, response)
42
42
 
package/src/Mockaton.js CHANGED
@@ -10,7 +10,7 @@ import * as mockBrokerCollection from './mockBrokersCollection.js'
10
10
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
11
11
  import { watchMocksDir, watchStaticDir } from './Watcher.js'
12
12
  import { apiPatchRequests, apiGetRequests } from './Api.js'
13
- import { BodyReaderError, isControlCharFree } from './utils/http-request.js'
13
+ import { BodyReaderError, hasControlChars } from './utils/http-request.js'
14
14
  import { sendNoContent, sendInternalServerError, sendUnprocessableContent, sendTooLongURI, sendBadRequest } from './utils/http-response.js'
15
15
 
16
16
 
@@ -26,8 +26,7 @@ export function Mockaton(options) {
26
26
  const server = createServer(onRequest)
27
27
  server.on('error', reject)
28
28
  server.listen(config.port, config.host, () => {
29
- const { address, port } = server.address()
30
- const url = `http://${address}:${port}`
29
+ const url = `http://${server.address().address}:${server.address().port}`
31
30
  const dashboardUrl = url + API.dashboard
32
31
  logger.info('Listening', url)
33
32
  logger.info('Dashboard', dashboardUrl)
@@ -47,27 +46,30 @@ async function onRequest(req, response) {
47
46
  sendTooLongURI(response)
48
47
  return
49
48
  }
50
-
51
- if (!isControlCharFree(url)) {
52
- sendBadRequest(response)
49
+ if (hasControlChars(url)) {
50
+ sendBadRequest(response)
53
51
  return
54
52
  }
55
-
56
- try {
57
- const route = new URL(url, 'http://_').pathname
58
- const { method } = req
59
53
 
54
+ try {
60
55
  if (config.corsAllowed)
61
56
  setCorsHeaders(req, response, config)
62
57
 
58
+ const { method } = req
59
+ const { pathname } = new URL(url, 'http://_')
60
+
63
61
  if (isPreflight(req))
64
62
  sendNoContent(response)
65
- else if (method === 'PATCH' && apiPatchRequests.has(route))
66
- await apiPatchRequests.get(route)(req, response)
67
- else if (method === 'GET' && apiGetRequests.has(route))
68
- apiGetRequests.get(route)(req, response)
69
- else if (method === 'GET' && staticCollection.brokerByRoute(route))
63
+
64
+ else if (method === 'PATCH' && apiPatchRequests.has(pathname))
65
+ await apiPatchRequests.get(pathname)(req, response)
66
+
67
+ else if (method === 'GET' && apiGetRequests.has(pathname))
68
+ apiGetRequests.get(pathname)(req, response)
69
+
70
+ else if (method === 'GET' && staticCollection.brokerByRoute(pathname))
70
71
  await dispatchStatic(req, response)
72
+
71
73
  else
72
74
  await dispatchMock(req, response)
73
75
  }
package/src/Watcher.js CHANGED
@@ -8,20 +8,34 @@ import * as staticCollection from './staticCollection.js'
8
8
  import * as mockBrokerCollection from './mockBrokersCollection.js'
9
9
 
10
10
 
11
- /** # ARR = Add, Remove, or Rename Mock Event */
11
+ /**
12
+ * ARR = Add, Remove, or Rename Mock Event
13
+ *
14
+ * The emitter is debounced so it handles e.g. bulk deletes,
15
+ * and also renames, which are two events (delete + add).
16
+ */
12
17
  export const uiSyncVersion = new class extends EventEmitter {
13
18
  version = 0
14
19
 
15
- increment() {
20
+ increment = this.#debounce(() => {
16
21
  this.version++
17
22
  super.emit('ARR')
18
- }
23
+ })
24
+
19
25
  subscribe(listener) {
20
26
  this.once('ARR', listener)
21
27
  }
22
28
  unsubscribe(listener) {
23
29
  this.removeListener('ARR', listener)
24
30
  }
31
+
32
+ #debounce(fn) {
33
+ let timer
34
+ return () => {
35
+ clearTimeout(timer)
36
+ timer = setTimeout(fn, 80)
37
+ }
38
+ }
25
39
  }
26
40
 
27
41
  export function watchMocksDir() {
@@ -29,7 +43,7 @@ export function watchMocksDir() {
29
43
  watch(dir, { recursive: true, persistent: false }, (_, file) => {
30
44
  if (!file)
31
45
  return
32
-
46
+
33
47
  const path = join(dir, file)
34
48
 
35
49
  if (isDirectory(path)) {
@@ -56,7 +70,7 @@ export function watchStaticDir() {
56
70
  watch(dir, { recursive: true, persistent: false }, (_, file) => {
57
71
  if (!file)
58
72
  return
59
-
73
+
60
74
  const path = join(dir, file)
61
75
 
62
76
  if (isDirectory(path)) {
@@ -77,4 +91,3 @@ export function watchStaticDir() {
77
91
  }
78
92
 
79
93
  // TODO config changes
80
- // TODO think about throttling e.g. bulk deletes/remove files
@@ -6,7 +6,6 @@ import { MockBroker } from './MockBroker.js'
6
6
  import { listFilesRecursively } from './utils/fs.js'
7
7
  import { config, isFileAllowed } from './config.js'
8
8
  import { parseFilename, validateFilename } from './Filename.js'
9
- import { AUTO_500_COMMENT } from './ApiConstants.js'
10
9
 
11
10
 
12
11
  /**
@@ -36,10 +35,7 @@ export function init() {
36
35
  .sort()
37
36
  .forEach(f => registerMock(f))
38
37
 
39
- forEachBroker(broker => {
40
- broker.ensureItHas500()
41
- broker.selectDefaultFile()
42
- })
38
+ forEachBroker(b => b.selectDefaultFile())
43
39
  }
44
40
 
45
41
  /** @returns {boolean} registered */
@@ -57,12 +53,9 @@ export function registerMock(file, isFromWatcher = false) {
57
53
  else
58
54
  collection[method][urlMask].register(file)
59
55
 
60
- if (isFromWatcher && !this.file)
56
+ if (isFromWatcher && !collection[method][urlMask].file) // TESTME e.g. auto500 is selected and adding a new one
61
57
  collection[method][urlMask].selectDefaultFile()
62
58
 
63
- if (isFromWatcher)
64
- collection[method][urlMask].ensureItHas500()
65
-
66
59
  return true
67
60
  }
68
61
 
@@ -117,7 +110,6 @@ export function extractAllComments() {
117
110
  comments.add(c)
118
111
  })
119
112
  return Array.from(comments)
120
- .filter(c => c !== AUTO_500_COMMENT)
121
113
  }
122
114
 
123
115
  export function setMocksMatchingComment(comment) {
@@ -50,13 +50,13 @@ export function readBody(req, parser = a => a) {
50
50
  }
51
51
 
52
52
  export const reControlAndDelChars = /[\x00-\x1f\x7f]/
53
- export function isControlCharFree(url) {
53
+ export function hasControlChars(url) {
54
54
  try {
55
55
  const decoded = decode(url)
56
- return decoded && !reControlAndDelChars.test(decoded)
56
+ return !decoded || reControlAndDelChars.test(decoded)
57
57
  }
58
58
  catch {
59
- return false
59
+ return true
60
60
  }
61
61
  }
62
62