@clidey/dory 0.30.0 → 0.33.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/bin/dist/dory.js +44 -20
- package/bin/dist/dory.js.map +1 -1
- package/index.html +54 -0
- package/package.json +9 -4
- package/src/app.css +5 -0
- package/src/app.tsx +13 -0
- package/src/components/dropdown.tsx +53 -0
- package/src/components/hooks.tsx +36 -0
- package/src/components/loading.tsx +21 -0
- package/src/components/mdx-provider.tsx +13 -0
- package/src/components/navigation.tsx +58 -0
- package/src/components/notification.tsx +13 -0
- package/src/components/routes.tsx +92 -0
- package/src/components/search-index.ts +43 -0
- package/src/components/search.tsx +448 -0
- package/src/components/store.ts +216 -0
- package/src/config/mdx.js +12 -0
- package/src/config/mdx.ts +12 -0
- package/src/embed/loader.ts +200 -0
- package/src/embed/types.ts +54 -0
- package/src/embed/widget-iframe.ts +302 -0
- package/src/main.tsx +22 -0
- package/src/mdx/accordion.tsx +41 -0
- package/src/mdx/api-playground.tsx +659 -0
- package/src/mdx/api.tsx +354 -0
- package/src/mdx/async-api.tsx +353 -0
- package/src/mdx/callout.tsx +64 -0
- package/src/mdx/card.tsx +54 -0
- package/src/mdx/code.tsx +289 -0
- package/src/mdx/expandable.tsx +36 -0
- package/src/mdx/fence.tsx +74 -0
- package/src/mdx/icon.tsx +73 -0
- package/src/mdx/latex.tsx +26 -0
- package/src/mdx/list.tsx +7 -0
- package/src/mdx/mdx.tsx +47 -0
- package/src/mdx/mermaid.tsx +147 -0
- package/src/mdx/open-api.tsx +311 -0
- package/src/mdx/processor.ts +174 -0
- package/src/mdx/source.tsx +58 -0
- package/src/mdx/step.tsx +51 -0
- package/src/mdx/table.tsx +20 -0
- package/src/mdx/tag.tsx +53 -0
- package/src/mdx/text.tsx +99 -0
- package/src/mdx/websocket-playground.tsx +824 -0
- package/src/plugins/docs-assets.ts +79 -0
- package/src/plugins/frontmatter-dev-server.ts +158 -0
- package/src/plugins/frontmatter-generator.ts +157 -0
- package/src/plugins/html-files-middleware.ts +108 -0
- package/src/plugins/html-metadata-injector.ts +76 -0
- package/src/plugins/llm-txt-dev-server.ts +183 -0
- package/src/plugins/llm-txt-generator.ts +186 -0
- package/src/plugins/sanitize.ts +163 -0
- package/src/plugins/unified-mdx-plugin.ts +35 -0
- package/src/sdk/index.ts +89 -0
- package/src/sdk/react.tsx +139 -0
- package/src/sdk/vue.ts +107 -0
- package/src/theme.scss +72 -0
- package/src/types/config.ts +89 -0
- package/src/ui/header.tsx +96 -0
- package/src/ui/layout.tsx +237 -0
- package/src/ui/mobile-navigation.tsx +72 -0
- package/src/ui/prev-next-link.tsx +74 -0
- package/src/ui/table-of-content.tsx +167 -0
- package/src/utils/colors.ts +54 -0
- package/src/utils/fonts.ts +23 -0
- package/src/utils/functions.ts +12 -0
- package/src/vite-env.d.ts +1 -0
- package/vite.config.embed-loader.ts +28 -0
- package/vite.config.embed-widget.ts +31 -0
- package/vite.config.ts +45 -0
package/src/mdx/api.tsx
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
createContext,
|
|
6
|
+
isValidElement,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import { Tag } from './tag'
|
|
13
|
+
import { CheckIcon } from 'lucide-react'
|
|
14
|
+
import { Button, Card } from '@clidey/ux'
|
|
15
|
+
|
|
16
|
+
const languageNames: Record<string, string> = {
|
|
17
|
+
js: 'JavaScript',
|
|
18
|
+
ts: 'TypeScript',
|
|
19
|
+
javascript: 'JavaScript',
|
|
20
|
+
typescript: 'TypeScript',
|
|
21
|
+
php: 'PHP',
|
|
22
|
+
python: 'Python',
|
|
23
|
+
ruby: 'Ruby',
|
|
24
|
+
go: 'Go',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getPanelTitle({
|
|
28
|
+
title,
|
|
29
|
+
language,
|
|
30
|
+
}: {
|
|
31
|
+
title?: string
|
|
32
|
+
language?: string
|
|
33
|
+
}) {
|
|
34
|
+
if (title) {
|
|
35
|
+
return title
|
|
36
|
+
}
|
|
37
|
+
if (language && language in languageNames) {
|
|
38
|
+
return languageNames[language]
|
|
39
|
+
}
|
|
40
|
+
return 'API'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ClipboardIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
44
|
+
return (
|
|
45
|
+
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
|
46
|
+
<path
|
|
47
|
+
strokeWidth="0"
|
|
48
|
+
d="M5.5 13.5v-5a2 2 0 0 1 2-2l.447-.894A2 2 0 0 1 9.737 4.5h.527a2 2 0 0 1 1.789 1.106l.447.894a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2Z"
|
|
49
|
+
/>
|
|
50
|
+
<path
|
|
51
|
+
fill="none"
|
|
52
|
+
strokeLinejoin="round"
|
|
53
|
+
d="M12.5 6.5a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2m5 0-.447-.894a2 2 0 0 0-1.79-1.106h-.527a2 2 0 0 0-1.789 1.106L7.5 6.5m5 0-1 1h-3l-1-1"
|
|
54
|
+
/>
|
|
55
|
+
</svg>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CopyButton({ code }: { code: string }) {
|
|
60
|
+
const [copyCount, setCopyCount] = useState(0)
|
|
61
|
+
const copied = copyCount > 0
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (copyCount > 0) {
|
|
65
|
+
const timeout = setTimeout(() => setCopyCount(0), 1000)
|
|
66
|
+
return () => {
|
|
67
|
+
clearTimeout(timeout)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, [copyCount])
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Button
|
|
74
|
+
className={classNames(
|
|
75
|
+
'group/button absolute top-3.5 right-4 overflow-hidden rounded-full py-1 pr-3 pl-2 text-2xs font-medium opacity-0 backdrop-blur-sm transition group-hover:opacity-100 focus:opacity-100',
|
|
76
|
+
copied
|
|
77
|
+
? 'bg-sky-400/10 ring-1 ring-sky-400/20 ring-inset'
|
|
78
|
+
: 'bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5',
|
|
79
|
+
)}
|
|
80
|
+
onClick={() => {
|
|
81
|
+
window.navigator.clipboard.writeText(code).then(() => {
|
|
82
|
+
setCopyCount((count) => count + 1)
|
|
83
|
+
})
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<span
|
|
87
|
+
aria-hidden={copied}
|
|
88
|
+
className={classNames(
|
|
89
|
+
'pointer-events-none flex items-center gap-0.5 text-zinc-400 transition duration-300',
|
|
90
|
+
copied && '-translate-y-1.5 opacity-0',
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
<ClipboardIcon className="h-4 w-4 fill-zinc-500/20 stroke-zinc-500 transition-colors group-hover/button:stroke-zinc-400" />
|
|
94
|
+
</span>
|
|
95
|
+
<span
|
|
96
|
+
aria-hidden={!copied}
|
|
97
|
+
className={classNames(
|
|
98
|
+
'pointer-events-none absolute inset-0 flex items-center justify-center text-sky-400 transition duration-300',
|
|
99
|
+
!copied && 'translate-y-1.5 opacity-0',
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
<CheckIcon className="h-4 w-4" />
|
|
103
|
+
</span>
|
|
104
|
+
</Button>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function APIPanelHeader({ tag, label }: { tag?: string; label?: string }) {
|
|
109
|
+
if (!tag && !label) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="flex h-9 items-center gap-2 px-4">
|
|
115
|
+
{tag && (
|
|
116
|
+
<div className="dark flex">
|
|
117
|
+
<Tag variant="small">{tag}</Tag>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
{tag && label && (
|
|
121
|
+
<span className="h-0.5 w-0.5 rounded-full bg-zinc-500" />
|
|
122
|
+
)}
|
|
123
|
+
{label && (
|
|
124
|
+
<span className="font-mono text-xs text-zinc-400">{label}</span>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function APIPanel({
|
|
131
|
+
children,
|
|
132
|
+
tag,
|
|
133
|
+
label,
|
|
134
|
+
code,
|
|
135
|
+
}: {
|
|
136
|
+
children: React.ReactNode
|
|
137
|
+
tag?: string
|
|
138
|
+
label?: string
|
|
139
|
+
code?: string
|
|
140
|
+
}) {
|
|
141
|
+
const child = Children.only(children)
|
|
142
|
+
|
|
143
|
+
if (isValidElement(child)) {
|
|
144
|
+
// @ts-expect-error child.props is not typed
|
|
145
|
+
tag = child.props.tag ?? tag
|
|
146
|
+
// @ts-expect-error child.props is not typed
|
|
147
|
+
label = child.props.label ?? label
|
|
148
|
+
// @ts-expect-error child.props is not typed
|
|
149
|
+
code = child.props.code ?? code
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="group dark:bg-white/2.5">
|
|
154
|
+
<APIPanelHeader tag={tag} label={label} />
|
|
155
|
+
<div className="relative">
|
|
156
|
+
<pre className="overflow-x-auto p-4 text-xs">{children}</pre>
|
|
157
|
+
{/* @ts-expect-error code is not typed */}
|
|
158
|
+
<CopyButton code={code} />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function APIGroupHeader({
|
|
165
|
+
title,
|
|
166
|
+
children,
|
|
167
|
+
selectedIndex,
|
|
168
|
+
}: {
|
|
169
|
+
title: string
|
|
170
|
+
children: React.ReactNode
|
|
171
|
+
selectedIndex: number
|
|
172
|
+
}) {
|
|
173
|
+
const hasTabs = Children.count(children) > 1
|
|
174
|
+
|
|
175
|
+
if (!title && !hasTabs) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="flex min-h-[calc(--spacing(12)+1px)] flex-wrap items-start gap-x-4 px-4 pt-2">
|
|
181
|
+
{title && (
|
|
182
|
+
<h3 className="mr-auto pt-3 text-xs font-semibold">
|
|
183
|
+
{title}
|
|
184
|
+
</h3>
|
|
185
|
+
)}
|
|
186
|
+
{hasTabs && (
|
|
187
|
+
<TabList className="-mb-px flex gap-4 text-xs font-medium">
|
|
188
|
+
{Children.map(children, (child, childIndex) => (
|
|
189
|
+
<Tab
|
|
190
|
+
className={classNames(
|
|
191
|
+
'pt-2 transition data-selected:not-data-focus:outline-hidden',
|
|
192
|
+
childIndex === selectedIndex
|
|
193
|
+
? 'border-sky-500 text-sky-400'
|
|
194
|
+
: 'border-transparent text-zinc-400 hover:text-zinc-300',
|
|
195
|
+
)}
|
|
196
|
+
>
|
|
197
|
+
{/* @ts-expect-error child.props is not typed */}
|
|
198
|
+
{getPanelTitle(isValidElement(child) ? child.props : {})}
|
|
199
|
+
</Tab>
|
|
200
|
+
))}
|
|
201
|
+
</TabList>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function APIGroupPanels({
|
|
208
|
+
children,
|
|
209
|
+
...props
|
|
210
|
+
}: React.ComponentPropsWithoutRef<typeof APIPanel>) {
|
|
211
|
+
const hasTabs = Children.count(children) > 1
|
|
212
|
+
|
|
213
|
+
if (hasTabs) {
|
|
214
|
+
return (
|
|
215
|
+
<TabPanels>
|
|
216
|
+
{Children.map(children, (child) => (
|
|
217
|
+
<TabPanel>
|
|
218
|
+
<APIPanel {...props}>{child}</APIPanel>
|
|
219
|
+
</TabPanel>
|
|
220
|
+
))}
|
|
221
|
+
</TabPanels>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return <APIPanel {...props}>{children}</APIPanel>
|
|
226
|
+
}
|
|
227
|
+
function usePreventLayoutShift() {
|
|
228
|
+
const positionRef = useRef<HTMLElement>(null)
|
|
229
|
+
const rafRef = useRef<number>()
|
|
230
|
+
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
return () => {
|
|
233
|
+
if (typeof rafRef.current !== 'undefined') {
|
|
234
|
+
window.cancelAnimationFrame(rafRef.current)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}, [])
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
positionRef,
|
|
241
|
+
preventLayoutShift(callback: () => void) {
|
|
242
|
+
if (!positionRef.current) {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const initialTop = positionRef.current.getBoundingClientRect().top
|
|
247
|
+
|
|
248
|
+
callback()
|
|
249
|
+
|
|
250
|
+
rafRef.current = window.requestAnimationFrame(() => {
|
|
251
|
+
const newTop =
|
|
252
|
+
positionRef.current?.getBoundingClientRect().top ?? initialTop
|
|
253
|
+
window.scrollBy(0, newTop - initialTop)
|
|
254
|
+
})
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function useTabGroupProps(availableLanguages: Array<string>) {
|
|
260
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
261
|
+
const [preferredLanguages, setPreferredLanguages] = useState<string[]>([])
|
|
262
|
+
|
|
263
|
+
const activeLanguage = [...availableLanguages].sort(
|
|
264
|
+
(a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a),
|
|
265
|
+
)[0]
|
|
266
|
+
const languageIndex = availableLanguages.indexOf(activeLanguage)
|
|
267
|
+
const newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex
|
|
268
|
+
|
|
269
|
+
if (newSelectedIndex !== selectedIndex) {
|
|
270
|
+
setSelectedIndex(newSelectedIndex)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const { positionRef, preventLayoutShift } = usePreventLayoutShift()
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
as: 'div' as const,
|
|
277
|
+
ref: positionRef,
|
|
278
|
+
selectedIndex,
|
|
279
|
+
onChange: (newSelectedIndex: number) => {
|
|
280
|
+
preventLayoutShift(() => {
|
|
281
|
+
const newLanguage = availableLanguages[newSelectedIndex]
|
|
282
|
+
setPreferredLanguages(prev => [
|
|
283
|
+
...prev.filter(lang => lang !== newLanguage),
|
|
284
|
+
newLanguage
|
|
285
|
+
])
|
|
286
|
+
})
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const APIGroupContext = createContext(false)
|
|
292
|
+
|
|
293
|
+
export function APIGroup({
|
|
294
|
+
children,
|
|
295
|
+
title,
|
|
296
|
+
...props
|
|
297
|
+
}: React.ComponentPropsWithoutRef<typeof APIGroupPanels> & { title: string }) {
|
|
298
|
+
const languages =
|
|
299
|
+
Children.map(children, (child) =>
|
|
300
|
+
// @ts-expect-error child.props is not typed
|
|
301
|
+
getPanelTitle(isValidElement(child) ? child.props : {}),
|
|
302
|
+
) ?? []
|
|
303
|
+
const tabGroupProps = useTabGroupProps(languages)
|
|
304
|
+
const hasTabs = Children.count(children) > 1
|
|
305
|
+
|
|
306
|
+
const header = (
|
|
307
|
+
<APIGroupHeader title={title} selectedIndex={tabGroupProps.selectedIndex}>
|
|
308
|
+
{children}
|
|
309
|
+
</APIGroupHeader>
|
|
310
|
+
)
|
|
311
|
+
const panels = <APIGroupPanels {...props}>{children}</APIGroupPanels>
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<APIGroupContext.Provider value={true}>
|
|
315
|
+
{hasTabs ? (
|
|
316
|
+
<Card className="py-0 border-black/10 bg-none overflow-hidden mb-4">
|
|
317
|
+
<TabGroup {...tabGroupProps}>
|
|
318
|
+
<div className="not-prose">
|
|
319
|
+
{header}
|
|
320
|
+
{panels}
|
|
321
|
+
</div>
|
|
322
|
+
</TabGroup>
|
|
323
|
+
</Card>
|
|
324
|
+
) : (
|
|
325
|
+
<Card className="py-0 border-black/10 bg-none overflow-hidden mb-4">
|
|
326
|
+
<div className="not-prose">
|
|
327
|
+
{header}
|
|
328
|
+
{panels}
|
|
329
|
+
</div>
|
|
330
|
+
</Card>
|
|
331
|
+
)}
|
|
332
|
+
</APIGroupContext.Provider>
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function API({
|
|
337
|
+
children,
|
|
338
|
+
...props
|
|
339
|
+
}: React.ComponentPropsWithoutRef<'code'>) {
|
|
340
|
+
const isGrouped = useContext(APIGroupContext)
|
|
341
|
+
|
|
342
|
+
if (isGrouped) {
|
|
343
|
+
if (typeof children !== 'string') {
|
|
344
|
+
throw new Error(
|
|
345
|
+
'`API` children must be a string when nested inside a `APIGroup`.',
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
return <code {...props}>
|
|
349
|
+
{children}
|
|
350
|
+
</code>
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return <code {...props}>{children}</code>
|
|
354
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { useState } from "preact/hooks"
|
|
2
|
+
import { APIGroup } from "./api"
|
|
3
|
+
import { PlayButton } from "./api-playground"
|
|
4
|
+
import { Fence } from "./fence"
|
|
5
|
+
import { Properties, Property } from "./open-api"
|
|
6
|
+
import { Tag } from "./tag"
|
|
7
|
+
import { Col, Row } from "./text"
|
|
8
|
+
import { WebSocketPlayground, type WebSocketServer } from "./websocket-playground"
|
|
9
|
+
import { Button, Sheet, SheetContent, SheetHeader, SheetTitle } from "@clidey/ux"
|
|
10
|
+
|
|
11
|
+
interface AsyncAPIMessage {
|
|
12
|
+
name?: string
|
|
13
|
+
title?: string
|
|
14
|
+
summary?: string
|
|
15
|
+
description?: string
|
|
16
|
+
payload?: Schema
|
|
17
|
+
headers?: Schema
|
|
18
|
+
correlationId?: {
|
|
19
|
+
description?: string
|
|
20
|
+
location?: string
|
|
21
|
+
}
|
|
22
|
+
schemaFormat?: string
|
|
23
|
+
contentType?: string
|
|
24
|
+
examples?: any[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AsyncAPIOperation {
|
|
28
|
+
operationId?: string
|
|
29
|
+
summary?: string
|
|
30
|
+
description?: string
|
|
31
|
+
tags?: Array<{
|
|
32
|
+
name: string
|
|
33
|
+
description?: string
|
|
34
|
+
}>
|
|
35
|
+
message?: AsyncAPIMessage | {
|
|
36
|
+
oneOf?: AsyncAPIMessage[]
|
|
37
|
+
}
|
|
38
|
+
bindings?: {
|
|
39
|
+
ws?: {
|
|
40
|
+
method?: string
|
|
41
|
+
query?: Schema
|
|
42
|
+
headers?: Schema
|
|
43
|
+
bindingVersion?: string
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AsyncAPIChannel {
|
|
49
|
+
address?: string
|
|
50
|
+
title?: string
|
|
51
|
+
summary?: string
|
|
52
|
+
description?: string
|
|
53
|
+
subscribe?: AsyncAPIOperation
|
|
54
|
+
publish?: AsyncAPIOperation
|
|
55
|
+
parameters?: Record<string, Parameter>
|
|
56
|
+
messages?: Record<string, AsyncAPIMessage>
|
|
57
|
+
bindings?: {
|
|
58
|
+
ws?: {
|
|
59
|
+
method?: string
|
|
60
|
+
query?: Schema
|
|
61
|
+
headers?: Schema
|
|
62
|
+
bindingVersion?: string
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface Schema {
|
|
68
|
+
$ref?: string
|
|
69
|
+
type?: string
|
|
70
|
+
items?: Schema
|
|
71
|
+
properties?: Record<string, Schema>
|
|
72
|
+
required?: string[]
|
|
73
|
+
description?: string
|
|
74
|
+
default?: any
|
|
75
|
+
example?: any
|
|
76
|
+
format?: string
|
|
77
|
+
enum?: any[]
|
|
78
|
+
oneOf?: Schema[]
|
|
79
|
+
anyOf?: Schema[]
|
|
80
|
+
allOf?: Schema[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface Parameter {
|
|
84
|
+
description?: string
|
|
85
|
+
schema?: Schema
|
|
86
|
+
location?: string
|
|
87
|
+
default?: any
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface Components {
|
|
91
|
+
schemas?: Record<string, Schema>
|
|
92
|
+
messages?: Record<string, AsyncAPIMessage>
|
|
93
|
+
parameters?: Record<string, Parameter>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface Info {
|
|
97
|
+
title: string
|
|
98
|
+
version: string
|
|
99
|
+
description?: string
|
|
100
|
+
termsOfService?: string
|
|
101
|
+
contact?: {
|
|
102
|
+
name?: string
|
|
103
|
+
url?: string
|
|
104
|
+
email?: string
|
|
105
|
+
}
|
|
106
|
+
license?: {
|
|
107
|
+
name: string
|
|
108
|
+
url?: string
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface AsyncAPIJson {
|
|
113
|
+
asyncapi: string
|
|
114
|
+
info: Info
|
|
115
|
+
servers?: Record<string, WebSocketServer & {
|
|
116
|
+
host?: string
|
|
117
|
+
pathname?: string
|
|
118
|
+
protocol?: string
|
|
119
|
+
protocolVersion?: string
|
|
120
|
+
description?: string
|
|
121
|
+
title?: string
|
|
122
|
+
summary?: string
|
|
123
|
+
variables?: Record<string, any>
|
|
124
|
+
security?: any[]
|
|
125
|
+
tags?: Array<{
|
|
126
|
+
name: string
|
|
127
|
+
description?: string
|
|
128
|
+
}>
|
|
129
|
+
bindings?: any
|
|
130
|
+
}>
|
|
131
|
+
channels: Record<string, AsyncAPIChannel>
|
|
132
|
+
components?: Components
|
|
133
|
+
defaultContentType?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveSchema(schema: Schema | undefined, components: Components): Schema {
|
|
137
|
+
if (!schema) return {}
|
|
138
|
+
|
|
139
|
+
if (schema.$ref) {
|
|
140
|
+
const refPath = schema.$ref.replace(/^#\/components\/schemas\//, '')
|
|
141
|
+
return resolveSchema(components.schemas?.[refPath], components)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (schema.type === 'array' && schema.items) {
|
|
145
|
+
return {
|
|
146
|
+
...schema,
|
|
147
|
+
items: resolveSchema(schema.items, components)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (schema.properties) {
|
|
152
|
+
const resolvedProps: Record<string, Schema> = {}
|
|
153
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
154
|
+
resolvedProps[key] = resolveSchema(value, components)
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
...schema,
|
|
158
|
+
properties: resolvedProps
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return schema
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isDirectMessage(message: AsyncAPIMessage | { oneOf?: AsyncAPIMessage[] } | undefined): message is AsyncAPIMessage {
|
|
166
|
+
return message != null && !('oneOf' in message);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function AsyncAPI({
|
|
170
|
+
asyncAPIJson,
|
|
171
|
+
channel: selectedChannel,
|
|
172
|
+
operation: selectedOperation
|
|
173
|
+
}: {
|
|
174
|
+
asyncAPIJson: AsyncAPIJson
|
|
175
|
+
channel: string
|
|
176
|
+
operation: 'subscribe' | 'publish'
|
|
177
|
+
}) {
|
|
178
|
+
const { channels, components, servers } = asyncAPIJson;
|
|
179
|
+
const [showPlayground, setShowPlayground] = useState(false)
|
|
180
|
+
|
|
181
|
+
const handleShowPlayground = () => {
|
|
182
|
+
setShowPlayground(true)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Convert AsyncAPI servers to WebSocket servers
|
|
186
|
+
const wsServers: WebSocketServer[] = servers ? Object.entries(servers).map(([key, server]) => ({
|
|
187
|
+
url: `${server.protocol}://${server.host}${server.pathname || ''}`,
|
|
188
|
+
description: server.description || server.title || key,
|
|
189
|
+
protocol: server.protocol
|
|
190
|
+
})) : [];
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className="max-w-none w-full">
|
|
194
|
+
<Sheet open={showPlayground} onOpenChange={setShowPlayground} modal={true}>
|
|
195
|
+
<SheetContent side="bottom" className="h-[90vh] w-full overflow-y-auto p-8">
|
|
196
|
+
<SheetHeader className="p-0">
|
|
197
|
+
<SheetTitle>WebSocket Playground</SheetTitle>
|
|
198
|
+
</SheetHeader>
|
|
199
|
+
<WebSocketPlayground
|
|
200
|
+
url={selectedChannel}
|
|
201
|
+
servers={wsServers}
|
|
202
|
+
/>
|
|
203
|
+
</SheetContent>
|
|
204
|
+
</Sheet>
|
|
205
|
+
|
|
206
|
+
{Object.entries(channels).map(([channelPath, channelDetails]) => {
|
|
207
|
+
if (channelPath !== selectedChannel) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const operation = channelDetails[selectedOperation]
|
|
212
|
+
if (!operation) {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { summary, description, message } = operation
|
|
217
|
+
const resolvedMessage = isDirectMessage(message) ? message :
|
|
218
|
+
(message?.oneOf && message.oneOf.length > 0 ? message.oneOf[0] : undefined)
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div key={channelPath} className="my-8">
|
|
222
|
+
<Row cols={2}>
|
|
223
|
+
<Col>
|
|
224
|
+
<h2 className="flex items-center mt-4">
|
|
225
|
+
<Tag variant="medium" className="mr-2 py-1 px-4 rounded-lg">
|
|
226
|
+
{selectedOperation.toUpperCase()}
|
|
227
|
+
</Tag>
|
|
228
|
+
<span className="ml-2 font-mono">{channelPath}</span>
|
|
229
|
+
</h2>
|
|
230
|
+
|
|
231
|
+
{(summary || description) && (
|
|
232
|
+
<p>{summary || description}</p>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{resolvedMessage && (
|
|
236
|
+
<div className="mt-4">
|
|
237
|
+
<h3>Message</h3>
|
|
238
|
+
{resolvedMessage.title && <h4>{resolvedMessage.title}</h4>}
|
|
239
|
+
{resolvedMessage.description && <p>{resolvedMessage.description}</p>}
|
|
240
|
+
|
|
241
|
+
{resolvedMessage.payload && (
|
|
242
|
+
<div className="mt-4">
|
|
243
|
+
<h4>Payload</h4>
|
|
244
|
+
{(() => {
|
|
245
|
+
const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
|
|
246
|
+
return (
|
|
247
|
+
<Properties>
|
|
248
|
+
{Object.entries(resolvedPayload.properties || {}).map(([propName, prop]) => (
|
|
249
|
+
<Property
|
|
250
|
+
key={propName}
|
|
251
|
+
name={propName}
|
|
252
|
+
type={prop.type || 'string'}
|
|
253
|
+
required={resolvedPayload.required?.includes(propName)}
|
|
254
|
+
>
|
|
255
|
+
{prop.description}
|
|
256
|
+
{prop.default !== undefined && (
|
|
257
|
+
<span className="text-gray-500 ml-1">(default: {JSON.stringify(prop.default)})</span>
|
|
258
|
+
)}
|
|
259
|
+
</Property>
|
|
260
|
+
))}
|
|
261
|
+
</Properties>
|
|
262
|
+
)
|
|
263
|
+
})()}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{resolvedMessage.headers && (
|
|
268
|
+
<div className="mt-4">
|
|
269
|
+
<h4>Headers</h4>
|
|
270
|
+
{(() => {
|
|
271
|
+
const resolvedHeaders = resolveSchema(resolvedMessage.headers, components || {})
|
|
272
|
+
return (
|
|
273
|
+
<Properties>
|
|
274
|
+
{Object.entries(resolvedHeaders.properties || {}).map(([headerName, header]) => (
|
|
275
|
+
<Property
|
|
276
|
+
key={headerName}
|
|
277
|
+
name={headerName}
|
|
278
|
+
type={header.type || 'string'}
|
|
279
|
+
required={resolvedHeaders.required?.includes(headerName)}
|
|
280
|
+
>
|
|
281
|
+
{header.description}
|
|
282
|
+
</Property>
|
|
283
|
+
))}
|
|
284
|
+
</Properties>
|
|
285
|
+
)
|
|
286
|
+
})()}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{resolvedMessage?.examples && resolvedMessage.examples.length > 0 && (
|
|
291
|
+
<div className="mt-4">
|
|
292
|
+
<h4>Examples</h4>
|
|
293
|
+
{resolvedMessage.examples.map((example: any, idx: number) => (
|
|
294
|
+
<Fence key={idx} language="json">
|
|
295
|
+
{JSON.stringify(example, null, 2)}
|
|
296
|
+
</Fence>
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{components?.schemas && (
|
|
304
|
+
<div className="mt-4">
|
|
305
|
+
<h3>Schemas</h3>
|
|
306
|
+
{Object.entries(components.schemas).filter(([, schema]) => schema.properties).map(([name, schema]) => (
|
|
307
|
+
<div key={name} className="mt-4">
|
|
308
|
+
<h4>{name}</h4>
|
|
309
|
+
<Properties>
|
|
310
|
+
{Object.entries(schema.properties || {}).map(([propName, prop]) => (
|
|
311
|
+
<Property
|
|
312
|
+
key={propName}
|
|
313
|
+
name={propName}
|
|
314
|
+
type={prop.type || 'string'}
|
|
315
|
+
required={schema.required?.includes(propName)}
|
|
316
|
+
>
|
|
317
|
+
{prop.description}
|
|
318
|
+
</Property>
|
|
319
|
+
))}
|
|
320
|
+
</Properties>
|
|
321
|
+
</div>
|
|
322
|
+
))}
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
</Col>
|
|
326
|
+
<Col sticky>
|
|
327
|
+
<Button
|
|
328
|
+
onClick={handleShowPlayground}
|
|
329
|
+
className='ml-auto mt-3 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium'
|
|
330
|
+
>
|
|
331
|
+
Try
|
|
332
|
+
<PlayButton className="h-4 w-4" />
|
|
333
|
+
</Button>
|
|
334
|
+
<APIGroup title="Message" tag={selectedOperation.toUpperCase()} label={channelPath}>
|
|
335
|
+
<>
|
|
336
|
+
{resolvedMessage?.payload && (() => {
|
|
337
|
+
const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
|
|
338
|
+
return (
|
|
339
|
+
<Fence language="json">
|
|
340
|
+
{JSON.stringify(resolvedPayload, null, 2)}
|
|
341
|
+
</Fence>
|
|
342
|
+
)
|
|
343
|
+
})()}
|
|
344
|
+
</>
|
|
345
|
+
</APIGroup>
|
|
346
|
+
</Col>
|
|
347
|
+
</Row>
|
|
348
|
+
</div>
|
|
349
|
+
)
|
|
350
|
+
})}
|
|
351
|
+
</div>
|
|
352
|
+
)
|
|
353
|
+
}
|