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 +2 -2
- package/index.d.ts +2 -1
- package/index.js +2 -1
- package/package.json +1 -1
- package/skills/mockaton/SKILL.md +2 -2
- package/src/client/dir/dittoSplitPaths.js +3 -3
- package/src/client/dir/groupByFolder.js +9 -9
- package/src/server/Api.js +7 -7
- package/src/server/MockDispatcher.js +5 -6
- package/src/server/Mockaton.js +3 -4
- package/src/server/Mockaton.test.config.js +3 -2
- package/src/server/Mockaton.test.js +14 -4
- package/src/server/UrlParsers.js +0 -7
- package/src/server/stores/brokers.js +2 -2
- package/src/server/stores/config.js +12 -4
- package/src/server/utils/HttpServerResponse.js +0 -1
- package/src/server/utils/resolveExtensionless.js +14 -12
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ npx skills add ericfortis/mockaton
|
|
|
69
69
|
|
|
70
70
|
## Basic Usage
|
|
71
71
|
```sh
|
|
72
|
-
mockaton
|
|
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
|
|
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
|
-
|
|
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
|
|
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
package/skills/mockaton/SKILL.md
CHANGED
|
@@ -15,7 +15,7 @@ npm install -g mockaton
|
|
|
15
15
|
|
|
16
16
|
## Basic Usage
|
|
17
17
|
```sh
|
|
18
|
-
mockaton
|
|
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
|
|
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
|
|
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 =
|
|
13
|
-
const curr =
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
26
|
+
const HEAD = new Map([
|
|
27
27
|
[API.health, (_, response) => response.ok()]
|
|
28
28
|
])
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
...
|
|
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
|
|
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' &&
|
|
72
|
-
req.method === 'HEAD' &&
|
|
73
|
-
req.method === 'PATCH' &&
|
|
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(
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
setTimeout(
|
|
42
|
+
() => response.partialContent(join(config.mocksDir, broker.file)),
|
|
43
|
+
Number(broker.delayed && calcDelay()))
|
|
45
44
|
return
|
|
46
45
|
}
|
|
47
46
|
|
package/src/server/Mockaton.js
CHANGED
|
@@ -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',
|
|
46
|
-
config.onReady(
|
|
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
|
-
|
|
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
|
+
}
|
package/src/server/UrlParsers.js
CHANGED
|
@@ -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
|
|
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) ||
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -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(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
}
|