mockaton 13.14.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 +15 -10
- package/index.d.ts +3 -3
- 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 +6 -10
- package/src/server/Mockaton.test.config.js +3 -2
- package/src/server/Mockaton.test.js +15 -8
- package/src/server/UrlParsers.js +0 -7
- package/src/server/stores/brokers.js +2 -2
- package/src/server/stores/config.js +12 -5
- package/src/server/utils/HttpServerResponse.js +0 -1
- package/src/server/utils/resolveBypassImportCache.js +21 -0
- package/src/server/utils/resolveExtensionless.js +22 -0
- package/src/server/resolveBypassImportCache.js +0 -19
- package/src/server/resolveExtensionless.js +0 -20
package/README.md
CHANGED
|
@@ -8,20 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
*No API state should be too difficult to test*
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### [Docs](https://mockaton.com) | [Changelog](https://mockaton.com/changelog) | [Skills](skills/mockaton/SKILL.md) | [Use Cases](https://mockaton.com/use-cases)
|
|
12
12
|
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
Mockaton is a local HTTP mock server. With it, you can test API states
|
|
16
|
+
that are difficult to reproduce from an actual backend.
|
|
17
|
+
|
|
18
|
+
For example, to quickly trigger an error on an endpoint, click the 500 button.
|
|
19
|
+
Then, unclick it to test your retry logic.
|
|
13
20
|
|
|
14
|
-
Simulate API states that are normally ignored. For example, quickly trigger an error on
|
|
15
|
-
an endpoint by clicking the 500 button. Then, unclick it to test your retry logic.
|
|
16
21
|
Similarly, pick a mock variant from the dropdown, say to respond with a 423 (locked account).
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
their animation midway.
|
|
22
|
+
|
|
23
|
+
Or click the clock button 🕓 to delay a response so you can test spinners.
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
## Dashboard
|
|
23
|
-
Besides the dashboard UI, there’s a [programmatic API](https://mockaton.com/api),
|
|
24
|
-
which is handy for setting up tests.
|
|
25
27
|
|
|
26
28
|
<picture>
|
|
27
29
|
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.light.gold.png">
|
|
@@ -29,6 +31,9 @@ which is handy for setting up tests.
|
|
|
29
31
|
<img alt="Mockaton Dashboard" src="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.dark.gold.png">
|
|
30
32
|
</picture>
|
|
31
33
|
|
|
34
|
+
Besides the dashboard, there’s a [programmatic API](https://mockaton.com/api),
|
|
35
|
+
which is handy for setting up tests.
|
|
36
|
+
|
|
32
37
|
|
|
33
38
|
## Demo (Docker)
|
|
34
39
|
This will spin up Mockaton with the [sample directory](./mockaton-mocks)
|
|
@@ -64,7 +69,7 @@ npx skills add ericfortis/mockaton
|
|
|
64
69
|
|
|
65
70
|
## Basic Usage
|
|
66
71
|
```sh
|
|
67
|
-
mockaton
|
|
72
|
+
mockaton --port 2020 my-mocks-dir
|
|
68
73
|
```
|
|
69
74
|
|
|
70
75
|
Mockaton will serve the files on the given directory. It's a file-system based router in which
|
|
@@ -77,7 +82,7 @@ code variants.
|
|
|
77
82
|
| Route | Filename | Description |
|
|
78
83
|
| -----| -----| ---|
|
|
79
84
|
| /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default. |
|
|
80
|
-
| /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. |
|
|
81
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` |
|
|
82
87
|
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment. Otherwise, the first mock variant in alphabetical order wins. |
|
|
83
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
|
|
@@ -46,7 +46,6 @@ export declare interface Config {
|
|
|
46
46
|
onReady?: (address: string) => void
|
|
47
47
|
|
|
48
48
|
hotReload?: boolean // For UI dev purposes only
|
|
49
|
-
bypassImportCache?: boolean
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
|
|
@@ -62,6 +61,7 @@ export declare const echoFilePlugin: Plugin
|
|
|
62
61
|
|
|
63
62
|
export declare function jwtCookie(cookieName: string, payload: any, path?: string): string
|
|
64
63
|
|
|
64
|
+
export declare function shouldIgnore(filepath: string): boolean
|
|
65
65
|
export declare function parseJSON(request: IncomingMessage): Promise<any>
|
|
66
66
|
export declare function parseSegments(reqUrl: string, filename: string): Record<string, string>
|
|
67
67
|
export declare function parseQueryParams(reqUrl: string): URLSearchParams
|
|
@@ -102,4 +102,4 @@ declare global {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export type { JsonPromise, ClientMockBroker, ClientBrokersByMethod, State }
|
|
105
|
+
export type { JsonPromise, ClientMockBroker, ClientBrokersByMethod, State }
|
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
|
@@ -8,6 +8,8 @@ import { ServerResponse } from './utils/HttpServerResponse.js'
|
|
|
8
8
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
9
9
|
import { IncomingMessage } from './utils/HttpIncomingMessage.js'
|
|
10
10
|
import { watchDevSPA } from './utils/WatcherDevClient.js'
|
|
11
|
+
import { resolveExtensionless } from './utils/resolveExtensionless.js'
|
|
12
|
+
import { bypassImportCache } from './utils/resolveBypassImportCache.js'
|
|
11
13
|
|
|
12
14
|
import { API } from '../client/ApiConstants.js'
|
|
13
15
|
import { dispatchMock } from './MockDispatcher.js'
|
|
@@ -18,9 +20,6 @@ import { cookie } from './stores/cookies.js'
|
|
|
18
20
|
import { config, initConfig } from './stores/config.js'
|
|
19
21
|
import { watchMocksDir } from './stores/Watcher.js'
|
|
20
22
|
|
|
21
|
-
import { resolveExtensionless } from './resolveExtensionless.js'
|
|
22
|
-
import { resolveBypassImportCache } from './resolveBypassImportCache.js'
|
|
23
|
-
|
|
24
23
|
|
|
25
24
|
export function Mockaton(options) {
|
|
26
25
|
return new Promise((resolve, reject) => {
|
|
@@ -28,10 +27,8 @@ export function Mockaton(options) {
|
|
|
28
27
|
cookie.init(config.cookies)
|
|
29
28
|
brokers.init()
|
|
30
29
|
|
|
31
|
-
registerHooks({ resolve: resolveExtensionless })
|
|
32
|
-
|
|
33
|
-
if (config.bypassImportCache)
|
|
34
|
-
registerHooks({ resolve: resolveBypassImportCache })
|
|
30
|
+
registerHooks({ resolve: resolveExtensionless(['.ts', '.js']) })
|
|
31
|
+
registerHooks({ resolve: bypassImportCache(config.mocksDir) })
|
|
35
32
|
|
|
36
33
|
if (config.watcherEnabled)
|
|
37
34
|
watchMocksDir()
|
|
@@ -43,10 +40,9 @@ export function Mockaton(options) {
|
|
|
43
40
|
server.on('error', reject)
|
|
44
41
|
server.listen(config.port, config.host, () => {
|
|
45
42
|
const url = `http://${server.address().address}:${server.address().port}`
|
|
46
|
-
const dashboardUrl = url + API.root
|
|
47
43
|
logger.info('Listening', url)
|
|
48
|
-
logger.info('Dashboard',
|
|
49
|
-
config.onReady(
|
|
44
|
+
logger.info('Dashboard', url + API.root)
|
|
45
|
+
config.onReady(url + API.root)
|
|
50
46
|
resolve(server)
|
|
51
47
|
})
|
|
52
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')
|
|
@@ -1218,10 +1224,7 @@ describe('Registering Mocks', () => {
|
|
|
1218
1224
|
})
|
|
1219
1225
|
|
|
1220
1226
|
|
|
1221
|
-
/**
|
|
1222
|
-
* It resolves when a new version is pushed that differs from `currSyncVer`;
|
|
1223
|
-
* when `currSyncVer` is omitted, the first push after the initial connection is
|
|
1224
|
-
* used. */
|
|
1227
|
+
/** Resolves when a new mock is added, deleted, or renamed. */
|
|
1225
1228
|
function resolveOnNextSyncVersion(currSyncVer = undefined) {
|
|
1226
1229
|
let skipFirst = currSyncVer === undefined
|
|
1227
1230
|
return new Promise((resolve, reject) => {
|
|
@@ -1243,3 +1246,7 @@ function resolveOnNextSyncVersion(currSyncVer = undefined) {
|
|
|
1243
1246
|
}
|
|
1244
1247
|
})
|
|
1245
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)],
|
|
@@ -48,7 +48,6 @@ const schema = {
|
|
|
48
48
|
corsMaxAge: [0, is(Number)],
|
|
49
49
|
|
|
50
50
|
hotReload: [false, is(Boolean)],
|
|
51
|
-
bypassImportCache: [true, is(Boolean)],
|
|
52
51
|
|
|
53
52
|
// Non-serializable
|
|
54
53
|
plugins: [[[/\.(js|ts)$/, jsToJsonPlugin]], is(Array)],
|
|
@@ -78,7 +77,7 @@ export function initConfig(opts) {
|
|
|
78
77
|
opts.mocksDir = resolve(opts.mocksDir)
|
|
79
78
|
|
|
80
79
|
Object.assign(config, opts)
|
|
81
|
-
originalOpts = deepCloneExcluding(config, 'plugins', 'onReady')
|
|
80
|
+
originalOpts = deepCloneExcluding(config, 'plugins', 'onReady', 'shouldIgnore')
|
|
82
81
|
|
|
83
82
|
validate(config, ConfigValidator)
|
|
84
83
|
logger.setLevel(config.logLevel)
|
|
@@ -89,7 +88,15 @@ export function reinitConfig() {
|
|
|
89
88
|
initConfig(originalOpts)
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
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
|
+
}
|
|
93
100
|
|
|
94
101
|
export const calcDelay = () => config.delayJitter
|
|
95
102
|
? config.delay * (1 + Math.random() * config.delayJitter)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
4
|
+
export function bypassImportCache(srcPath) {
|
|
5
|
+
const resolvedSrcPath = pathToFileURL(realpathSync(srcPath)).href
|
|
6
|
+
|
|
7
|
+
// We register this `resolve` hook at runtime so it doesn't interfere with non-dynamic imports.
|
|
8
|
+
return function resolve(specifier, context, nextResolve) {
|
|
9
|
+
const result = nextResolve(specifier, context)
|
|
10
|
+
if (result.url.startsWith(resolvedSrcPath)) {
|
|
11
|
+
const url = new URL(result.url)
|
|
12
|
+
url.searchParams.set('t', performance.now())
|
|
13
|
+
return {
|
|
14
|
+
...result,
|
|
15
|
+
url: url.href,
|
|
16
|
+
shortCircuit: true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
|
|
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
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { resolve as _resolve } from 'node:path'
|
|
2
|
-
|
|
3
|
-
const mockatonSrcRoot = `file://${_resolve(import.meta.dirname, '..')}`
|
|
4
|
-
|
|
5
|
-
// We register this hook at runtime so it doesn’t interfere with non-dynamic imports.
|
|
6
|
-
// It cache-busts by appending timestamp query param
|
|
7
|
-
export function resolveBypassImportCache(specifier, context, nextResolve) {
|
|
8
|
-
const result = nextResolve(specifier, context)
|
|
9
|
-
if (result.url?.startsWith('file://') && !result.url.startsWith(mockatonSrcRoot)) {
|
|
10
|
-
const url = new URL(result.url)
|
|
11
|
-
url.searchParams.set('t', performance.now())
|
|
12
|
-
return {
|
|
13
|
-
...result,
|
|
14
|
-
url: url.href,
|
|
15
|
-
shortCircuit: true
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return result
|
|
19
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join, dirname } from 'node:path'
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
|
-
|
|
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)
|
|
17
|
-
}
|
|
18
|
-
throw error
|
|
19
|
-
}
|
|
20
|
-
}
|