mockaton 8.12.1 → 8.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,11 +4,11 @@
4
4
  ![NPM Version](https://img.shields.io/npm/l/mockaton)
5
5
 
6
6
  An HTTP mock server for simulating APIs with minimal setup
7
- — ideal for testing edge cases and prototyping UIs.
7
+ — ideal for triggering difficult to reproduce backend states.
8
+
8
9
 
9
10
  ## Convention Over Code
10
- With Mockaton you don’t need to write code for wiring mocks. Instead, it scans a
11
- given directory for filenames following a convention similar to the URLs.
11
+ Mockaton scans a given directory for filenames following a convention similar to the URLs.
12
12
 
13
13
  For example, for <code>/<b>api/user</b>/1234</code> the filename would be:
14
14
  <pre>
@@ -22,7 +22,7 @@ On the dashboard you can select a mock variant for a particular route, delaying
22
22
  or triggering an autogenerated `500` (Internal Server Error), among other features.
23
23
 
24
24
  Nonetheless, there’s a programmatic API, which is handy
25
- for setting up tests. See **Commander&nbsp;API** section.
25
+ for setting up tests (see **Commander&nbsp;API** section).
26
26
 
27
27
  <picture>
28
28
  <source media="(prefers-color-scheme: light)" srcset="./pixaton-tests/pic-for-readme.vp840x800.light.gold.png">
@@ -47,12 +47,12 @@ api/login<b>(invalid login attempt)</b>.POST.401.json
47
47
 
48
48
  ### Different response status code
49
49
  For instance, you can use a `4xx` or `5xx` status code for triggering error
50
- responses, or a `2xx` such as `204` (No Content) for testing empty collections.
50
+ responses, or a `2xx` such as `204` for testing empty collections.
51
51
 
52
52
  <pre>
53
- api/videos(empty list).GET.<b>204</b>.json
54
- api/videos.GET.<b>403</b>.json
55
- api/videos.GET.<b>500</b>.txt
53
+ api/videos(empty list).GET.<b>204</b>.json # No Content
54
+ api/videos.GET.<b>403</b>.json # Forbidden
55
+ api/videos.GET.<b>500</b>.txt # Internal Server Error
56
56
  </pre>
57
57
 
58
58
 
@@ -265,12 +265,15 @@ want a `Content-Type` header in the response.
265
265
  </p>
266
266
  </details>
267
267
 
268
+ <br/>
269
+
268
270
  ### Dynamic parameters
269
- Anything within square brackets is always matched. For example, for this route
270
- `/api/company/1234/user/5678`
271
- <pre>
272
- api/company/<b>[id]</b>/user/<b>[uid]</b>.GET.200.json
273
- </pre>
271
+ Anything within square brackets is always matched. For example, for this route:
272
+ <code>/api/company/<b>1234</b>/user/<b>5678</b></code>
273
+
274
+ <pre><code>api/company/<b>[id]</b>/user/<b>[uid]</b>.GET.200.json</code></pre>
275
+
276
+ <br/>
274
277
 
275
278
  ### Comments
276
279
  Comments are anything within parentheses, including them.
@@ -283,6 +286,8 @@ api/foo.GET.200.json
283
286
 
284
287
  A filename can have many comments.
285
288
 
289
+ <br/>
290
+
286
291
  ### Default mock for a route
287
292
  You can add the comment: `(default)`.
288
293
  Otherwise, the first file in **alphabetical order** wins.
@@ -291,9 +296,10 @@ Otherwise, the first file in **alphabetical order** wins.
291
296
  api/user<b>(default)</b>.GET.200.json
292
297
  </pre>
293
298
 
299
+ <br/>
294
300
 
295
301
  ### Query string params
296
- The query string is ignored when routing to it. In other words, it’s only used for
302
+ The query string is ignored for routing purposes. In other words, it’s only used for
297
303
  documenting the URL contract.
298
304
  <pre>
299
305
  api/video<b>?limit=[limit]</b>.GET.200.json
@@ -302,17 +308,18 @@ api/video<b>?limit=[limit]</b>.GET.200.json
302
308
  On Windows filenames containing "?" are [not
303
309
  permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query string it’s ignored anyway.
304
310
 
311
+ <br/>
305
312
 
306
313
  ### Index-like routes
307
314
  If you have `api/foo` and `api/foo/bar`, you have two options:
308
315
 
309
- **Option A:**
316
+ **Option A.** Standard naming:
310
317
  ```
311
318
  api/foo.GET.200.json
312
319
  api/foo/bar.GET.200.json
313
320
  ```
314
321
 
315
- **Option B:** Omit the filename.
322
+ **Option B.** Omit the URL on the filename:
316
323
  ```text
317
324
  api/foo/.GET.200.json
318
325
  api/foo/bar.GET.200.json
@@ -324,25 +331,39 @@ api/foo/bar.GET.200.json
324
331
  ### `mocksDir: string`
325
332
  This is the only required field. The directory must exist.
326
333
 
334
+ ### `staticDir?: string`
335
+ - Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
336
+ - Use Case 2: For a standalone demo server. For example,
337
+ build your frontend bundle, and serve it from Mockaton.
338
+
339
+ Files under `config.staticDir` don’t use the filename convention, and
340
+ they take precedence over corresponding `GET` mocks in `config.mocksDir`.
341
+ For example, if you have two files for `GET /foo/bar.jpg`
342
+
343
+ <pre>
344
+ my-static-dir<b>/foo/bar.jpg</b>
345
+ my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg // Unreachable
346
+ </pre>
347
+
348
+ ### `ignore?: RegExp`
349
+ Defaults to `/(\.DS_Store|~)$/`
350
+
351
+ <br/>
352
+
327
353
 
328
354
  ### `host?: string`
329
355
  Defaults to `'localhost'`
330
356
 
331
-
332
357
  ### `port?: number`
333
358
  Defaults to `0`, which means auto-assigned
334
359
 
335
-
336
- ### `ignore?: RegExp`
337
- Defaults to `/(\.DS_Store|~)$/`
338
-
360
+ <br/>
339
361
 
340
362
  ### `delay?: number`
341
- Defaults to `1200` milliseconds.
342
-
343
- Although routes can individually be delayed with the 🕓
344
- checkbox, the delay amount is globally configurable.
363
+ Defaults to `1200` milliseconds. Although routes can individually be delayed
364
+ with the 🕓 checkbox, the delay amount is globally configurable with option.
345
365
 
366
+ <br/>
346
367
 
347
368
  ### `proxyFallback?: string`
348
369
  For example, `config.proxyFallback = 'http://example.com'`
@@ -384,19 +405,7 @@ Defaults to `true`. Saves the mock with the formatting output
384
405
  of `JSON.stringify(data, null, ' ')` (two spaces indentation).
385
406
 
386
407
 
387
- ### `staticDir?: string`
388
- - Use Case 1: If you have a bunch of static assets you don’t want to add `.GET.200.ext`
389
- - Use Case 2: For a standalone demo server. For example,
390
- build your frontend bundle, and serve it from Mockaton.
391
-
392
- Files under `config.staticDir` don’t use the filename convention, and
393
- they take precedence over corresponding `GET` mocks in `config.mocksDir`.
394
- For example, if you have two files for `GET /foo/bar.jpg`
395
-
396
- <pre>
397
- my-static-dir<b>/foo/bar.jpg</b>
398
- my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg // Unreachable
399
- </pre>
408
+ <br/>
400
409
 
401
410
 
402
411
  ### `cookies?: { [label: string]: string }`
@@ -422,6 +431,7 @@ in `config.extraHeaders`, or in function `.js` or `.ts` mock.
422
431
  By the way, the `jwtCookie` helper has a hardcoded header and signature.
423
432
  In other words, it’s useful only if you care about its payload.
424
433
 
434
+ <br/>
425
435
 
426
436
  ### `extraHeaders?: string[]`
427
437
  Note: it’s a one-dimensional array. The header name goes at even indices.
@@ -434,6 +444,7 @@ config.extraHeaders = [
434
444
  ]
435
445
  ```
436
446
 
447
+ <br/>
437
448
 
438
449
  ### `extraMimes?: { [fileExt: string]: string }`
439
450
  ```js
@@ -444,6 +455,7 @@ config.extraMimes = {
444
455
  Those extra media types take precedence over the built-in
445
456
  [utils/mime.js](src/utils/mime.js), so you can override them.
446
457
 
458
+ <br/>
447
459
 
448
460
  ### `plugins?: [filenameTester: RegExp, plugin: Plugin][]`
449
461
  ```ts
@@ -499,6 +511,7 @@ function capitalizePlugin(filePath) {
499
511
  ```
500
512
  </details>
501
513
 
514
+ <br/>
502
515
 
503
516
  ### `corsAllowed?: boolean`
504
517
  Defaults to `true`. When `true`, these are the default options:
@@ -511,6 +524,7 @@ config.corsMaxAge = 0 // seconds to cache the preflight req
511
524
  config.corsExposedHeaders = [] // headers you need to access in client-side JS
512
525
  ```
513
526
 
527
+ <br/>
514
528
 
515
529
  ### `onReady?: (dashboardUrl: string) => void`
516
530
  By default, it will open the dashboard in your default browser on macOS and
@@ -536,10 +550,13 @@ const myMockatonAddr = 'http://localhost:2345'
536
550
  const mockaton = new Commander(myMockatonAddr)
537
551
  ```
538
552
 
553
+ <br/>
554
+
539
555
  ### Select a mock file for a route
540
556
  ```js
541
557
  await mockaton.select('api/foo.200.GET.json')
542
558
  ```
559
+
543
560
  ### Select all mocks that have a particular comment
544
561
  ```js
545
562
  await mockaton.bulkSelectByComment('(demo-a)')
@@ -548,33 +565,41 @@ Parentheses are optional, so you can pass a partial match.
548
565
  For example, passing `'demo-'` (without the final `a`), selects the
549
566
  first mock in alphabetical order that matches.
550
567
 
568
+ <br/>
569
+
551
570
  ### Set route is delayed flag
552
571
  ```js
553
572
  await mockaton.setRouteIsDelayed('GET', '/api/foo', true)
554
573
  ```
555
574
 
556
- ### Set route is proxied
575
+ ### Set route is proxied flag
557
576
  ```js
558
577
  await mockaton.setRouteIsProxied('GET', '/api/foo', true)
559
578
  ```
560
579
 
580
+ <br/>
581
+
561
582
  ### Select a cookie
562
583
  In `config.cookies`, each key is the label used for selecting it.
563
584
  ```js
564
585
  await mockaton.selectCookie('My Normal User')
565
586
  ```
566
587
 
567
- ### Set fallback proxy
588
+ <br/>
589
+
590
+ ### Set fallback proxy server address
568
591
  ```js
569
592
  await mockaton.setProxyFallback('http://example.com')
570
593
  ```
571
594
  Pass an empty string to disable it.
572
595
 
573
- ### Set save proxied mocks
596
+ ### Set save proxied responses as mocks flag
574
597
  ```js
575
598
  await mockaton.setCollectProxied(true)
576
599
  ```
577
600
 
601
+ <br/>
602
+
578
603
  ### Reset
579
604
  Re-initialize the collection. The selected mocks, cookies, and delays go back to
580
605
  default, but the `proxyFallback`, `colledProxied`, and `corsAllowed` are not affected.
package/index.d.ts CHANGED
@@ -16,13 +16,17 @@ interface Config {
16
16
 
17
17
  host?: string,
18
18
  port?: number
19
+
19
20
  proxyFallback?: string
20
21
  collectProxied?: boolean
21
22
  formatCollectedJSON?: boolean
22
23
 
23
24
  delay?: number
25
+
24
26
  cookies?: { [label: string]: string }
27
+
25
28
  extraHeaders?: string[]
29
+
26
30
  extraMimes?: { [fileExt: string]: string }
27
31
 
28
32
  plugins?: [filenameTester: RegExp, plugin: Plugin][]
@@ -46,7 +50,7 @@ export const jsToJsonPlugin: Plugin
46
50
 
47
51
  // Utils
48
52
 
49
- export function jwtCookie(cookieName: string, payload: any): string
53
+ export function jwtCookie(cookieName: string, payload: any, path?: string): string
50
54
 
51
55
  export function parseJSON(request: IncomingMessage): Promise<any>
52
56
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "A deterministic server-side for developing and testing APIs",
4
4
  "type": "module",
5
- "version": "8.12.1",
5
+ "version": "8.12.3",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  import { join } from 'node:path'
7
7
  import { cookie } from './cookie.js'
8
- import { config } from './config.js'
9
8
  import { arEvents } from './Watcher.js'
10
9
  import { parseJSON } from './utils/http-request.js'
11
10
  import { listFilesRecursively } from './utils/fs.js'
12
11
  import * as mockBrokersCollection from './mockBrokersCollection.js'
12
+ import { config, isFileAllowed, ConfigValidator } from './config.js'
13
13
  import { DF, API, LONG_POLL_SERVER_TIMEOUT } from './ApiConstants.js'
14
14
  import { sendOK, sendJSON, sendUnprocessableContent, sendFile } from './utils/http-response.js'
15
15
 
@@ -50,6 +50,7 @@ export const apiPatchRequests = new Map([
50
50
  [API.collectProxied, setCollectProxied]
51
51
  ])
52
52
 
53
+
53
54
  /* === GET === */
54
55
 
55
56
  function serveDashboard(_, response) {
@@ -70,20 +71,18 @@ function getIsCorsAllowed(_, response) { sendJSON(response, config.corsAllowed)
70
71
  function getCollectProxied(_, response) { sendJSON(response, config.collectProxied) }
71
72
 
72
73
  function listStaticFiles(req, response) {
73
- const files = config.staticDir
74
- ? listFilesRecursively(config.staticDir).filter(f => !config.ignore.test(f))
75
- : []
76
- sendJSON(response, files)
74
+ sendJSON(response, config.staticDir
75
+ ? listFilesRecursively(config.staticDir).filter(isFileAllowed)
76
+ : [])
77
77
  }
78
78
 
79
79
  function longPollAR_Events(req, response) {
80
- // e.g. tab was hidden while new mocks were added or removed
80
+ // needs sync e.g. when tab was hidden while new mocks were added or removed
81
81
  const clientIsOutOfSync = parseInt(req.headers[DF.lastReceived_nAR], 10) !== arEvents.count
82
82
  if (clientIsOutOfSync) {
83
83
  sendJSON(response, arEvents.count)
84
84
  return
85
85
  }
86
-
87
86
  function onAddOrRemoveMock() {
88
87
  arEvents.unsubscribe(onAddOrRemoveMock)
89
88
  sendJSON(response, arEvents.count)
@@ -115,7 +114,7 @@ async function selectCookie(req, response) {
115
114
 
116
115
  async function selectMock(req, response) {
117
116
  const file = await parseJSON(req)
118
- const broker = mockBrokersCollection.getBrokerByFilename(file)
117
+ const broker = mockBrokersCollection.findBrokerByFilename(file)
119
118
  if (!broker || !broker.hasMock(file))
120
119
  sendUnprocessableContent(response, `Missing Mock: ${file}`)
121
120
  else {
@@ -127,7 +126,7 @@ async function selectMock(req, response) {
127
126
  async function setRouteIsDelayed(req, response) {
128
127
  const body = await parseJSON(req)
129
128
  const delayed = body[DF.delayed]
130
- const broker = mockBrokersCollection.getBrokerForUrl(
129
+ const broker = mockBrokersCollection.findBrokerByRoute(
131
130
  body[DF.routeMethod],
132
131
  body[DF.routeUrlMask])
133
132
 
@@ -144,7 +143,7 @@ async function setRouteIsDelayed(req, response) {
144
143
  async function setRouteIsProxied(req, response) { // TESTME
145
144
  const body = await parseJSON(req)
146
145
  const proxied = body[DF.proxied]
147
- const broker = mockBrokersCollection.getBrokerForUrl(
146
+ const broker = mockBrokersCollection.findBrokerByRoute(
148
147
  body[DF.routeMethod],
149
148
  body[DF.routeUrlMask])
150
149
 
@@ -162,7 +161,7 @@ async function setRouteIsProxied(req, response) { // TESTME
162
161
 
163
162
  async function updateProxyFallback(req, response) {
164
163
  const fallback = await parseJSON(req)
165
- if (fallback && !URL.canParse(fallback)) {
164
+ if (!ConfigValidator.proxyFallback(fallback)) {
166
165
  sendUnprocessableContent(response)
167
166
  return
168
167
  }
@@ -173,7 +172,12 @@ async function updateProxyFallback(req, response) {
173
172
  }
174
173
 
175
174
  async function setCollectProxied(req, response) {
176
- config.collectProxied = await parseJSON(req)
175
+ const collectProxied = await parseJSON(req)
176
+ if (!ConfigValidator.collectProxied(collectProxied)) { // TESTME
177
+ sendUnprocessableContent(response)
178
+ return
179
+ }
180
+ config.collectProxied = collectProxied
177
181
  sendOK(response)
178
182
  }
179
183
 
package/src/MockBroker.js CHANGED
@@ -24,7 +24,7 @@ export class MockBroker {
24
24
  hasMock(file) { return this.mocks.includes(file) }
25
25
 
26
26
  register(file) {
27
- if (parseFilename(file).status === 500) {
27
+ if (this.#is500(file)) {
28
28
  if (this.temp500IsSelected)
29
29
  this.selectFile(file)
30
30
  this.#deleteTemp500()
@@ -33,6 +33,10 @@ export class MockBroker {
33
33
  this.#sortMocks()
34
34
  }
35
35
 
36
+ #is500(file) {
37
+ return parseFilename(file).status === 500
38
+ }
39
+
36
40
  #deleteTemp500() {
37
41
  this.mocks = this.mocks.filter(file => !this.#isTemp500(file))
38
42
  }
@@ -44,7 +48,7 @@ export class MockBroker {
44
48
  #sortMocks() {
45
49
  this.mocks.sort()
46
50
  const defaults = this.mocks.filter(file => includesComment(file, DEFAULT_MOCK_COMMENT))
47
- const temp500 = this.mocks.filter(file => includesComment(file, DEFAULT_500_COMMENT))
51
+ const temp500 = this.mocks.filter(this.#isTemp500)
48
52
  this.mocks = [
49
53
  ...defaults,
50
54
  ...this.mocks.filter(file => !defaults.includes(file) && !temp500.includes(file)),
@@ -53,10 +57,9 @@ export class MockBroker {
53
57
  }
54
58
 
55
59
  ensureItHas500() {
56
- if (!this.#has500())
60
+ if (!this.mocks.some(this.#is500))
57
61
  this.#registerTemp500()
58
62
  }
59
- #has500() { return this.mocks.some(mock => parseFilename(mock).status === 500) }
60
63
  #registerTemp500() {
61
64
  const { urlMask, method } = parseFilename(this.mocks[0])
62
65
  const file = urlMask.replace(/^\//, '') // Removes leading slash
@@ -11,7 +11,7 @@ import { sendInternalServerError, sendNotFound, sendUnprocessableContent } from
11
11
 
12
12
  export async function dispatchMock(req, response) {
13
13
  try {
14
- const broker = mockBrokerCollection.getBrokerForUrl(req.method, req.url)
14
+ const broker = mockBrokerCollection.findBrokerByRoute(req.method, req.url)
15
15
  if (!broker || broker.proxied) {
16
16
  if (config.proxyFallback)
17
17
  await proxy(req, response, config.delay * Boolean(broker?.delayed))
@@ -435,6 +435,7 @@ async function testAutogenerates500(url, file) {
435
435
  await describe('autogenerated in-memory 500', () => {
436
436
  it('body is empty', () => equal(body, ''))
437
437
  it('status is: 500', () => equal(res.status, 500))
438
+ it('mime is empty', () => equal(res.headers.get('content-type'), ''))
438
439
  })
439
440
  }
440
441
 
@@ -1,9 +1,9 @@
1
1
  import { join } from 'node:path'
2
2
  import fs, { readFileSync } from 'node:fs'
3
3
 
4
- import { config } from './config.js'
5
4
  import { mimeFor } from './utils/mime.js'
6
5
  import { isDirectory, isFile } from './utils/fs.js'
6
+ import { config, isFileAllowed } from './config.js'
7
7
  import { sendInternalServerError } from './utils/http-response.js'
8
8
 
9
9
 
@@ -11,7 +11,7 @@ export function isStatic(req) {
11
11
  if (!config.staticDir)
12
12
  return false
13
13
  const f = resolvePath(req.url)
14
- return f && !config.ignore.test(f)
14
+ return f && isFileAllowed(f)
15
15
  }
16
16
 
17
17
  export async function dispatchStatic(req, response) {
@@ -19,14 +19,8 @@ export async function dispatchStatic(req, response) {
19
19
  if (req.headers.range)
20
20
  await sendPartialContent(response, req.headers.range, file)
21
21
  else {
22
- const contentType = mimeFor(file)
23
- response.setHeader('Content-Type', contentType)
24
- // Use binary encoding for non-text files
25
- const isTextFile = contentType.startsWith('text/') ||
26
- contentType === 'application/json' ||
27
- contentType === 'application/javascript' ||
28
- contentType === 'application/xml';
29
- response.end(readFileSync(file, isTextFile ? 'utf8' : null))
22
+ response.setHeader('Content-Type', mimeFor(file))
23
+ response.end(readFileSync(file))
30
24
  }
31
25
  }
32
26
 
package/src/config.js CHANGED
@@ -2,90 +2,76 @@ import { realpathSync } from 'node:fs'
2
2
  import { isDirectory } from './utils/fs.js'
3
3
  import { openInBrowser } from './utils/openInBrowser.js'
4
4
  import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
5
+ import { optional, is, validate } from './utils/validate.js'
5
6
  import { SUPPORTED_METHODS } from './utils/http-request.js'
6
7
  import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
7
8
 
8
9
 
9
- /** @type {Config} */
10
- export const config = Object.seal({
11
- mocksDir: '',
12
- staticDir: '',
13
- ignore: /(\.DS_Store|~)$/,
14
-
15
- host: '127.0.0.1',
16
- port: 0, // auto-assigned
17
- proxyFallback: '', // e.g. http://localhost:9999
18
- collectProxied: false,
19
- formatCollectedJSON: true,
20
-
21
- delay: 1200, // milliseconds
22
- cookies: {}, // defaults to the first kv
23
- extraHeaders: [],
24
- extraMimes: {},
10
+ /** @type {{
11
+ * [K in keyof Config]-?: [
12
+ * defaultVal: Config[K],
13
+ * validator: (val: unknown) => boolean
14
+ * ]
15
+ * }} */
16
+ const schema = {
17
+ mocksDir: ['', isDirectory],
18
+ staticDir: ['', optional(isDirectory)],
19
+ ignore: [/(\.DS_Store|~)$/, is(RegExp)],
25
20
 
26
- plugins: [
27
- [/\.(js|ts)$/, jsToJsonPlugin]
28
- ],
21
+ host: ['127.0.0.1', is(String)],
22
+ port: [0, port => Number.isInteger(port) && port >= 0 && port < 2 ** 16], // 0 means auto-assigned
29
23
 
30
- corsAllowed: true,
31
- corsOrigins: ['*'],
32
- corsMethods: SUPPORTED_METHODS,
33
- corsHeaders: ['content-type'],
34
- corsExposedHeaders: [],
35
- corsCredentials: true,
36
- corsMaxAge: 0,
24
+ proxyFallback: ['', optional(URL.canParse)], // e.g. http://localhost:9999
25
+ collectProxied: [false, is(Boolean)],
26
+ formatCollectedJSON: [true, is(Boolean)],
37
27
 
38
- onReady: await openInBrowser
39
- })
28
+ delay: [1200, ms => Number.isInteger(ms) && ms >= 0],
40
29
 
30
+ cookies: [{}, is(Object)], // defaults to the first kv
41
31
 
42
- export function setup(options) {
43
- Object.assign(config, options)
44
- validate(config, {
45
- mocksDir: isDirectory,
46
- staticDir: optional(isDirectory),
47
- ignore: is(RegExp),
48
-
49
- host: is(String),
50
- port: port => Number.isInteger(port) && port >= 0 && port < 2 ** 16,
51
- proxyFallback: optional(URL.canParse),
52
- collectProxied: is(Boolean),
53
- formatCollectedJSON: is(Boolean),
54
-
55
- delay: ms => Number.isInteger(ms) && ms > 0,
56
- cookies: is(Object),
57
- extraHeaders: val => Array.isArray(val) && val.length % 2 === 0,
58
- extraMimes: is(Object),
59
-
60
- plugins: Array.isArray,
61
-
62
- corsAllowed: is(Boolean),
63
- corsOrigins: validateCorsAllowedOrigins,
64
- corsMethods: validateCorsAllowedMethods,
65
- corsHeaders: Array.isArray,
66
- corsExposedHeaders: Array.isArray,
67
- corsCredentials: is(Boolean),
68
- corsMaxAge: is(Number),
69
-
70
- onReady: is(Function)
71
- })
32
+ extraHeaders: [[], val => Array.isArray(val) && val.length % 2 === 0],
72
33
 
73
- config.mocksDir = realpathSync(config.mocksDir)
74
- if (config.staticDir)
75
- config.staticDir = realpathSync(config.staticDir)
34
+ extraMimes: [{}, is(Object)],
35
+
36
+ plugins: [
37
+ [
38
+ [/\.(js|ts)$/, jsToJsonPlugin]
39
+ ], Array.isArray],
40
+
41
+ corsAllowed: [true, is(Boolean)],
42
+ corsOrigins: [['*'], validateCorsAllowedOrigins],
43
+ corsMethods: [SUPPORTED_METHODS, validateCorsAllowedMethods],
44
+ corsHeaders: [['content-type'], Array.isArray],
45
+ corsExposedHeaders: [[], Array.isArray],
46
+ corsCredentials: [true, is(Boolean)],
47
+ corsMaxAge: [0, is(Number)],
48
+
49
+ onReady: [await openInBrowser, is(Function)]
76
50
  }
77
51
 
78
52
 
79
- function validate(obj, shape) {
80
- for (const [field, value] of Object.entries(obj))
81
- if (!shape[field](value))
82
- throw new TypeError(`config.${field}=${JSON.stringify(value)} is invalid`)
53
+ const defaults = {}
54
+ const validators = {}
55
+ for (const [k, [defaultVal, validator]] of Object.entries(schema)) {
56
+ defaults[k] = defaultVal
57
+ validators[k] = validator
83
58
  }
84
59
 
85
- function is(ctor) {
86
- return val => val.constructor === ctor
87
- }
60
+ /** @type {Config} */
61
+ export const config = Object.seal(defaults)
88
62
 
89
- function optional(tester) {
90
- return val => !val || tester(val)
63
+ /** @type {Record<keyof Config, (val: unknown) => boolean>} */
64
+ export const ConfigValidator = Object.freeze(validators)
65
+
66
+
67
+ export const isFileAllowed = f => !config.ignore.test(f)
68
+
69
+
70
+ export function setup(options) {
71
+ Object.assign(config, options)
72
+ validate(config, ConfigValidator)
73
+
74
+ config.mocksDir = realpathSync(config.mocksDir)
75
+ if (config.staticDir)
76
+ config.staticDir = realpathSync(config.staticDir)
91
77
  }