mockaton 13.3.3 → 13.3.5

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "13.3.3",
5
+ "version": "13.3.5",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -1,11 +1,12 @@
1
- import { createElement as r, t, defineClassNames } from './dom-utils.js'
1
+ import { createElement as r, t, extractClassNames } from './dom-utils.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
- defineClassNames(CSS)
7
6
 
8
7
 
8
+ Object.assign(CSS, extractClassNames(CSS))
9
+
9
10
  export function Header() {
10
11
  return (
11
12
  r('header', null,
@@ -1,12 +1,13 @@
1
- import { createElement as r, t, defineClassNames } from './dom-utils.js'
1
+ import { createElement as r, t, extractClassNames } from './dom-utils.js'
2
2
  import { HEADER_502 } from './ApiConstants.js'
3
3
  import { parseFilename } from './Filename.js'
4
4
  import { store } from './app-store.js'
5
5
 
6
6
  import CSS from './app.css' with { type: 'css' }
7
- defineClassNames(CSS)
8
7
 
9
8
 
9
+ Object.assign(CSS, extractClassNames(CSS))
10
+
10
11
  const titleRef = {}
11
12
  const codeRef = {}
12
13
 
@@ -93,8 +94,6 @@ export async function previewMock() {
93
94
 
94
95
 
95
96
  async function updatePayloadViewer(proxied, file, response) {
96
- const mime = response.headers.get('content-type') || ''
97
-
98
97
  titleRef.elem.replaceChildren(proxied
99
98
  ? PayloadViewerTitleWhenProxied(response)
100
99
  : PayloadViewerTitle(file, response.statusText))
@@ -104,6 +103,12 @@ async function updatePayloadViewer(proxied, file, response) {
104
103
  return
105
104
  }
106
105
 
106
+ async function bodyAsText() {
107
+ return (await response.text()) || t`/* Empty Response Body */`
108
+ }
109
+
110
+ const mime = response.headers.get('content-type') || ''
111
+
107
112
  if (mime.startsWith('image/'))
108
113
  codeRef.elem.replaceChildren(r('img', {
109
114
  src: URL.createObjectURL(await response.blob())
@@ -140,11 +145,6 @@ async function updatePayloadViewer(proxied, file, response) {
140
145
  href: URL.createObjectURL(await response.blob()),
141
146
  download: store.chosenLink.urlMask
142
147
  }, t`Download`))
143
-
144
-
145
- async function bodyAsText() {
146
- return (await response.text()) || t`/* Empty Response Body */`
147
- }
148
148
  }
149
149
 
150
150
 
@@ -1,5 +1,7 @@
1
1
  import { Commander } from './ApiCommander.js'
2
+ import { dittoSplitPaths, groupByFolder } from './dir-tree.js'
2
3
  import { parseFilename, extractComments } from './Filename.js'
4
+ import { QueryParamBool, LocalStorageSet } from './dom-utils.js'
3
5
  import { EXT_UNKNOWN_MIME, EXT_EMPTY } from './ApiConstants.js'
4
6
 
5
7
 
@@ -22,25 +24,21 @@ export const store = {
22
24
  collectProxied: false,
23
25
  proxyFallback: '',
24
26
  showProxyField: null,
25
- get canProxy() {
26
- return Boolean(store.proxyFallback)
27
- },
27
+ get canProxy() { return Boolean(store.proxyFallback) },
28
28
 
29
- groupByMethod: initPreference('groupByMethod'),
29
+ _groupByMethod: new QueryParamBool('groupByMethod'),
30
+ get groupByMethod() { return store._groupByMethod.value },
30
31
  toggleGroupByMethod() {
31
- store.groupByMethod = !store.groupByMethod
32
- togglePreference('groupByMethod', store.groupByMethod)
32
+ store._groupByMethod.toggle()
33
33
  store.render()
34
34
  },
35
35
 
36
- collapsedFolders: new Set(JSON.parse(globalThis.localStorage?.getItem('collapsedFolders') || '[]')),
36
+ collapsedFolders: new LocalStorageSet('collapsedFolders'),
37
37
  setFolderCollapsed(folder, collapsed) {
38
38
  if (collapsed)
39
39
  store.collapsedFolders.add(folder)
40
40
  else
41
41
  store.collapsedFolders.delete(folder)
42
-
43
- globalThis.localStorage?.setItem('collapsedFolders', JSON.stringify([...store.collapsedFolders]))
44
42
  },
45
43
 
46
44
  chosenLink: { method: '', urlMask: '' },
@@ -123,10 +121,6 @@ export const store = {
123
121
 
124
122
  _dittoCache: new Map(),
125
123
 
126
- brokerFor(method, urlMask) {
127
- return store.brokersByMethod[method]?.[urlMask]
128
- },
129
-
130
124
  brokerAsRow(method, urlMask) {
131
125
  const b = store.brokerFor(method, urlMask)
132
126
  const r = new BrokerRowModel(b, store.canProxy)
@@ -134,13 +128,15 @@ export const store = {
134
128
  return r
135
129
  },
136
130
 
137
- _setBroker(broker) {
138
- const { method, urlMask } = parseFilename(broker.file)
139
- store.brokersByMethod[method] ??= {}
140
- store.brokersByMethod[method][urlMask] = broker
131
+ brokerFor(method, urlMask) {
132
+ return store.brokersByMethod[method]?.[urlMask]
133
+ },
134
+
135
+ folderGroupsByMethod(method) {
136
+ return groupByFolder(store._brokersAsRowsByMethod(method))
141
137
  },
142
138
 
143
- brokersAsRowsByMethod(method) {
139
+ _brokersAsRowsByMethod(method) {
144
140
  const rows = store._brokersAsArray(method)
145
141
  .map(b => new BrokerRowModel(b, store.canProxy))
146
142
  .sort((a, b) => a.urlMask.localeCompare(b.urlMask))
@@ -200,27 +196,31 @@ export const store = {
200
196
  store._request(() => api.setRouteIsDelayed(method, urlMask, checked), async response => {
201
197
  store._setBroker(await response.json())
202
198
  })
199
+ },
200
+
201
+ _setBroker(broker) {
202
+ const { method, urlMask } = parseFilename(broker.file)
203
+ store.brokersByMethod[method] ??= {}
204
+ store.brokersByMethod[method][urlMask] = broker
203
205
  }
204
206
  }
205
207
 
206
-
207
-
208
208
  // When false, the URL will be updated with param=false
209
209
  function initPreference(param) {
210
210
  const qs = new URLSearchParams(globalThis.location?.search)
211
211
  if (!qs.has(param)) {
212
- const group = globalThis.localStorage?.getItem(param) !== 'false'
212
+ const group = globalThis.localStorage?.getItem(param) !== '0'
213
213
  if (!group) {
214
214
  const url = new URL(globalThis.location?.href)
215
- url.searchParams.set(param, false)
215
+ url.searchParams.set(param, '0')
216
216
  history.replaceState(null, '', url)
217
217
  }
218
218
  return group
219
219
  }
220
- return qs.get(param) !== 'false'
220
+ return qs.get(param) !== '0'
221
221
  }
222
222
 
223
- // When false, the URL and localStorage will have param=false
223
+ // When false, the URL and localStorage will have param='0'
224
224
  function togglePreference(param, nextVal) {
225
225
  if (nextVal)
226
226
  globalThis.localStorage?.removeItem(param)
@@ -231,39 +231,12 @@ function togglePreference(param, nextVal) {
231
231
  if (nextVal)
232
232
  url.searchParams.delete(param)
233
233
  else
234
- url.searchParams.set(param, false)
234
+ url.searchParams.set(param, '0')
235
235
  history.replaceState(null, '', url)
236
236
  }
237
237
 
238
238
 
239
239
 
240
- /**
241
- * Think of this as a way of printing a directory tree in which
242
- * the repeated folder paths are kept but styled differently.
243
- * @param {string[]} paths - sorted
244
- */
245
- export function dittoSplitPaths(paths) {
246
- const pParts = paths.map(p => p.split('/').filter(Boolean))
247
- return paths.map((p, i) => {
248
- if (i === 0)
249
- return ['', p]
250
-
251
- const prev = pParts[i - 1]
252
- const curr = pParts[i]
253
- const min = Math.min(curr.length, prev.length)
254
- let j = 0
255
- while (j < min && curr[j] === prev[j])
256
- j++
257
-
258
- if (!j) // no common dirs
259
- return ['', p]
260
-
261
- const ditto = '/' + curr.slice(0, j).join('/') + '/'
262
- return [ditto, p.slice(ditto.length)]
263
- })
264
- }
265
-
266
-
267
240
  export class BrokerRowModel {
268
241
  opts = /** @type {[key:string, label:string, selected:boolean][]} */ []
269
242
  isNew = false
@@ -1,31 +1,7 @@
1
1
  import { test } from 'node:test'
2
2
  import { deepEqual } from 'node:assert/strict'
3
3
 
4
- import { dittoSplitPaths, BrokerRowModel, t } from './app-store.js'
5
-
6
-
7
- test('dittoSplitPaths', () => {
8
- const input = [
9
- '/api/user',
10
- '/api/user/avatar',
11
- '/api/user/friends',
12
- '/api/vid',
13
- '/api/video/id',
14
- '/api/video/stats',
15
- '/v2/foo',
16
- '/v2/foo/bar'
17
- ]
18
- deepEqual(dittoSplitPaths(input), [
19
- ['', '/api/user'],
20
- ['/api/user/', 'avatar'],
21
- ['/api/user/', 'friends'],
22
- ['/api/', 'vid'],
23
- ['/api/', 'video/id'],
24
- ['/api/video/', 'stats'],
25
- ['', '/v2/foo'],
26
- ['/v2/foo/', 'bar']
27
- ])
28
- })
4
+ import { BrokerRowModel, t } from './app-store.js'
29
5
 
30
6
 
31
7
  test('BrokerRowModel', () => {
@@ -6,7 +6,7 @@
6
6
  --colorBgHeader: light-dark(#f2f2f3, #141414);
7
7
  --colorBgHeaderField: light-dark(#fff, #222);
8
8
 
9
- --colorBorder: light-dark(#e0e0e0, #323232);
9
+ --colorBorder: light-dark(#e0e0e0, #2c2c2c);
10
10
  --colorBorderActive: light-dark(#c8c8c8, #3c3c3c);
11
11
 
12
12
  --colorLabel: light-dark(#555, #aaa);
@@ -16,7 +16,7 @@
16
16
  --colorHover: light-dark(#dbedff, #062d59);
17
17
 
18
18
  --colorRed: light-dark(#da0f00, #f41606);
19
- --colorPink: light-dark(#ed206a, #f92672);
19
+ --colorPink: light-dark(#ed206a, #ff2e7a);
20
20
  --colorPurple: light-dark(#9b71e8, #ae81ff);
21
21
  --colorGreen: light-dark(#388e3c, #a6e22e);
22
22
  --colorBg4xx: light-dark(#ffedd1, #68554a);
@@ -44,6 +44,8 @@ body {
44
44
  padding: 0;
45
45
  border: 0;
46
46
  margin: 0;
47
+ letter-spacing: -0.374px;
48
+ line-height: 1.2;
47
49
  font-family: inherit;
48
50
  font-size: 100%;
49
51
  scrollbar-width: thin;
@@ -119,13 +121,8 @@ header {
119
121
  align-self: end;
120
122
  margin-right: 22px;
121
123
  margin-bottom: 3px;
122
- opacity: 94%;
123
124
  transition: opacity 240ms ease-in-out;
124
125
 
125
- &:hover {
126
- opacity: 1;
127
- }
128
-
129
126
  svg {
130
127
  width: 120px;
131
128
  pointer-events: none;
@@ -188,16 +185,22 @@ header {
188
185
  }
189
186
 
190
187
  .HelpLink {
191
- width: 24px;
192
- height: 24px;
188
+ width: 22px;
189
+ height: 22px;
193
190
  flex-shrink: 0;
194
191
  align-self: end;
192
+ margin-bottom: 3px;
195
193
  margin-left: auto;
196
194
  border-radius: 50%;
197
- fill: var(--colorLabel);
195
+ fill: var(--colorBgHeader);
196
+ background: var(--colorLabel);
198
197
 
199
198
  &:hover {
200
- fill: var(--colorAccent);
199
+ background: var(--colorAccent);
200
+ }
201
+
202
+ svg {
203
+ transform: scale(.7);
201
204
  }
202
205
  }
203
206
  }
package/src/client/app.js CHANGED
@@ -1,16 +1,15 @@
1
- import { createElement as r, t, classNames, restoreFocus, Fragment, defineClassNames } from './dom-utils.js'
1
+ import { createElement as r, t, restoreFocus, Fragment, classNames, extractClassNames } from './dom-utils.js'
2
2
 
3
3
  import { store } from './app-store.js'
4
4
  import { API } from './ApiConstants.js'
5
5
  import { Header } from './app-header.js'
6
- import { dirStructure } from './dirStructure.js'
7
6
  import { PayloadViewer, previewMock } from './app-payload-viewer.js'
8
7
  import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
9
8
 
10
9
  import CSS from './app.css' with { type: 'css' }
11
10
  document.adoptedStyleSheets.push(CSS)
12
- defineClassNames(CSS)
13
11
 
12
+ Object.assign(CSS, extractClassNames(CSS))
14
13
 
15
14
  store.onError = onError
16
15
  store.render = render
@@ -22,36 +21,36 @@ initKeyboardNavigation()
22
21
  let mounted = false
23
22
  function render() {
24
23
  restoreFocus(() => document.body.replaceChildren(App()))
25
- if (store.hasChosenLink)
26
- previewMock()
24
+ if (store.hasChosenLink) previewMock()
25
+ if (!mounted) LeftSide.$('a')?.focus()
27
26
  mounted = true
28
27
  }
29
28
 
30
-
31
- const leftSideRef = {}
32
-
33
29
  function App() {
34
- return Fragment(Header(), Main())
35
- }
36
-
37
-
38
- function Main() {
39
- return (
30
+ return Fragment(
31
+ Header(),
40
32
  r('main', null,
41
- r('div', {
42
- ref: leftSideRef,
43
- style: { width: leftSideRef.width },
44
- className: CSS.leftSide
45
- },
46
- r('div', { className: CSS.SubToolbar },
47
- GroupByMethod(),
48
- BulkSelector()),
49
- r('div', { className: CSS.Table }, MockList())),
33
+ LeftSide(),
50
34
  r('div', { className: CSS.rightSide },
51
- Resizer(leftSideRef),
35
+ Resizer(LeftSide.ref),
52
36
  PayloadViewer())))
53
37
  }
54
38
 
39
+ function LeftSide() {
40
+ return r('div', {
41
+ ref: LeftSide.ref,
42
+ style: { width: LeftSide.ref.width },
43
+ className: CSS.leftSide
44
+ },
45
+ r('div', { className: CSS.SubToolbar },
46
+ GroupByMethod(),
47
+ BulkSelector()),
48
+ r('div', { className: CSS.Table }, MockList()))
49
+ }
50
+ LeftSide.ref = { width: undefined }
51
+ LeftSide.$ = selector => LeftSide.ref.elem.querySelector(selector)
52
+ LeftSide.$$ = selector => LeftSide.ref.elem.querySelectorAll(selector)
53
+
55
54
 
56
55
  function GroupByMethod() {
57
56
  return (
@@ -91,7 +90,6 @@ function BulkSelector() {
91
90
  }
92
91
 
93
92
 
94
-
95
93
  function MockList() {
96
94
  if (!Object.keys(store.brokersByMethod).length)
97
95
  return r('div', null, t`No mocks found`)
@@ -101,14 +99,14 @@ function MockList() {
101
99
  r('div', {
102
100
  className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
103
101
  }, method),
104
- FolderGroups(store.brokersAsRowsByMethod(method))))
102
+ FolderGroups(store.folderGroupsByMethod(method))))
105
103
 
106
- return FolderGroups(store.brokersAsRowsByMethod('*'))
104
+ return FolderGroups(store.folderGroupsByMethod('*'))
107
105
  }
108
106
 
109
- function FolderGroups(bRows) {
107
+ function FolderGroups(brokersTree) {
110
108
  const res = []
111
- for (const b of dirStructure(bRows)) {
109
+ for (const b of brokersTree) {
112
110
  if (!b.children.length)
113
111
  res.push(Row(b))
114
112
  else
@@ -151,25 +149,46 @@ function Row(row) {
151
149
  key: row.key,
152
150
  className: classNames(CSS.TableRow, mounted && row.isNew && CSS.animIn)
153
151
  },
154
- store.canProxy && ProxyToggler(method, urlMask, row.proxied),
155
152
 
156
- DelayToggler({
157
- checked: row.delayed,
153
+ store.canProxy && ClickDragToggler({
154
+ className: CSS.ProxyToggler,
155
+ title: t`Proxy Toggler`,
156
+ body: CloudIcon(),
157
+ checked: row.proxied,
158
158
  commit(checked) {
159
- store.setDelayed(method, urlMask, checked)
160
- },
159
+ store.setProxied(method, urlMask, checked)
160
+ }
161
161
  }),
162
162
 
163
- StatusCodeToggler({
164
- title: row.isStatic ? t`Not Found` : t`Internal Server Error`,
165
- body: row.isStatic ? t`404` : t`500`,
166
- disabled: row.opts.length === 1 && (row.isStatic ? row.status === 404 : row.status === 500),
167
- checked: !row.proxied && (row.isStatic ? row.status === 404 : row.status === 500),
168
- commit() {
169
- store.toggleStatus(method, urlMask, row.isStatic ? 404 : 500)
163
+ ClickDragToggler({
164
+ className: CSS.DelayToggler,
165
+ title: t`Delay`,
166
+ body: TimerIcon(),
167
+ checked: row.delayed,
168
+ commit(checked) {
169
+ store.setDelayed(method, urlMask, checked)
170
170
  }
171
171
  }),
172
172
 
173
+ ClickDragToggler(
174
+ row.isStatic
175
+ ? {
176
+ className: CSS.StatusCodeToggler,
177
+ title: t`Not Found`,
178
+ body: t`404`,
179
+ disabled: row.opts.length === 1 && row.status === 404,
180
+ checked: !row.proxied && row.status === 404,
181
+ commit() { store.toggleStatus(method, urlMask, 404) }
182
+ }
183
+ : {
184
+ className: CSS.StatusCodeToggler,
185
+ title: t`Internal Server Error`,
186
+ body: t`500`,
187
+ disabled: row.opts.length === 1 && row.status === 500,
188
+ checked: !row.proxied && row.status === 500,
189
+ commit() { store.toggleStatus(method, urlMask, 500) }
190
+ }),
191
+
173
192
  !store.groupByMethod && r('span', { className: CSS.Method }, method),
174
193
 
175
194
  PreviewLink(method, urlMask, row.urlMaskDittoed),
@@ -177,15 +196,16 @@ function Row(row) {
177
196
  MockSelector(row)))
178
197
  }
179
198
 
199
+
180
200
  function renderRow(method, urlMask) {
181
201
  unChooseOld()
182
202
  const row = store.brokerAsRow(method, urlMask)
183
- const tr = leftSideRef.elem.querySelector(`.${CSS.TableRow}[key="${row.key}"]`)
203
+ const tr = LeftSide.$(`.${CSS.TableRow}[key="${row.key}"]`)
184
204
  mergeTableRow(tr, Row(row))
185
205
  previewMock()
186
206
 
187
207
  function unChooseOld() {
188
- return leftSideRef.elem.querySelector(`a.${CSS.chosen}`)
208
+ return LeftSide.$(`a.${CSS.chosen}`)
189
209
  ?.classList.remove(CSS.chosen)
190
210
  }
191
211
 
@@ -216,7 +236,6 @@ function renderRow(method, urlMask) {
216
236
  }
217
237
 
218
238
 
219
-
220
239
  function PreviewLink(method, urlMask, urlMaskDittoed) {
221
240
  function onClick(event) {
222
241
  event.preventDefault()
@@ -259,42 +278,6 @@ function MockSelector(row) {
259
278
  }
260
279
 
261
280
 
262
- function ProxyToggler(method, urlMask, checked) {
263
- return ClickDragToggler({
264
- className: CSS.ProxyToggler,
265
- title: t`Proxy Toggler`,
266
- body: CloudIcon(),
267
- checked,
268
- commit(checked) {
269
- store.setProxied(method, urlMask, checked)
270
- }
271
- })
272
- }
273
-
274
-
275
-
276
- function StatusCodeToggler({ title, body, commit, checked, disabled }) {
277
- return ClickDragToggler({
278
- title,
279
- disabled,
280
- className: CSS.StatusCodeToggler,
281
- commit,
282
- checked,
283
- body
284
- })
285
- }
286
-
287
- function DelayToggler({ checked, commit, className }) {
288
- return ClickDragToggler({
289
- checked,
290
- commit,
291
- className: classNames(CSS.DelayToggler, className),
292
- canClickDrag: true,
293
- title: t`Delay`,
294
- body: TimerIcon()
295
- })
296
- }
297
-
298
281
  function ClickDragToggler({ checked, commit, className, title, body }) {
299
282
  function onPointerEnter(event) {
300
283
  if (event.buttons === 1)
@@ -317,7 +300,7 @@ function ClickDragToggler({ checked, commit, className, title, body }) {
317
300
  return
318
301
 
319
302
  // Uncheck all other in the column.
320
- for (const elem of leftSideRef.elem.querySelectorAll(selector))
303
+ for (const elem of LeftSide.$$(selector))
321
304
  if (elem !== this && elem.checked && !elem.disabled) {
322
305
  elem.checked = false
323
306
  elem.dispatchEvent(new Event('change'))
@@ -500,7 +483,6 @@ function columnSelectors() {
500
483
  ]
501
484
  }
502
485
 
503
-
504
486
  function initKeyboardNavigation() {
505
487
  const rowSelectors = [
506
488
  ...columnSelectors(),
@@ -515,7 +497,7 @@ function initKeyboardNavigation() {
515
497
  const sel = selectorForColumnOf(pivot)
516
498
  if (sel) {
517
499
  const offset = key === 'ArrowDown' ? +1 : -1
518
- const siblings = leftSideRef.elem.querySelectorAll(sel)
500
+ const siblings = LeftSide.$$(sel)
519
501
  circularAdjacent(offset, siblings, pivot).focus()
520
502
  }
521
503
  break
@@ -0,0 +1,82 @@
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
+ /**
30
+ * @param {Partial<BrokerRowModel>[]} brokers
31
+ * @returns {Partial<BrokerRowModel>[]}
32
+ */
33
+ export function groupByFolder(brokers) {
34
+ return dfs(trie(brokers))
35
+ }
36
+
37
+ function trie(brokers) {
38
+ const root = new TrieNode()
39
+ for (const b of brokers) {
40
+ let node = root
41
+ for (const seg of b.urlMask.split('/')) { // TODO it should ignore query string
42
+ const segNode = node.getChild(seg) || new TrieNode()
43
+ node.addChild(seg, segNode)
44
+ node = segNode
45
+ }
46
+ node.brokers.push(b)
47
+ }
48
+ return root
49
+ }
50
+
51
+ class TrieNode {
52
+ #children
53
+ constructor() {
54
+ this.brokers = []
55
+ this.#children = new Map()
56
+ }
57
+ addChild(k, v) { this.#children.set(k, v) }
58
+ getChild(k) { return this.#children.get(k) }
59
+ getChildren() { return this.#children.values() }
60
+ }
61
+
62
+ /** @param {TrieNode} node */
63
+ function dfs(node) {
64
+ const childBrokers = []
65
+ for (const tnc of node.getChildren())
66
+ childBrokers.push(...dfs(tnc))
67
+
68
+ const brokers = node.brokers.length
69
+ ? [node.brokers[0], ...childBrokers, ...node.brokers.slice(1)]
70
+ : childBrokers
71
+
72
+ if (!brokers.length)
73
+ return []
74
+
75
+ const [b0, ...rest] = brokers
76
+ if (node.brokers.length || !b0.children.length) {
77
+ b0.children.push(...rest)
78
+ return [b0]
79
+ }
80
+ return brokers
81
+ }
82
+
@@ -0,0 +1,106 @@
1
+ import { test } from 'node:test'
2
+ import { deepEqual } from 'node:assert/strict'
3
+ import { groupByFolder, dittoSplitPaths } from './dir-tree.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
+
29
+
30
+ test('dirStructure', () => {
31
+ const input = [
32
+ { children: [], method: 'GET', urlMask: '/api/user' },
33
+ { children: [], method: 'GET', urlMask: '/api/user/avatar' },
34
+ { children: [], method: 'GET', urlMask: '/api/video/[id]' },
35
+ { children: [], method: 'GET', urlMask: '/index.html' },
36
+ { children: [], method: 'GET', urlMask: '/media/file-a.txt' },
37
+ { children: [], method: 'GET', urlMask: '/media/file-b.txt' },
38
+ { children: [], method: 'GET', urlMask: '/media/sub/file-aa.txt' },
39
+ { children: [], method: 'GET', urlMask: '/media/sub/file-bb.txt' },
40
+ { children: [], method: 'POST', urlMask: '/api/user' },
41
+ { children: [], method: 'POST', urlMask: '/api/user/avatar/foo' },
42
+ { children: [], method: 'PATCH', urlMask: '/api/user' }
43
+ ]
44
+
45
+
46
+ const expected = [
47
+ {
48
+ urlMask: '/api/user',
49
+ method: 'GET',
50
+ children: [
51
+ {
52
+ urlMask: '/api/user/avatar',
53
+ method: 'GET',
54
+ children: [
55
+ {
56
+ urlMask: '/api/user/avatar/foo',
57
+ method: 'POST',
58
+ children: []
59
+ }
60
+ ]
61
+ }, {
62
+ urlMask: '/api/user',
63
+ method: 'POST',
64
+ children: []
65
+ }, {
66
+ urlMask: '/api/user',
67
+ method: 'PATCH',
68
+ children: []
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ urlMask: '/api/video/[id]',
74
+ method: 'GET',
75
+ children: []
76
+ },
77
+ {
78
+ urlMask: '/index.html',
79
+ method: 'GET',
80
+ children: []
81
+ },
82
+ {
83
+ urlMask: '/media/file-a.txt',
84
+ method: 'GET',
85
+ children: [
86
+ {
87
+ urlMask: '/media/file-b.txt',
88
+ method: 'GET',
89
+ children: []
90
+ }, {
91
+ urlMask: '/media/sub/file-aa.txt',
92
+ method: 'GET',
93
+ children: [
94
+ {
95
+ urlMask: '/media/sub/file-bb.txt',
96
+ method: 'GET',
97
+ children: []
98
+ }
99
+ ]
100
+ }
101
+ ]
102
+ }
103
+ ]
104
+
105
+ deepEqual(groupByFolder(input), expected)
106
+ })
@@ -0,0 +1,74 @@
1
+ import { test } from 'node:test'
2
+ import { deepEqual, equal } from 'node:assert/strict'
3
+ import { classNames, extractClassNames } from './dom-utils.js'
4
+
5
+
6
+ test('classNames', () => equal(classNames('a', false && 'b'), 'a'))
7
+
8
+
9
+ test('extractClassNames', () => {
10
+ const cssRules = [
11
+ { cssText: '.TopLevelPascal { color: red; }' },
12
+ { cssText: '.topLevelCamel { color: blue; }' },
13
+ { cssText: '.top_level_snake { color: green; }' },
14
+ { cssText: '.top-level-kebab { color: yellow; }' },
15
+ { cssText: '.Level2Parent {\n & .level2ChildCamel { color: purple; }\n}' },
16
+ { cssText: '.level2Base {\n &.level2ModifierCamel { font-weight: bold; }\n}' },
17
+ { cssText: '.Level3Parent {\n & .level3ChildCamel {\n & .level3_grand_child_snake { color: orange; }\n}\n}' },
18
+ { cssText: '.pseudoParent {\n &:hover { background: red; }\n & .pseudoNestedChild { color: pink; }\n}' },
19
+ { cssText: '.multiClass1, .multi_class_2 { padding: 10px; }' },
20
+ { cssText: '.combParent {\n & > .combChildDirect { margin: 5px; }\n}' },
21
+ { cssText: '.siblingBase {\n & + .siblingAdjacent { border: 1px solid; }\n}' },
22
+ { cssText: '@media (max-width: 768px) {\n .mediaQueryClass {\n & .mqNestedChild { display: none; }\n}\n}' },
23
+ { cssText: '.class_with_123_numbers { color: cyan; }' },
24
+ { cssText: '._privateStyleClass { opacity: 0.5; }' },
25
+ { cssText: '.stringTest { content: ".shouldNotBeExtracted"; background: url(".alsoIgnored"); }' },
26
+ { cssText: '.ComplexRoot {\n & .level2-kebab {\n &.level2ModCamel { color: red; }\n & .level3_snake {\n & .level4PascalChild { color: blue; }\n}\n}\n}' }
27
+ ]
28
+
29
+ const expected = {
30
+ TopLevelPascal: null,
31
+ topLevelCamel: null,
32
+ top_level_snake: null,
33
+ 'top-level-kebab': null,
34
+
35
+ Level2Parent: null,
36
+ level2ChildCamel: null,
37
+ level2Base: null,
38
+ level2ModifierCamel: null,
39
+
40
+ Level3Parent: null,
41
+ level3ChildCamel: null,
42
+ level3_grand_child_snake: null,
43
+
44
+ pseudoParent: null,
45
+ pseudoNestedChild: null,
46
+
47
+ multiClass1: null,
48
+ multi_class_2: null,
49
+
50
+ combParent: null,
51
+ combChildDirect: null,
52
+
53
+ siblingBase: null,
54
+ siblingAdjacent: null,
55
+
56
+ mediaQueryClass: null,
57
+ mqNestedChild: null,
58
+
59
+ class_with_123_numbers: null,
60
+ _privateStyleClass: null,
61
+
62
+ stringTest: null,
63
+
64
+ ComplexRoot: null,
65
+ 'level2-kebab': null,
66
+ level2ModCamel: null,
67
+ level3_snake: null,
68
+ level4PascalChild: null
69
+ }
70
+ for (const k of Object.keys(expected))
71
+ expected[k] = k
72
+
73
+ deepEqual(extractClassNames({ cssRules }), expected)
74
+ })
@@ -70,12 +70,9 @@ function selectorFor(elem) {
70
70
  }
71
71
 
72
72
 
73
- export function defineClassNames(sheet) {
74
- Object.assign(sheet, extractClassNames(sheet))
75
- }
76
-
77
73
  export function extractClassNames({ cssRules }) {
78
74
  // Class names must begin with _ or a letter, then it can have numbers and hyphens
75
+ // TODO think about tag.className selectors
79
76
  const reClassName = /(?:^|[\s,{>])&?\s*\.([a-zA-Z_][\w-]*)/g
80
77
  const cNames = {}
81
78
  let match
@@ -85,3 +82,73 @@ export function extractClassNames({ cssRules }) {
85
82
  return cNames
86
83
  }
87
84
 
85
+
86
+ export class QueryParamBool {
87
+ constructor(param) {
88
+ this.param = param
89
+ this.value = this.#init()
90
+ }
91
+
92
+ #init() {
93
+ const qs = new URLSearchParams(globalThis.location?.search)
94
+ if (qs.has(this.param))
95
+ return qs.get(this.param) !== '0'
96
+ const stored = globalThis.localStorage?.getItem(this.param) !== '0'
97
+ if (!stored)
98
+ this.#applyToUrl(false)
99
+ return stored
100
+ }
101
+
102
+ toggle() {
103
+ this.value = !this.value
104
+ if (this.value)
105
+ globalThis.localStorage?.removeItem(this.param)
106
+ else
107
+ globalThis.localStorage?.setItem(this.param, '0')
108
+ this.#applyToUrl(this.value)
109
+ }
110
+
111
+ #applyToUrl(nextVal) {
112
+ const url = new URL(globalThis.location?.href)
113
+ if (nextVal)
114
+ url.searchParams.delete(this.param)
115
+ else
116
+ url.searchParams.set(this.param, '0')
117
+ history.replaceState(null, '', url)
118
+ }
119
+ }
120
+
121
+
122
+ export class LocalStorageSet {
123
+ constructor(key) {
124
+ this.key = key
125
+ this.value = this.#parse()
126
+ }
127
+
128
+ add(item) {
129
+ this.value.add(item)
130
+ this.#persist()
131
+ }
132
+
133
+ delete(item) {
134
+ this.value.delete(item)
135
+ this.#persist()
136
+ }
137
+
138
+ has(item) {
139
+ return this.value.has(item)
140
+ }
141
+
142
+ #parse() {
143
+ try {
144
+ return new Set(JSON.parse(globalThis.localStorage?.getItem(this.key) || '[]'))
145
+ }
146
+ catch {
147
+ return new Set()
148
+ }
149
+ }
150
+
151
+ #persist() {
152
+ globalThis.localStorage?.setItem(this.key, JSON.stringify([...this.value]))
153
+ }
154
+ }
@@ -1,35 +1,25 @@
1
1
  import { createSvgElement as s } from './dom-utils.js'
2
2
 
3
3
 
4
- export function Logo() {
5
- return (
6
- s('svg', { viewBox: '0 0 556 100' },
7
- s('path', { d: 'm13.75 1.8789c-5.9487 0.19352-10.865 4.5652-11.082 11.686v81.445c-1e-7 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-64.982c0.02794-3.4488 3.0988-3.5551 4.2031-1.1562l16.615 59.059c1.4393 5.3711 5.1083 7.9633 8.7656 7.9473 3.6573 0.01603 7.3263-2.5762 8.7656-7.9473l16.615-59.059c1.1043-2.3989 4.1752-2.2925 4.2031 1.1562v64.982c0 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-81.445c-0.17732-7.0807-5.1334-11.492-11.082-11.686-5.9487-0.19352-12.652 3.8309-15.609 13.619l-15.686 57.334-15.686-57.334c-2.9569-9.7882-9.6607-13.813-15.609-13.619zm239.19 0.074219c-2.216 0-4 1.784-4 4v89.057c0 2.216 1.784 4 4 4h4.793c2.216 0 3.9868-1.784 4-4l0.10644-17.94c0.0734-0.07237 12.175-13.75 12.175-13.75 5.6772 11.091 11.404 22.158 17.113 33.232 1.0168 1.9689 3.4217 2.7356 5.3906 1.7188l4.2578-2.1992c1.9689-1.0168 2.7356-3.4217 1.7188-5.3906-6.4691-12.585-12.958-25.16-19.442-37.738l17.223-19.771c1.4555-1.671 1.2803-4.189-0.39062-5.6445l-3.6133-3.1465c-0.73105-0.63679-1.6224-0.96212-2.5176-0.98633-1.151-0.03113-2.3063 0.43508-3.125 1.375l-28.896 33.174v-51.99c0-2.216-1.784-4-4-4zm-58.255 23.316c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312l-0.125-7.8457c0-2.216-1.784-4-4-4h-4.6524c-2.216 0-4 1.784-4 4l3e-3 6.7888c3e-3 3.8063-1.5601 9.3694-8.4716 9.3694h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.6937 0 8.3697 5.2207 8.4687 11.828v2.2207c0 2.216 1.784 4 4 4h4.6524c2.216 0 4-1.784 4-4l0.125-5.7363c0-10.699-8.6117-19.312-19.311-19.312zm-72.182 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }),
8
- s('path', { opacity: 0.6, d: 'm331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z' }),
9
- s('path', { d: 'm392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' })))
10
- }
4
+ export const Logo = () =>
5
+ s('svg', { viewBox: '0 0 556 100' },
6
+ s('path', { d: 'm13.75 1.8789c-5.9487 0.19352-10.865 4.5652-11.082 11.686v81.445c-1e-7 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-64.982c0.02794-3.4488 3.0988-3.5551 4.2031-1.1562l16.615 59.059c1.4393 5.3711 5.1083 7.9633 8.7656 7.9473 3.6573 0.01603 7.3263-2.5762 8.7656-7.9473l16.615-59.059c1.1043-2.3989 4.1752-2.2925 4.2031 1.1562v64.982c0 2.216 1.784 4 4 4h4.793c2.216 0 4-1.784 4-4v-81.445c-0.17732-7.0807-5.1334-11.492-11.082-11.686-5.9487-0.19352-12.652 3.8309-15.609 13.619l-15.686 57.334-15.686-57.334c-2.9569-9.7882-9.6607-13.813-15.609-13.619zm239.19 0.074219c-2.216 0-4 1.784-4 4v89.057c0 2.216 1.784 4 4 4h4.793c2.216 0 3.9868-1.784 4-4l0.10644-17.94c0.0734-0.07237 12.175-13.75 12.175-13.75 5.6772 11.091 11.404 22.158 17.113 33.232 1.0168 1.9689 3.4217 2.7356 5.3906 1.7188l4.2578-2.1992c1.9689-1.0168 2.7356-3.4217 1.7188-5.3906-6.4691-12.585-12.958-25.16-19.442-37.738l17.223-19.771c1.4555-1.671 1.2803-4.189-0.39062-5.6445l-3.6133-3.1465c-0.73105-0.63679-1.6224-0.96212-2.5176-0.98633-1.151-0.03113-2.3063 0.43508-3.125 1.375l-28.896 33.174v-51.99c0-2.216-1.784-4-4-4zm-58.255 23.316c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312l-0.125-7.8457c0-2.216-1.784-4-4-4h-4.6524c-2.216 0-4 1.784-4 4l3e-3 6.7888c3e-3 3.8063-1.5601 9.3694-8.4716 9.3694h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.6937 0 8.3697 5.2207 8.4687 11.828v2.2207c0 2.216 1.784 4 4 4h4.6524c2.216 0 4-1.784 4-4l0.125-5.7363c0-10.699-8.6117-19.312-19.311-19.312zm-72.182 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }),
7
+ s('path', { opacity: 0.85, fill: 'currentColor', d: 'm331.9 25.27c-10.699 0-19.312 8.6137-19.312 19.312v4.3682c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-0.20414c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v7.0148h-28.059c-10.699 0-19.312 8.6117-19.312 19.311v4.0477c0 10.699 8.6137 19.313 19.312 19.312h17.812c2.216-1e-6 4-1.784 4-4v-4.7715c0-2.216-1.784-4-4-4h-13.648c-6.9115-2e-5 -12.477-1.5651-12.477-8.5649 0-6.9998 5.5651-8.5629 12.477-8.5629h23.895v25.897c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312z' }),
8
+ s('path', { d: 'm392.75 1.373c-2.216 0-4 1.784-4 4v18.043h-5.3086c-2.216 0-4 1.784-4 4v4.793c0 2.216 1.784 4 4 4h5.3086v51.398c0 6.1465 3.7064 10.823 9.232 10.823h16.531c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-12.97v-49.428h9.8711c2.216 0 4-1.784 4-4v-4.793c0-2.216-1.784-4-4-4h-9.8711v-18.043c0-2.216-1.784-4-4-4zm122.96 23.896c-10.699 0-19.312 8.6137-19.312 19.312v49.812c0 2.216 1.784 4 4 4h4.7715c2.216 0 4-1.784 4-4v-45.648c0-6.9115 1.5651-12.477 8.4766-12.477h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v45.684c0 2.216 1.784 4 4 4h4.7715c2.216-1e-6 4-1.784 4-4v-49.848c0-10.699-8.6117-19.312-19.311-19.312zm-69.999 0c-10.699 0-19.312 8.6137-19.312 19.312v34.535c0 10.699 8.6137 19.312 19.312 19.312h19.717c10.699 0 19.311-8.6137 19.311-19.312v-34.535c0-10.699-8.6117-19.312-19.311-19.312zm1.9356 11h15.846c6.9115 0 8.4746 5.5651 8.4746 12.477v26.209c0 6.9115-1.5631 12.475-8.4746 12.475h-15.846c-6.9115 0-8.4766-5.5631-8.4766-12.475v-26.209c0-6.9115 1.5651-12.477 8.4766-12.477z' }))
11
9
 
12
- export function TimerIcon() {
13
- return (
14
- s('svg', { viewBox: '0 0 24 24' },
15
- s('path', { d: 'm11 5.6 0.14 7.2 6 3.7' })))
16
- }
10
+ export const TimerIcon = () =>
11
+ s('svg', { viewBox: '0 0 24 24' },
12
+ s('path', { d: 'm11 5.6 0.14 7.2 6 3.7' }))
17
13
 
18
- export function CloudIcon() {
19
- return (
20
- s('svg', { viewBox: '0 0 24 24' },
21
- s('path', { d: 'm6.1 8.9c0.98-2.3 3.3-3.9 6-3.9 3.3-2e-7 6 2.5 6.4 5.7 0.018 0.15 0.024 0.18 0.026 0.23 0.0016 0.037 8.2e-4 0.084 0.098 0.14 0.097 0.054 0.29 0.05 0.48 0.05 2.2 0 4 1.8 4 4s-1.8 4-4 4c-4-0.038-9-0.038-13-0.018-2.8 0-5-2.2-5-5-2.2e-7 -2.8 2.2-5 5-5 2.8 2e-7 5 2.2 5 5' }),
22
- s('path', { d: 'm6.1 9.1c2.8 0 5 2.3 5 5' })))
23
- }
14
+ export const CloudIcon = () =>
15
+ s('svg', { viewBox: '0 0 24 24' },
16
+ s('path', { d: 'm6.1 8.9c0.98-2.3 3.3-3.9 6-3.9 3.3-2e-7 6 2.5 6.4 5.7 0.018 0.15 0.024 0.18 0.026 0.23 0.0016 0.037 8.2e-4 0.084 0.098 0.14 0.097 0.054 0.29 0.05 0.48 0.05 2.2 0 4 1.8 4 4s-1.8 4-4 4c-4-0.038-9-0.038-13-0.018-2.8 0-5-2.2-5-5-2.2e-7 -2.8 2.2-5 5-5 2.8 2e-7 5 2.2 5 5' }),
17
+ s('path', { d: 'm6.1 9.1c2.8 0 5 2.3 5 5' }))
24
18
 
25
- export function HelpIcon() {
26
- return (
27
- s('svg', { viewBox: '0 0 24 24' },
28
- s('path', { d: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m1 17h-2v-2h2zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25' })))
29
- }
19
+ export const HelpIcon = () =>
20
+ s('svg', { viewBox: '0 0 24 24' },
21
+ s('path', { d: 'M11.07 12.85c.77-1.39 2.25-2.21 3.11-3.44.91-1.29.4-3.7-2.18-3.7-1.69 0-2.52 1.28-2.87 2.34L6.54 6.96C7.25 4.83 9.18 3 11.99 3c2.35 0 3.96 1.07 4.78 2.41.7 1.15 1.11 3.3.03 4.9-1.2 1.77-2.35 2.31-2.97 3.45-.25.46-.35.76-.35 2.24h-2.89c-.01-.78-.13-2.05.48-3.15M14 20c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2' }))
30
22
 
31
- export function ChevronDownIcon() {
32
- return (
33
- s('svg', { viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', 'stroke-width': '2' },
34
- s('path', { d: 'M6 9l6 6 6-6' })))
35
- }
23
+ export const ChevronDownIcon = () =>
24
+ s('svg', { viewBox: '0 0 24 24', stroke: 'currentColor', fill: 'none', 'stroke-width': '2' },
25
+ s('path', { d: 'M6 9l6 6 6-6' }))
@@ -10,6 +10,7 @@ import { IncomingMessage, BodyReaderError, hasControlChars } from './utils/HttpI
10
10
 
11
11
  import { API } from '../client/ApiConstants.js'
12
12
 
13
+ import { cookie } from './cookie.js'
13
14
  import { config, setup } from './config.js'
14
15
  import { apiPatchReqs, apiGetReqs } from './Api.js'
15
16
 
@@ -24,6 +25,7 @@ import { watchMocksDir } from './Watcher.js'
24
25
  export function Mockaton(options) {
25
26
  return new Promise((resolve, reject) => {
26
27
  setup(options)
28
+ cookie.init(config.cookies)
27
29
  mockBrokerCollection.init()
28
30
 
29
31
  if (config.watcherEnabled) {
@@ -35,7 +35,7 @@ export function init() {
35
35
 
36
36
  /** @returns {boolean} registered */
37
37
  export function registerMock(file, isFromWatcher = false) {
38
- if (brokerByFilename(file)?.hasMock(file) ||
38
+ if (brokerByFilename(file)?.hasMock(file) ||
39
39
  !isFileAllowed(basename(file)))
40
40
  return false
41
41
 
@@ -1,83 +0,0 @@
1
- class TrieNode {
2
- constructor() {
3
- this.items = []
4
- this.kids = new Map()
5
- }
6
- }
7
-
8
- // TODO it should ignore query string
9
-
10
- /**
11
- * @typedef {{method: string, urlMask:string, children: BrokerLite[]}} BrokerLite
12
- * @param {BrokerLite[]} brokers
13
- * @returns {BrokerLite[]}
14
- */
15
- export function dirStructure(brokers) {
16
- const root = new TrieNode()
17
-
18
- for (let b of brokers) {
19
- let curr = root
20
- for (const seg of b.urlMask.split('/').filter(Boolean)) {
21
- if (!curr.kids.has(seg))
22
- curr.kids.set(seg, new TrieNode())
23
- curr = curr.kids.get(seg)
24
- }
25
- curr.items.push(b)
26
- }
27
-
28
- const result = []
29
- for (const child of root.kids.values())
30
- result.push(...convertNode(child))
31
-
32
- if (root.items.length) {
33
- const elems = [root.items[0], ...result]
34
- for (let i = 1; i < root.items.length; i++)
35
- elems.push(root.items[i])
36
-
37
- const parentNode = elems[0]
38
- for (let i = 1; i < elems.length; i++)
39
- parentNode.children.push(elems[i])
40
-
41
- return [parentNode]
42
- }
43
-
44
- return result
45
- }
46
-
47
- /**
48
- * Recursively converts a TrieNode into a flattened, nested array of objects.
49
- * Flattens the tree by having the first available route at a given directory level
50
- * act as the parent wrapper for the remaining items and subdirectories.
51
- *
52
- * @param {TrieNode} node
53
- * @returns {BrokerLite[]}
54
- */
55
- function convertNode(node) {
56
- const childNodes = []
57
- for (const child of node.kids.values())
58
- childNodes.push(...convertNode(child))
59
-
60
- const elems = []
61
- if (node.items.length) {
62
- elems.push(node.items[0])
63
- elems.push(...childNodes)
64
- for (let i = 1; i < node.items.length; i++)
65
- elems.push(node.items[i])
66
- }
67
- else
68
- elems.push(...childNodes)
69
-
70
- if (!elems.length)
71
- return []
72
-
73
- const parentNode = elems[0]
74
-
75
- if (node.items.length || !parentNode.children.length) {
76
- for (let i = 1; i < elems.length; i++)
77
- parentNode.children.push(elems[i])
78
- return [parentNode]
79
- }
80
-
81
- return elems
82
- }
83
-
@@ -1,86 +0,0 @@
1
- import { test } from 'node:test'
2
- import { deepEqual } from 'node:assert/strict'
3
- import { dirStructure } from './dirStructure.js'
4
-
5
-
6
- const input = [
7
- { children: [], method: 'GET', urlMask: '/api/user' },
8
- { children: [], method: 'GET', urlMask: '/api/user/avatar' },
9
- { children: [], method: 'GET', urlMask: '/api/video/[id]' },
10
- { children: [], method: 'GET', urlMask: '/index.html' },
11
- { children: [], method: 'GET', urlMask: '/media/file-a.txt' },
12
- { children: [], method: 'GET', urlMask: '/media/file-b.txt' },
13
- { children: [], method: 'GET', urlMask: '/media/sub/file-aa.txt' },
14
- { children: [], method: 'GET', urlMask: '/media/sub/file-bb.txt' },
15
- { children: [], method: 'POST', urlMask: '/api/user' },
16
- { children: [], method: 'POST', urlMask: '/api/user/avatar/foo' },
17
- { children: [], method: 'PATCH', urlMask: '/api/user' }
18
- ]
19
-
20
-
21
- const expected = [
22
- {
23
- urlMask: '/api/user',
24
- method: 'GET',
25
- children: [
26
- {
27
- urlMask: '/api/user/avatar',
28
- method: 'GET',
29
- children: [
30
- {
31
- urlMask: '/api/user/avatar/foo',
32
- method: 'POST',
33
- children: []
34
- }
35
- ]
36
- },
37
- {
38
- urlMask: '/api/user',
39
- method: 'POST',
40
- children: []
41
- },
42
- {
43
- urlMask: '/api/user',
44
- method: 'PATCH',
45
- children: []
46
- }
47
- ]
48
- },
49
- {
50
- urlMask: '/api/video/[id]',
51
- method: 'GET',
52
- children: []
53
- },
54
- {
55
- urlMask: '/index.html',
56
- method: 'GET',
57
- children: []
58
- },
59
- {
60
- urlMask: '/media/file-a.txt',
61
- method: 'GET',
62
- children: [
63
- {
64
- urlMask: '/media/file-b.txt',
65
- method: 'GET',
66
- children: []
67
- },
68
- {
69
- urlMask: '/media/sub/file-aa.txt',
70
- method: 'GET',
71
- children: [
72
- {
73
- urlMask: '/media/sub/file-bb.txt',
74
- method: 'GET',
75
- children: []
76
- }
77
- ]
78
- }
79
- ]
80
- }
81
- ]
82
-
83
- test('acceptance', () => {
84
- deepEqual(dirStructure(input), expected)
85
- })
86
-
@@ -1,76 +0,0 @@
1
- import { test } from 'node:test'
2
- import { deepEqual, equal } from 'node:assert/strict'
3
- import { extractClassNames, classNames } from './dom-utils.js'
4
-
5
-
6
- test('classNames', () => {
7
- equal(classNames('a', false && 'b'), 'a')
8
- })
9
-
10
-
11
- const cssRules = [
12
- { cssText: '.TopLevelPascal { color: red; }' },
13
- { cssText: '.topLevelCamel { color: blue; }' },
14
- { cssText: '.top_level_snake { color: green; }' },
15
- { cssText: '.top-level-kebab { color: yellow; }' },
16
- { cssText: '.Level2Parent {\n & .level2ChildCamel { color: purple; }\n}' },
17
- { cssText: '.level2Base {\n &.level2ModifierCamel { font-weight: bold; }\n}' },
18
- { cssText: '.Level3Parent {\n & .level3ChildCamel {\n & .level3_grand_child_snake { color: orange; }\n}\n}' },
19
- { cssText: '.pseudoParent {\n &:hover { background: red; }\n & .pseudoNestedChild { color: pink; }\n}' },
20
- { cssText: '.multiClass1, .multi_class_2 { padding: 10px; }' },
21
- { cssText: '.combParent {\n & > .combChildDirect { margin: 5px; }\n}' },
22
- { cssText: '.siblingBase {\n & + .siblingAdjacent { border: 1px solid; }\n}' },
23
- { cssText: '@media (max-width: 768px) {\n .mediaQueryClass {\n & .mqNestedChild { display: none; }\n}\n}' },
24
- { cssText: '.class_with_123_numbers { color: cyan; }' },
25
- { cssText: '._privateStyleClass { opacity: 0.5; }' },
26
- { cssText: '.stringTest { content: ".shouldNotBeExtracted"; background: url(".alsoIgnored"); }' },
27
- { cssText: '.ComplexRoot {\n & .level2-kebab {\n &.level2ModCamel { color: red; }\n & .level3_snake {\n & .level4PascalChild { color: blue; }\n}\n}\n}' }
28
- ]
29
-
30
- const expected = {
31
- TopLevelPascal: null,
32
- topLevelCamel: null,
33
- top_level_snake: null,
34
- 'top-level-kebab': null,
35
-
36
- Level2Parent: null,
37
- level2ChildCamel: null,
38
- level2Base: null,
39
- level2ModifierCamel: null,
40
-
41
- Level3Parent: null,
42
- level3ChildCamel: null,
43
- level3_grand_child_snake: null,
44
-
45
- pseudoParent: null,
46
- pseudoNestedChild: null,
47
-
48
- multiClass1: null,
49
- multi_class_2: null,
50
-
51
- combParent: null,
52
- combChildDirect: null,
53
-
54
- siblingBase: null,
55
- siblingAdjacent: null,
56
-
57
- mediaQueryClass: null,
58
- mqNestedChild: null,
59
-
60
- class_with_123_numbers: null,
61
- _privateStyleClass: null,
62
-
63
- stringTest: null,
64
-
65
- ComplexRoot: null,
66
- 'level2-kebab': null,
67
- level2ModCamel: null,
68
- level3_snake: null,
69
- level4PascalChild: null
70
- }
71
- for (const k of Object.keys(expected))
72
- expected[k] = k
73
-
74
-
75
- test('extracts', () => deepEqual(extractClassNames({ cssRules }), expected))
76
-