mockaton 13.3.5 → 13.4.1
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 +28 -16
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/client/ApiCommander.js +1 -1
- package/src/client/ApiConstants.js +2 -2
- package/src/client/IndexHtml.js +18 -18
- package/src/client/app-header.js +3 -3
- package/src/client/app-payload-viewer.js +4 -8
- package/src/client/app-store.js +3 -34
- package/src/client/app.css +5 -3
- package/src/client/app.js +3 -2
- package/src/client/dir/dittoSplitPaths.js +25 -0
- package/src/client/dir/dittoSplitPaths.test.js +28 -0
- package/src/client/{dir-tree.js → dir/groupByFolder.js} +2 -33
- package/src/client/{dir-tree.test.js → dir/groupByFolder.test.js} +2 -26
- package/src/client/graphics.js +5 -5
- package/src/client/utils/LocalStorage.js +69 -0
- package/src/client/utils/css.js +16 -0
- package/src/client/{dom-utils-test.js → utils/css.test.js} +1 -1
- package/src/client/utils/dom.js +68 -0
- package/src/client/utils/watcherDev.js +46 -0
- package/src/server/Api.js +16 -3
- package/src/server/MockDispatcher.js +11 -8
- package/src/server/Mockaton.js +16 -8
- package/src/server/Mockaton.test.js +14 -9
- package/src/server/ProxyRelay.js +9 -3
- package/src/server/{utils/UrlParsers.js → UrlParsers.js} +10 -4
- package/src/server/{utils/UrlParsers.test.js → UrlParsers.test.js} +14 -14
- package/src/server/config.js +2 -0
- package/src/server/utils/HttpServerResponse.js +18 -39
- package/src/server/{WatcherDevClient.js → utils/WatcherDevClient.js} +3 -17
- package/src/server/utils/fs.js +1 -1
- package/src/server/utils/logger.js +4 -4
- package/src/server/utils/mime.js +11 -11
- package/src/server/utils/mime.test.js +15 -11
- package/www/src/assets/openapi.json +147 -147
- package/src/client/dom-utils.js +0 -154
- package/src/client/watcherDev.js +0 -39
package/README.md
CHANGED
|
@@ -10,6 +10,26 @@ for testing difficult to reproduce backend states.
|
|
|
10
10
|
|
|
11
11
|
## [Documentation ↗](https://mockaton.com) | [Changelog ↗](https://mockaton.com/changelog)
|
|
12
12
|
|
|
13
|
+
## TL;DR
|
|
14
|
+
```shell
|
|
15
|
+
npx mockaton my-mocks-dir
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
It’s like `servedir`, but supports dynamic segments in filenames. For example:
|
|
19
|
+
|
|
20
|
+
**Route**: [/api/company/123](#) <br/>
|
|
21
|
+
**File**: my-mocks-dir/api/company/[id].GET.200.json
|
|
22
|
+
|
|
23
|
+
Statics assets don’t need that extension:
|
|
24
|
+
|
|
25
|
+
**Route**: [/media/avatar.png](#) <br/>
|
|
26
|
+
**File**: my-mocks-dir/media/avatar.png
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Dashboard
|
|
30
|
+
Besides the dashboard, there’s a programmatic [Control API](https://mockaton.com/api).
|
|
31
|
+
Also, there’s a [Browser Extension](https://mockaton.com/scraping) for scraping responses from your backend.
|
|
32
|
+
|
|
13
33
|
<picture>
|
|
14
34
|
<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">
|
|
15
35
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ericfortis/mockaton/refs/heads/main/pixaton-tests/tests/macos/pic-for-readme.vp762x762.dark.gold.png">
|
|
@@ -36,12 +56,8 @@ curl localhost:2020/api/user
|
|
|
36
56
|
```
|
|
37
57
|
|
|
38
58
|
|
|
39
|
-
##
|
|
40
|
-
|
|
41
|
-
mocks. Instead, a given directory is scanned for filenames
|
|
42
|
-
following a convention similar to the URLs.
|
|
43
|
-
|
|
44
|
-
For example, for [/api/company/123](#), the file could be:
|
|
59
|
+
## Examples
|
|
60
|
+
[/api/company/123](#)
|
|
45
61
|
|
|
46
62
|
<code>my_mocks_dir/<b>api/company/[id]</b>.GET.200.json</code>
|
|
47
63
|
```json
|
|
@@ -50,6 +66,8 @@ For example, for [/api/company/123](#), the file could be:
|
|
|
50
66
|
}
|
|
51
67
|
```
|
|
52
68
|
|
|
69
|
+
<br/>
|
|
70
|
+
|
|
53
71
|
Or, you can write it in TypeScript (it will be sent as JSON).
|
|
54
72
|
|
|
55
73
|
<code>my_mocks_dir/<b>api/company/[id]</b>.GET.200.ts</code>
|
|
@@ -59,15 +77,17 @@ export default {
|
|
|
59
77
|
}
|
|
60
78
|
```
|
|
61
79
|
|
|
80
|
+
<br/>
|
|
81
|
+
|
|
62
82
|
Similarly, you can handle logic with [Functional Mocks](https://mockaton.com/functional-mocks):
|
|
63
83
|
|
|
64
84
|
<code>my_mocks_dir/<b>api/company/[companyId]/user/[userId]</b>.GET.200.ts</code>
|
|
65
85
|
```ts
|
|
66
86
|
import { IncomingMessage, OutgoingMessage } from 'node:http'
|
|
67
|
-
import {
|
|
87
|
+
import { parseSegments } from 'mockaton'
|
|
68
88
|
|
|
69
89
|
export default async function (req: IncomingMessage, response: OutgoingMessage) {
|
|
70
|
-
const { companyId, userId } =
|
|
90
|
+
const { companyId, userId } = parseSegments(req.url, import.meta.filename)
|
|
71
91
|
const foo = await getFoo()
|
|
72
92
|
return JSON.stringify({
|
|
73
93
|
foo,
|
|
@@ -77,11 +97,3 @@ export default async function (req: IncomingMessage, response: OutgoingMessage)
|
|
|
77
97
|
})
|
|
78
98
|
}
|
|
79
99
|
```
|
|
80
|
-
|
|
81
|
-
## Browser Extension
|
|
82
|
-
[Browser Extension](https://mockaton.com/scraping) for scraping responses from your backend.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
## API
|
|
86
|
-
Programmatic [Control API](https://mockaton.com/api).
|
|
87
|
-
|
package/index.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export const echoFilePlugin: Plugin
|
|
|
62
62
|
export function jwtCookie(cookieName: string, payload: any, path?: string): string
|
|
63
63
|
|
|
64
64
|
export function parseJSON(request: IncomingMessage): Promise<any>
|
|
65
|
-
export function
|
|
65
|
+
export function parseSegments(reqUrl: string, filename: string): Record<string, string>
|
|
66
66
|
export function parseQueryParams(reqUrl: string): URLSearchParams
|
|
67
67
|
|
|
68
68
|
export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
|
package/index.js
CHANGED
|
@@ -5,6 +5,6 @@ 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 {
|
|
8
|
+
export { parseSegments, parseQueryParams, parseSplats } from './src/server/UrlParsers.js'
|
|
9
9
|
|
|
10
10
|
export const defineConfig = opts => opts
|
package/package.json
CHANGED
|
@@ -25,9 +25,9 @@ export const API = {
|
|
|
25
25
|
deleteMock: MOUNT + '/delete-mock',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export const HEADER_502 = 'Mockaton502'
|
|
29
|
-
|
|
30
28
|
export const DEFAULT_MOCK_COMMENT = '(default)'
|
|
31
29
|
|
|
32
30
|
export const EXT_UNKNOWN_MIME = 'unknown'
|
|
33
31
|
export const EXT_EMPTY = 'empty'
|
|
32
|
+
|
|
33
|
+
export const FILENAME_HEADER = 'Mockaton-File'
|
package/src/client/IndexHtml.js
CHANGED
|
@@ -7,24 +7,24 @@ export const CSP = [
|
|
|
7
7
|
|
|
8
8
|
// language=html
|
|
9
9
|
export const IndexHtml = (hotReloadEnabled, version) => `
|
|
10
|
-
<!DOCTYPE html>
|
|
11
|
-
<html lang="en-US">
|
|
12
|
-
<head>
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
<!DOCTYPE html>
|
|
11
|
+
<html lang="en-US">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="UTF-8">
|
|
14
|
+
<base href="${API.dashboard}/">
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
<script type="module" src="app.js"></script>
|
|
17
|
+
<link rel="preload" href="${API.state}" as="fetch" crossorigin>
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</head>
|
|
24
|
-
<body>
|
|
25
|
-
${hotReloadEnabled
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</body>
|
|
29
|
-
</html>
|
|
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">
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
21
|
+
<meta name="description" content="HTTP Mock Server">
|
|
22
|
+
<title>Mockaton v${version}</title>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
${hotReloadEnabled
|
|
26
|
+
? `<script type="module" src="utils/watcherDev.js?url=${API.watchHotReload}"></script>`
|
|
27
|
+
: ''}
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
30
|
`
|
package/src/client/app-header.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createElement as r, t
|
|
1
|
+
import { createElement as r, t } from './utils/dom.js'
|
|
2
2
|
import { Logo, HelpIcon } from './graphics.js'
|
|
3
3
|
import { store } from './app-store.js'
|
|
4
4
|
|
|
5
5
|
import CSS from './app.css' with { type: 'css' }
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { extractClassNames } from './utils/css.js'
|
|
8
7
|
Object.assign(CSS, extractClassNames(CSS))
|
|
9
8
|
|
|
9
|
+
|
|
10
10
|
export function Header() {
|
|
11
11
|
return (
|
|
12
12
|
r('header', null,
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { createElement as r, t
|
|
2
|
-
import { HEADER_502 } from './ApiConstants.js'
|
|
1
|
+
import { createElement as r, t } from './utils/dom.js'
|
|
3
2
|
import { parseFilename } from './Filename.js'
|
|
4
3
|
import { store } from './app-store.js'
|
|
5
4
|
|
|
6
5
|
import CSS from './app.css' with { type: 'css' }
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { extractClassNames } from './utils/css.js'
|
|
9
7
|
Object.assign(CSS, extractClassNames(CSS))
|
|
10
8
|
|
|
9
|
+
|
|
11
10
|
const titleRef = {}
|
|
12
11
|
const codeRef = {}
|
|
13
12
|
|
|
@@ -42,12 +41,9 @@ function PayloadViewerTitle(file, statusText) {
|
|
|
42
41
|
|
|
43
42
|
function PayloadViewerTitleWhenProxied(response) {
|
|
44
43
|
const mime = response.headers.get('content-type') || ''
|
|
45
|
-
const badGateway = response.headers.get(HEADER_502)
|
|
46
44
|
return (
|
|
47
45
|
r('span', null,
|
|
48
|
-
|
|
49
|
-
? r('span', null, t`⛔ Fallback Backend Error` + ' ')
|
|
50
|
-
: r('span', null, t`Got` + ' '),
|
|
46
|
+
r('span', null, t`Got` + ' '),
|
|
51
47
|
r('abbr', { title: response.statusText }, response.status),
|
|
52
48
|
' ' + mime))
|
|
53
49
|
}
|
package/src/client/app-store.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Commander } from './ApiCommander.js'
|
|
2
|
-
import {
|
|
2
|
+
import { groupByFolder } from './dir/groupByFolder.js'
|
|
3
|
+
import { dittoSplitPaths } from './dir/dittoSplitPaths.js'
|
|
3
4
|
import { parseFilename, extractComments } from './Filename.js'
|
|
4
|
-
import {
|
|
5
|
+
import { LocalStorageSet, QueryParamBool } from './utils/LocalStorage.js'
|
|
5
6
|
import { EXT_UNKNOWN_MIME, EXT_EMPTY } from './ApiConstants.js'
|
|
6
7
|
|
|
7
8
|
|
|
@@ -205,38 +206,6 @@ export const store = {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
// When false, the URL will be updated with param=false
|
|
209
|
-
function initPreference(param) {
|
|
210
|
-
const qs = new URLSearchParams(globalThis.location?.search)
|
|
211
|
-
if (!qs.has(param)) {
|
|
212
|
-
const group = globalThis.localStorage?.getItem(param) !== '0'
|
|
213
|
-
if (!group) {
|
|
214
|
-
const url = new URL(globalThis.location?.href)
|
|
215
|
-
url.searchParams.set(param, '0')
|
|
216
|
-
history.replaceState(null, '', url)
|
|
217
|
-
}
|
|
218
|
-
return group
|
|
219
|
-
}
|
|
220
|
-
return qs.get(param) !== '0'
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// When false, the URL and localStorage will have param='0'
|
|
224
|
-
function togglePreference(param, nextVal) {
|
|
225
|
-
if (nextVal)
|
|
226
|
-
globalThis.localStorage?.removeItem(param)
|
|
227
|
-
else
|
|
228
|
-
globalThis.localStorage?.setItem(param, nextVal)
|
|
229
|
-
|
|
230
|
-
const url = new URL(location.href)
|
|
231
|
-
if (nextVal)
|
|
232
|
-
url.searchParams.delete(param)
|
|
233
|
-
else
|
|
234
|
-
url.searchParams.set(param, '0')
|
|
235
|
-
history.replaceState(null, '', url)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
209
|
export class BrokerRowModel {
|
|
241
210
|
opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
|
|
242
211
|
isNew = false
|
package/src/client/app.css
CHANGED
|
@@ -119,12 +119,12 @@ header {
|
|
|
119
119
|
|
|
120
120
|
.Logo {
|
|
121
121
|
align-self: end;
|
|
122
|
-
margin-right:
|
|
123
|
-
margin-bottom:
|
|
122
|
+
margin-right: 18px;
|
|
123
|
+
margin-bottom: 2px;
|
|
124
124
|
transition: opacity 240ms ease-in-out;
|
|
125
125
|
|
|
126
126
|
svg {
|
|
127
|
-
width:
|
|
127
|
+
width: 122px;
|
|
128
128
|
pointer-events: none;
|
|
129
129
|
fill: var(--colorText);
|
|
130
130
|
}
|
|
@@ -185,6 +185,7 @@ header {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
.HelpLink {
|
|
188
|
+
opacity: 0.8;
|
|
188
189
|
width: 22px;
|
|
189
190
|
height: 22px;
|
|
190
191
|
flex-shrink: 0;
|
|
@@ -197,6 +198,7 @@ header {
|
|
|
197
198
|
|
|
198
199
|
&:hover {
|
|
199
200
|
background: var(--colorAccent);
|
|
201
|
+
opacity: 1;
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
svg {
|
package/src/client/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement as r, t, restoreFocus, Fragment
|
|
1
|
+
import { createElement as r, t, restoreFocus, Fragment } from './utils/dom.js'
|
|
2
2
|
|
|
3
3
|
import { store } from './app-store.js'
|
|
4
4
|
import { API } from './ApiConstants.js'
|
|
@@ -7,10 +7,11 @@ import { PayloadViewer, previewMock } from './app-payload-viewer.js'
|
|
|
7
7
|
import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
|
|
8
8
|
|
|
9
9
|
import CSS from './app.css' with { type: 'css' }
|
|
10
|
+
import { extractClassNames, classNames } from './utils/css.js'
|
|
10
11
|
document.adoptedStyleSheets.push(CSS)
|
|
11
|
-
|
|
12
12
|
Object.assign(CSS, extractClassNames(CSS))
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
store.onError = onError
|
|
15
16
|
store.render = render
|
|
16
17
|
store.renderRow = renderRow
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Think of this as a way of printing a directory tree in which
|
|
3
|
+
* the repeated folder paths are kept but styled differently.
|
|
4
|
+
* @param {string[]} paths - sorted
|
|
5
|
+
*/
|
|
6
|
+
export function dittoSplitPaths(paths) {
|
|
7
|
+
const pParts = paths.map(p => p.split('/').filter(Boolean))
|
|
8
|
+
return paths.map((p, i) => {
|
|
9
|
+
if (i === 0)
|
|
10
|
+
return ['', p]
|
|
11
|
+
|
|
12
|
+
const prev = pParts[i - 1]
|
|
13
|
+
const curr = pParts[i]
|
|
14
|
+
const min = Math.min(curr.length, prev.length)
|
|
15
|
+
let j = 0
|
|
16
|
+
while (j < min && curr[j] === prev[j])
|
|
17
|
+
j++
|
|
18
|
+
|
|
19
|
+
if (!j) // no common dirs
|
|
20
|
+
return ['', p]
|
|
21
|
+
|
|
22
|
+
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
23
|
+
return [ditto, p.slice(ditto.length)]
|
|
24
|
+
})
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { deepEqual } from 'node:assert/strict'
|
|
3
|
+
import { dittoSplitPaths } from './dittoSplitPaths.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
test('dittoSplitPaths', () => {
|
|
7
|
+
const input = [
|
|
8
|
+
'/api/user',
|
|
9
|
+
'/api/user/avatar',
|
|
10
|
+
'/api/user/friends',
|
|
11
|
+
'/api/vid',
|
|
12
|
+
'/api/video/id',
|
|
13
|
+
'/api/video/stats',
|
|
14
|
+
'/v2/foo',
|
|
15
|
+
'/v2/foo/bar'
|
|
16
|
+
]
|
|
17
|
+
deepEqual(dittoSplitPaths(input), [
|
|
18
|
+
['', '/api/user'],
|
|
19
|
+
['/api/user/', 'avatar'],
|
|
20
|
+
['/api/user/', 'friends'],
|
|
21
|
+
['/api/', 'vid'],
|
|
22
|
+
['/api/', 'video/id'],
|
|
23
|
+
['/api/video/', 'stats'],
|
|
24
|
+
['', '/v2/foo'],
|
|
25
|
+
['/v2/foo/', 'bar']
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Think of this as a way of printing a directory tree in which
|
|
3
|
-
* the repeated folder paths are kept but styled differently.
|
|
4
|
-
* @param {string[]} paths - sorted
|
|
5
|
-
*/
|
|
6
|
-
export function dittoSplitPaths(paths) {
|
|
7
|
-
const pParts = paths.map(p => p.split('/').filter(Boolean))
|
|
8
|
-
return paths.map((p, i) => {
|
|
9
|
-
if (i === 0)
|
|
10
|
-
return ['', p]
|
|
11
|
-
|
|
12
|
-
const prev = pParts[i - 1]
|
|
13
|
-
const curr = pParts[i]
|
|
14
|
-
const min = Math.min(curr.length, prev.length)
|
|
15
|
-
let j = 0
|
|
16
|
-
while (j < min && curr[j] === prev[j])
|
|
17
|
-
j++
|
|
18
|
-
|
|
19
|
-
if (!j) // no common dirs
|
|
20
|
-
return ['', p]
|
|
21
|
-
|
|
22
|
-
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
23
|
-
return [ditto, p.slice(ditto.length)]
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
1
|
/**
|
|
30
2
|
* @param {Partial<BrokerRowModel>[]} brokers
|
|
31
3
|
* @returns {Partial<BrokerRowModel>[]}
|
|
@@ -49,11 +21,8 @@ function trie(brokers) {
|
|
|
49
21
|
}
|
|
50
22
|
|
|
51
23
|
class TrieNode {
|
|
52
|
-
#children
|
|
53
|
-
|
|
54
|
-
this.brokers = []
|
|
55
|
-
this.#children = new Map()
|
|
56
|
-
}
|
|
24
|
+
#children = new Map()
|
|
25
|
+
brokers = []
|
|
57
26
|
addChild(k, v) { this.#children.set(k, v) }
|
|
58
27
|
getChild(k) { return this.#children.get(k) }
|
|
59
28
|
getChildren() { return this.#children.values() }
|
|
@@ -1,33 +1,9 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import { deepEqual } from 'node:assert/strict'
|
|
3
|
-
import { groupByFolder
|
|
3
|
+
import { groupByFolder } from './groupByFolder.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
test('
|
|
7
|
-
const input = [
|
|
8
|
-
'/api/user',
|
|
9
|
-
'/api/user/avatar',
|
|
10
|
-
'/api/user/friends',
|
|
11
|
-
'/api/vid',
|
|
12
|
-
'/api/video/id',
|
|
13
|
-
'/api/video/stats',
|
|
14
|
-
'/v2/foo',
|
|
15
|
-
'/v2/foo/bar'
|
|
16
|
-
]
|
|
17
|
-
deepEqual(dittoSplitPaths(input), [
|
|
18
|
-
['', '/api/user'],
|
|
19
|
-
['/api/user/', 'avatar'],
|
|
20
|
-
['/api/user/', 'friends'],
|
|
21
|
-
['/api/', 'vid'],
|
|
22
|
-
['/api/', 'video/id'],
|
|
23
|
-
['/api/video/', 'stats'],
|
|
24
|
-
['', '/v2/foo'],
|
|
25
|
-
['/v2/foo/', 'bar']
|
|
26
|
-
])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
test('dirStructure', () => {
|
|
6
|
+
test('groupByFolder', () => {
|
|
31
7
|
const input = [
|
|
32
8
|
{ children: [], method: 'GET', urlMask: '/api/user' },
|
|
33
9
|
{ children: [], method: 'GET', urlMask: '/api/user/avatar' },
|
package/src/client/graphics.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createSvgElement as s } from './dom
|
|
1
|
+
import { createSvgElement as s } from './utils/dom.js'
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export const Logo = () =>
|
|
5
|
-
s('svg', { viewBox: '0 0
|
|
6
|
-
s('path', { d: '
|
|
7
|
-
s('path', {
|
|
8
|
-
s('path', { d: '
|
|
5
|
+
s('svg', { viewBox: '0 0 460 80' },
|
|
6
|
+
s('path', { d: 'm332 29v34.9q0 2.5 1.06 3.84t4.51 1.34h2.5q3.74 0 3.55 4.42 0 4.51-3.55 4.51h-3.17q-3.07 0-5.76-0.768-2.69-0.864-5.18-2.4v0.096q-4.7-3.07-4.7-10.9v-35h-3.74q-3.46 0-3.65-4.51 0-4.51 3.65-4.51h3.74v-10.3q0-4.61 5.47-4.61 5.28 0 5.28 4.61v10.3h7.97q3.74 0 3.55 4.51 0 4.51-3.55 4.51zm50.9-9.02q2.5 0 5.09 1.06 2.69 1.06 5.18 2.69h-0.096q2.5 1.82 3.84 4.8 1.34 2.88 1.34 6.72v27.3q0 7.87-5.18 11.6-5.09 3.65-10.2 3.65h-15.4q-2.5 0-5.09-0.864t-5.09-2.78q-5.18-3.65-5.18-11.6v-27.3q0-3.84 1.34-6.72 1.34-2.98 3.84-4.8h-0.096q2.4-1.63 5.09-2.69t5.18-1.06zm-1.73 48.8q3.55 0 4.99-2.02 1.54-2.02 1.54-4.32v-27.1q0-2.4-1.54-4.32-1.44-2.02-4.99-2.02h-11.9q-3.55 0-5.09 2.02-1.44 1.92-1.44 4.32v27.1q0 2.3 1.44 4.32 1.54 2.02 5.09 2.02zm40 4.99q0 4.61-5.28 4.61t-5.28-4.61v-38.6q0-3.94 1.25-6.82t3.94-4.7q5.09-3.65 10.2-3.65h15.5q4.9 0 10.2 3.65 5.18 3.65 5.18 11.5v38.6q0 4.61-5.28 4.61t-5.28-4.61v-37.7q0-2.5-1.54-4.8l0.096 0.096q-1.25-2.3-5.09-2.3h-12q-3.94 0-5.18 2.3-1.34 2.02-1.34 4.7z' }),
|
|
7
|
+
s('path', { fill: 'currentColor', d: 'm282 11-21 64.5v-0.096q-0.576 1.44-1.63 2.4-0.96 0.864-2.4 0.864-0.768 0-1.54-0.192-0.672-0.096-1.44-0.288h0.096q-5.47-1.73-4.03-6.14l21.9-66.5q0.48-1.54 1.63-2.59 1.25-1.06 2.88-1.25 1.63-0.288 3.17-0.384 1.54-0.192 2.4-0.192t2.4 0.192q1.54 0.096 3.17 0.288 1.34 0.192 2.59 1.44 1.34 1.25 2.02 2.5 5.38 16.5 10.8 33.3 5.57 16.7 10.9 33.2 1.34 4.42-4.03 6.14-0.768 0.192-1.44 0.288-0.576 0.192-1.34 0.192-1.34 0-2.5-0.864-1.06-0.96-1.54-2.4v0.096zm7.2 43.8q0 3.07-2.21 5.28-2.11 2.11-5.18 2.11t-5.18-2.11q-2.11-2.21-2.11-5.28t2.11-5.18q2.11-2.21 5.18-2.21t5.18 2.21q2.21 2.11 2.21 5.18z' }),
|
|
8
|
+
s('path', { d: 'm31.4 36q0-1.25-0.384-2.4-0.384-1.25-0.96-2.3-1.25-2.3-5.18-2.3h-6.14q-3.94 0-5.28 2.3-1.15 2.21-1.15 4.7v38q0 4.51-5.28 4.51-5.47 0-5.47-4.51v-38.9q0-7.87 5.18-11.5 5.28-3.65 10.3-3.65h9.7q2.21 0 4.9 0.864t5.18 2.69q2.4-1.82 4.99-2.69 2.69-0.864 5.09-0.864h9.6q5.09 0 10.2 3.65 5.28 3.55 5.28 11.5v38.9q0 4.51-5.38 4.51t-5.38-4.51v-38q0-1.25-0.288-2.5t-1.06-2.3l0.096 0.096q-0.768-1.25-1.92-1.73-1.15-0.576-3.36-0.576h-6.05q-4.03 0-5.28 2.3-1.25 2.11-1.25 4.7v38q0 4.51-5.28 4.51-5.38 0-5.38-4.51zm83.2-16q2.5 0 5.09 1.06 2.69 1.06 5.18 2.69h-0.096q2.5 1.82 3.84 4.8 1.34 2.88 1.34 6.72v27.3q0 7.87-5.18 11.6-5.09 3.65-10.2 3.65h-15.4q-2.5 0-5.09-0.864t-5.09-2.78q-5.18-3.65-5.18-11.6v-27.3q0-3.84 1.34-6.72 1.34-2.98 3.84-4.8h-0.096q2.4-1.63 5.09-2.69t5.18-1.06zm-1.73 48.8q3.55 0 4.99-2.02 1.54-2.02 1.54-4.32v-27.1q0-2.4-1.54-4.32-1.44-2.02-4.99-2.02h-11.9q-3.55 0-5.09 2.02-1.44 1.92-1.44 4.32v27.1q0 2.3 1.44 4.32 1.54 2.02 5.09 2.02zm75.6-6.24q0 7.87-5.18 11.6-5.09 3.65-10.2 3.65h-16.1q-5.09 0-9.89-3.65-2.3-1.92-3.55-4.8-1.25-2.98-1.25-6.82v-27.4q0-7.78 4.8-11.5 2.4-1.73 4.9-2.69 2.59-1.06 4.99-1.06h16.1q2.3 0 5.09 0.96 2.78 0.864 5.18 2.4 2.3 1.63 3.65 4.51 1.44 2.88 1.44 6.82v4.8q0 4.61-5.28 4.61t-5.28-4.61v-4.13q0-2.4-1.54-4.32-1.44-2.02-4.99-2.02h-12q-3.55 0-5.09 2.02-1.44 1.92-1.44 4.32v27.2q0 2.3 1.44 4.32 1.54 2.02 5.09 2.02h12q3.55 0 4.99-2.02 1.54-2.02 1.54-4.32v-4.42q0-4.51 5.28-4.51t5.28 4.51zm22.8-0.768v12.2q0 4.51-5.28 4.51t-5.28-4.51v-68.4q0-4.61 5.28-4.61t5.28 4.61v40.7q2.4-2.88 5.28-6.14 2.98-3.36 5.86-6.72 2.98-3.46 5.86-6.72 2.88-3.36 5.38-6.24v0.096q1.73-2.02 3.46-2.02 0.96 0 1.82 0.48 0.96 0.384 2.02 1.15h-0.096q2.3 1.92 2.3 3.84 0 0.672-0.288 1.54-0.288 0.768-0.96 1.34zm32.1 9.6q0.672 1.25 0.672 2.59 0 2.5-2.88 3.74-1.92 1.15-3.46 1.15-1.06 0-2.02-0.768-0.96-0.672-1.54-1.73l-9.6-18.5q-0.864-1.44-0.864-2.88 0-2.4 2.5-4.03 0.672-0.48 1.34-0.864 0.768-0.384 1.63-0.576 1.06-0.192 2.02 0.384t1.63 1.54z' }))
|
|
9
9
|
|
|
10
10
|
export const TimerIcon = () =>
|
|
11
11
|
s('svg', { viewBox: '0 0 24 24' },
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class QueryParamBool {
|
|
2
|
+
constructor(param) {
|
|
3
|
+
this.param = param
|
|
4
|
+
this.value = this.#init()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
#init() {
|
|
8
|
+
const qs = new URLSearchParams(globalThis.location?.search)
|
|
9
|
+
if (qs.has(this.param))
|
|
10
|
+
return qs.get(this.param) !== '0'
|
|
11
|
+
const stored = globalThis.localStorage?.getItem(this.param) !== '0'
|
|
12
|
+
if (!stored)
|
|
13
|
+
this.#applyToUrl(false)
|
|
14
|
+
return stored
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toggle() {
|
|
18
|
+
this.value = !this.value
|
|
19
|
+
if (this.value)
|
|
20
|
+
globalThis.localStorage?.removeItem(this.param)
|
|
21
|
+
else
|
|
22
|
+
globalThis.localStorage?.setItem(this.param, '0')
|
|
23
|
+
this.#applyToUrl(this.value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#applyToUrl(nextVal) {
|
|
27
|
+
const url = new URL(globalThis.location?.href)
|
|
28
|
+
if (nextVal)
|
|
29
|
+
url.searchParams.delete(this.param)
|
|
30
|
+
else
|
|
31
|
+
url.searchParams.set(this.param, '0')
|
|
32
|
+
history.replaceState(null, '', url)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export class LocalStorageSet {
|
|
38
|
+
constructor(key) {
|
|
39
|
+
this.key = key
|
|
40
|
+
this.value = this.#parse()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
add(item) {
|
|
44
|
+
this.value.add(item)
|
|
45
|
+
this.#persist()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
delete(item) {
|
|
49
|
+
this.value.delete(item)
|
|
50
|
+
this.#persist()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
has(item) {
|
|
54
|
+
return this.value.has(item)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#parse() {
|
|
58
|
+
try {
|
|
59
|
+
return new Set(JSON.parse(globalThis.localStorage?.getItem(this.key) || '[]'))
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return new Set()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#persist() {
|
|
67
|
+
globalThis.localStorage?.setItem(this.key, JSON.stringify([...this.value]))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function classNames(...args) {
|
|
2
|
+
return args.filter(Boolean).join(' ')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export function extractClassNames({ cssRules }) {
|
|
7
|
+
// Class names must begin with _ or a letter, then it can have numbers and hyphens
|
|
8
|
+
// TODO think about tag.className selectors
|
|
9
|
+
const reClassName = /(?:^|[\s,{>])&?\s*\.([a-zA-Z_][\w-]*)/g
|
|
10
|
+
const cNames = {}
|
|
11
|
+
let match
|
|
12
|
+
for (const rule of cssRules)
|
|
13
|
+
while (match = reClassName.exec(rule.cssText))
|
|
14
|
+
cNames[match[1]] = match[1]
|
|
15
|
+
return cNames
|
|
16
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import { deepEqual, equal } from 'node:assert/strict'
|
|
3
|
-
import {
|
|
3
|
+
import { extractClassNames, classNames } from './css.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
test('classNames', () => equal(classNames('a', false && 'b'), 'a'))
|