mockaton 8.24.0 → 8.26.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/README.md CHANGED
@@ -55,9 +55,9 @@ Nonetheless, there’s a programmatic API, which is handy
55
55
  for setting up tests (see **Commander API** section below).
56
56
 
57
57
  <picture>
58
- <source media="(prefers-color-scheme: light)" srcset="pixaton-tests/macos/pic-for-readme.vp880x800.light.gold.png">
59
- <source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/macos/pic-for-readme.vp880x800.dark.gold.png">
60
- <img alt="Mockaton Dashboard" src="pixaton-tests/macos/pic-for-readme.vp880x800.light.gold.png">
58
+ <source media="(prefers-color-scheme: light)" srcset="pixaton-tests/macos/pic-for-readme.vp880x768.light.gold.png">
59
+ <source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/macos/pic-for-readme.vp880x768.dark.gold.png">
60
+ <img alt="Mockaton Dashboard" src="pixaton-tests/macos/pic-for-readme.vp880x768.light.gold.png">
61
61
  </picture>
62
62
 
63
63
 
@@ -132,13 +132,17 @@ npx mockaton --port 2345
132
132
  CLI options override their counterparts in `mockaton.config.js`
133
133
 
134
134
  ```txt
135
- -c, --config <file> (default: ./mockaton.config.js)
135
+ -c, --config <file> (default: ./mockaton.config.js)
136
136
 
137
- -H, --host <host> (default: 127.0.0.1)
138
- -p, --port <port> (default: 0) which means auto-assigned
137
+ -m, --mocks-dir <dir> (default: ./mockaton-mocks/)
138
+ -s, --static-dir <dir> (default: ./mockaton-static-mocks/)
139
139
 
140
- -m, --mocks-dir <dir> (default: ./mockaton-mocks/)
141
- -s, --static-dir <dir> (default: ./mockaton-static-mocks/)
140
+ -H, --host <host> (default: 127.0.0.1)
141
+ -p, --port <port> (default: 0) which means auto-assigned
142
+
143
+ -q, --quiet Errors only
144
+ -h, --help Show this help
145
+ -v, --version Show version
142
146
  ```
143
147
 
144
148
 
@@ -152,7 +156,6 @@ import {
152
156
  SUPPORTED_METHODS
153
157
  } from 'mockaton'
154
158
 
155
-
156
159
  export default defineConfig({
157
160
  mocksDir: 'mockaton-mocks',
158
161
  staticDir: 'mockaton-static-mocks',
@@ -186,7 +189,9 @@ export default defineConfig({
186
189
  corsCredentials: true,
187
190
  corsMaxAge: 0,
188
191
 
189
- onReady: await openInBrowser
192
+ onReady: await openInBrowser,
193
+
194
+ logLevel: 'normal'
190
195
  })
191
196
  ```
192
197
 
@@ -421,9 +426,29 @@ config.onReady = () => {}
421
426
 
422
427
  At any rate, you can trigger any command besides opening a browser.
423
428
 
429
+ <br/>
430
+
431
+ ### `logLevel?: 'quiet' | 'normal'`
432
+ Defaults to `'normal'`.
433
+
434
+ - `quiet`: only errors (stderr)
435
+ - `normal`: info, access, warnings, and errors
436
+
437
+ </details>
438
+
424
439
 
440
+ <details>
441
+ <summary>Programmatic Launch (Optional)</summary>
425
442
 
443
+ ```js
444
+ import { Mockaton } from 'mockaton'
445
+ import mockatonConfig from './mockaton.config.js'
426
446
 
447
+ Mockaton({
448
+ ...mockatonConfig, // Not required, but it’s not read by default.
449
+ port: 3333, // etc.
450
+ })
451
+ ```
427
452
  </details>
428
453
 
429
454
 
package/index.d.ts CHANGED
@@ -41,10 +41,12 @@ interface Config {
41
41
  corsMaxAge?: number
42
42
 
43
43
  onReady?: (address: string) => void
44
+
45
+ logLevel?: 'normal' | 'quiet'
44
46
  }
45
47
 
46
48
 
47
- export function Mockaton(options: Partial<Config>): Server
49
+ export function Mockaton(options: Partial<Config>): Server | undefined
48
50
  export function defineConfig(options: Partial<Config>): Config
49
51
 
50
52
  export const jsToJsonPlugin: Plugin
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "8.24.0",
5
+ "version": "8.26.0",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "test": "node --test \"src/**/*.test.js\"",
22
22
  "coverage": "node --test --test-reporter=lcov --test-reporter-destination=.coverage/lcov.info --experimental-test-coverage \"src/**/*.test.js\"",
23
- "start": "node --watch src/cli.js -c dev.config.js",
23
+ "start": "node --watch src/cli.js",
24
24
  "pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none \"pixaton-tests/**/*.test.js\"",
25
25
  "outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
26
26
  },
package/src/Dashboard.css CHANGED
@@ -305,6 +305,10 @@ main {
305
305
  table {
306
306
  border-collapse: collapse;
307
307
 
308
+ tr {
309
+ border-top: 2px solid transparent;
310
+ }
311
+
308
312
  th {
309
313
  padding-bottom: 2px;
310
314
  padding-left: 4px;
@@ -314,12 +318,6 @@ table {
314
318
  tbody {
315
319
  border-bottom: 20px solid transparent;
316
320
  }
317
-
318
- td:nth-child(3),
319
- td:nth-child(4),
320
- td:nth-child(5) {
321
- max-width: 280px;
322
- }
323
321
  }
324
322
 
325
323
  .empty {
@@ -332,7 +330,7 @@ table {
332
330
  left: -8px;
333
331
  display: inline-block;
334
332
  width: 100%;
335
- padding: 8px;
333
+ padding: 6px 8px;
336
334
  margin-left: 4px;
337
335
  border-radius: var(--radius);
338
336
  color: var(--colorAccent);
@@ -349,9 +347,8 @@ table {
349
347
  }
350
348
 
351
349
  .MockSelector {
352
- width: 100%;
353
350
  height: 26px;
354
- padding-right: 16px;
351
+ padding-right: 20px;
355
352
  padding-left: 8px;
356
353
  text-overflow: ellipsis;
357
354
  font-size: 12px;
package/src/Dashboard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
2
- import { parseFilename } from './Filename.js'
2
+ import { parseFilename, extractComments } from './Filename.js'
3
3
  import { Commander } from './ApiCommander.js'
4
4
 
5
5
 
@@ -394,11 +394,15 @@ function MockSelector({ broker }) {
394
394
  CSS.MockSelector,
395
395
  selected !== files[0] && CSS.nonDefault,
396
396
  status >= 400 && status < 500 && CSS.status4xx)
397
- }, files.map(file =>
398
- r('option', {
399
- value: file,
400
- selected: file === selected
401
- }, file))))
397
+ }, files.map(file => {
398
+ const { status, ext } = parseFilename(file)
399
+ return (
400
+ r('option', {
401
+ value: file,
402
+ selected: file === selected
403
+ }, `${status} ${ext} ${extractComments(file).join(' ')}`)
404
+ )
405
+ })))
402
406
  }
403
407
 
404
408
  /** @param {{ broker: ClientMockBroker }} props */
@@ -582,7 +586,7 @@ function PayloadViewerTitle({ file, status, statusText }) {
582
586
  const { urlMask, method, ext } = parseFilename(file)
583
587
  return (
584
588
  r('span', null,
585
- urlMask + '.' + method + '.',
589
+ urlMask.replace(/^\//, '') + '.' + method + '.',
586
590
  r('abbr', { title: statusText }, status),
587
591
  '.' + ext))
588
592
  }
@@ -851,9 +855,12 @@ function dittoSplitPaths(paths) {
851
855
 
852
856
 
853
857
  function syntaxJSON(json) {
858
+ const MAX_NODES = 1000
859
+ let nNodes = 0
854
860
  const frag = document.createDocumentFragment()
855
861
 
856
862
  function span(className, textContent) {
863
+ nNodes++
857
864
  const s = document.createElement('span')
858
865
  s.className = className
859
866
  s.textContent = textContent
@@ -861,13 +868,17 @@ function syntaxJSON(json) {
861
868
  }
862
869
 
863
870
  function text(t) {
871
+ nNodes++
864
872
  frag.appendChild(document.createTextNode(t))
865
873
  }
866
874
 
867
875
  let match
868
876
  let lastIndex = 0
869
- syntaxJSON.regex.lastIndex = 0
877
+ syntaxJSON.regex.lastIndex = 0 // resets regex
870
878
  while ((match = syntaxJSON.regex.exec(json)) !== null) {
879
+ if (nNodes > MAX_NODES)
880
+ break
881
+
871
882
  if (match.index > lastIndex)
872
883
  text(json.slice(lastIndex, match.index))
873
884
 
@@ -882,6 +893,7 @@ function syntaxJSON(json) {
882
893
  else if (str) span(CSS.syntaxStr, str)
883
894
  else span(CSS.syntaxVal, full)
884
895
  }
896
+ frag.normalize()
885
897
  text(json.slice(lastIndex))
886
898
  return frag
887
899
  }
@@ -891,9 +903,12 @@ syntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s
891
903
 
892
904
 
893
905
  function syntaxXML(xml) {
906
+ const MAX_NODES = 1000
907
+ let nNodes = 0
894
908
  const frag = document.createDocumentFragment()
895
909
 
896
910
  function span(className, textContent) {
911
+ nNodes++
897
912
  const s = document.createElement('span')
898
913
  s.className = className
899
914
  s.textContent = textContent
@@ -901,6 +916,7 @@ function syntaxXML(xml) {
901
916
  }
902
917
 
903
918
  function text(t) {
919
+ nNodes++
904
920
  frag.appendChild(document.createTextNode(t))
905
921
  }
906
922
 
@@ -908,6 +924,9 @@ function syntaxXML(xml) {
908
924
  let lastIndex = 0
909
925
  syntaxXML.regex.lastIndex = 0
910
926
  while ((match = syntaxXML.regex.exec(xml)) !== null) {
927
+ if (nNodes > MAX_NODES)
928
+ break
929
+
911
930
  if (match.index > lastIndex)
912
931
  text(xml.slice(lastIndex, match.index))
913
932
 
@@ -919,6 +938,7 @@ function syntaxXML(xml) {
919
938
  else if (match[4]) span(CSS.syntaxAttrVal, match[4])
920
939
  }
921
940
  text(xml.slice(lastIndex))
941
+ frag.normalize()
922
942
  return frag
923
943
  }
924
944
  syntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
package/src/Filename.js CHANGED
@@ -22,15 +22,8 @@ export const includesComment = (filename, search) =>
22
22
  extractComments(filename).some(comment => comment.includes(search))
23
23
 
24
24
 
25
- export function filenameIsValid(file) {
26
- const error = validateFilename(file)
27
- if (error)
28
- console.error(error, file)
29
- return !error
30
- }
31
-
32
25
  // TODO ThinkAbout 206 (reject, handle, or send in full?)
33
- function validateFilename(file) {
26
+ export function validateFilename(file) {
34
27
  const tokens = file.replace(reComments, '').split('.')
35
28
  if (tokens.length < 4)
36
29
  return 'Invalid Filename Convention'
@@ -2,6 +2,7 @@ import { join } from 'node:path'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { pathToFileURL } from 'node:url'
4
4
 
5
+ import { log } from './utils/log.js'
5
6
  import { proxy } from './ProxyRelay.js'
6
7
  import { cookie } from './cookie.js'
7
8
  import { mimeFor } from './utils/mime.js'
@@ -22,7 +23,7 @@ export async function dispatchMock(req, response) {
22
23
  return
23
24
  }
24
25
 
25
- console.log('%s %s → %s', new Date().toISOString(), decodeURIComponent(req.url), broker.file)
26
+ log.access(req.url, broker.file)
26
27
  response.statusCode = broker.status
27
28
 
28
29
  if (cookie.getCurrent())
@@ -45,7 +46,7 @@ export async function dispatchMock(req, response) {
45
46
  sendNotFound(response)
46
47
  else if (error.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
47
48
  if (error.toString().includes('Unknown file extension ".ts'))
48
- console.error('\nLooks like you need a TypeScript compiler\n')
49
+ log.warn('\nLooks like you need a TypeScript compiler\n')
49
50
  sendInternalServerError(response, error)
50
51
  }
51
52
  else
package/src/Mockaton.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createServer } from 'node:http'
2
2
 
3
+ import { log } from './utils/log.js'
3
4
  import { API } from './ApiConstants.js'
4
5
  import { config, setup } from './config.js'
5
6
  import { dispatchMock } from './MockDispatcher.js'
@@ -18,7 +19,7 @@ process.on('unhandledRejection', error => { throw error })
18
19
  export function Mockaton(options) {
19
20
  const error = setup(options)
20
21
  if (error) {
21
- console.error(error)
22
+ log.error(error)
22
23
  process.exitCode = 1
23
24
  return
24
25
  }
@@ -32,19 +33,19 @@ export function Mockaton(options) {
32
33
 
33
34
  server.listen(config.port, config.host, function (error) {
34
35
  if (error) {
35
- console.error(error)
36
+ log.error(error)
36
37
  process.exit(1)
37
38
  return
38
39
  }
39
40
  const { address, port } = this.address()
40
41
  const url = `http://${address}:${port}`
41
- console.log('Listening', url)
42
- console.log('Dashboard', url + API.dashboard)
42
+ log.info('Listening', url)
43
+ log.info('Dashboard', url + API.dashboard)
43
44
  config.onReady(url + API.dashboard)
44
45
  })
45
46
 
46
47
  server.on('error', error => {
47
- console.error(error.message)
48
+ log.error(error.message)
48
49
  process.exit(1)
49
50
  })
50
51
 
@@ -53,7 +54,7 @@ export function Mockaton(options) {
53
54
 
54
55
 
55
56
  async function onRequest(req, response) {
56
- response.on('error', console.error)
57
+ response.on('error', log.warn)
57
58
 
58
59
  try {
59
60
  response.setHeader('Server', 'Mockaton')
@@ -1,6 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { readFileSync } from 'node:fs'
3
3
 
4
+ import { log } from './utils/log.js'
4
5
  import { mimeFor } from './utils/mime.js'
5
6
  import { brokerByRoute } from './staticCollection.js'
6
7
  import { config, calcDelay } from './config.js'
@@ -12,11 +13,12 @@ export async function dispatchStatic(req, response) {
12
13
 
13
14
  setTimeout(async () => {
14
15
  if (!broker || broker.status === 404) { // TESTME
16
+ log.access(req.url, 'static404')
15
17
  sendNotFound(response)
16
18
  return
17
19
  }
18
20
 
19
- console.log('%s %s (static)', new Date().toISOString(), decodeURIComponent(req.url))
21
+ log.access(req.url, 'static200')
20
22
 
21
23
  const file = join(config.staticDir, broker.route)
22
24
  if (req.headers.range)
package/src/cli.js CHANGED
@@ -19,7 +19,10 @@ const args = parseArgs({
19
19
  'static-dir': { short: 's', type: 'string' },
20
20
 
21
21
  help: { short: 'h', type: 'boolean' },
22
- version: { short: 'v', type: 'boolean' }
22
+ version: { short: 'v', type: 'boolean' },
23
+
24
+ quiet: { short: 'q', type: 'boolean' },
25
+ debug: { type: 'boolean' }
23
26
  }
24
27
  }).values
25
28
 
@@ -32,16 +35,17 @@ else if (args.help)
32
35
  Usage: mockaton [options]
33
36
 
34
37
  Options:
35
- -c, --config <file> (default: ./mockaton.config.js)
38
+ -c, --config <file> (default: ./mockaton.config.js)
36
39
 
37
- -m, --mocks-dir <dir> (default: ./mockaton-mocks/)
38
- -s, --static-dir <dir> (default: ./mockaton-static-mocks/)
40
+ -m, --mocks-dir <dir> (default: ./mockaton-mocks/)
41
+ -s, --static-dir <dir> (default: ./mockaton-static-mocks/)
39
42
 
40
- -H, --host <host> (default: 127.0.0.1)
41
- -p, --port <port> (default: 0) which means auto-assigned
43
+ -H, --host <host> (default: 127.0.0.1)
44
+ -p, --port <port> (default: 0) which means auto-assigned
42
45
 
43
- -h, --help Show this help
44
- -v, --version Show version
46
+ -q, --quiet Errors only
47
+ -h, --help Show this help
48
+ -v, --version Show version
45
49
 
46
50
  Notes:
47
51
  * mockaton.config.js supports more options, see:
@@ -64,5 +68,7 @@ else {
64
68
  if (args['mocks-dir']) opts.mocksDir = args['mocks-dir']
65
69
  if (args['static-dir']) opts.staticDir = args['static-dir']
66
70
 
71
+ if (args.quiet) opts.logLevel = 'quiet'
72
+
67
73
  Mockaton(opts)
68
74
  }
package/src/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { join, isAbsolute } from 'node:path'
2
2
 
3
+ import { log } from './utils/log.js'
3
4
  import { isDirectory } from './utils/fs.js'
4
5
  import { openInBrowser } from './utils/openInBrowser.js'
5
6
  import { jsToJsonPlugin } from './MockDispatcher.js'
@@ -48,7 +49,9 @@ const schema = {
48
49
  corsCredentials: [true, is(Boolean)],
49
50
  corsMaxAge: [0, is(Number)],
50
51
 
51
- onReady: [await openInBrowser, is(Function)]
52
+ onReady: [await openInBrowser, is(Function)],
53
+
54
+ logLevel: ['normal', val => ['normal', 'quiet'].includes(val)]
52
55
  }
53
56
 
54
57
 
@@ -68,18 +71,19 @@ export const ConfigValidator = Object.freeze(validators)
68
71
 
69
72
  /** @param {Partial<Config>} options */
70
73
  export function setup(options) {
71
- if (options.mocksDir && !isAbsolute(options.mocksDir))
74
+ if (options.mocksDir && !isAbsolute(options.mocksDir))
72
75
  options.mocksDir = join(process.cwd(), options.mocksDir)
73
-
76
+
74
77
  if (options.staticDir && !isAbsolute(options.staticDir))
75
78
  options.staticDir = join(process.cwd(), options.staticDir)
76
-
77
- if (!options.staticDir && !isDirectory(defaults.staticDir))
79
+
80
+ if (!options.staticDir && !isDirectory(defaults.staticDir))
78
81
  options.staticDir = ''
79
82
 
80
83
  try {
81
84
  Object.assign(config, options)
82
85
  validate(config, ConfigValidator)
86
+ log.setLevel(config.logLevel)
83
87
  }
84
88
  catch (err) {
85
89
  return err.message
@@ -1,10 +1,11 @@
1
1
  import { basename } from 'node:path'
2
2
 
3
+ import { log } from './utils/log.js'
3
4
  import { cookie } from './cookie.js'
4
5
  import { MockBroker } from './MockBroker.js'
5
6
  import { listFilesRecursively } from './utils/fs.js'
6
7
  import { config, isFileAllowed } from './config.js'
7
- import { parseFilename, filenameIsValid } from './Filename.js'
8
+ import { parseFilename, validateFilename } from './Filename.js'
8
9
 
9
10
 
10
11
  /**
@@ -64,6 +65,13 @@ export function registerMock(file, isFromWatcher = false) {
64
65
  return true
65
66
  }
66
67
 
68
+ function filenameIsValid(file) {
69
+ const error = validateFilename(file)
70
+ if (error)
71
+ log.warn(error, file)
72
+ return !error
73
+ }
74
+
67
75
  export function unregisterMock(file) {
68
76
  const broker = brokerByFilename(file)
69
77
  if (!broker)
package/src/utils/fs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { join, dirname, sep, posix } from 'node:path'
2
2
  import { lstatSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
3
+ import { log } from './log.js'
3
4
 
4
5
 
5
6
  export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
@@ -24,6 +25,6 @@ export const write = (path, body) => {
24
25
  writeFileSync(path, body)
25
26
  }
26
27
  catch (err) {
27
- console.error('Write access denied', err)
28
+ log.warn('Write access denied', err)
28
29
  }
29
30
  }
@@ -1,4 +1,5 @@
1
1
  import fs, { readFileSync } from 'node:fs'
2
+ import { log } from './log.js'
2
3
  import { mimeFor } from './mime.js'
3
4
  import { HEADER_FOR_502 } from '../ApiConstants.js'
4
5
 
@@ -28,19 +29,19 @@ export function sendNotFound(response) {
28
29
  }
29
30
 
30
31
  export function sendUnprocessableContent(response, error) {
31
- console.error(error)
32
+ log.warn(error)
32
33
  response.statusCode = 422
33
34
  response.end(error)
34
35
  }
35
36
 
36
37
  export function sendInternalServerError(response, error) {
37
- console.error(error)
38
+ log.error(error)
38
39
  response.statusCode = 500
39
40
  response.end()
40
41
  }
41
42
 
42
43
  export function sendBadGateway(response, error) {
43
- console.error('Fallback Proxy Error:', error.cause.message)
44
+ log.warn('Fallback Proxy Error:', error.cause.message)
44
45
  response.statusCode = 502
45
46
  response.setHeader(HEADER_FOR_502, 1)
46
47
  response.end()
@@ -0,0 +1,34 @@
1
+ export const log = new class {
2
+ #level = 'normal'
3
+
4
+ setLevel(level) {
5
+ this.#level = level
6
+ }
7
+
8
+ info(...msg) {
9
+ if (this.#level !== 'quiet')
10
+ console.info([this.#date, 'INFO', ...msg].join('::'))
11
+ }
12
+
13
+ access(url, ...msg) {
14
+ if (this.#level !== 'quiet')
15
+ console.log([this.#date, 'ACCESS', this.#sanitizeURL(url), ...msg].join('::'))
16
+ }
17
+
18
+ warn(...msg) {
19
+ console.warn([this.#date, 'WARN', ...msg].join('::'))
20
+ }
21
+
22
+ error(...msg) {
23
+ console.error([this.#date, 'ERROR', ...msg].join('::'))
24
+ }
25
+
26
+
27
+ get #date() {
28
+ return new Date().toISOString()
29
+ }
30
+
31
+ #sanitizeURL(url) {
32
+ return decodeURIComponent(url).replace(/[\x00-\x1F\x7F\x9B]/g, '')
33
+ }
34
+ }