@raystack/chronicle 0.10.0 → 0.10.2
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/dist/cli/index.js +24 -0
- package/package.json +3 -2
- package/src/cli/commands/dev.ts +12 -0
- package/src/cli/commands/start.ts +12 -0
- package/src/components/api/api-overview.tsx +2 -2
- package/src/components/api/playground-dialog.tsx +42 -20
- package/src/components/mdx/link.tsx +5 -31
- package/src/components/ui/PrefetchProvider.tsx +70 -0
- package/src/components/ui/search.module.css +6 -0
- package/src/components/ui/search.tsx +4 -1
- package/src/lib/env.ts +9 -0
- package/src/lib/openapi.ts +2 -1
- package/src/lib/page-context.tsx +11 -6
- package/src/lib/preload.ts +37 -0
- package/src/lib/source.ts +21 -2
- package/src/pages/DocsLayout.tsx +11 -8
- package/src/server/App.module.css +4 -0
- package/src/server/App.tsx +32 -15
- package/src/server/api/page.ts +2 -2
- package/src/server/api/search.ts +16 -6
- package/src/server/entry-client.tsx +18 -14
- package/src/server/entry-server.tsx +6 -2
- package/src/server/routes/[...slug].md.ts +5 -1
- package/src/server/{routes/apis/[...slug].md.ts → utils/api-markdown.ts} +3 -6
- package/src/themes/default/ContentDirButtons.tsx +1 -1
- package/src/themes/default/Layout.tsx +38 -21
- package/src/themes/default/Page.module.css +9 -0
- package/src/themes/default/Page.tsx +6 -2
- package/src/themes/default/Skeleton.tsx +5 -15
- package/src/themes/default/VersionSwitcher.tsx +2 -2
- package/src/themes/paper/VersionSwitcher.tsx +2 -2
- package/src/types/content.ts +1 -0
- package/src/components/common/breadcrumb.tsx +0 -3
- package/src/components/common/button.tsx +0 -3
- package/src/components/common/code-block.tsx +0 -3
- package/src/components/common/dialog.tsx +0 -3
- package/src/components/common/index.ts +0 -10
- package/src/components/common/input-field.tsx +0 -3
- package/src/components/common/sidebar.tsx +0 -3
- package/src/components/common/switch.tsx +0 -3
- package/src/components/common/table.tsx +0 -3
- package/src/components/common/tabs.tsx +0 -3
package/dist/cli/index.js
CHANGED
|
@@ -822,6 +822,18 @@ var devCommand = new Command2("dev").description("Start development server").opt
|
|
|
822
822
|
});
|
|
823
823
|
await server.listen();
|
|
824
824
|
server.printUrls();
|
|
825
|
+
let shuttingDown = false;
|
|
826
|
+
const shutdown = async () => {
|
|
827
|
+
if (shuttingDown)
|
|
828
|
+
return;
|
|
829
|
+
shuttingDown = true;
|
|
830
|
+
try {
|
|
831
|
+
await server.close();
|
|
832
|
+
} catch {}
|
|
833
|
+
process.exit(0);
|
|
834
|
+
};
|
|
835
|
+
process.once("SIGINT", shutdown);
|
|
836
|
+
process.once("SIGTERM", shutdown);
|
|
825
837
|
});
|
|
826
838
|
|
|
827
839
|
// src/cli/commands/init.ts
|
|
@@ -953,6 +965,18 @@ var startCommand = new Command5("start").description("Start production server").
|
|
|
953
965
|
preview: { port, host: options.host }
|
|
954
966
|
});
|
|
955
967
|
server.printUrls();
|
|
968
|
+
let shuttingDown = false;
|
|
969
|
+
const shutdown = async () => {
|
|
970
|
+
if (shuttingDown)
|
|
971
|
+
return;
|
|
972
|
+
shuttingDown = true;
|
|
973
|
+
try {
|
|
974
|
+
await server.close();
|
|
975
|
+
} catch {}
|
|
976
|
+
process.exit(0);
|
|
977
|
+
};
|
|
978
|
+
process.once("SIGINT", shutdown);
|
|
979
|
+
process.once("SIGTERM", shutdown);
|
|
956
980
|
});
|
|
957
981
|
|
|
958
982
|
// src/cli/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raystack/chronicle",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Config-driven documentation framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -47,8 +47,9 @@
|
|
|
47
47
|
"@opentelemetry/sdk-metrics": "^2.6.1",
|
|
48
48
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
49
49
|
"@radix-ui/react-icons": "^1.3.2",
|
|
50
|
-
"@raystack/apsara": "1.0.0-rc.
|
|
50
|
+
"@raystack/apsara": "1.0.0-rc.7",
|
|
51
51
|
"@shikijs/rehype": "^4.0.2",
|
|
52
|
+
"@tanstack/react-query": "5.100.10",
|
|
52
53
|
"@vitejs/plugin-react": "^6.0.1",
|
|
53
54
|
"chalk": "^5.6.2",
|
|
54
55
|
"class-variance-authority": "^0.7.1",
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -28,4 +28,16 @@ export const devCommand = new Command('dev')
|
|
|
28
28
|
|
|
29
29
|
await server.listen();
|
|
30
30
|
server.printUrls();
|
|
31
|
+
|
|
32
|
+
let shuttingDown = false;
|
|
33
|
+
const shutdown = async () => {
|
|
34
|
+
if (shuttingDown) return;
|
|
35
|
+
shuttingDown = true;
|
|
36
|
+
try {
|
|
37
|
+
await server.close();
|
|
38
|
+
} catch { /* ignore close errors */ }
|
|
39
|
+
process.exit(0);
|
|
40
|
+
};
|
|
41
|
+
process.once('SIGINT', shutdown);
|
|
42
|
+
process.once('SIGTERM', shutdown);
|
|
31
43
|
});
|
|
@@ -26,4 +26,16 @@ export const startCommand = new Command('start')
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
server.printUrls();
|
|
29
|
+
|
|
30
|
+
let shuttingDown = false;
|
|
31
|
+
const shutdown = async () => {
|
|
32
|
+
if (shuttingDown) return;
|
|
33
|
+
shuttingDown = true;
|
|
34
|
+
try {
|
|
35
|
+
await server.close();
|
|
36
|
+
} catch { /* ignore close errors */ }
|
|
37
|
+
process.exit(0);
|
|
38
|
+
};
|
|
39
|
+
process.once('SIGINT', shutdown);
|
|
40
|
+
process.once('SIGTERM', shutdown);
|
|
29
41
|
});
|
|
@@ -21,7 +21,7 @@ interface ApiOverviewProps {
|
|
|
21
21
|
auth?: { type: string; header: string; placeholder?: string }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) {
|
|
24
|
+
export function ApiOverview({ method, path, operation, serverUrl, auth }: ApiOverviewProps) {
|
|
25
25
|
const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
|
|
26
26
|
const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined)
|
|
27
27
|
|
|
@@ -36,7 +36,7 @@ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps)
|
|
|
36
36
|
? headerFields
|
|
37
37
|
: []
|
|
38
38
|
|
|
39
|
-
const fullUrl =
|
|
39
|
+
const fullUrl = serverUrl + path
|
|
40
40
|
const snippetHeaders: Record<string, string> = {}
|
|
41
41
|
if (auth) snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
|
|
42
42
|
if (body) snippetHeaders['Content-Type'] = body.contentType ?? 'application/json'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback, useMemo } from 'react'
|
|
3
|
+
import { useState, useCallback, useMemo, useEffect } from 'react'
|
|
4
4
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
5
|
-
import { Dialog, Button, Badge, IconButton,
|
|
5
|
+
import { Dialog, Button, Badge, IconButton, Input, CopyButton, Select, Menu } from '@raystack/apsara'
|
|
6
6
|
import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon, PlusIcon } from '@radix-ui/react-icons'
|
|
7
7
|
import { CounterClockwiseClockIcon, CodeIcon } from '@radix-ui/react-icons'
|
|
8
8
|
import { MethodBadge } from '@/components/api/method-badge'
|
|
@@ -68,11 +68,21 @@ export function PlaygroundDialog({
|
|
|
68
68
|
|
|
69
69
|
const authSchemes = useMemo(() => getAuthSchemes(document, auth), [document, auth])
|
|
70
70
|
const defaultScheme = authSchemes.find((s) => s.type !== 'none') ?? authSchemes[0]
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
const storageKey = `chronicle:auth:${specName}`
|
|
72
|
+
const savedAuth = useMemo(() => {
|
|
73
|
+
try {
|
|
74
|
+
const raw = sessionStorage.getItem(storageKey)
|
|
75
|
+
return raw ? JSON.parse(raw) : null
|
|
76
|
+
} catch { return null }
|
|
77
|
+
}, [storageKey])
|
|
78
|
+
|
|
79
|
+
const [selectedScheme, setSelectedScheme] = useState(() => {
|
|
80
|
+
if (savedAuth?.scheme && authSchemes.some((s) => s.name === savedAuth.scheme)) return savedAuth.scheme
|
|
81
|
+
return defaultScheme.name
|
|
82
|
+
})
|
|
83
|
+
const [authToken, setAuthToken] = useState(savedAuth?.token ?? '')
|
|
84
|
+
const [basicUser, setBasicUser] = useState(savedAuth?.basicUser ?? '')
|
|
85
|
+
const [basicPass, setBasicPass] = useState(savedAuth?.basicPass ?? '')
|
|
76
86
|
const [headerValues, setHeaderValues] = useState<Record<string, string>>({})
|
|
77
87
|
const [pathValues, setPathValues] = useState<Record<string, string>>({})
|
|
78
88
|
const [queryValues, setQueryValues] = useState<Record<string, string>>({})
|
|
@@ -89,6 +99,17 @@ export function PlaygroundDialog({
|
|
|
89
99
|
})
|
|
90
100
|
const [bodyJsonStr, setBodyJsonStr] = useState(() => body ? body.jsonExample : '{}')
|
|
91
101
|
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
try {
|
|
104
|
+
sessionStorage.setItem(storageKey, JSON.stringify({
|
|
105
|
+
scheme: selectedScheme,
|
|
106
|
+
token: authToken,
|
|
107
|
+
basicUser,
|
|
108
|
+
basicPass,
|
|
109
|
+
}))
|
|
110
|
+
} catch { /* ignore */ }
|
|
111
|
+
}, [storageKey, selectedScheme, authToken, basicUser, basicPass])
|
|
112
|
+
|
|
92
113
|
const [responseData, setResponseData] = useState<{
|
|
93
114
|
status: number; statusText: string; body: unknown; headers?: Record<string, string>; time: number
|
|
94
115
|
} | null>(null)
|
|
@@ -119,6 +140,7 @@ export function PlaygroundDialog({
|
|
|
119
140
|
setAuthToken('')
|
|
120
141
|
setBasicUser('')
|
|
121
142
|
setBasicPass('')
|
|
143
|
+
try { sessionStorage.removeItem(storageKey) } catch { /* ignore */ }
|
|
122
144
|
setHeaderValues({})
|
|
123
145
|
setPathValues({})
|
|
124
146
|
setQueryValues({})
|
|
@@ -263,13 +285,13 @@ export function PlaygroundDialog({
|
|
|
263
285
|
<div className={styles.fieldRow}>
|
|
264
286
|
<span className={styles.fieldLabel}>Username</span>
|
|
265
287
|
<div className={styles.fieldInput}>
|
|
266
|
-
<
|
|
288
|
+
<Input size="small" placeholder="Enter username" value={basicUser} onValueChange={setBasicUser} />
|
|
267
289
|
</div>
|
|
268
290
|
</div>
|
|
269
291
|
<div className={styles.fieldRow}>
|
|
270
292
|
<span className={styles.fieldLabel}>Password</span>
|
|
271
293
|
<div className={styles.fieldInput}>
|
|
272
|
-
<
|
|
294
|
+
<Input size="small" type="password" placeholder="Enter password" value={basicPass} onValueChange={setBasicPass} />
|
|
273
295
|
</div>
|
|
274
296
|
</div>
|
|
275
297
|
</>
|
|
@@ -277,7 +299,7 @@ export function PlaygroundDialog({
|
|
|
277
299
|
<div className={styles.fieldRow}>
|
|
278
300
|
<span className={styles.fieldLabel}>{currentScheme.headerName}</span>
|
|
279
301
|
<div className={styles.fieldInput}>
|
|
280
|
-
<
|
|
302
|
+
<Input size="small" placeholder={currentScheme.placeholder} value={authToken} onValueChange={setAuthToken} />
|
|
281
303
|
</div>
|
|
282
304
|
</div>
|
|
283
305
|
) : null}
|
|
@@ -285,7 +307,7 @@ export function PlaygroundDialog({
|
|
|
285
307
|
<div key={f.name} className={styles.fieldRow}>
|
|
286
308
|
<span className={styles.fieldLabel}>{f.name}</span>
|
|
287
309
|
<div className={styles.fieldInput}>
|
|
288
|
-
<
|
|
310
|
+
<Input size="small" placeholder="Enter value" value={headerValues[f.name] ?? ''} onValueChange={(v) => setHeaderValues({ ...headerValues, [f.name]: v })} />
|
|
289
311
|
</div>
|
|
290
312
|
</div>
|
|
291
313
|
))}
|
|
@@ -307,11 +329,11 @@ export function PlaygroundDialog({
|
|
|
307
329
|
<div key={f.name} className={styles.fieldRow}>
|
|
308
330
|
<span className={styles.fieldLabel}>{f.name}</span>
|
|
309
331
|
<div className={styles.fieldInput}>
|
|
310
|
-
<
|
|
332
|
+
<Input
|
|
311
333
|
size="small"
|
|
312
334
|
placeholder="Enter value"
|
|
313
335
|
value={pathValues[f.name] ?? ''}
|
|
314
|
-
|
|
336
|
+
onValueChange={(v) => setPathValues({ ...pathValues, [f.name]: v })}
|
|
315
337
|
/>
|
|
316
338
|
</div>
|
|
317
339
|
</div>
|
|
@@ -332,11 +354,11 @@ export function PlaygroundDialog({
|
|
|
332
354
|
<div key={f.name} className={styles.fieldRow}>
|
|
333
355
|
<span className={styles.fieldLabel}>{f.name}</span>
|
|
334
356
|
<div className={styles.fieldInput}>
|
|
335
|
-
<
|
|
357
|
+
<Input
|
|
336
358
|
size="small"
|
|
337
359
|
placeholder={f.description ?? 'Enter value'}
|
|
338
360
|
value={queryValues[f.name] ?? ''}
|
|
339
|
-
|
|
361
|
+
onValueChange={(v) => setQueryValues({ ...queryValues, [f.name]: v })}
|
|
340
362
|
/>
|
|
341
363
|
</div>
|
|
342
364
|
</div>
|
|
@@ -494,13 +516,13 @@ function BodyFieldRow({ field, value, onChange }: {
|
|
|
494
516
|
{items.map((item, i) => (
|
|
495
517
|
<div key={i} className={styles.arrayItemRow}>
|
|
496
518
|
<div className={styles.fieldInput}>
|
|
497
|
-
<
|
|
519
|
+
<Input
|
|
498
520
|
size="small"
|
|
499
521
|
placeholder={`${field.name}[${i}]`}
|
|
500
522
|
value={String(item)}
|
|
501
|
-
|
|
523
|
+
onValueChange={(v) => {
|
|
502
524
|
const updated = [...items]
|
|
503
|
-
updated[i] =
|
|
525
|
+
updated[i] = v
|
|
504
526
|
onChange(updated)
|
|
505
527
|
}}
|
|
506
528
|
/>
|
|
@@ -539,11 +561,11 @@ function BodyFieldRow({ field, value, onChange }: {
|
|
|
539
561
|
<div className={styles.fieldRow}>
|
|
540
562
|
<span className={styles.fieldLabel}>{field.name} {field.required && <Badge variant="danger" size="micro">required</Badge>}</span>
|
|
541
563
|
<div className={styles.fieldInput}>
|
|
542
|
-
<
|
|
564
|
+
<Input
|
|
543
565
|
size="small"
|
|
544
566
|
placeholder={field.description ?? 'Enter value'}
|
|
545
567
|
value={String(value ?? '')}
|
|
546
|
-
|
|
568
|
+
onValueChange={(v) => onChange(v)}
|
|
547
569
|
/>
|
|
548
570
|
</div>
|
|
549
571
|
</div>
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Link as ApsaraLink } from '@raystack/apsara';
|
|
2
|
-
import type { ComponentProps
|
|
3
|
-
import {
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { Link as RouterLink } from 'react-router';
|
|
4
4
|
|
|
5
5
|
type LinkProps = ComponentProps<'a'>;
|
|
6
6
|
|
|
7
|
-
export function Link({ href, children,
|
|
8
|
-
const navigate = useNavigate();
|
|
9
|
-
|
|
7
|
+
export function Link({ href, children, ...props }: LinkProps) {
|
|
10
8
|
if (!href) {
|
|
11
9
|
return <span {...props}>{children}</span>;
|
|
12
10
|
}
|
|
@@ -16,12 +14,7 @@ export function Link({ href, children, onClick: onClickProp, ...props }: LinkPro
|
|
|
16
14
|
|
|
17
15
|
if (isExternal) {
|
|
18
16
|
return (
|
|
19
|
-
<ApsaraLink
|
|
20
|
-
href={href}
|
|
21
|
-
target='_blank'
|
|
22
|
-
rel='noopener noreferrer'
|
|
23
|
-
{...props}
|
|
24
|
-
>
|
|
17
|
+
<ApsaraLink href={href} external {...props}>
|
|
25
18
|
{children}
|
|
26
19
|
</ApsaraLink>
|
|
27
20
|
);
|
|
@@ -35,27 +28,8 @@ export function Link({ href, children, onClick: onClickProp, ...props }: LinkPro
|
|
|
35
28
|
);
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
|
39
|
-
if (
|
|
40
|
-
e.defaultPrevented ||
|
|
41
|
-
e.button !== 0 ||
|
|
42
|
-
e.metaKey ||
|
|
43
|
-
e.ctrlKey ||
|
|
44
|
-
e.shiftKey ||
|
|
45
|
-
e.altKey
|
|
46
|
-
) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
onClickProp?.(e);
|
|
51
|
-
if (e.defaultPrevented) return;
|
|
52
|
-
|
|
53
|
-
e.preventDefault();
|
|
54
|
-
navigate(href);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
31
|
return (
|
|
58
|
-
<ApsaraLink
|
|
32
|
+
<ApsaraLink render={<RouterLink to={href} />} {...props}>
|
|
59
33
|
{children}
|
|
60
34
|
</ApsaraLink>
|
|
61
35
|
);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { prefetchPageData } from '@/lib/preload';
|
|
3
|
+
|
|
4
|
+
function resolvePathname(href: string | null): string | null {
|
|
5
|
+
if (!href) return null;
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(href, location.href);
|
|
8
|
+
if (url.origin !== location.origin) return null;
|
|
9
|
+
return url.pathname;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PrefetchProvider({ children }: { children: React.ReactNode }) {
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleMouseOver = (e: MouseEvent) => {
|
|
18
|
+
const anchor = (e.target as HTMLElement).closest?.('a[href]');
|
|
19
|
+
if (!anchor) return;
|
|
20
|
+
const pathname = resolvePathname(anchor.getAttribute('href'));
|
|
21
|
+
if (pathname) prefetchPageData(pathname);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleFocusIn = (e: FocusEvent) => {
|
|
25
|
+
const anchor = (e.target as HTMLElement).closest?.('a[href]');
|
|
26
|
+
if (!anchor) return;
|
|
27
|
+
const pathname = resolvePathname(anchor.getAttribute('href'));
|
|
28
|
+
if (pathname) prefetchPageData(pathname);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
document.addEventListener('mouseover', handleMouseOver);
|
|
32
|
+
document.addEventListener('focusin', handleFocusIn);
|
|
33
|
+
|
|
34
|
+
const observer = new IntersectionObserver(
|
|
35
|
+
(entries) => {
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (entry.isIntersecting) {
|
|
38
|
+
const pathname = resolvePathname((entry.target as HTMLAnchorElement).getAttribute('href'));
|
|
39
|
+
if (pathname) prefetchPageData(pathname);
|
|
40
|
+
observer.unobserve(entry.target);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{ rootMargin: '200px' },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const observeLinks = () => {
|
|
48
|
+
document.querySelectorAll('a[href]:not([data-prefetch-observed])').forEach((link) => {
|
|
49
|
+
const pathname = resolvePathname(link.getAttribute('href'));
|
|
50
|
+
if (pathname) {
|
|
51
|
+
link.setAttribute('data-prefetch-observed', '');
|
|
52
|
+
observer.observe(link);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const mutationObserver = new MutationObserver(observeLinks);
|
|
58
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
59
|
+
observeLinks();
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
document.removeEventListener('mouseover', handleMouseOver);
|
|
63
|
+
document.removeEventListener('focusin', handleFocusIn);
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
mutationObserver.disconnect();
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
return children;
|
|
70
|
+
}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
HashtagIcon,
|
|
4
4
|
MagnifyingGlassIcon
|
|
5
5
|
} from '@heroicons/react/24/outline';
|
|
6
|
-
import { Command, IconButton, Text } from '@raystack/apsara';
|
|
6
|
+
import { Badge, Command, IconButton, Text } from '@raystack/apsara';
|
|
7
7
|
import { debounce } from 'lodash-es';
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
import { useNavigate } from 'react-router';
|
|
@@ -18,6 +18,7 @@ interface SearchResult {
|
|
|
18
18
|
content: string;
|
|
19
19
|
match?: 'title' | 'heading' | 'body';
|
|
20
20
|
snippet?: string;
|
|
21
|
+
section?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface SearchProps {
|
|
@@ -157,6 +158,7 @@ export function Search({ classNames }: SearchProps) {
|
|
|
157
158
|
html={stripMethod(result.content)}
|
|
158
159
|
/>
|
|
159
160
|
</Text>
|
|
161
|
+
{result.section && <Badge size="small" className={styles.sectionBadge}>{result.section}</Badge>}
|
|
160
162
|
</div>
|
|
161
163
|
</Command.Item>
|
|
162
164
|
))}
|
|
@@ -187,6 +189,7 @@ export function Search({ classNames }: SearchProps) {
|
|
|
187
189
|
</Text>
|
|
188
190
|
)}
|
|
189
191
|
</div>
|
|
192
|
+
{result.section && <Badge size="small" className={styles.sectionBadge}>{result.section}</Badge>}
|
|
190
193
|
</div>
|
|
191
194
|
</Command.Item>
|
|
192
195
|
))}
|
package/src/lib/env.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function substituteEnvVars(value: string): string {
|
|
2
|
+
return value.replace(/\$\{(\w+)\}/g, (_, name) => {
|
|
3
|
+
const val = process.env[name];
|
|
4
|
+
if (val === undefined) {
|
|
5
|
+
throw new Error(`Environment variable '${name}' is not set`);
|
|
6
|
+
}
|
|
7
|
+
return val;
|
|
8
|
+
});
|
|
9
|
+
}
|
package/src/lib/openapi.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { parse as parseYaml } from 'yaml'
|
|
4
4
|
import type { OpenAPIV2, OpenAPIV3 } from 'openapi-types'
|
|
5
5
|
import type { ApiConfig, ApiServerConfig, ApiAuthConfig } from '@/types/config'
|
|
6
|
+
import { substituteEnvVars } from '@/lib/env'
|
|
6
7
|
|
|
7
8
|
type JsonObject = Record<string, unknown>
|
|
8
9
|
|
|
@@ -41,7 +42,7 @@ export async function loadApiSpec(config: ApiConfig, projectRoot: string): Promi
|
|
|
41
42
|
return {
|
|
42
43
|
name: config.name,
|
|
43
44
|
basePath: config.basePath,
|
|
44
|
-
server: config.server,
|
|
45
|
+
server: { ...config.server, url: substituteEnvVars(config.server.url) },
|
|
45
46
|
auth: config.auth,
|
|
46
47
|
document: v3Doc,
|
|
47
48
|
}
|
package/src/lib/page-context.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { resolveRoute, RouteType } from '@/lib/route-resolver';
|
|
|
13
13
|
import type { VersionContext } from '@/lib/version-source';
|
|
14
14
|
import { LATEST_CONTEXT } from '@/lib/version-source';
|
|
15
15
|
import type { ChronicleConfig, Frontmatter, Page, PageNavLink, Root, TableOfContents } from '@/types';
|
|
16
|
+
import { queryClient } from '@/lib/preload';
|
|
16
17
|
|
|
17
18
|
export type MdxLoader = (relativePath: string) => Promise<{ content: ReactNode; toc: TableOfContents }>;
|
|
18
19
|
|
|
@@ -114,12 +115,16 @@ export function PageProvider({
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
const fetchPageData = useCallback(async (slug: string[]): Promise<PageData> => {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
const key = slug.length === 0 ? '' : slug.map(s => encodeURIComponent(s)).join(',');
|
|
119
|
+
const apiPath = key ? `/api/page?slug=${key}` : '/api/page';
|
|
120
|
+
return queryClient.fetchQuery({
|
|
121
|
+
queryKey: ['pageData', key],
|
|
122
|
+
queryFn: async () => {
|
|
123
|
+
const res = await fetch(apiPath);
|
|
124
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
125
|
+
return res.json();
|
|
126
|
+
},
|
|
127
|
+
});
|
|
123
128
|
}, []);
|
|
124
129
|
|
|
125
130
|
const loadDocsPage = useCallback(async (slug: string[], cancelled: { current: boolean }) => {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
export const queryClient = new QueryClient({
|
|
4
|
+
defaultOptions: {
|
|
5
|
+
queries: {
|
|
6
|
+
staleTime: Infinity,
|
|
7
|
+
refetchOnWindowFocus: false,
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function pageDataQueryKey(pathname: string) {
|
|
13
|
+
const slug = pathname.split('/').filter(Boolean);
|
|
14
|
+
const key = slug.length === 0 ? '' : slug.map(s => encodeURIComponent(s)).join(',');
|
|
15
|
+
return ['pageData', key] as const;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function fetchPageDataByPathname(pathname: string) {
|
|
19
|
+
const slug = pathname.split('/').filter(Boolean);
|
|
20
|
+
const key = slug.length === 0 ? '' : slug.map(s => encodeURIComponent(s)).join(',');
|
|
21
|
+
const apiPath = key ? `/api/page?slug=${key}` : '/api/page';
|
|
22
|
+
const res = await fetch(apiPath);
|
|
23
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isApisRoute(pathname: string): boolean {
|
|
28
|
+
return pathname === '/apis' || pathname.startsWith('/apis/');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function prefetchPageData(pathname: string) {
|
|
32
|
+
if (isApisRoute(pathname)) return;
|
|
33
|
+
queryClient.prefetchQuery({
|
|
34
|
+
queryKey: pageDataQueryKey(pathname),
|
|
35
|
+
queryFn: () => fetchPageDataByPathname(pathname),
|
|
36
|
+
});
|
|
37
|
+
}
|
package/src/lib/source.ts
CHANGED
|
@@ -174,17 +174,31 @@ function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[], me
|
|
|
174
174
|
return { ...tree, children: sortNodes(tree.children, pageOrderMap, folderOrderMap) };
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
function filterDraftsFromTree(tree: Root, draftUrls: Set<string>): Root {
|
|
178
|
+
function filterNodes(nodes: Node[]): Node[] {
|
|
179
|
+
return nodes
|
|
180
|
+
.filter(n => n.type !== NodeType.Page || !draftUrls.has(n.url))
|
|
181
|
+
.map(n => n.type === NodeType.Folder
|
|
182
|
+
? { ...n, children: filterNodes(n.children) } as Folder
|
|
183
|
+
: n
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return { ...tree, children: filterNodes(tree.children) };
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
export async function getPageTree(): Promise<Root> {
|
|
178
190
|
if (cachedTree) return cachedTree;
|
|
179
191
|
const s = await getSource();
|
|
180
192
|
const metaFiles = buildFiles().filter(f => f.type === 'meta') as { path: string; data: Record<string, unknown> }[];
|
|
181
|
-
|
|
193
|
+
const sorted = sortTreeByOrder(s.pageTree as Root, s.getPages(), metaFiles);
|
|
194
|
+
const draftUrls = new Set(s.getPages().filter(p => isDraft(p)).map(p => p.url));
|
|
195
|
+
cachedTree = draftUrls.size > 0 ? filterDraftsFromTree(sorted, draftUrls) : sorted;
|
|
182
196
|
return cachedTree;
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
export async function getPages() {
|
|
186
200
|
const s = await getSource();
|
|
187
|
-
return s.getPages();
|
|
201
|
+
return s.getPages().filter(p => !isDraft(p));
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
export async function getPage(slugs?: string[]) {
|
|
@@ -254,10 +268,15 @@ export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: stri
|
|
|
254
268
|
order: d.order as number | undefined,
|
|
255
269
|
icon: d.icon as string | undefined,
|
|
256
270
|
lastModified: d.lastModified as string | undefined,
|
|
271
|
+
draft: d.draft as boolean | undefined,
|
|
257
272
|
_readingTime: d._readingTime as number | undefined,
|
|
258
273
|
};
|
|
259
274
|
}
|
|
260
275
|
|
|
276
|
+
export function isDraft(page: { data: unknown }): boolean {
|
|
277
|
+
return (page.data as Record<string, unknown>).draft === true;
|
|
278
|
+
}
|
|
279
|
+
|
|
261
280
|
export function getRelativePath(page: { data: unknown }): string {
|
|
262
281
|
return ((page.data as Record<string, unknown>)._relativePath as string) ?? '';
|
|
263
282
|
}
|
package/src/pages/DocsLayout.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { useLocation } from 'react-router';
|
|
3
|
+
import { PrefetchProvider } from '@/components/ui/PrefetchProvider';
|
|
3
4
|
import { usePageContext } from '@/lib/page-context';
|
|
4
5
|
import { getActiveContentDir } from '@/lib/navigation';
|
|
5
6
|
import {
|
|
@@ -27,13 +28,15 @@ export function DocsLayout({ children, hideSidebar }: DocsLayoutProps) {
|
|
|
27
28
|
);
|
|
28
29
|
|
|
29
30
|
return (
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
<PrefetchProvider>
|
|
32
|
+
<Layout
|
|
33
|
+
config={config}
|
|
34
|
+
tree={scopedTree}
|
|
35
|
+
hideSidebar={hideSidebar}
|
|
36
|
+
classNames={{ layout: className }}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</Layout>
|
|
40
|
+
</PrefetchProvider>
|
|
38
41
|
);
|
|
39
42
|
}
|