mockaton 13.11.3 → 13.14.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 +46 -30
- package/index.d.ts +30 -29
- package/package.json +1 -1
- package/skills/mockaton/SKILL.md +24 -21
- package/src/client/ApiCommander.js +20 -19
- package/src/client/{app.js → App.js} +6 -6
- package/src/client/Filename.js +7 -8
- package/src/client/{app-header.css → Header.css} +1 -1
- package/src/client/{app-header.js → Header.js} +4 -4
- package/src/client/IndexHtml.js +1 -1
- package/src/client/{app-mock-list.js → MockList.js} +7 -8
- package/src/client/{app-payload-viewer.js → PayloadViewer.js} +3 -3
- package/src/client/{app-store.test.js → store.test.js} +1 -1
- package/src/server/Api.js +25 -18
- package/src/server/MockBroker.js +2 -8
- package/src/server/Mockaton.js +13 -12
- package/src/server/Mockaton.test.js +23 -35
- package/src/server/ProxyRelay.js +2 -2
- package/src/server/{ResolverBypassImportCache.js → resolveBypassImportCache.js} +3 -3
- package/src/server/{ResolverResolveExtensionless.js → resolveExtensionless.js} +3 -3
- package/src/server/stores/brokers.js +1 -4
- package/src/server/stores/config.js +2 -2
- package/src/server/utils/HttpIncomingMessage.js +17 -16
- package/src/server/utils/logger.js +7 -1
- package/src/server/utils/mime.js +2 -0
- /package/src/client/{app.css → App.css} +0 -0
- /package/src/client/{app-mock-list.css → MockList.css} +0 -0
- /package/src/client/{app-payload-viewer.css → PayloadViewer.css} +0 -0
- /package/src/client/{graphics.js → icons.js} +0 -0
- /package/src/client/{app-store.js → store.js} +0 -0
package/README.md
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
<!-- SKILLS_IGNORE_BEGIN -->
|
|
2
|
-
<img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
|
|
3
2
|
|
|
4
3
|

|
|
5
4
|
[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
|
|
6
5
|
[](https://codecov.io/github/ericfortis/mockaton)
|
|
7
6
|
|
|
7
|
+
<img src="logo.svg" alt="Mockaton Logo" width="180" style="margin-top: 30px"/>
|
|
8
|
+
|
|
9
|
+
*No API state should be too difficult to test*
|
|
10
|
+
|
|
8
11
|
## [Docs ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog) | [Skills](skills/mockaton/SKILL.md)
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
|
|
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
|
+
Similarly, pick a mock variant from the dropdown, say to respond with a 423 (locked account).
|
|
17
|
+
As another example, click the clock button 🕓 to delay a response so you can test
|
|
18
|
+
spinners — you’ve seen them in production at the top left, or restart
|
|
19
|
+
their animation midway.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Dashboard
|
|
23
|
+
Besides the dashboard UI, there’s a [programmatic API](https://mockaton.com/api),
|
|
24
|
+
which is handy for setting up tests.
|
|
12
25
|
|
|
13
26
|
<picture>
|
|
14
27
|
<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">
|
|
@@ -35,59 +48,57 @@ curl localhost:2020/api/user
|
|
|
35
48
|
|
|
36
49
|
|
|
37
50
|
# Installation ([more options ↗](https://mockaton.com/installation))
|
|
38
|
-
Mockaton has no dependencies.
|
|
39
|
-
|
|
40
51
|
```sh
|
|
41
52
|
npm install -g mockaton
|
|
42
53
|
```
|
|
54
|
+
<i>Mockaton is a Node.js app with no dependencies.</i>
|
|
55
|
+
|
|
56
|
+
<!-- SKILLS_IGNORE_BEGIN -->
|
|
57
|
+
## Skills
|
|
58
|
+
```sh
|
|
59
|
+
npx skills add ericfortis/mockaton
|
|
60
|
+
```
|
|
61
|
+
<!-- SKILLS_IGNORE_END -->
|
|
62
|
+
|
|
63
|
+
|
|
43
64
|
|
|
44
65
|
## Basic Usage
|
|
45
66
|
```sh
|
|
46
|
-
mockaton --port 2020 my-mocks-dir
|
|
67
|
+
mockaton --port 2020 my-mocks-dir
|
|
47
68
|
```
|
|
48
69
|
|
|
49
|
-
Mockaton will serve the files on the given directory. It's a file-system based router
|
|
50
|
-
filenames can have dynamic parameters.
|
|
51
|
-
|
|
52
|
-
Similarly, each route can have different response status
|
|
70
|
+
Mockaton will serve the files on the given directory. It's a file-system based router in which
|
|
71
|
+
filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
|
|
72
|
+
and for comments use parentheses `()`. Comments are handy because this way each route
|
|
73
|
+
can have different mock file variants. Similarly, each route can have different response status
|
|
74
|
+
code variants.
|
|
53
75
|
|
|
54
76
|
|
|
55
77
|
| Route | Filename | Description |
|
|
56
78
|
| -----| -----| ---|
|
|
57
|
-
| /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
|
|
58
|
-
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
|
|
59
|
-
| /api/login | api/login(invalid attempt).POST.401.ts |
|
|
60
|
-
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment
|
|
61
|
-
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## Docs
|
|
65
|
-
- [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
|
|
66
|
-
- [Programmatic API](https://mockaton.com/api), in which
|
|
67
|
-
you can delay a route, select a different mock file, such as a 500 error, among other options.
|
|
79
|
+
| /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't need the above extension. |
|
|
81
|
+
| /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
|
+
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment. Otherwise, the first mock variant in alphabetical order wins. |
|
|
83
|
+
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
|
|
68
84
|
|
|
69
85
|
<!-- SKILLS_IGNORE_BEGIN -->
|
|
70
86
|
## How to scrape your backend APIs?
|
|
71
|
-
|
|
87
|
+
There’s a sister [Browser Extension](https://mockaton.com/scraping) that lets
|
|
72
88
|
you download in bulk all your API responses following Mockaton's filename convention.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## Skills
|
|
76
|
-
```sh
|
|
77
|
-
npx skills add ericfortis/mockaton
|
|
78
|
-
```
|
|
79
89
|
<!-- SKILLS_IGNORE_END -->
|
|
80
90
|
|
|
81
91
|
|
|
82
92
|
## How to create mocks?
|
|
83
|
-
Write it to your mocks directory.
|
|
93
|
+
Write it to your mocks directory. TypeScript files are sent as JSON by default.
|
|
84
94
|
```sh
|
|
85
95
|
mkdir -p my-mocks-dir/api
|
|
86
96
|
echo "export default { name: 'John' }" > my-mocks-dir/api/user.GET.200.ts
|
|
87
97
|
```
|
|
98
|
+
Alternatively, there’s a [write-mock API](https://mockaton.com/api).
|
|
88
99
|
|
|
89
100
|
### Example A: JSON
|
|
90
|
-
For JSON responses, use TypeScript (or JS)
|
|
101
|
+
For JSON responses, you can use TypeScript (or JS) and `export default` an Object, Array, or
|
|
91
102
|
String.
|
|
92
103
|
|
|
93
104
|
- **Route:** /api/company/123
|
|
@@ -135,3 +146,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
|
|
|
135
146
|
})
|
|
136
147
|
}
|
|
137
148
|
```
|
|
149
|
+
|
|
150
|
+
## Docs
|
|
151
|
+
- [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
|
|
152
|
+
- [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
|
|
153
|
+
|
package/index.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export declare interface Config {
|
|
|
46
46
|
onReady?: (address: string) => void
|
|
47
47
|
|
|
48
48
|
hotReload?: boolean // For UI dev purposes only
|
|
49
|
-
bypassImportCache?: boolean
|
|
49
|
+
bypassImportCache?: boolean
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
|
|
@@ -66,39 +66,40 @@ 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
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
[urlMask: string]: ClientMockBroker
|
|
69
|
+
declare global {
|
|
70
|
+
type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
|
|
71
|
+
|
|
72
|
+
type ClientMockBroker = {
|
|
73
|
+
mocks: string[]
|
|
74
|
+
file: string
|
|
75
|
+
status: number
|
|
76
|
+
isStatic: boolean
|
|
77
|
+
autoStatus: number
|
|
78
|
+
delayed: boolean
|
|
79
|
+
proxied: boolean
|
|
80
|
+
}
|
|
81
|
+
type ClientBrokersByMethod = {
|
|
82
|
+
[method: string]: {
|
|
83
|
+
[urlMask: string]: ClientMockBroker
|
|
84
|
+
}
|
|
86
85
|
}
|
|
87
|
-
}
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
interface State {
|
|
88
|
+
brokersByMethod: ClientBrokersByMethod
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
cookies: [label: string, selected: boolean][]
|
|
91
|
+
comments: string[]
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
delay: number
|
|
94
|
+
delayJitter: number
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
collectProxied: boolean
|
|
97
|
+
proxyFallback: string
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
readOnly: boolean
|
|
102
100
|
|
|
103
|
-
|
|
101
|
+
corsAllowed?: boolean
|
|
102
|
+
}
|
|
104
103
|
}
|
|
104
|
+
|
|
105
|
+
export type { JsonPromise, ClientMockBroker, ClientBrokersByMethod, State }
|
package/package.json
CHANGED
package/skills/mockaton/SKILL.md
CHANGED
|
@@ -4,49 +4,48 @@ description: Generates and serves mock HTTP APIs. Use when creating, editing, or
|
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
# Installation ([more options ↗](https://mockaton.com/installation))
|
|
7
|
-
Mockaton has no dependencies.
|
|
8
|
-
|
|
9
7
|
```sh
|
|
10
8
|
npm install -g mockaton
|
|
11
9
|
```
|
|
10
|
+
<i>Mockaton is a Node.js app with no dependencies.</i>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
## Basic Usage
|
|
14
17
|
```sh
|
|
15
|
-
mockaton --port 2020 my-mocks-dir
|
|
18
|
+
mockaton --port 2020 my-mocks-dir
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
Mockaton will serve the files on the given directory. It's a file-system based router
|
|
19
|
-
filenames can have dynamic parameters.
|
|
20
|
-
|
|
21
|
-
Similarly, each route can have different response status
|
|
21
|
+
Mockaton will serve the files on the given directory. It's a file-system based router in which
|
|
22
|
+
filenames can have dynamic parameters and comments. For paraments use square brackets `[]`,
|
|
23
|
+
and for comments use parentheses `()`. Comments are handy because this way each route
|
|
24
|
+
can have different mock file variants. Similarly, each route can have different response status
|
|
25
|
+
code variants.
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
| Route | Filename | Description |
|
|
25
29
|
| -----| -----| ---|
|
|
26
|
-
| /api/company/123 | api/company/[id].GET.200.ts | `[id]` is a dynamic parameter. `.ts`, and `.js` are sent as JSON by default |
|
|
27
|
-
| /media/avatar.png | media/avatar.png | Statics assets don't need the above extension |
|
|
28
|
-
| /api/login | api/login(invalid attempt).POST.401.ts |
|
|
29
|
-
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment
|
|
30
|
-
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too |
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Docs
|
|
34
|
-
- [Configuration: CLI and mockaton.config.js](https://mockaton.com/config).
|
|
35
|
-
- [Programmatic API](https://mockaton.com/api), in which
|
|
36
|
-
you can delay a route, select a different mock file, such as a 500 error, among other options.
|
|
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. |
|
|
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
|
+
| /api/login | api/login(default).GET.200.ts | `(default)` is a special comment. Otherwise, the first mock variant in alphabetical order wins. |
|
|
34
|
+
| /api/login | api/login(locked out user).POST.423.json | `.json` is allowed too. |
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
## How to create mocks?
|
|
42
|
-
Write it to your mocks directory.
|
|
40
|
+
Write it to your mocks directory. TypeScript files are sent as JSON by default.
|
|
43
41
|
```sh
|
|
44
42
|
mkdir -p my-mocks-dir/api
|
|
45
43
|
echo "export default { name: 'John' }" > my-mocks-dir/api/user.GET.200.ts
|
|
46
44
|
```
|
|
45
|
+
Alternatively, there’s a [write-mock API](https://mockaton.com/api).
|
|
47
46
|
|
|
48
47
|
### Example A: JSON
|
|
49
|
-
For JSON responses, use TypeScript (or JS)
|
|
48
|
+
For JSON responses, you can use TypeScript (or JS) and `export default` an Object, Array, or
|
|
50
49
|
String.
|
|
51
50
|
|
|
52
51
|
- **Route:** /api/company/123
|
|
@@ -93,4 +92,8 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
|
|
|
93
92
|
name: 'Acme, Inc.'
|
|
94
93
|
})
|
|
95
94
|
}
|
|
96
|
-
```
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Docs
|
|
98
|
+
- [Configuration](https://mockaton.com/config): CLI and mockaton.config.js
|
|
99
|
+
- [API](https://mockaton.com/api): Programatically, you can delay a route, select a different mock file, etc.
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { API } from './ApiConstants.js'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Client for controlling Mockaton via its HTTP API
|
|
6
|
+
* @see https://mockaton.com/api
|
|
7
|
+
* @example const mockaton = new Commander('http://localhost:2020')
|
|
8
|
+
*/
|
|
5
9
|
export class Commander {
|
|
6
|
-
/** @param {string}
|
|
7
|
-
constructor(
|
|
8
|
-
this.addr =
|
|
10
|
+
/** @param {string} mockatonServerAddr */
|
|
11
|
+
constructor(mockatonServerAddr) {
|
|
12
|
+
this.addr = mockatonServerAddr
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/** @returns {Promise<Response>} */
|
|
@@ -16,38 +20,35 @@ export class Commander {
|
|
|
16
20
|
|
|
17
21
|
reset = () => this.#patch(API.reset)
|
|
18
22
|
|
|
19
|
-
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
20
|
-
setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
|
|
21
|
-
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
22
|
-
setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
|
|
23
|
-
setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
|
|
24
|
-
setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
|
|
25
|
-
|
|
26
|
-
/** @returns {JsonPromise<State.cookies>} */
|
|
27
|
-
selectCookie = label => this.#patch(API.cookies, label)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
|
|
31
23
|
|
|
32
24
|
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
33
25
|
select = file => this.#patch(API.select, file)
|
|
34
26
|
|
|
27
|
+
bulkSelectByComment = comment => this.#patch(API.bulkSelect, comment)
|
|
35
28
|
|
|
36
29
|
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
37
30
|
toggleStatus = (method, urlMask, status) => this.#patch(API.toggleStatus, [method, urlMask, status])
|
|
38
31
|
|
|
32
|
+
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
33
|
+
setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
|
|
39
34
|
|
|
40
35
|
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
41
36
|
setRouteIsProxied = (method, urlMask, proxied) => this.#patch(API.proxied, [method, urlMask, proxied])
|
|
42
37
|
|
|
43
|
-
/** @returns {JsonPromise<ClientMockBroker>} */
|
|
44
|
-
setRouteIsDelayed = (method, urlMask, delayed) => this.#patch(API.delay, [method, urlMask, delayed])
|
|
45
38
|
|
|
39
|
+
/** @returns {JsonPromise<State['cookies']>} */
|
|
40
|
+
selectCookie = label => this.#patch(API.cookies, label)
|
|
41
|
+
|
|
42
|
+
setGlobalDelay = delay => this.#patch(API.globalDelay, delay)
|
|
43
|
+
setGlobalDelayJitter = jitterPct => this.#patch(API.globalDelayJitter, jitterPct)
|
|
44
|
+
setCorsAllowed = value => this.#patch(API.cors, value)
|
|
45
|
+
setWatchMocks = enabled => this.#patch(API.watchMocks, enabled)
|
|
46
|
+
setProxyFallback = proxyAddr => this.#patch(API.fallback, proxyAddr)
|
|
47
|
+
setCollectProxied = shouldCollect => this.#patch(API.collectProxied, shouldCollect)
|
|
46
48
|
|
|
47
49
|
writeMock = (file, content) => this.#patch(API.writeMock, [file, content])
|
|
48
50
|
deleteMock = file => this.#patch(API.deleteMock, file)
|
|
49
51
|
|
|
50
|
-
|
|
51
52
|
/** @returns {JsonPromise<State>} */
|
|
52
53
|
getState = () => fetch(this.addr + API.state)
|
|
53
54
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createElement as r, t, restoreFocus, Fragment } from './utils/dom.js'
|
|
2
2
|
|
|
3
|
-
import { store } from './
|
|
3
|
+
import { store } from './store.js'
|
|
4
4
|
import { API } from './ApiConstants.js'
|
|
5
|
-
import { Header } from './
|
|
5
|
+
import { Header } from './Header.js'
|
|
6
6
|
import { adoptSheet } from './utils/css.js'
|
|
7
|
-
import { PayloadViewer, previewMock } from './
|
|
8
|
-
import { MockList, initKeyboardNavigation, renderRow } from './
|
|
7
|
+
import { PayloadViewer, previewMock } from './PayloadViewer.js'
|
|
8
|
+
import { MockList, initKeyboardNavigation, renderRow } from './MockList.js'
|
|
9
9
|
|
|
10
|
-
import CSS from './
|
|
11
|
-
adoptSheet(CSS, './
|
|
10
|
+
import CSS from './App.css' with { type: 'css' }
|
|
11
|
+
adoptSheet(CSS, './App.css')
|
|
12
12
|
|
|
13
13
|
store.onError = onError
|
|
14
14
|
store.render = render
|
package/src/client/Filename.js
CHANGED
|
@@ -11,13 +11,12 @@ const METHODS = [ // @KeepSync node:http.METHODS
|
|
|
11
11
|
|
|
12
12
|
const reComments = /\([^()]*\)/g // Anything within parentheses
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
export const extractComments = file =>
|
|
15
|
+
Array.from(file.matchAll(reComments), ([c]) => c)
|
|
16
|
+
|
|
17
|
+
export const includesComment = (file, search) =>
|
|
18
|
+
extractComments(file).some(c => c.includes(search))
|
|
17
19
|
|
|
18
|
-
export function includesComment(file, search) {
|
|
19
|
-
return extractComments(file).some(c => c.includes(search))
|
|
20
|
-
}
|
|
21
20
|
|
|
22
21
|
export function parseFilename(file) {
|
|
23
22
|
const tokens = file.replace(reComments, '').split('.')
|
|
@@ -62,6 +61,6 @@ export function makeMockFilename(url, method, status, ext, comment = '') {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
function replaceIds(filename) {
|
|
65
|
-
|
|
64
|
+
const reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
|
|
65
|
+
return filename.replaceAll(reUuidV4, '[id]')
|
|
66
66
|
}
|
|
67
|
-
replaceIds.reUuidV4 = /([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/gi
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createElement as r, t } from './utils/dom.js'
|
|
2
|
-
import { Logo, HelpIcon } from './
|
|
2
|
+
import { Logo, HelpIcon } from './icons.js'
|
|
3
3
|
import { adoptSheet } from './utils/css.js'
|
|
4
|
-
import { store } from './
|
|
4
|
+
import { store } from './store.js'
|
|
5
5
|
|
|
6
|
-
import CSS from './
|
|
7
|
-
adoptSheet(CSS, './
|
|
6
|
+
import CSS from './Header.css' with { type: 'css' }
|
|
7
|
+
adoptSheet(CSS, './Header.css')
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
export function Header() {
|
package/src/client/IndexHtml.js
CHANGED
|
@@ -13,7 +13,7 @@ export const IndexHtml = (hotReloadEnabled, version) => `
|
|
|
13
13
|
<meta charset="UTF-8">
|
|
14
14
|
<base href="${API.root}/">
|
|
15
15
|
|
|
16
|
-
<script type="module" src="
|
|
16
|
+
<script type="module" src="App.js"></script>
|
|
17
17
|
<link rel="preload" href="${API.state}" as="fetch" crossorigin>
|
|
18
18
|
|
|
19
19
|
<link rel="icon" href="data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m235 33.7v202c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-151c-0.115-4.49-6.72-5.88-8.46-0.87l-48.3 155c-2.22 7.01-7.72 10.1-16 9.9-3.63-0.191-7.01-1.14-9.66-2.89-2.89-1.72-4.83-4.34-5.57-7.72-11.1-37-22.6-74.3-34.1-111-4.34-14-8.95-31.4-14-48.3-1.82-4.83-8.16-5.32-8.46 1.16v156c0 9.19-5.81 14-17.4 14-11.6 0-17.4-4.83-17.4-14v-207c0-5.74 2.62-13.2 9.39-16.3 7.5-3.14 15-4.05 21.8-3.8 3.14 0 6.03 0.686 8.95 1.46 3.14 0.797 6.03 1.98 8.7 3.63 2.65 1.38 5.32 3.14 7.5 5.57 2.22 2.22 3.87 4.83 5.07 7.72l45.8 157c4.63-15.9 32.4-117 33.3-121 4.12-13.8 7.72-26.5 10.9-38.7 1.16-2.65 2.89-5.32 5.07-7.5 2.15-2.15 4.58-4.12 7.5-5.32 2.65-1.57 5.57-2.89 8.46-3.63 3.14-0.797 9.44-0.988 12.1-0.988 11.6 1.07 29.4 9.14 29.4 27z' fill='%23808080'/%3E%3C/svg%3E">
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createElement as r, t, Fragment } from './utils/dom.js'
|
|
2
2
|
|
|
3
|
-
import { store } from './
|
|
4
|
-
import { previewMock } from './
|
|
3
|
+
import { store } from './store.js'
|
|
4
|
+
import { previewMock } from './PayloadViewer.js'
|
|
5
5
|
import { classNames, adoptSheet } from './utils/css.js'
|
|
6
|
-
import { TimerIcon, CloudIcon, ChevronDownIcon } from './
|
|
6
|
+
import { TimerIcon, CloudIcon, ChevronDownIcon } from './icons.js'
|
|
7
7
|
|
|
8
|
-
import CSS from './
|
|
9
|
-
adoptSheet(CSS, './
|
|
8
|
+
import CSS from './MockList.css' with { type: 'css' }
|
|
9
|
+
adoptSheet(CSS, './MockList.css')
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
export function MockList() {
|
|
@@ -76,9 +76,8 @@ function TableContent() {
|
|
|
76
76
|
|
|
77
77
|
if (store.groupByMethod)
|
|
78
78
|
return Object.keys(store.brokersByMethod).map(method => Fragment(
|
|
79
|
-
r('div', {
|
|
80
|
-
|
|
81
|
-
}, method),
|
|
79
|
+
r('div', { className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy) },
|
|
80
|
+
method),
|
|
82
81
|
FolderGroups(store.folderGroupsByMethod(method))))
|
|
83
82
|
|
|
84
83
|
return FolderGroups(store.folderGroupsByMethod('*'))
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createElement as r, t } from './utils/dom.js'
|
|
2
2
|
import { parseFilename } from './Filename.js'
|
|
3
|
-
import { store } from './
|
|
3
|
+
import { store } from './store.js'
|
|
4
4
|
import { adoptSheet } from './utils/css.js'
|
|
5
5
|
|
|
6
|
-
import CSS from './
|
|
7
|
-
adoptSheet(CSS, './
|
|
6
|
+
import CSS from './PayloadViewer.css' with { type: 'css' }
|
|
7
|
+
adoptSheet(CSS, './PayloadViewer.css')
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
const titleRef = {}
|
package/src/server/Api.js
CHANGED
|
@@ -18,7 +18,7 @@ import { IndexHtml, CSP } from '../client/IndexHtml.js'
|
|
|
18
18
|
import { cookie } from './stores/cookies.js'
|
|
19
19
|
import { config, ConfigValidator, reinitConfig } from './stores/config.js'
|
|
20
20
|
import * as brokers from './stores/brokers.js'
|
|
21
|
-
import * as Watcher
|
|
21
|
+
import * as Watcher from './stores/Watcher.js'
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
|
|
@@ -269,19 +269,21 @@ async function setRouteIsProxied(req, response) {
|
|
|
269
269
|
|
|
270
270
|
|
|
271
271
|
async function writeMock(req, response) {
|
|
272
|
-
if (config.readOnly)
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
if (config.readOnly) {
|
|
273
|
+
response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
|
|
274
|
+
return
|
|
275
|
+
}
|
|
275
276
|
const [file, content] = await req.json()
|
|
276
|
-
if (typeof file !== 'string')
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
if (typeof file !== 'string') {
|
|
278
|
+
response.unprocessable('Invalid or missing filename. Expected: JSON [filename, content]')
|
|
279
|
+
return
|
|
280
|
+
}
|
|
279
281
|
const path = await resolveIn(config.mocksDir, file)
|
|
280
|
-
if (!path)
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
if (!path) {
|
|
283
|
+
response.forbidden('Filename path resolves outside config.mocksDir')
|
|
284
|
+
return
|
|
285
|
+
}
|
|
283
286
|
await write(path, content)
|
|
284
|
-
|
|
285
287
|
if (!config.watcherEnabled) {
|
|
286
288
|
brokers.registerMock(file, true)
|
|
287
289
|
Watcher.emitChange()
|
|
@@ -291,17 +293,22 @@ async function writeMock(req, response) {
|
|
|
291
293
|
|
|
292
294
|
|
|
293
295
|
async function deleteMock(req, response) {
|
|
294
|
-
if (config.readOnly)
|
|
295
|
-
|
|
296
|
-
|
|
296
|
+
if (config.readOnly) {
|
|
297
|
+
response.forbidden('Forbidden: Mockaton is in read-only mode. See config.readOnly')
|
|
298
|
+
return
|
|
299
|
+
}
|
|
297
300
|
const file = await req.json()
|
|
298
301
|
const path = await resolveIn(config.mocksDir, file)
|
|
299
302
|
|
|
300
|
-
if (!path)
|
|
301
|
-
|
|
303
|
+
if (!path) {
|
|
304
|
+
response.forbidden('Filename path resolves outside config.mocksDir')
|
|
305
|
+
return
|
|
306
|
+
}
|
|
302
307
|
|
|
303
|
-
if (!isFile(path))
|
|
304
|
-
|
|
308
|
+
if (!isFile(path)) {
|
|
309
|
+
response.unprocessable(`Missing Mock: ${file}`)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
305
312
|
|
|
306
313
|
await rm(path)
|
|
307
314
|
|
package/src/server/MockBroker.js
CHANGED
|
@@ -108,21 +108,15 @@ class UrlMatcher {
|
|
|
108
108
|
this.#urlRegex = this.#buildUrlRegex(file)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
#escapeRegex(str) {
|
|
112
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
113
|
-
}
|
|
114
|
-
|
|
115
111
|
#buildUrlRegex(file) {
|
|
116
112
|
let { urlMask } = parseFilename(file)
|
|
117
113
|
urlMask = removeQueryStringAndFragment(urlMask)
|
|
118
|
-
urlMask =
|
|
114
|
+
urlMask = RegExp.escape(urlMask)
|
|
119
115
|
urlMask = this.#disregardVariables(urlMask)
|
|
120
116
|
return new RegExp('^' + urlMask + '/*$')
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
#disregardVariables(
|
|
124
|
-
return str.replace(/\\\[.*?\\]/g, '[^/]+')
|
|
125
|
-
}
|
|
119
|
+
#disregardVariables = str => str.replace(/\\\[.*?\\]/g, '[^/]+')
|
|
126
120
|
|
|
127
121
|
// Appending a '/' so URLs ending with variables don't match
|
|
128
122
|
// URLs that have a path after that variable. For example,
|
package/src/server/Mockaton.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registerHooks } from 'node:module'
|
|
2
2
|
import { createServer } from 'node:http'
|
|
3
3
|
|
|
4
4
|
import pkgJSON from '../../package.json' with { type: 'json' }
|
|
@@ -6,18 +6,21 @@ import pkgJSON from '../../package.json' with { type: 'json' }
|
|
|
6
6
|
import { logger } from './utils/logger.js'
|
|
7
7
|
import { ServerResponse } from './utils/HttpServerResponse.js'
|
|
8
8
|
import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
|
|
9
|
-
import { IncomingMessage
|
|
9
|
+
import { IncomingMessage } from './utils/HttpIncomingMessage.js'
|
|
10
10
|
import { watchDevSPA } from './utils/WatcherDevClient.js'
|
|
11
11
|
|
|
12
12
|
import { API } from '../client/ApiConstants.js'
|
|
13
|
-
|
|
13
|
+
import { dispatchMock } from './MockDispatcher.js'
|
|
14
14
|
import { CLIENT_ASSETS, handleApiRequest } from './Api.js'
|
|
15
|
+
|
|
16
|
+
import * as brokers from './stores/brokers.js'
|
|
15
17
|
import { cookie } from './stores/cookies.js'
|
|
16
18
|
import { config, initConfig } from './stores/config.js'
|
|
17
|
-
import * as brokers from './stores/brokers.js'
|
|
18
|
-
import { dispatchMock } from './MockDispatcher.js'
|
|
19
19
|
import { watchMocksDir } from './stores/Watcher.js'
|
|
20
20
|
|
|
21
|
+
import { resolveExtensionless } from './resolveExtensionless.js'
|
|
22
|
+
import { resolveBypassImportCache } from './resolveBypassImportCache.js'
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
export function Mockaton(options) {
|
|
23
26
|
return new Promise((resolve, reject) => {
|
|
@@ -25,10 +28,10 @@ export function Mockaton(options) {
|
|
|
25
28
|
cookie.init(config.cookies)
|
|
26
29
|
brokers.init()
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
registerHooks({ resolve: resolveExtensionless })
|
|
29
32
|
|
|
30
33
|
if (config.bypassImportCache)
|
|
31
|
-
|
|
34
|
+
registerHooks({ resolve: resolveBypassImportCache })
|
|
32
35
|
|
|
33
36
|
if (config.watcherEnabled)
|
|
34
37
|
watchMocksDir()
|
|
@@ -60,13 +63,11 @@ async function onRequest(req, response) {
|
|
|
60
63
|
logger.verbose('APP', response)
|
|
61
64
|
})
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (url.length > 2048) {
|
|
66
|
+
if (req.url?.length > 2048) {
|
|
66
67
|
response.uriTooLong()
|
|
67
68
|
return
|
|
68
69
|
}
|
|
69
|
-
if (
|
|
70
|
+
if (req.urlHasControlChars()) {
|
|
70
71
|
response.badRequest()
|
|
71
72
|
return
|
|
72
73
|
}
|
|
@@ -83,7 +84,7 @@ async function onRequest(req, response) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
catch (error) {
|
|
86
|
-
if (error instanceof BodyReaderError)
|
|
87
|
+
if (error instanceof IncomingMessage.BodyReaderError)
|
|
87
88
|
response.unprocessable(`${error.name}: ${error.message}`)
|
|
88
89
|
else {
|
|
89
90
|
logger.error(500, req.url, error?.message || error, error?.stack || '')
|
|
@@ -36,7 +36,7 @@ const proc = spawn(join(import.meta.dirname, 'cli.js'), [
|
|
|
36
36
|
mocksDir.value,
|
|
37
37
|
'--config', join(import.meta.dirname, 'Mockaton.test.config.js'),
|
|
38
38
|
'--no-open'
|
|
39
|
-
])
|
|
39
|
+
], { env: { ...process.env, FORCE_COLOR: 0, NO_COLOR: 1 } })
|
|
40
40
|
|
|
41
41
|
const DEBUG = false
|
|
42
42
|
proc.stdout.on('data', data => {
|
|
@@ -234,7 +234,7 @@ describe('Dashboard', () => {
|
|
|
234
234
|
})
|
|
235
235
|
|
|
236
236
|
test('serves assets', async () => {
|
|
237
|
-
const r = await request(API.root + '/
|
|
237
|
+
const r = await request(API.root + '/App.css')
|
|
238
238
|
match(await r.text(), new RegExp(':root {'))
|
|
239
239
|
})
|
|
240
240
|
})
|
|
@@ -1178,6 +1178,7 @@ describe('Registering Mocks', () => {
|
|
|
1178
1178
|
before(async () => {
|
|
1179
1179
|
await mocksDir.mkdir('reg0')
|
|
1180
1180
|
await mocksDir.write(fx0.file, fx0.body)
|
|
1181
|
+
await sleep(config.watcherDebounceMs + 50)
|
|
1181
1182
|
version = await resolveOnNextSyncVersion(-1)
|
|
1182
1183
|
})
|
|
1183
1184
|
|
|
@@ -1217,41 +1218,28 @@ describe('Registering Mocks', () => {
|
|
|
1217
1218
|
})
|
|
1218
1219
|
|
|
1219
1220
|
|
|
1220
|
-
/**
|
|
1221
|
-
*
|
|
1222
|
-
|
|
1221
|
+
/** Uses the native `EventSource` to listen for real-time sync-version updates.
|
|
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. */
|
|
1225
|
+
function resolveOnNextSyncVersion(currSyncVer = undefined) {
|
|
1223
1226
|
let skipFirst = currSyncVer === undefined
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
.
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
if (done) break
|
|
1234
|
-
buffer += value
|
|
1227
|
+
return new Promise((resolve, reject) => {
|
|
1228
|
+
const es = new EventSource(api.addr + API.syncVersion)
|
|
1229
|
+
es.onmessage = event => {
|
|
1230
|
+
const v = Number(event.data)
|
|
1231
|
+
if (skipFirst || v === currSyncVer)
|
|
1232
|
+
skipFirst = false
|
|
1233
|
+
else {
|
|
1234
|
+
es.close()
|
|
1235
|
+
resolve(v)
|
|
1235
1236
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1237
|
+
}
|
|
1238
|
+
es.onerror = () => {
|
|
1239
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
1240
|
+
es.close()
|
|
1241
|
+
reject(new Error('sync-version stream closed'))
|
|
1238
1242
|
}
|
|
1239
|
-
const parts = buffer.split('\n\n')
|
|
1240
|
-
buffer = parts.pop() || ''
|
|
1241
|
-
|
|
1242
|
-
for (const event of parts)
|
|
1243
|
-
for (const line of event.split(/\r?\n/))
|
|
1244
|
-
if (line.startsWith('data:')) {
|
|
1245
|
-
const v = Number(line.slice(5).trim())
|
|
1246
|
-
if (skipFirst || v === currSyncVer)
|
|
1247
|
-
skipFirst = false
|
|
1248
|
-
else
|
|
1249
|
-
return v
|
|
1250
|
-
}
|
|
1251
1243
|
}
|
|
1252
|
-
}
|
|
1253
|
-
finally {
|
|
1254
|
-
reader.cancel().catch(() => {})
|
|
1255
|
-
}
|
|
1244
|
+
})
|
|
1256
1245
|
}
|
|
1257
|
-
|
package/src/server/ProxyRelay.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
3
3
|
|
|
4
4
|
import { extFor } from './utils/mime.js'
|
|
5
5
|
import { write, isFile, resolveIn } from './utils/fs.js'
|
|
6
|
-
import {
|
|
6
|
+
import { IncomingMessage } from './utils/HttpIncomingMessage.js'
|
|
7
7
|
import { logger } from './utils/logger.js'
|
|
8
8
|
|
|
9
9
|
import { makeMockFilename } from '../client/Filename.js'
|
|
@@ -24,7 +24,7 @@ export async function proxy(req, response, delay) {
|
|
|
24
24
|
})
|
|
25
25
|
}
|
|
26
26
|
catch (error) { // TESTME
|
|
27
|
-
if (error instanceof BodyReaderError)
|
|
27
|
+
if (error instanceof IncomingMessage.BodyReaderError)
|
|
28
28
|
response.unprocessable(error.name)
|
|
29
29
|
else {
|
|
30
30
|
response.badGateway()
|
|
@@ -3,9 +3,9 @@ import { resolve as _resolve } from 'node:path'
|
|
|
3
3
|
const mockatonSrcRoot = `file://${_resolve(import.meta.dirname, '..')}`
|
|
4
4
|
|
|
5
5
|
// We register this hook at runtime so it doesn’t interfere with non-dynamic imports.
|
|
6
|
-
//
|
|
7
|
-
export
|
|
8
|
-
const result =
|
|
6
|
+
// It cache-busts by appending timestamp query param
|
|
7
|
+
export function resolveBypassImportCache(specifier, context, nextResolve) {
|
|
8
|
+
const result = nextResolve(specifier, context)
|
|
9
9
|
if (result.url?.startsWith('file://') && !result.url.startsWith(mockatonSrcRoot)) {
|
|
10
10
|
const url = new URL(result.url)
|
|
11
11
|
url.searchParams.set('t', performance.now())
|
|
@@ -3,9 +3,9 @@ import { join, dirname } from 'node:path'
|
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
export
|
|
6
|
+
export function resolveExtensionless(specifier, context, nextResolve) {
|
|
7
7
|
try {
|
|
8
|
-
return
|
|
8
|
+
return nextResolve(specifier, context)
|
|
9
9
|
}
|
|
10
10
|
catch (error) {
|
|
11
11
|
// Attempt to resolve imports as .ts and .js
|
|
@@ -13,7 +13,7 @@ export async function resolve(specifier, context, nextResolve) {
|
|
|
13
13
|
const absPath = join(dirname(fileURLToPath(context.parentURL)), specifier)
|
|
14
14
|
for (const candidate of ['.ts', '.js'].map(ext => absPath + ext))
|
|
15
15
|
if (existsSync(candidate))
|
|
16
|
-
return
|
|
16
|
+
return resolveExtensionless(pathToFileURL(candidate).href, context, nextResolve)
|
|
17
17
|
}
|
|
18
18
|
throw error
|
|
19
19
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { basename } from 'node:path'
|
|
2
|
-
|
|
3
1
|
import { config, isFileAllowed } from './config.js'
|
|
4
2
|
import { MockBroker } from '../MockBroker.js'
|
|
5
3
|
import { listFilesRecursively } from '../utils/fs.js'
|
|
@@ -35,8 +33,7 @@ export function init() {
|
|
|
35
33
|
|
|
36
34
|
/** @returns {boolean} registered */
|
|
37
35
|
export function registerMock(file, isFromWatcher = false) {
|
|
38
|
-
if (brokerByFilename(file)?.hasMock(file) ||
|
|
39
|
-
!isFileAllowed(basename(file)))
|
|
36
|
+
if (brokerByFilename(file)?.hasMock(file) || !isFileAllowed(file))
|
|
40
37
|
return false
|
|
41
38
|
|
|
42
39
|
const { method, urlMask } = parseFilename(file)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
1
|
+
import { resolve, basename } from 'node:path'
|
|
2
2
|
import { lstatSync } from 'node:fs'
|
|
3
3
|
import { METHODS } from 'node:http'
|
|
4
4
|
|
|
@@ -89,7 +89,7 @@ export function reinitConfig() {
|
|
|
89
89
|
initConfig(originalOpts)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export const isFileAllowed = f => !config.ignore.test(f)
|
|
92
|
+
export const isFileAllowed = f => !config.ignore.test(basename(f))
|
|
93
93
|
|
|
94
94
|
export const calcDelay = () => config.delayJitter
|
|
95
95
|
? config.delay * (1 + Math.random() * config.delayJitter)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import http, { METHODS } from 'node:http'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
export const reControlAndDelChars = /[\x00-\x1f\x7f]/
|
|
5
|
+
|
|
4
6
|
export const methodIsSupported = method => METHODS.includes(method)
|
|
5
7
|
|
|
6
8
|
export function removeQueryStringAndFragment(url = '') {
|
|
@@ -16,6 +18,8 @@ export class BodyReaderError extends Error {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export class IncomingMessage extends http.IncomingMessage {
|
|
21
|
+
static BodyReaderError = BodyReaderError
|
|
22
|
+
|
|
19
23
|
json() {
|
|
20
24
|
return this.body(JSON.parse)
|
|
21
25
|
}
|
|
@@ -29,18 +33,28 @@ export class IncomingMessage extends http.IncomingMessage {
|
|
|
29
33
|
for await (const chunk of this) {
|
|
30
34
|
lengthSoFar += chunk.length
|
|
31
35
|
if (lengthSoFar > MAX_BODY_SIZE)
|
|
32
|
-
throw new BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
|
|
36
|
+
throw new IncomingMessage.BodyReaderError(`Body too large. Max is ${MAX_BODY_SIZE} bytes`)
|
|
33
37
|
chunks.push(chunk)
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
if (lengthSoFar !== expectedLength)
|
|
37
|
-
throw new BodyReaderError('Length mismatch')
|
|
41
|
+
throw new IncomingMessage.BodyReaderError('Length mismatch')
|
|
38
42
|
|
|
39
43
|
try {
|
|
40
44
|
return parser(Buffer.concat(chunks).toString())
|
|
41
45
|
}
|
|
42
46
|
catch (_) {
|
|
43
|
-
throw new BodyReaderError('Could not parse')
|
|
47
|
+
throw new IncomingMessage.BodyReaderError('Could not parse')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
urlHasControlChars() {
|
|
52
|
+
try {
|
|
53
|
+
const decoded = decode(this.url || '')
|
|
54
|
+
return !decoded || reControlAndDelChars.test(decoded)
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return true
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
}
|
|
@@ -49,19 +63,6 @@ export const parseJSON = req =>
|
|
|
49
63
|
IncomingMessage.prototype.body.call(req, JSON.parse)
|
|
50
64
|
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
export const reControlAndDelChars = /[\x00-\x1f\x7f]/
|
|
54
|
-
|
|
55
|
-
export function hasControlChars(url) {
|
|
56
|
-
try {
|
|
57
|
-
const decoded = decode(url)
|
|
58
|
-
return !decoded || reControlAndDelChars.test(decoded)
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
66
|
export function decode(url) {
|
|
66
67
|
const candidate = decodeURIComponent(url)
|
|
67
68
|
return candidate === decodeURIComponent(candidate)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { styleText } from 'node:util'
|
|
1
2
|
import { decode, reControlAndDelChars } from './HttpIncomingMessage.js'
|
|
2
3
|
|
|
3
4
|
|
|
@@ -39,7 +40,12 @@ export const logger = new class {
|
|
|
39
40
|
#msg(...msg) {
|
|
40
41
|
if (!msg.at(-1))
|
|
41
42
|
msg.pop()
|
|
42
|
-
|
|
43
|
+
const parts = msg.map(this.#sanitize)
|
|
44
|
+
parts[1] = styleText(['bold', 'green'], parts[1])
|
|
45
|
+
return [
|
|
46
|
+
styleText('yellow', new Date().toISOString()),
|
|
47
|
+
...parts
|
|
48
|
+
].join(styleText('dim', '::'))
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
#sanitize(url) {
|
package/src/server/utils/mime.js
CHANGED
|
@@ -57,6 +57,7 @@ const extToMime = {
|
|
|
57
57
|
js: 'application/javascript',
|
|
58
58
|
json: 'application/json',
|
|
59
59
|
jsonld: 'application/ld+json',
|
|
60
|
+
jxl: 'image/jxl',
|
|
60
61
|
lz: 'application/x-lzip',
|
|
61
62
|
m4a: 'audio/mp4',
|
|
62
63
|
md: 'text/markdown',
|
|
@@ -91,6 +92,7 @@ const extToMime = {
|
|
|
91
92
|
stl: 'model/stl',
|
|
92
93
|
svg: 'image/svg+xml',
|
|
93
94
|
tar: 'application/x-tar',
|
|
95
|
+
tga: 'image/x-targa',
|
|
94
96
|
tif: 'image/tiff',
|
|
95
97
|
ts: 'video/mp2t',
|
|
96
98
|
ttf: 'font/ttf',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|