mockaton 12.2.2 → 12.3.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.
@@ -1,4 +1,4 @@
1
- export function className(...args) {
1
+ export function classNames(...args) {
2
2
  return {
3
3
  className: args.filter(Boolean).join(' ')
4
4
  }
@@ -6,12 +6,14 @@ export function className(...args) {
6
6
 
7
7
  export function createElement(tag, props, ...children) {
8
8
  const elem = document.createElement(tag)
9
- for (const [k, v] of Object.entries(props || {}))
10
- if (k === 'ref') v.elem = elem
11
- else if (k === 'style') Object.assign(elem.style, v)
12
- else if (k.startsWith('on')) elem.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
13
- else if (k in elem) elem[k] = v
14
- else elem.setAttribute(k, v)
9
+ if (props)
10
+ for (const [k, v] of Object.entries(props))
11
+ if (v === undefined) continue
12
+ else if (k === 'ref') v.elem = elem
13
+ else if (k === 'style') Object.assign(elem.style, v)
14
+ else if (k.startsWith('on')) elem.addEventListener(k.slice(2).toLowerCase(), ...[v].flat())
15
+ else if (k in elem) elem[k] = v
16
+ else elem.setAttribute(k, v)
15
17
  elem.append(...children.flat().filter(Boolean))
16
18
  return elem
17
19
  }
@@ -66,13 +68,6 @@ function selectorFor(elem) {
66
68
  }
67
69
 
68
70
 
69
-
70
- // Minimal implementation of CSS Modules in the browser
71
- // TODO think about avoiding clashes when using multiple files. e.g.:
72
- // - should the user pass a prefix?, or
73
- // - should the ensure there's a unique top-level classname on each file
74
- // TODO ignore rules in comments?
75
-
76
71
  export function adoptCSS(sheet) {
77
72
  document.adoptedStyleSheets.push(sheet)
78
73
  Object.assign(sheet, extractClassNames(sheet))
@@ -1,6 +1,5 @@
1
1
  import { API } from './ApiConstants.js'
2
2
 
3
-
4
3
  export const CSP = [
5
4
  `default-src 'self'`,
6
5
  `img-src data: blob: 'self'`
@@ -9,24 +8,24 @@ export const CSP = [
9
8
 
10
9
  // language=html
11
10
  export const IndexHtml = (hotReloadEnabled, version) => `
12
- <!DOCTYPE html>
13
- <html lang="en-US">
14
- <head>
15
- <meta charset="UTF-8">
16
- <base href="${API.dashboard}/">
11
+ <!DOCTYPE html>
12
+ <html lang="en-US">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <base href="${API.dashboard}/">
16
+
17
+ <script type="module" src="app.js"></script>
18
+ <link rel="preload" href="${API.state}" as="fetch" crossorigin>
17
19
 
18
- <script type="module" src="app.js"></script>
19
- <link rel="preload" href="${API.state}" as="fetch" crossorigin>
20
-
21
- <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
22
- <meta name="viewport" content="width=device-width, initial-scale=1">
23
- <meta name="description" content="HTTP Mock Server">
24
- <title>Mockaton v${version}</title>
25
- </head>
26
- <body>
27
- ${hotReloadEnabled
28
- ? '<script type="module" src="watcherDev.js"></script>'
29
- : ''}
30
- </body>
31
- </html>
20
+ <link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
21
+ <meta name="viewport" content="width=device-width, initial-scale=1">
22
+ <meta name="description" content="HTTP Mock Server">
23
+ <title>Mockaton v${version}</title>
24
+ </head>
25
+ <body>
26
+ ${hotReloadEnabled
27
+ ? '<script type="module" src="watcherDev.js"></script>'
28
+ : ''}
29
+ </body>
30
+ </html>
32
31
  `
@@ -400,7 +400,7 @@ main {
400
400
  padding-bottom: 4px;
401
401
  padding-left: 1px;
402
402
  border-top: 24px solid transparent;
403
- margin-left: 74px;
403
+ margin-left: 71px;
404
404
  font-weight: bold;
405
405
  text-align: left;
406
406
 
@@ -409,7 +409,7 @@ main {
409
409
  }
410
410
 
411
411
  &.canProxy {
412
- margin-left: 110px;
412
+ margin-left: 100px;
413
413
  }
414
414
  &.nonGroupedByMethod {
415
415
  margin-left: 122px;
@@ -505,11 +505,11 @@ main {
505
505
  }
506
506
  }
507
507
 
508
- .DelayToggler,
509
- .ProxyToggler {
508
+
509
+ .Toggler {
510
510
  display: flex;
511
511
 
512
- > input {
512
+ input {
513
513
  /* For click drag target */
514
514
  position: absolute;
515
515
  width: 22px;
@@ -518,137 +518,85 @@ main {
518
518
 
519
519
  &:focus-visible {
520
520
  outline: 0;
521
- & + svg {
521
+ & + .checkboxBody {
522
522
  outline: 2px solid var(--colorAccent)
523
523
  }
524
524
  }
525
- }
526
525
 
527
- > svg {
528
- fill: none;
529
- stroke: var(--colorSecondaryAction);
530
- }
531
- }
526
+ &:disabled + .checkboxBody {
527
+ cursor: not-allowed;
528
+ opacity: 0.7;
529
+ }
532
530
 
533
- .DelayToggler {
534
- > input {
535
- &:checked + svg {
531
+ &:checked + .checkboxBody {
536
532
  border-color: var(--colorAccent);
537
533
  fill: var(--colorAccent);
538
534
  background: var(--colorAccent);
539
535
  stroke: var(--colorBackground);
540
536
  }
541
537
 
542
- &:enabled:hover:not(:checked) + svg {
538
+ &:enabled:hover:not(:checked) + .checkboxBody {
543
539
  border-color: var(--colorHover);
544
540
  background: var(--colorHover);
545
541
  stroke: var(--colorText);
546
542
  }
547
543
  }
548
544
 
549
- > svg {
545
+ .checkboxBody {
546
+ display: flex;
550
547
  width: 22px;
551
548
  height: 22px;
549
+ align-items: center;
550
+ justify-content: center;
552
551
  border: 1px solid var(--colorSecondaryActionBorder);
552
+ fill: none;
553
+ stroke: var(--colorSecondaryAction);
553
554
  stroke-width: 2.5px;
554
555
  border-radius: 50%;
555
556
  }
556
557
 
557
558
  &.canProxy {
558
- margin-left: 36px;
559
+ margin-left: 30px;
559
560
  }
560
561
  }
561
562
 
563
+ .DelayToggler {
564
+ }
565
+
562
566
  .ProxyToggler {
563
- border: 1px solid var(--colorSecondaryActionBorder);
564
567
  margin-right: 8px;
565
- border-radius: var(--radius);
566
-
567
- &:has(input:checked),
568
- &:has(input:disabled) {
569
- background: transparent;
570
- box-shadow: none;
571
- }
572
-
573
- > input {
574
- &:checked + svg {
575
- fill: var(--colorAccent);
576
- stroke: var(--colorAccent);
577
-
578
- path:last-of-type { /* inner cloud curve */
579
- stroke: var(--colorBackground);
580
- }
581
- transform: scale(1.1);
582
- }
583
-
584
- &:enabled:hover:not(:checked) + svg {
585
- fill: var(--colorHover);
586
- stroke: var(--colorText);
587
- }
588
568
 
589
- &:disabled + svg {
590
- stroke-opacity: 0.4;
591
- cursor: not-allowed;
592
- box-shadow: none;
593
- fill: transparent;
594
-
595
- path:last-of-type {
596
- stroke-opacity: 0;
597
- stroke: transparent;
598
- }
569
+ .checkboxBody {
570
+ svg {
571
+ width: 16px;
599
572
  }
600
573
  }
601
-
602
- > svg {
603
- width: 26px;
604
- padding: 1px 4px;
605
- stroke-width: 2px;
606
- border-radius: var(--radius);
607
- }
608
574
  }
609
575
 
610
- .InternalServerErrorToggler,
611
- .NotFoundToggler {
612
- display: flex;
576
+ .StatusCodeToggler {
613
577
  margin-right: 10px;
614
578
  margin-left: 8px;
615
- cursor: pointer;
616
-
617
- > input {
618
- appearance: none;
619
579
 
620
- &:focus-visible {
621
- outline: 0;
622
- & + .checkboxBody {
623
- outline: 2px solid var(--colorAccent)
624
- }
625
- }
580
+ input {
581
+ width: 26px;
626
582
 
627
- &:disabled + .checkboxBody {
628
- cursor: not-allowed;
629
- opacity: 0.7;
583
+ &:not(:checked):enabled:hover + .checkboxBody {
584
+ border-color: var(--colorRed);
585
+ color: var(--colorRed);
630
586
  }
631
587
  &:checked + .checkboxBody {
632
588
  border-color: var(--colorRed);
633
589
  color: white;
634
590
  background: var(--colorRed);
635
591
  }
636
- &:not(:checked):enabled:hover + .checkboxBody {
637
- border-color: var(--colorRed);
638
- color: var(--colorRed);
639
- }
640
- &:enabled:active + .checkboxBody {
641
- cursor: grabbing;
642
- }
643
592
  }
644
593
 
645
- > .checkboxBody {
594
+ .checkboxBody {
595
+ width: 27px;
646
596
  padding: 4px;
647
- border: 1px solid var(--colorSecondaryActionBorder);
648
597
  font-size: 10px;
649
598
  font-weight: bold;
650
599
  color: var(--colorSecondaryAction);
651
- border-radius: var(--radius);
652
600
  }
653
601
  }
654
602
 
package/src/server/Api.js CHANGED
@@ -13,9 +13,10 @@ import {
13
13
  import { longPollClientSyncVersion } from './Watcher.js'
14
14
 
15
15
  import pkgJSON from '../../package.json' with { type: 'json' }
16
+
17
+ import { API } from '../client/ApiConstants.js'
16
18
  import { IndexHtml, CSP } from '../client/indexHtml.js'
17
19
 
18
- import { API } from './ApiConstants.js'
19
20
  import { cookie } from './cookie.js'
20
21
  import { config, ConfigValidator } from './config.js'
21
22
 
@@ -29,7 +30,7 @@ export const apiGetReqs = new Map([
29
30
 
30
31
  [API.state, getState],
31
32
  [API.syncVersion, longPollClientSyncVersion],
32
-
33
+
33
34
  [API.watchHotReload, longPollDevClientHotReload],
34
35
  [API.throws, () => { throw new Error('Test500') }]
35
36
  ])
@@ -41,17 +42,17 @@ export const apiPatchReqs = new Map([
41
42
  [API.cookies, selectCookie],
42
43
  [API.globalDelay, setGlobalDelay],
43
44
  [API.globalDelayJitter, setGlobalDelayJitter],
44
-
45
+
45
46
  [API.fallback, setProxyFallback],
46
47
  [API.collectProxied, setCollectProxied],
47
-
48
+
48
49
  [API.bulkSelect, bulkUpdateBrokersByCommentTag],
49
50
 
50
51
  [API.delay, setRouteIsDelayed],
51
52
  [API.select, selectMock],
52
53
  [API.proxied, setRouteIsProxied],
53
54
  [API.toggle500, toggleRoute500],
54
-
55
+
55
56
  [API.delayStatic, setStaticRouteIsDelayed],
56
57
  [API.staticStatus, setStaticRouteStatusCode]
57
58
  ])
@@ -220,7 +221,7 @@ async function setRouteIsProxied(req, response) {
220
221
 
221
222
  const broker = mockBrokersCollection.brokerByRoute(method, urlMask)
222
223
  if (!broker)
223
- response.unprocessable( `Route does not exist: ${method} ${urlMask}`)
224
+ response.unprocessable(`Route does not exist: ${method} ${urlMask}`)
224
225
  else if (typeof proxied !== 'boolean')
225
226
  response.unprocessable(`Expected boolean for "proxied"`)
226
227
  else if (proxied && !config.proxyFallback)
@@ -1,5 +1,5 @@
1
- import { DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
2
- import { includesComment, extractComments, parseFilename } from './Filename.js'
1
+ import { DEFAULT_MOCK_COMMENT } from '../client/ApiConstants.js'
2
+ import { parseFilename, includesComment, extractComments } from '../client/Filename.js'
3
3
 
4
4
 
5
5
  /**
@@ -8,7 +8,8 @@ import { IncomingMessage } from './utils/HttpIncomingMessage.js'
8
8
  import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
9
9
  import { BodyReaderError, hasControlChars } from './utils/HttpIncomingMessage.js'
10
10
 
11
- import { API } from './ApiConstants.js'
11
+ import { API } from '../client/ApiConstants.js'
12
+
12
13
  import { config, setup } from './config.js'
13
14
  import { apiPatchReqs, apiGetReqs } from './Api.js'
14
15
 
@@ -9,17 +9,18 @@ import { describe, test, before, beforeEach, after } from 'node:test'
9
9
  import { mkdtempSync } from 'node:fs'
10
10
  import { writeFile, unlink, mkdir, readFile, rename } from 'node:fs/promises'
11
11
 
12
+ import { API } from '../client/ApiConstants.js'
13
+
12
14
  import { logger } from './utils/logger.js'
13
15
  import { mimeFor } from './utils/mime.js'
14
16
  import { readBody } from './utils/HttpIncomingMessage.js'
15
17
  import { CorsHeader } from './utils/http-cors.js'
16
18
 
17
- import { API } from './ApiConstants.js'
18
19
  import { Mockaton } from './Mockaton.js'
19
- import { parseFilename } from './Filename.js'
20
20
  import { watchMocksDir, watchStaticDir } from './Watcher.js'
21
21
 
22
22
  import { Commander } from '../client/ApiCommander.js'
23
+ import { parseFilename } from '../client/Filename.js'
23
24
 
24
25
 
25
26
  const mocksDir = mkdtempSync(join(tmpdir(), 'mocks'))
@@ -6,7 +6,8 @@ import { write, isFile } from './utils/fs.js'
6
6
  import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
7
7
 
8
8
  import { config } from './config.js'
9
- import { makeMockFilename } from './Filename.js'
9
+
10
+ import { makeMockFilename } from '../client/Filename.js'
10
11
 
11
12
 
12
13
  export async function proxy(req, response, delay) {
@@ -24,7 +24,7 @@ export async function dispatchStatic(req, response) {
24
24
  response.mockNotFound()
25
25
  return
26
26
  }
27
-
27
+
28
28
  logger.accessMock(req.url, 'static200')
29
29
  if (req.headers.range)
30
30
  await response.partialContent(req.headers.range, file)
@@ -5,7 +5,7 @@ import { EventEmitter } from 'node:events'
5
5
  import {
6
6
  HEADER_SYNC_VERSION,
7
7
  LONG_POLL_SERVER_TIMEOUT
8
- } from './ApiConstants.js'
8
+ } from '../client/ApiConstants.js'
9
9
 
10
10
  import { config } from './config.js'
11
11
  import { isFile, isDirectory } from './utils/fs.js'
@@ -1,8 +1,9 @@
1
1
  import { join } from 'node:path'
2
2
  import { EventEmitter } from 'node:events'
3
3
  import { watch, readdirSync } from 'node:fs'
4
+
4
5
  import { config } from './config.js'
5
- import { LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
6
+ import { LONG_POLL_SERVER_TIMEOUT } from '../client/ApiConstants.js'
6
7
 
7
8
 
8
9
  export const CLIENT_DIR = join(import.meta.dirname, '../client')
@@ -31,7 +32,7 @@ export function longPollDevClientHotReload(req, response) {
31
32
  response.notFound()
32
33
  return
33
34
  }
34
-
35
+
35
36
  function onDevChange(file) {
36
37
  devClientWatcher.unsubscribe(onDevChange)
37
38
  response.json(file)
@@ -6,7 +6,7 @@ import { listFilesRecursively } from './utils/fs.js'
6
6
  import { cookie } from './cookie.js'
7
7
  import { MockBroker } from './MockBroker.js'
8
8
  import { config, isFileAllowed } from './config.js'
9
- import { parseFilename, validateFilename } from './Filename.js'
9
+ import { parseFilename, validateFilename } from '../client/Filename.js'
10
10
 
11
11
 
12
12
  /**
@@ -69,7 +69,7 @@ export function hasControlChars(url) {
69
69
 
70
70
  export function decode(url) {
71
71
  const candidate = decodeURIComponent(url)
72
- return candidate === decodeURIComponent(candidate)
73
- ? candidate
72
+ return candidate === decodeURIComponent(candidate)
73
+ ? candidate
74
74
  : '' // reject multiple encodings
75
75
  }
@@ -3,7 +3,7 @@ import fs, { readFileSync } from 'node:fs'
3
3
 
4
4
  import { logger } from './logger.js'
5
5
  import { mimeFor } from './mime.js'
6
- import { HEADER_502 } from '../ApiConstants.js'
6
+ import { HEADER_502 } from '../../client/ApiConstants.js'
7
7
 
8
8
 
9
9
  export class ServerResponse extends http.ServerResponse {
@@ -1,5 +1,5 @@
1
1
  import { config } from '../config.js'
2
- import { UNKNOWN_MIME_EXT } from '../ApiConstants.js'
2
+ import { UNKNOWN_MIME_EXT } from '../../client/ApiConstants.js'
3
3
 
4
4
 
5
5
  // Generated with:
@@ -1,32 +0,0 @@
1
- // @KeepSync src/client/ApiConstants.js
2
-
3
- const MOUNT = '/mockaton'
4
- export const API = {
5
- dashboard: MOUNT,
6
-
7
- bulkSelect: MOUNT + '/bulk-select-by-comment',
8
- collectProxied: MOUNT + '/collect-proxied',
9
- cookies: MOUNT + '/cookies',
10
- cors: MOUNT + '/cors',
11
- delay: MOUNT + '/delay',
12
- delayStatic: MOUNT + '/delay-static',
13
- fallback: MOUNT + '/fallback',
14
- globalDelay: MOUNT + '/global-delay',
15
- globalDelayJitter: MOUNT + '/global-delay-jitter',
16
- proxied: MOUNT + '/proxied',
17
- reset: MOUNT + '/reset',
18
- select: MOUNT + '/select',
19
- state: MOUNT + '/state',
20
- staticStatus: MOUNT + '/static-status',
21
- syncVersion: MOUNT + '/sync-version',
22
- throws: MOUNT + '/throws',
23
- toggle500: MOUNT + '/toggle500',
24
- watchHotReload: MOUNT + '/watch-hot-reload',
25
- }
26
-
27
- export const HEADER_502 = 'Mockaton502'
28
- export const HEADER_SYNC_VERSION = 'sync_version'
29
-
30
- export const DEFAULT_MOCK_COMMENT = '(default)'
31
- export const UNKNOWN_MIME_EXT = 'unknown'
32
- export const LONG_POLL_SERVER_TIMEOUT = 8_000
@@ -1,65 +0,0 @@
1
- /** @KeepSync src/client/Filename.js */
2
-
3
- import { METHODS } from 'node:http'
4
-
5
-
6
- const reComments = /\(.*?\)/g // Anything within parentheses
7
-
8
- export function extractComments(file) {
9
- return Array.from(file.matchAll(reComments), ([c]) => c)
10
- }
11
-
12
- export function includesComment(file, search) {
13
- return extractComments(file).some(c => c.includes(search))
14
- }
15
-
16
-
17
- export function validateFilename(file) {
18
- const tokens = file.replace(reComments, '').split('.')
19
- if (tokens.length < 4)
20
- return 'Invalid Filename Convention'
21
-
22
- const { status, method } = parseFilename(file)
23
- if (!responseStatusIsValid(status))
24
- return `Invalid HTTP Response Status: "${status}"`
25
-
26
- if (!METHODS.includes(method))
27
- return `Unrecognized HTTP Method: "${method}"`
28
- }
29
-
30
-
31
- export function parseFilename(file) {
32
- const tokens = file.replace(reComments, '').split('.')
33
- return {
34
- ext: tokens.pop(),
35
- status: Number(tokens.pop()),
36
- method: tokens.pop(),
37
- urlMask: '/' + removeTrailingSlash(tokens.join('.'))
38
- }
39
- }
40
-
41
- function removeTrailingSlash(url = '') {
42
- return url
43
- .replace(/\/$/, '')
44
- .replace('/?', '?')
45
- .replace('/#', '#')
46
- }
47
-
48
- function responseStatusIsValid(status) {
49
- return Number.isInteger(status)
50
- && status >= 100
51
- && status <= 599
52
- }
53
-
54
-
55
- export function makeMockFilename(url, method, status, ext) {
56
- const urlMask = replaceIds(removeTrailingSlash(url))
57
- return [urlMask, method, status, ext].join('.')
58
- }
59
-
60
- const reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
61
- function replaceIds(filename) {
62
- return filename.replaceAll(reUuidV4, '[id]')
63
- }
64
-
65
-