@clidey/dory 0.9.0 → 0.11.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/.claude/settings.json +29 -0
- package/CLAUDE.md +161 -0
- package/docs/api-reference/asyncapi.json +324 -0
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
- package/docs/dory.json +16 -0
- package/docs/websocket-playground-demo.mdx +135 -0
- package/k8s/Dockerfile +3 -0
- package/package.json +1 -1
- package/scripts/generate-frontmatter-json.js +154 -0
- package/src/components/store.ts +71 -2
- package/src/mdx/async-api.tsx +369 -0
- package/src/mdx/mdx.tsx +5 -1
- package/src/mdx/mermaid.tsx +19 -2
- package/src/mdx/websocket-playground.tsx +755 -0
- package/src/plugins/frontmatter-dev-server.ts +158 -0
- package/src/plugins/frontmatter-generator.ts +157 -0
- package/src/plugins/sanitize.ts +40 -0
- package/src/ui/layout.tsx +33 -8
- package/vite.config.ts +6 -1
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { motion } from "motion/react"
|
|
2
|
+
import { useState } from "preact/hooks"
|
|
3
|
+
import { APIGroup } from "./api"
|
|
4
|
+
import { PlayButton } from "./api-playground"
|
|
5
|
+
import { Fence } from "./fence"
|
|
6
|
+
import { Properties, Property } from "./open-api"
|
|
7
|
+
import { Tag } from "./tag"
|
|
8
|
+
import { Col, Row } from "./text"
|
|
9
|
+
import { WebSocketPlayground, type WebSocketServer } from "./websocket-playground"
|
|
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
|
+
{showPlayground && (
|
|
195
|
+
<motion.div
|
|
196
|
+
initial={{ x: "100%" }}
|
|
197
|
+
animate={{ x: 0 }}
|
|
198
|
+
exit={{ x: "100%" }}
|
|
199
|
+
transition={{ type: "spring", damping: 20 }}
|
|
200
|
+
className="fixed inset-y-0 right-0 w-full max-w-3xl bg-white dark:bg-[#1e1e1e] shadow-xl z-50 overflow-y-auto border-l border-zinc-200 dark:border-zinc-800"
|
|
201
|
+
>
|
|
202
|
+
<div className="p-6">
|
|
203
|
+
<div className="flex justify-between items-center mb-6">
|
|
204
|
+
<h2 className="text-xl font-semibold">WebSocket Playground</h2>
|
|
205
|
+
<button
|
|
206
|
+
onClick={() => setShowPlayground(false)}
|
|
207
|
+
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
|
208
|
+
>
|
|
209
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
210
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
211
|
+
</svg>
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
<WebSocketPlayground
|
|
215
|
+
url={selectedChannel}
|
|
216
|
+
servers={wsServers}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
</motion.div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{Object.entries(channels).map(([channelPath, channelDetails]) => {
|
|
223
|
+
if (channelPath !== selectedChannel) {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const operation = channelDetails[selectedOperation]
|
|
228
|
+
if (!operation) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { summary, description, message } = operation
|
|
233
|
+
const resolvedMessage = isDirectMessage(message) ? message :
|
|
234
|
+
(message?.oneOf && message.oneOf.length > 0 ? message.oneOf[0] : undefined)
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div key={channelPath} className="my-8">
|
|
238
|
+
<Row cols={2}>
|
|
239
|
+
<Col>
|
|
240
|
+
<h2 className="flex items-center mt-4">
|
|
241
|
+
<Tag variant="medium" className="mr-2 py-1 px-4 rounded-lg">
|
|
242
|
+
{selectedOperation.toUpperCase()}
|
|
243
|
+
</Tag>
|
|
244
|
+
<span className="ml-2 font-mono">{channelPath}</span>
|
|
245
|
+
</h2>
|
|
246
|
+
|
|
247
|
+
{(summary || description) && (
|
|
248
|
+
<p>{summary || description}</p>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{resolvedMessage && (
|
|
252
|
+
<div className="mt-4">
|
|
253
|
+
<h3>Message</h3>
|
|
254
|
+
{resolvedMessage.title && <h4>{resolvedMessage.title}</h4>}
|
|
255
|
+
{resolvedMessage.description && <p>{resolvedMessage.description}</p>}
|
|
256
|
+
|
|
257
|
+
{resolvedMessage.payload && (
|
|
258
|
+
<div className="mt-4">
|
|
259
|
+
<h4>Payload</h4>
|
|
260
|
+
{(() => {
|
|
261
|
+
const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
|
|
262
|
+
return (
|
|
263
|
+
<Properties>
|
|
264
|
+
{Object.entries(resolvedPayload.properties || {}).map(([propName, prop]) => (
|
|
265
|
+
<Property
|
|
266
|
+
key={propName}
|
|
267
|
+
name={propName}
|
|
268
|
+
type={prop.type || 'string'}
|
|
269
|
+
required={resolvedPayload.required?.includes(propName)}
|
|
270
|
+
>
|
|
271
|
+
{prop.description}
|
|
272
|
+
{prop.default !== undefined && (
|
|
273
|
+
<span className="text-gray-500 ml-1">(default: {JSON.stringify(prop.default)})</span>
|
|
274
|
+
)}
|
|
275
|
+
</Property>
|
|
276
|
+
))}
|
|
277
|
+
</Properties>
|
|
278
|
+
)
|
|
279
|
+
})()}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{resolvedMessage.headers && (
|
|
284
|
+
<div className="mt-4">
|
|
285
|
+
<h4>Headers</h4>
|
|
286
|
+
{(() => {
|
|
287
|
+
const resolvedHeaders = resolveSchema(resolvedMessage.headers, components || {})
|
|
288
|
+
return (
|
|
289
|
+
<Properties>
|
|
290
|
+
{Object.entries(resolvedHeaders.properties || {}).map(([headerName, header]) => (
|
|
291
|
+
<Property
|
|
292
|
+
key={headerName}
|
|
293
|
+
name={headerName}
|
|
294
|
+
type={header.type || 'string'}
|
|
295
|
+
required={resolvedHeaders.required?.includes(headerName)}
|
|
296
|
+
>
|
|
297
|
+
{header.description}
|
|
298
|
+
</Property>
|
|
299
|
+
))}
|
|
300
|
+
</Properties>
|
|
301
|
+
)
|
|
302
|
+
})()}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
{resolvedMessage?.examples && resolvedMessage.examples.length > 0 && (
|
|
307
|
+
<div className="mt-4">
|
|
308
|
+
<h4>Examples</h4>
|
|
309
|
+
{resolvedMessage.examples.map((example: any, idx: number) => (
|
|
310
|
+
<Fence key={idx} language="json">
|
|
311
|
+
{JSON.stringify(example, null, 2)}
|
|
312
|
+
</Fence>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{components?.schemas && (
|
|
320
|
+
<div className="mt-4">
|
|
321
|
+
<h3>Schemas</h3>
|
|
322
|
+
{Object.entries(components.schemas).filter(([, schema]) => schema.properties).map(([name, schema]) => (
|
|
323
|
+
<div key={name} className="mt-4">
|
|
324
|
+
<h4>{name}</h4>
|
|
325
|
+
<Properties>
|
|
326
|
+
{Object.entries(schema.properties || {}).map(([propName, prop]) => (
|
|
327
|
+
<Property
|
|
328
|
+
key={propName}
|
|
329
|
+
name={propName}
|
|
330
|
+
type={prop.type || 'string'}
|
|
331
|
+
required={schema.required?.includes(propName)}
|
|
332
|
+
>
|
|
333
|
+
{prop.description}
|
|
334
|
+
</Property>
|
|
335
|
+
))}
|
|
336
|
+
</Properties>
|
|
337
|
+
</div>
|
|
338
|
+
))}
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
</Col>
|
|
342
|
+
<Col sticky>
|
|
343
|
+
<button
|
|
344
|
+
onClick={handleShowPlayground}
|
|
345
|
+
className='ml-auto mt-3 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition bg-sky-600 text-white hover:bg-sky-700'
|
|
346
|
+
>
|
|
347
|
+
Try
|
|
348
|
+
<PlayButton className="h-4 w-4" />
|
|
349
|
+
</button>
|
|
350
|
+
<APIGroup title="Message" tag={selectedOperation.toUpperCase()} label={channelPath}>
|
|
351
|
+
<>
|
|
352
|
+
{resolvedMessage?.payload && (() => {
|
|
353
|
+
const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
|
|
354
|
+
return (
|
|
355
|
+
<Fence language="json">
|
|
356
|
+
{JSON.stringify(resolvedPayload, null, 2)}
|
|
357
|
+
</Fence>
|
|
358
|
+
)
|
|
359
|
+
})()}
|
|
360
|
+
</>
|
|
361
|
+
</APIGroup>
|
|
362
|
+
</Col>
|
|
363
|
+
</Row>
|
|
364
|
+
</div>
|
|
365
|
+
)
|
|
366
|
+
})}
|
|
367
|
+
</div>
|
|
368
|
+
)
|
|
369
|
+
}
|
package/src/mdx/mdx.tsx
CHANGED
|
@@ -18,6 +18,8 @@ export { Steps, Step } from './step';
|
|
|
18
18
|
export { Table as table, Th as th, Td as td } from './table';
|
|
19
19
|
export { UnorderedList as ul, OrderedList as ol, ListItem as li } from './list';
|
|
20
20
|
export { APIPlayground } from './api-playground';
|
|
21
|
+
export { WebSocketPlayground } from './websocket-playground';
|
|
22
|
+
export { AsyncAPI } from './async-api';
|
|
21
23
|
export { Source } from './source';
|
|
22
24
|
|
|
23
25
|
export const wrapper = ({ children }: { children: preact.ComponentChildren }) => {
|
|
@@ -40,4 +42,6 @@ export const wrapper = ({ children }: { children: preact.ComponentChildren }) =>
|
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
|
|
43
|
-
export const hr = () => <hr className="my-4 text-gray-500/25 dark:text-gray-200/25" />;
|
|
45
|
+
export const hr = () => <hr className="my-4 text-gray-500/25 dark:text-gray-200/25" />;
|
|
46
|
+
|
|
47
|
+
export const br = () => <br />;
|
package/src/mdx/mermaid.tsx
CHANGED
|
@@ -2,6 +2,20 @@ import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
|
|
2
2
|
import classNames from 'classnames'
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'preact/compat'
|
|
4
4
|
|
|
5
|
+
function fixNodeLabelsWithSpecialChars(content: string): string {
|
|
6
|
+
// Pattern to match node definitions with brackets containing special characters
|
|
7
|
+
// Matches: NodeId[text content] where text content has special chars and isn't already quoted
|
|
8
|
+
const nodePattern = /(\w+)\[([^\]"]*[(),\s][^\]"]*)\]/g;
|
|
9
|
+
|
|
10
|
+
return content.replace(nodePattern, (match, nodeId, labelContent) => {
|
|
11
|
+
// Only wrap in quotes if not already quoted and contains special characters
|
|
12
|
+
if (!labelContent.startsWith('"') || !labelContent.endsWith('"')) {
|
|
13
|
+
return `${nodeId}["${labelContent}"]`;
|
|
14
|
+
}
|
|
15
|
+
return match;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export function MermaidRenderer({ content }: { content: string }) {
|
|
6
20
|
const mermaidRef = useRef<HTMLDivElement>(null)
|
|
7
21
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
@@ -47,9 +61,12 @@ export function MermaidRenderer({ content }: { content: string }) {
|
|
|
47
61
|
.replace(/^\s*classDef .*$/gm, '')
|
|
48
62
|
.replace(/^\s*class .*$/gm, '');
|
|
49
63
|
|
|
64
|
+
// Fix node labels with special characters by wrapping them in quotes
|
|
65
|
+
const processedContent = fixNodeLabelsWithSpecialChars(contentWithoutStyle);
|
|
66
|
+
|
|
50
67
|
// Validate the mermaid syntax first
|
|
51
68
|
try {
|
|
52
|
-
await mermaid.parse(
|
|
69
|
+
await mermaid.parse(processedContent)
|
|
53
70
|
setIsValid(true)
|
|
54
71
|
} catch (parseError) {
|
|
55
72
|
setIsValid(false)
|
|
@@ -59,7 +76,7 @@ export function MermaidRenderer({ content }: { content: string }) {
|
|
|
59
76
|
return
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
const { svg } = await mermaid.render(diagramId,
|
|
79
|
+
const { svg } = await mermaid.render(diagramId, processedContent);
|
|
63
80
|
|
|
64
81
|
if (mounted && mermaidRef.current) {
|
|
65
82
|
mermaidRef.current.innerHTML = svg
|