mockaton 13.15.0 → 14.0.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
@@ -69,7 +69,7 @@ npx skills add ericfortis/mockaton
69
69
 
70
70
  ## Basic Usage
71
71
  ```sh
72
- mockaton --port 2020 my-mocks-dir
72
+ mockaton --port 2020 my-mocks-dir
73
73
  ```
74
74
 
75
75
  Mockaton will serve the files on the given directory. It's a file-system based router in which
@@ -82,7 +82,7 @@ code variants.
82
82
  | Route | Filename | Description |
83
83
  | -----| -----| ---|
84
84
  | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
85
- | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
85
+ | /media/avatar.png | media/avatar.png | Statics assets (GET 200) don’t need the extension convention. |
86
86
  | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. You can add many comments, `foo(c0)(c1).png` |
87
87
  | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment. Otherwise, the first mock variant in alphabetical order wins. |
88
88
  | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
package/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare type Plugin = (
11
11
 
12
12
  export declare interface Config {
13
13
  mocksDir?: string
14
- ignore?: RegExp
14
+ shouldIgnore?: (filepath: string) => boolean
15
15
  watcherEnabled?: boolean
16
16
  watcherDebounceMs?: number
17
17
  readOnly?: boolean
@@ -61,6 +61,7 @@ export declare const echoFilePlugin: Plugin
61
61
 
62
62
  export declare function jwtCookie(cookieName: string, payload: any, path?: string): string
63
63
 
64
+ export declare function shouldIgnore(filepath: string): boolean
64
65
  export declare function parseJSON(request: IncomingMessage): Promise<any>
65
66
  export declare function parseSegments(reqUrl: string, filename: string): Record<string, string>
66
67
  export declare function parseQueryParams(reqUrl: string): URLSearchParams
package/index.js CHANGED
@@ -5,6 +5,7 @@ export { Mockaton } from './src/server/Mockaton.js'
5
5
  export { jwtCookie } from './src/server/utils/jwt.js'
6
6
  export { jsToJsonPlugin, echoFilePlugin } from './src/server/MockDispatcherPlugins.js'
7
7
  export { parseJSON, BodyReaderError } from './src/server/utils/HttpIncomingMessage.js'
8
- export { parseSegments, parseQueryParams, parseSplats } from './src/server/UrlParsers.js'
8
+ export { parseSegments, parseQueryParams } from './src/server/UrlParsers.js'
9
+ export { shouldIgnore } from './src/server/stores/config.js'
9
10
 
10
11
  export const defineConfig = opts => opts
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.15.0",
5
+ "version": "14.0.0",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -15,7 +15,7 @@ npm install -g mockaton
15
15
 
16
16
  ## Basic Usage
17
17
  ```sh
18
- mockaton --port 2020 my-mocks-dir
18
+ mockaton --port 2020 my-mocks-dir
19
19
  ```
20
20
 
21
21
  Mockaton will serve the files on the given directory. It's a file-system based router in which
@@ -28,7 +28,7 @@ code variants.
28
28
  | Route | Filename | Description |
29
29
  | -----| -----| ---|
30
30
  | /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
31
- | /media/avatar.png | media/avatar.png | Statics assets don't need the above extension. |
31
+ | /media/avatar.png | media/avatar.png | Statics assets (GET 200) don’t need the extension convention. |
32
32
  | /api/login | api/login(invalid attempt).POST.401.ts | Anything within parenthesis is a comment. They are ignored when routing. You can add many comments, `foo(c0)(c1).png` |
33
33
  | /api/login | api/login(default).GET.200.ts | `(default)` is a special comment. Otherwise, the first mock variant in alphabetical order wins. |
34
34
  | /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
@@ -4,13 +4,13 @@
4
4
  * @param {string[]} paths - sorted
5
5
  */
6
6
  export function dittoSplitPaths(paths) {
7
- const segments = paths.map(p => p.split('/').filter(Boolean))
7
+ const pathsSplit = paths.map(p => p.split('/').filter(Boolean))
8
8
  return paths.map((p, i) => {
9
9
  if (i === 0)
10
10
  return ['', p]
11
11
 
12
- const prev = segments[i - 1]
13
- const curr = segments[i]
12
+ const prev = pathsSplit[i - 1]
13
+ const curr = pathsSplit[i]
14
14
  const min = Math.min(curr.length, prev.length)
15
15
  let j = 0
16
16
  while (j < min && curr[j] === prev[j])
@@ -28,19 +28,19 @@ function trie(brokers) {
28
28
  function dfs(node) {
29
29
  const childBrokers = node.getChildren().flatMap(dfs)
30
30
 
31
- const brokers = node.brokers.length
32
- ? [node.brokers[0], ...childBrokers, ...node.brokers.slice(1)]
33
- : childBrokers
34
-
35
- if (!brokers.length)
36
- return []
31
+ if (node.brokers.length) {
32
+ const [b0, ...rest] = node.brokers
33
+ b0.children.push(...childBrokers, ...rest)
34
+ return [b0]
35
+ }
37
36
 
38
- const [b0, ...rest] = brokers
39
- if (node.brokers.length || !b0.children.length) {
37
+ if (childBrokers.length && !childBrokers[0].children.length) {
38
+ const [b0, ...rest] = childBrokers
40
39
  b0.children.push(...rest)
41
40
  return [b0]
42
41
  }
43
- return brokers
42
+
43
+ return childBrokers
44
44
  }
45
45
 
46
46
  class TrieNode {
package/src/server/Api.js CHANGED
@@ -23,12 +23,12 @@ import * as Watcher from './stores/Watcher.js'
23
23
 
24
24
  export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
25
25
 
26
- const headReqs = new Map([
26
+ const HEAD = new Map([
27
27
  [API.health, (_, response) => response.ok()]
28
28
  ])
29
29
 
30
- const getReqs = new Map([
31
- ...headReqs.entries(),
30
+ const GET = new Map([
31
+ ...HEAD.entries(),
32
32
 
33
33
  [API.root, serveDashboard],
34
34
  [API.state, getState],
@@ -39,7 +39,7 @@ const getReqs = new Map([
39
39
  [API.throws, () => { throw new Error('Test500') }]
40
40
  ])
41
41
 
42
- const patchReqs = new Map([
42
+ const PATCH = new Map([
43
43
  [API.cors, setCorsAllowed],
44
44
  [API.reset, reset],
45
45
  [API.cookies, selectCookie],
@@ -68,9 +68,9 @@ export async function handleApiRequest(req, response) {
68
68
  const url = removeQueryStringAndFragment(req.url)
69
69
 
70
70
  const handler = (
71
- req.method === 'GET' && getReqs.get(url) ||
72
- req.method === 'HEAD' && headReqs.get(url) ||
73
- req.method === 'PATCH' && patchReqs.get(url))
71
+ req.method === 'GET' && GET.get(url) ||
72
+ req.method === 'HEAD' && HEAD.get(url) ||
73
+ req.method === 'PATCH' && PATCH.get(url))
74
74
  if (handler) {
75
75
  await handler(req, response)
76
76
  return true
@@ -29,9 +29,8 @@ export async function dispatchMock(req, response) {
29
29
  return
30
30
  }
31
31
 
32
- response.on('finish', () => {
33
- logger.normal('MOCK', req.url, broker.file)
34
- })
32
+ response.on('finish', () =>
33
+ logger.normal('MOCK', req.url, broker.file))
35
34
 
36
35
  if (cookie.getCurrent())
37
36
  response.setHeader('Set-Cookie', cookie.getCurrent())
@@ -39,9 +38,9 @@ export async function dispatchMock(req, response) {
39
38
  const { isStatic } = parseFilename(broker.file)
40
39
 
41
40
  if (isStatic && req.headers.range && !broker.autoStatus) {
42
- setTimeout(async () => {
43
- await response.partialContent(join(config.mocksDir, broker.file))
44
- }, Number(broker.delayed && calcDelay()))
41
+ setTimeout(
42
+ () => response.partialContent(join(config.mocksDir, broker.file)),
43
+ Number(broker.delayed && calcDelay()))
45
44
  return
46
45
  }
47
46
 
@@ -27,7 +27,7 @@ export function Mockaton(options) {
27
27
  cookie.init(config.cookies)
28
28
  brokers.init()
29
29
 
30
- registerHooks({ resolve: resolveExtensionless })
30
+ registerHooks({ resolve: resolveExtensionless(['.ts', '.js']) })
31
31
  registerHooks({ resolve: bypassImportCache(config.mocksDir) })
32
32
 
33
33
  if (config.watcherEnabled)
@@ -40,10 +40,9 @@ export function Mockaton(options) {
40
40
  server.on('error', reject)
41
41
  server.listen(config.port, config.host, () => {
42
42
  const url = `http://${server.address().address}:${server.address().port}`
43
- const dashboardUrl = url + API.root
44
43
  logger.info('Listening', url)
45
- logger.info('Dashboard', dashboardUrl)
46
- config.onReady(dashboardUrl)
44
+ logger.info('Dashboard', url + API.root)
45
+ config.onReady(url + API.root)
47
46
  resolve(server)
48
47
  })
49
48
  })
@@ -1,4 +1,4 @@
1
- import { jwtCookie } from '../../index.js'
1
+ import { jwtCookie, shouldIgnore } from '../../index.js'
2
2
 
3
3
  export default {
4
4
  cookies: {
@@ -12,5 +12,6 @@ export default {
12
12
  corsExposedHeaders: ['Content-Encoding'],
13
13
  readOnly: false,
14
14
  watcherEnabled: false, // But we enable it at run-time
15
- watcherDebounceMs: 0
15
+ watcherDebounceMs: 0,
16
+ shouldIgnore: f => shouldIgnore(f) || f.endsWith('.skip.js'),
16
17
  }
@@ -536,6 +536,14 @@ describe('404', () => {
536
536
  equal(r.status, 404)
537
537
  await fx.delete()
538
538
  })
539
+
540
+ test('404s ignored files', async () => {
541
+ const fx = new Fixture('custom_ignore.GET.200.skip.js')
542
+ await fx.write()
543
+ const r = await fx.request()
544
+ equal(r.status, 404)
545
+ await fx.delete()
546
+ })
539
547
  })
540
548
 
541
549
 
@@ -1021,8 +1029,9 @@ describe('Write and Delete Mock', () => {
1021
1029
 
1022
1030
  test('write and delete (with watcher)', async () => {
1023
1031
  await api.setWatchMocks(true)
1024
- const file = 'new-mock.GET.200.txt'
1032
+ await sleep(0) // for macOS
1025
1033
 
1034
+ const file = 'new-mock.GET.200.txt'
1026
1035
  const nextVerPromise = resolveOnNextSyncVersion()
1027
1036
  const res = await api.writeMock(file, '')
1028
1037
  equal(res.status, 200)
@@ -1117,9 +1126,6 @@ describe('Registering Mocks', () => {
1117
1126
  }
1118
1127
  }
1119
1128
 
1120
- function sleep(ms) {
1121
- return new Promise(resolve => setTimeout(resolve, ms))
1122
- }
1123
1129
 
1124
1130
  const fxA = new FixtureExternal('register(default).GET.200.json')
1125
1131
  const fxB = new FixtureExternal('register(alt).GET.200.json')
@@ -1240,3 +1246,7 @@ function resolveOnNextSyncVersion(currSyncVer = undefined) {
1240
1246
  }
1241
1247
  })
1242
1248
  }
1249
+
1250
+ function sleep(ms) {
1251
+ return new Promise(resolve => setTimeout(resolve, ms))
1252
+ }
@@ -8,13 +8,6 @@ export function parseQueryParams(url) {
8
8
  return new URL(url, 'http://_').searchParams
9
9
  }
10
10
 
11
- /** @deprecated Use parseSegments */
12
- export function parseSplats(url, filename) {
13
- console.info('parseSplats is deprecated in favor of parseSegments')
14
- return parseSegments(url, filename)
15
- }
16
-
17
-
18
11
  export function parseSegments(url, filename) {
19
12
  const { urlMask } = parseFilename(relative(config.mocksDir, filename))
20
13
 
@@ -1,4 +1,4 @@
1
- import { config, isFileAllowed } from './config.js'
1
+ import { config } from './config.js'
2
2
  import { MockBroker } from '../MockBroker.js'
3
3
  import { listFilesRecursively } from '../utils/fs.js'
4
4
  import { parseFilename } from '../../client/Filename.js'
@@ -33,7 +33,7 @@ export function init() {
33
33
 
34
34
  /** @returns {boolean} registered */
35
35
  export function registerMock(file, isFromWatcher = false) {
36
- if (brokerByFilename(file)?.hasMock(file) || !isFileAllowed(file))
36
+ if (brokerByFilename(file)?.hasMock(file) || config.shouldIgnore(file))
37
37
  return false
38
38
 
39
39
  const { method, urlMask } = parseFilename(file)
@@ -1,4 +1,4 @@
1
- import { resolve, basename } from 'node:path'
1
+ import { resolve } from 'node:path'
2
2
  import { lstatSync } from 'node:fs'
3
3
  import { METHODS } from 'node:http'
4
4
 
@@ -18,7 +18,7 @@ import { jsToJsonPlugin } from '../MockDispatcherPlugins.js'
18
18
  * }} */
19
19
  const schema = {
20
20
  mocksDir: [resolve('mockaton-mocks'), p => !lstatSync(p).isDirectory()],
21
- ignore: [/(\.DS_Store|~)$/, is(RegExp)],
21
+ shouldIgnore: [shouldIgnore, is(Function)],
22
22
  readOnly: [true, is(Boolean)],
23
23
  watcherEnabled: [true, is(Boolean)],
24
24
  watcherDebounceMs: [80, isInt(0, 5000)],
@@ -77,7 +77,7 @@ export function initConfig(opts) {
77
77
  opts.mocksDir = resolve(opts.mocksDir)
78
78
 
79
79
  Object.assign(config, opts)
80
- originalOpts = deepCloneExcluding(config, 'plugins', 'onReady')
80
+ originalOpts = deepCloneExcluding(config, 'plugins', 'onReady', 'shouldIgnore')
81
81
 
82
82
  validate(config, ConfigValidator)
83
83
  logger.setLevel(config.logLevel)
@@ -88,7 +88,15 @@ export function reinitConfig() {
88
88
  initConfig(originalOpts)
89
89
  }
90
90
 
91
- export const isFileAllowed = f => !config.ignore.test(basename(f))
91
+ export function shouldIgnore(f) {
92
+ return (
93
+ f.startsWith('.git/') ||
94
+ f.startsWith('.idea/') ||
95
+ f.startsWith('node_modules/') ||
96
+ f.endsWith('~') ||
97
+ f.endsWith('.DS_Store')
98
+ )
99
+ }
92
100
 
93
101
  export const calcDelay = () => config.delayJitter
94
102
  ? config.delay * (1 + Math.random() * config.delayJitter)
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs'
2
2
  import http from 'node:http'
3
3
  import { pipeline } from 'node:stream/promises'
4
-
5
4
  import { mimeFor } from './mime.js'
6
5
 
7
6
 
@@ -3,18 +3,20 @@ import { join, dirname } from 'node:path'
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url'
4
4
 
5
5
 
6
- export function resolveExtensionless(specifier, context, nextResolve) {
7
- try {
8
- return nextResolve(specifier, context)
9
- }
10
- catch (error) {
11
- // Attempt to resolve imports as .ts and .js
12
- if (error.code === 'ERR_MODULE_NOT_FOUND' && specifier.startsWith('.') && context.parentURL) {
13
- const absPath = join(dirname(fileURLToPath(context.parentURL)), specifier)
14
- for (const candidate of ['.ts', '.js'].map(ext => absPath + ext))
15
- if (existsSync(candidate))
16
- return resolveExtensionless(pathToFileURL(candidate).href, context, nextResolve)
6
+ export function resolveExtensionless(extensions) {
7
+ return function resolve(specifier, context, nextResolve) {
8
+ try {
9
+ return nextResolve(specifier, context)
10
+ }
11
+ catch (error) {
12
+ // Attempt to resolve imports with appended extension
13
+ if (error.code === 'ERR_MODULE_NOT_FOUND' && specifier.startsWith('.') && context.parentURL) {
14
+ const absPath = join(dirname(fileURLToPath(context.parentURL)), specifier)
15
+ for (const candidate of extensions.map(ext => absPath + ext))
16
+ if (existsSync(candidate))
17
+ return resolve(pathToFileURL(candidate).href, context, nextResolve)
18
+ }
19
+ throw error
17
20
  }
18
- throw error
19
21
  }
20
22
  }