@raybase/docs 0.0.2 → 0.1.3
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/LICENSE +21 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +35 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +29 -0
- package/dist/install.js.map +1 -0
- package/package.json +38 -8
- package/templates/astro-emdash/content/docs/getting-started.mdx +13 -0
- package/templates/astro-emdash/content/docs/index.mdx +13 -0
- package/templates/astro-emdash/content/docs/mcp.mdx +11 -0
- package/templates/astro-emdash/content/docs/meta.json +4 -0
- package/templates/astro-emdash/site/content/docs/getting-started.mdx +12 -0
- package/templates/astro-emdash/site/content/docs/index.mdx +12 -0
- package/templates/astro-emdash/site/content/docs/mcp.mdx +10 -0
- package/templates/astro-emdash/site/content/docs/meta.json +4 -0
- package/templates/astro-emdash/site/source.config.ts +10 -0
- package/templates/astro-emdash/site/src/docs/docs-assets.ts +1 -0
- package/templates/astro-emdash/site/src/docs/docs-client.tsx +22 -0
- package/templates/astro-emdash/site/src/docs/docs-data.ts +24 -0
- package/templates/astro-emdash/site/src/docs/docs-handler.tsx +105 -0
- package/templates/astro-emdash/site/src/docs/docs-layout.tsx +56 -0
- package/templates/astro-emdash/site/src/docs/docs-mdx.tsx +79 -0
- package/templates/astro-emdash/site/src/docs/docs-page-actions.tsx +146 -0
- package/templates/astro-emdash/site/src/docs/docs-page.tsx +70 -0
- package/templates/astro-emdash/site/src/docs/llm-text.ts +61 -0
- package/templates/astro-emdash/site/src/docs/source.ts +7 -0
- package/templates/astro-emdash/site/src/pages/docs/[...slug].ts +6 -0
- package/templates/astro-emdash/site/src/pages/docs/api/markdown.ts +4 -0
- package/templates/astro-emdash/site/src/pages/docs/api/search.ts +4 -0
- package/templates/astro-emdash/site/src/pages/llms.txt.ts +8 -0
- package/templates/astro-emdash/site/src/styles-docs.css +3 -0
- package/templates/astro-emdash/source.config.ts +10 -0
- package/templates/astro-emdash/src/docs/docs-assets.ts +1 -0
- package/templates/astro-emdash/src/docs/docs-client.tsx +22 -0
- package/templates/astro-emdash/src/docs/docs-data.ts +24 -0
- package/templates/astro-emdash/src/docs/docs-handler.tsx +105 -0
- package/templates/astro-emdash/src/docs/docs-layout.tsx +56 -0
- package/templates/astro-emdash/src/docs/docs-mdx.tsx +79 -0
- package/templates/astro-emdash/src/docs/docs-page-actions.tsx +146 -0
- package/templates/astro-emdash/src/docs/docs-page.tsx +70 -0
- package/templates/astro-emdash/src/docs/llm-text.ts +61 -0
- package/templates/astro-emdash/src/docs/source.ts +7 -0
- package/templates/astro-emdash/src/routes/docs/$.tsx +22 -0
- package/templates/astro-emdash/src/routes/docs/api/markdown.ts +12 -0
- package/templates/astro-emdash/src/routes/docs/api/search.ts +12 -0
- package/templates/astro-emdash/src/routes/docs.index.tsx +22 -0
- package/templates/astro-emdash/src/routes/docs.tsx +22 -0
- package/templates/astro-emdash/src/styles-docs.css +3 -0
- package/templates/react-router/app/components/mdx.tsx +79 -0
- package/templates/react-router/app/lib/layout.shared.tsx +54 -0
- package/templates/react-router/app/lib/source.ts +7 -0
- package/templates/react-router/app/routes/docs.$.tsx +49 -0
- package/templates/react-router/app/routes/docs.api.search.ts +10 -0
- package/templates/react-router/app/routes/docs.tsx +13 -0
- package/templates/react-router/content/docs/getting-started.mdx +8 -0
- package/templates/react-router/content/docs/index.mdx +8 -0
- package/templates/react-router/content/docs/mcp.mdx +121 -0
- package/templates/react-router/content/docs/meta.json +4 -0
- package/templates/react-router/source.config.ts +5 -0
- package/templates/tanstack-start/content/docs/getting-started.mdx +10 -0
- package/templates/tanstack-start/content/docs/index.mdx +8 -0
- package/templates/tanstack-start/content/docs/mcp.mdx +121 -0
- package/templates/tanstack-start/content/docs/meta.json +4 -0
- package/templates/tanstack-start/source.config.ts +10 -0
- package/templates/tanstack-start/src/docs/docs-assets.ts +1 -0
- package/templates/tanstack-start/src/docs/docs-client.tsx +22 -0
- package/templates/tanstack-start/src/docs/docs-data.ts +24 -0
- package/templates/tanstack-start/src/docs/docs-handler.tsx +105 -0
- package/templates/tanstack-start/src/docs/docs-layout.tsx +56 -0
- package/templates/tanstack-start/src/docs/docs-mdx.tsx +79 -0
- package/templates/tanstack-start/src/docs/docs-page-actions.tsx +146 -0
- package/templates/tanstack-start/src/docs/docs-page.tsx +70 -0
- package/templates/tanstack-start/src/docs/llm-text.ts +61 -0
- package/templates/tanstack-start/src/docs/source.ts +7 -0
- package/templates/tanstack-start/src/routes/docs/$.tsx +22 -0
- package/templates/tanstack-start/src/routes/docs/api/markdown.ts +12 -0
- package/templates/tanstack-start/src/routes/docs/api/search.ts +12 -0
- package/templates/tanstack-start/src/routes/docs.index.tsx +22 -0
- package/templates/tanstack-start/src/routes/docs.tsx +22 -0
- package/templates/tanstack-start/src/styles-docs.css +3 -0
- package/README.md +0 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Check, ChevronDown, Copy, ExternalLink, FileText, Sparkles } from "lucide-react";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
interface DocsPageActionsProps {
|
|
5
|
+
markdownUrl: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DocsPageActions(props: DocsPageActionsProps) {
|
|
10
|
+
const [copyState, setCopyState] = useState<"idle" | "copying" | "copied" | "failed">("idle");
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
12
|
+
const [origin, setOrigin] = useState("");
|
|
13
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setOrigin(window.location.origin);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!open) return;
|
|
21
|
+
|
|
22
|
+
function onPointerDown(event: PointerEvent) {
|
|
23
|
+
if (!menuRef.current?.contains(event.target as Node)) {
|
|
24
|
+
setOpen(false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
29
|
+
if (event.key === "Escape") {
|
|
30
|
+
setOpen(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
35
|
+
document.addEventListener("keydown", onKeyDown);
|
|
36
|
+
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
39
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
40
|
+
};
|
|
41
|
+
}, [open]);
|
|
42
|
+
|
|
43
|
+
const absoluteMarkdownUrl = useMemo(() => {
|
|
44
|
+
if (!origin) return props.markdownUrl;
|
|
45
|
+
return new URL(props.markdownUrl, origin).toString();
|
|
46
|
+
}, [origin, props.markdownUrl]);
|
|
47
|
+
|
|
48
|
+
const aiPrompt = useMemo(() => {
|
|
49
|
+
return `Read this documentation page and help me use it:\n\n${absoluteMarkdownUrl}`;
|
|
50
|
+
}, [absoluteMarkdownUrl]);
|
|
51
|
+
|
|
52
|
+
const options = [
|
|
53
|
+
{
|
|
54
|
+
label: "View as Markdown",
|
|
55
|
+
href: props.markdownUrl,
|
|
56
|
+
icon: FileText
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "Open in ChatGPT",
|
|
60
|
+
href: `https://chatgpt.com/?q=${encodeURIComponent(aiPrompt)}`,
|
|
61
|
+
icon: Sparkles
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "Open in Claude",
|
|
65
|
+
href: `https://claude.ai/new?q=${encodeURIComponent(aiPrompt)}`,
|
|
66
|
+
icon: Sparkles
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const copyLabel = {
|
|
71
|
+
idle: "Copy as Markdown",
|
|
72
|
+
copying: "Copying",
|
|
73
|
+
copied: "Copied",
|
|
74
|
+
failed: "Copy failed"
|
|
75
|
+
}[copyState];
|
|
76
|
+
|
|
77
|
+
async function copyMarkdown() {
|
|
78
|
+
setCopyState("copying");
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(props.markdownUrl);
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`Failed to load Markdown for ${props.title}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await navigator.clipboard.writeText(await response.text());
|
|
88
|
+
setCopyState("copied");
|
|
89
|
+
} catch {
|
|
90
|
+
setCopyState("failed");
|
|
91
|
+
} finally {
|
|
92
|
+
window.setTimeout(() => setCopyState("idle"), 1600);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="not-prose my-6 flex flex-wrap items-center gap-2 border-y border-fd-border/70 py-3 text-sm">
|
|
98
|
+
<button
|
|
99
|
+
className="inline-flex h-9 items-center gap-2 rounded-md border border-fd-border bg-fd-background px-3 font-medium text-fd-foreground shadow-xs transition hover:bg-fd-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
100
|
+
disabled={copyState === "copying"}
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => void copyMarkdown()}
|
|
103
|
+
>
|
|
104
|
+
{copyState === "copied" ? <Check className="size-4" aria-hidden="true" /> : <Copy className="size-4" aria-hidden="true" />}
|
|
105
|
+
{copyLabel}
|
|
106
|
+
</button>
|
|
107
|
+
|
|
108
|
+
<div className="relative" ref={menuRef}>
|
|
109
|
+
<button
|
|
110
|
+
aria-expanded={open}
|
|
111
|
+
aria-haspopup="menu"
|
|
112
|
+
className="inline-flex h-9 items-center gap-2 rounded-md border border-fd-border bg-fd-background px-3 font-medium text-fd-foreground shadow-xs transition hover:bg-fd-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => setOpen((value) => !value)}
|
|
115
|
+
>
|
|
116
|
+
Open
|
|
117
|
+
<ChevronDown className="size-4 text-fd-muted-foreground" aria-hidden="true" />
|
|
118
|
+
</button>
|
|
119
|
+
|
|
120
|
+
{open ? (
|
|
121
|
+
<div className="absolute left-0 top-10 z-20 w-56 overflow-hidden rounded-md border border-fd-border bg-fd-popover p-1 text-fd-popover-foreground shadow-lg" role="menu">
|
|
122
|
+
{options.map((option) => {
|
|
123
|
+
const Icon = option.icon;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<a
|
|
127
|
+
className="flex min-h-9 items-center gap-2 rounded-sm px-2.5 py-2 text-sm outline-none transition hover:bg-fd-muted focus-visible:bg-fd-muted"
|
|
128
|
+
href={option.href}
|
|
129
|
+
key={option.label}
|
|
130
|
+
onClick={() => setOpen(false)}
|
|
131
|
+
rel="noreferrer"
|
|
132
|
+
role="menuitem"
|
|
133
|
+
target="_blank"
|
|
134
|
+
>
|
|
135
|
+
<Icon className="size-4 text-fd-muted-foreground" aria-hidden="true" />
|
|
136
|
+
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
|
137
|
+
<ExternalLink className="size-3.5 text-fd-muted-foreground" aria-hidden="true" />
|
|
138
|
+
</a>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import browserCollections from "collections/browser";
|
|
2
|
+
import { FrameworkProvider } from "fumadocs-core/framework";
|
|
3
|
+
import { useFumadocsLoader, type SerializedPageTree } from "fumadocs-core/source/client";
|
|
4
|
+
import { DocsLayout } from "fumadocs-ui/layouts/docs";
|
|
5
|
+
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
|
|
6
|
+
import { RootProvider } from "fumadocs-ui/provider/base";
|
|
7
|
+
import { DocsPageActions } from "./docs-page-actions";
|
|
8
|
+
import { baseOptions } from "./docs-layout";
|
|
9
|
+
import { getMDXComponents } from "./docs-mdx";
|
|
10
|
+
|
|
11
|
+
export type DocsPageData = {
|
|
12
|
+
path: string;
|
|
13
|
+
url: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
pageTree: SerializedPageTree;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const clientLoader = browserCollections.docs.createClientLoader({
|
|
20
|
+
component({ toc, frontmatter, default: MDX }) {
|
|
21
|
+
const pageUrl = currentDocsData.url.replace(/\/$/, "") || "/docs";
|
|
22
|
+
const markdownUrl = pageUrl === "/docs" ? "/docs/index.md" : `${pageUrl}.md`;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<DocsPage toc={toc}>
|
|
26
|
+
<DocsTitle>{frontmatter.title}</DocsTitle>
|
|
27
|
+
<DocsDescription>{frontmatter.description}</DocsDescription>
|
|
28
|
+
<DocsPageActions markdownUrl={markdownUrl} title={frontmatter.title} />
|
|
29
|
+
<DocsBody>
|
|
30
|
+
<MDX components={getMDXComponents()} />
|
|
31
|
+
</DocsBody>
|
|
32
|
+
</DocsPage>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let currentDocsData: { url: string } = { url: "/docs" };
|
|
38
|
+
|
|
39
|
+
export async function preloadDocsContent(data: DocsPageData): Promise<void> {
|
|
40
|
+
currentDocsData = data;
|
|
41
|
+
await clientLoader.preload(data.path);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function DocsRoot({ data }: { data: DocsPageData }) {
|
|
45
|
+
currentDocsData = data;
|
|
46
|
+
const loaderData = useFumadocsLoader(data);
|
|
47
|
+
const content = clientLoader.useContent(loaderData.path);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<FrameworkProvider
|
|
51
|
+
Link={({ href, prefetch: _prefetch, ...props }) => <a href={href} {...props} />}
|
|
52
|
+
useParams={() => ({})}
|
|
53
|
+
usePathname={() => currentDocsData.url}
|
|
54
|
+
useRouter={() => ({
|
|
55
|
+
push(url) {
|
|
56
|
+
if (typeof window !== "undefined") window.location.href = url;
|
|
57
|
+
},
|
|
58
|
+
refresh() {
|
|
59
|
+
if (typeof window !== "undefined") window.location.reload();
|
|
60
|
+
}
|
|
61
|
+
})}
|
|
62
|
+
>
|
|
63
|
+
<RootProvider search={{ options: { api: "/docs/api/search" } }}>
|
|
64
|
+
<DocsLayout {...baseOptions()} tree={loaderData.pageTree}>
|
|
65
|
+
{content}
|
|
66
|
+
</DocsLayout>
|
|
67
|
+
</RootProvider>
|
|
68
|
+
</FrameworkProvider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { source } from "./source";
|
|
2
|
+
|
|
3
|
+
type DocsPage = ReturnType<typeof source.getPage>;
|
|
4
|
+
|
|
5
|
+
export async function getDocsMarkdownResponse(path: string | null) {
|
|
6
|
+
const page = source.getPage(getDocsSlugs(path));
|
|
7
|
+
|
|
8
|
+
if (!page) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return markdownResponse(await getLLMText(page));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getLLMText(page: NonNullable<DocsPage>) {
|
|
16
|
+
const markdown = renderRuntimeMdxPlaceholders((await page.data.getText("processed")) ?? "");
|
|
17
|
+
|
|
18
|
+
return `# ${page.data.title} (${page.url})\n\n${markdown}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderRuntimeMdxPlaceholders(markdown: string) {
|
|
22
|
+
return markdown
|
|
23
|
+
.replace(/<McpServerUrl(Block)?([^>]*)\/>/g, (_match, block: string | undefined, attrs: string) => {
|
|
24
|
+
const value = `[current-app-origin]${mdxStringAttribute(attrs, "path") ?? "/mcp"}`;
|
|
25
|
+
return block ? `\`\`\`text\n${value}\n\`\`\`` : `\`${value}\``;
|
|
26
|
+
})
|
|
27
|
+
.replace(/<ClaudeMcpAddCommand([^>]*)\/>/g, (_match, attrs: string) => {
|
|
28
|
+
const path = mdxStringAttribute(attrs, "path") ?? "/mcp";
|
|
29
|
+
const name = mdxStringAttribute(attrs, "name") ?? "this-app";
|
|
30
|
+
return `\`\`\`bash\nclaude mcp add --transport http ${name} "[current-app-origin]${path}"\n\`\`\``;
|
|
31
|
+
})
|
|
32
|
+
.replace(/<CodexMcpAddCommand([^>]*)\/>/g, (_match, attrs: string) => {
|
|
33
|
+
const path = mdxStringAttribute(attrs, "path") ?? "/mcp";
|
|
34
|
+
const name = mdxStringAttribute(attrs, "name") ?? "this-app";
|
|
35
|
+
return `\`\`\`bash\ncodex mcp add ${name} --url "[current-app-origin]${path}"\n\`\`\``;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mdxStringAttribute(attrs: string, name: string) {
|
|
40
|
+
const match = attrs.match(new RegExp(`\\b${name}="([^"]*)"`));
|
|
41
|
+
return match?.[1] ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function markdownResponse(markdown: string) {
|
|
45
|
+
return new Response(markdown, {
|
|
46
|
+
headers: {
|
|
47
|
+
"content-type": "text/markdown; charset=utf-8"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getDocsSlugs(path: string | null) {
|
|
53
|
+
const pathname = path?.trim() || "/docs";
|
|
54
|
+
const withoutBase = pathname
|
|
55
|
+
.replace(/\.mdx?$/, "")
|
|
56
|
+
.replace(/^\/docs(?:\/|$)/, "")
|
|
57
|
+
.replace(/^\/+|\/+$/g, "");
|
|
58
|
+
const withoutIndex = withoutBase === "index" ? "" : withoutBase.replace(/\/index$/, "");
|
|
59
|
+
|
|
60
|
+
return withoutIndex ? withoutIndex.split("/").filter(Boolean) : [];
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getLLMText, markdownResponse } from "../docs/llm-text";
|
|
2
|
+
import { source } from "../docs/source";
|
|
3
|
+
|
|
4
|
+
export async function GET(): Promise<Response> {
|
|
5
|
+
const pages = source.getPages().sort((left, right) => left.url.localeCompare(right.url));
|
|
6
|
+
const body = (await Promise.all(pages.map((page) => getLLMText(page)))).join("\n\n---\n\n");
|
|
7
|
+
return markdownResponse(body);
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const docsClientScriptSrc = import.meta.env.DEV ? "/src/docs/docs-client.tsx" : "/assets/raybase-docs-client.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "@vitejs/plugin-react/preamble";
|
|
2
|
+
import { StrictMode, startTransition } from "react";
|
|
3
|
+
import { hydrateRoot } from "react-dom/client";
|
|
4
|
+
import { DocsRoot, preloadDocsContent, type DocsPageData } from "./docs-page";
|
|
5
|
+
|
|
6
|
+
const root = document.getElementById("raybase-docs-root");
|
|
7
|
+
const dataScript = document.getElementById("raybase-docs-page-data");
|
|
8
|
+
|
|
9
|
+
if (root && dataScript?.textContent) {
|
|
10
|
+
const data = JSON.parse(dataScript.textContent) as DocsPageData;
|
|
11
|
+
|
|
12
|
+
startTransition(() => {
|
|
13
|
+
void preloadDocsContent(data).then(() => {
|
|
14
|
+
hydrateRoot(
|
|
15
|
+
root,
|
|
16
|
+
<StrictMode>
|
|
17
|
+
<DocsRoot data={data} />
|
|
18
|
+
</StrictMode>
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SerializedPageTree } from "fumadocs-core/source/client";
|
|
2
|
+
import type { DocsPageData } from "./docs-page";
|
|
3
|
+
import { preloadDocsContent } from "./docs-page";
|
|
4
|
+
import { source } from "./source";
|
|
5
|
+
|
|
6
|
+
export async function loadDocsPageTree(): Promise<SerializedPageTree> {
|
|
7
|
+
return source.serializePageTree(source.getPageTree());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadDocsPageData(slugs: string[]): Promise<DocsPageData | null> {
|
|
11
|
+
const page = source.getPage(slugs);
|
|
12
|
+
if (!page) return null;
|
|
13
|
+
|
|
14
|
+
const data = {
|
|
15
|
+
path: page.path,
|
|
16
|
+
url: page.url,
|
|
17
|
+
title: page.data.title,
|
|
18
|
+
description: page.data.description,
|
|
19
|
+
pageTree: await loadDocsPageTree()
|
|
20
|
+
} satisfies DocsPageData;
|
|
21
|
+
|
|
22
|
+
await preloadDocsContent(data);
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createFromSource } from "fumadocs-core/search/server";
|
|
2
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
3
|
+
import docsCss from "../styles-docs.css?inline";
|
|
4
|
+
import { docsClientScriptSrc } from "./docs-assets";
|
|
5
|
+
import { loadDocsPageData, loadDocsPageTree } from "./docs-data";
|
|
6
|
+
import { DocsRoot, type DocsPageData } from "./docs-page";
|
|
7
|
+
import { getDocsMarkdownResponse, getDocsSlugs } from "./llm-text";
|
|
8
|
+
import { source } from "./source";
|
|
9
|
+
|
|
10
|
+
const docsThemeInitScript =
|
|
11
|
+
"(function(){try{var t=localStorage.getItem('theme');var d=t==='dark'||((!t||t==='system')&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.classList.toggle('dark',d);}catch(e){}})();";
|
|
12
|
+
|
|
13
|
+
const search = createFromSource(source, {
|
|
14
|
+
language: "english"
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function handleDocsRequest(request: Request): Promise<Response> {
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
|
|
20
|
+
if (url.pathname === "/docs/api/search") {
|
|
21
|
+
return search.GET(request);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (url.pathname === "/docs/api/markdown") {
|
|
25
|
+
const response = await getDocsMarkdownResponse(url.searchParams.get("path"));
|
|
26
|
+
return response ?? new Response("Not found", { status: 404 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (/\.mdx?$/.test(url.pathname)) {
|
|
30
|
+
const response = await getDocsMarkdownResponse(url.pathname);
|
|
31
|
+
return response ?? new Response("Not found", { status: 404 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const data = await loadDocsPageData(getDocsSlugs(url.pathname));
|
|
35
|
+
if (!data) {
|
|
36
|
+
return docsHtmlResponse(
|
|
37
|
+
{
|
|
38
|
+
path: "",
|
|
39
|
+
url: url.pathname,
|
|
40
|
+
title: "Docs page not found",
|
|
41
|
+
pageTree: await loadDocsPageTree()
|
|
42
|
+
},
|
|
43
|
+
"<main class=\"mx-auto flex min-h-screen max-w-3xl flex-col justify-center px-6 py-16\"><p class=\"text-sm font-medium text-fd-muted-foreground\">404</p><h1 class=\"mt-3 text-3xl font-semibold text-fd-foreground\">Docs page not found</h1><p class=\"mt-3 text-fd-muted-foreground\">The requested documentation page does not exist.</p><a class=\"mt-6 inline-flex text-sm font-medium text-fd-primary underline-offset-4 hover:underline\" href=\"/docs\">Back to docs</a></main>",
|
|
44
|
+
404
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return docsHtmlResponse(data, await renderDocsBody(data));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function renderDocsBody(data: DocsPageData): Promise<string> {
|
|
52
|
+
const stream = await renderToReadableStream(<DocsRoot data={cloneDocsPageData(data)} />);
|
|
53
|
+
await stream.allReady;
|
|
54
|
+
return new Response(stream).text();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cloneDocsPageData(data: DocsPageData): DocsPageData {
|
|
58
|
+
return JSON.parse(JSON.stringify(data)) as DocsPageData;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function docsHtmlResponse(data: DocsPageData, body: string, status = 200): Response {
|
|
62
|
+
const html = `<!doctype html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="utf-8" />
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
67
|
+
<title>${escapeHtml(data.title)}</title>
|
|
68
|
+
<script>${docsThemeInitScript}</script>
|
|
69
|
+
<style id="raybase-docs-styles">${docsCss}</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<div id="raybase-docs-root">${body}</div>
|
|
73
|
+
<script type="application/json" id="raybase-docs-page-data">${escapeJson(data)}</script>
|
|
74
|
+
<script type="module" async src="${docsClientScriptSrc}"></script>
|
|
75
|
+
</body>
|
|
76
|
+
</html>`;
|
|
77
|
+
|
|
78
|
+
return new Response(html, {
|
|
79
|
+
status,
|
|
80
|
+
headers: {
|
|
81
|
+
"content-type": "text/html; charset=utf-8"
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function escapeJson(value: unknown): string {
|
|
87
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function escapeHtml(value: string): string {
|
|
91
|
+
return value.replace(/[&<>"']/g, (char) => {
|
|
92
|
+
switch (char) {
|
|
93
|
+
case "&":
|
|
94
|
+
return "&";
|
|
95
|
+
case "<":
|
|
96
|
+
return "<";
|
|
97
|
+
case ">":
|
|
98
|
+
return ">";
|
|
99
|
+
case "\"":
|
|
100
|
+
return """;
|
|
101
|
+
default:
|
|
102
|
+
return "'";
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
import { ThemeSwitch } from "fumadocs-ui/layouts/shared/slots/theme-switch";
|
|
4
|
+
import { ArrowLeft } from "lucide-react";
|
|
5
|
+
import { appConfig } from "../raybase.config";
|
|
6
|
+
|
|
7
|
+
const appHomeHref = "/";
|
|
8
|
+
|
|
9
|
+
export function baseOptions(): Omit<DocsLayoutProps, "children" | "tree"> {
|
|
10
|
+
return {
|
|
11
|
+
nav: {
|
|
12
|
+
title: DocsNavTitle,
|
|
13
|
+
url: appHomeHref
|
|
14
|
+
},
|
|
15
|
+
sidebar: {
|
|
16
|
+
footer: <DocsSidebarFooter />
|
|
17
|
+
},
|
|
18
|
+
themeSwitch: {
|
|
19
|
+
enabled: false
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function DocsNavTitle({ className, href = appHomeHref, ...props }: ComponentProps<"a">) {
|
|
25
|
+
return (
|
|
26
|
+
<div className={className}>
|
|
27
|
+
<div className="inline-flex min-w-0 flex-col items-start gap-0.5 leading-none">
|
|
28
|
+
<a
|
|
29
|
+
{...props}
|
|
30
|
+
href={href}
|
|
31
|
+
title={`Back to ${appConfig.appName}`}
|
|
32
|
+
className="max-w-44 truncate rounded-sm text-[0.9375rem] font-semibold leading-none text-fd-foreground underline-offset-4 transition-colors hover:text-fd-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
33
|
+
>
|
|
34
|
+
{appConfig.appName}
|
|
35
|
+
</a>
|
|
36
|
+
<span className="text-[0.6875rem] font-medium leading-none text-fd-muted-foreground">Docs</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function DocsSidebarFooter() {
|
|
43
|
+
return (
|
|
44
|
+
<div className="mt-2 flex min-w-0 items-center justify-between gap-3 border-t pt-3">
|
|
45
|
+
<a
|
|
46
|
+
href={appHomeHref}
|
|
47
|
+
title={`Back to ${appConfig.appName}`}
|
|
48
|
+
className="inline-flex min-w-0 items-center gap-1.5 rounded-md px-1 py-1 text-xs font-medium text-fd-muted-foreground transition-colors hover:text-fd-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring"
|
|
49
|
+
>
|
|
50
|
+
<ArrowLeft className="size-3.5 shrink-0" aria-hidden="true" />
|
|
51
|
+
<span className="truncate">Back to app</span>
|
|
52
|
+
</a>
|
|
53
|
+
<ThemeSwitch className="shrink-0 p-0" />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
|
|
2
|
+
import defaultMdxComponents from "fumadocs-ui/mdx";
|
|
3
|
+
import type { MDXComponents } from "mdx/types";
|
|
4
|
+
|
|
5
|
+
interface McpServerUrlProps {
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ClaudeMcpAddCommandProps extends McpServerUrlProps {
|
|
10
|
+
name?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CodexMcpAddCommandProps extends McpServerUrlProps {
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function browserOrigin() {
|
|
18
|
+
return typeof window === "undefined" ? "" : window.location.origin;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function currentUrl(path = "/mcp") {
|
|
22
|
+
const origin = browserOrigin();
|
|
23
|
+
return origin ? new URL(path, origin).toString() : path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function McpServerUrl(props: McpServerUrlProps) {
|
|
27
|
+
return <code suppressHydrationWarning>{currentUrl(props.path)}</code>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function McpServerUrlBlock(props: McpServerUrlProps) {
|
|
31
|
+
return (
|
|
32
|
+
<CodeBlock>
|
|
33
|
+
<Pre>
|
|
34
|
+
<code suppressHydrationWarning>{currentUrl(props.path)}</code>
|
|
35
|
+
</Pre>
|
|
36
|
+
</CodeBlock>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ClaudeMcpAddCommand(props: ClaudeMcpAddCommandProps) {
|
|
41
|
+
const command = `claude mcp add --transport http ${props.name ?? "this-app"} "${currentUrl(props.path)}"`;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<CodeBlock>
|
|
45
|
+
<Pre>
|
|
46
|
+
<code suppressHydrationWarning>{command}</code>
|
|
47
|
+
</Pre>
|
|
48
|
+
</CodeBlock>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function CodexMcpAddCommand(props: CodexMcpAddCommandProps) {
|
|
53
|
+
const command = `codex mcp add ${props.name ?? "this-app"} --url "${currentUrl(props.path)}"`;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<CodeBlock>
|
|
57
|
+
<Pre>
|
|
58
|
+
<code suppressHydrationWarning>{command}</code>
|
|
59
|
+
</Pre>
|
|
60
|
+
</CodeBlock>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getMDXComponents(components?: MDXComponents) {
|
|
65
|
+
return {
|
|
66
|
+
...defaultMdxComponents,
|
|
67
|
+
McpServerUrl,
|
|
68
|
+
McpServerUrlBlock,
|
|
69
|
+
ClaudeMcpAddCommand,
|
|
70
|
+
CodexMcpAddCommand,
|
|
71
|
+
...components
|
|
72
|
+
} satisfies MDXComponents;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const useMDXComponents = getMDXComponents;
|
|
76
|
+
|
|
77
|
+
declare global {
|
|
78
|
+
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
|
|
79
|
+
}
|