@raystack/chronicle 0.1.0-canary.323385a → 0.1.0-canary.3e58cd9
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 +268 -9902
- package/package.json +20 -12
- package/src/cli/commands/build.ts +27 -25
- package/src/cli/commands/dev.ts +24 -25
- package/src/cli/commands/init.ts +38 -132
- package/src/cli/commands/serve.ts +36 -49
- package/src/cli/commands/start.ts +20 -25
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +25 -26
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/resolve.ts +9 -3
- package/src/cli/utils/scaffold.ts +11 -124
- package/src/components/mdx/code.tsx +10 -1
- package/src/components/mdx/details.module.css +1 -26
- package/src/components/mdx/details.tsx +2 -3
- package/src/components/mdx/image.tsx +5 -34
- package/src/components/mdx/index.tsx +15 -1
- package/src/components/mdx/link.tsx +18 -15
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/components/ui/search.tsx +63 -51
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/config.ts +12 -35
- package/src/lib/head.tsx +49 -0
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +111 -0
- package/src/lib/source.ts +134 -63
- package/src/pages/ApiLayout.tsx +33 -0
- package/src/pages/ApiPage.tsx +73 -0
- package/src/pages/DocsLayout.tsx +18 -0
- package/src/pages/DocsPage.tsx +43 -0
- package/src/pages/NotFound.tsx +17 -0
- package/src/server/App.tsx +67 -0
- package/src/server/api/apis-proxy.ts +69 -0
- package/src/server/api/health.ts +5 -0
- package/src/server/api/page/[...slug].ts +17 -0
- package/src/server/api/search.ts +118 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +117 -0
- package/src/server/entry-client.tsx +86 -0
- package/src/server/entry-server.tsx +100 -0
- package/src/server/routes/llms.txt.ts +21 -0
- package/src/server/routes/og.tsx +75 -0
- package/src/server/routes/robots.txt.ts +11 -0
- package/src/server/routes/sitemap.xml.ts +40 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +126 -0
- package/src/themes/default/Layout.tsx +78 -48
- package/src/themes/default/Page.module.css +44 -0
- package/src/themes/default/Page.tsx +9 -11
- package/src/themes/default/Toc.tsx +25 -39
- package/src/themes/default/index.ts +7 -9
- package/src/themes/paper/ChapterNav.tsx +64 -45
- package/src/themes/paper/Layout.module.css +1 -1
- package/src/themes/paper/Layout.tsx +24 -12
- package/src/themes/paper/Page.module.css +16 -4
- package/src/themes/paper/Page.tsx +56 -63
- package/src/themes/paper/ReadingProgress.tsx +160 -139
- package/src/themes/paper/index.ts +5 -5
- package/src/themes/registry.ts +7 -7
- package/src/types/config.ts +11 -0
- package/src/types/content.ts +6 -21
- package/src/types/globals.d.ts +4 -0
- package/src/types/theme.ts +4 -3
- package/tsconfig.json +2 -3
- package/next.config.mjs +0 -10
- package/source.config.ts +0 -50
- package/src/app/[[...slug]]/layout.tsx +0 -15
- package/src/app/[[...slug]]/page.tsx +0 -57
- package/src/app/api/apis-proxy/route.ts +0 -59
- package/src/app/api/health/route.ts +0 -3
- package/src/app/api/search/route.ts +0 -90
- package/src/app/apis/[[...slug]]/layout.tsx +0 -26
- package/src/app/apis/[[...slug]]/page.tsx +0 -57
- package/src/app/layout.tsx +0 -26
- package/src/app/llms-full.txt/route.ts +0 -18
- package/src/app/llms.txt/route.ts +0 -15
- package/src/app/providers.tsx +0 -8
- package/src/cli/utils/process.ts +0 -7
- package/src/themes/default/font.ts +0 -6
- /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Head } from '@/lib/head';
|
|
2
|
+
import { usePageContext } from '@/lib/page-context';
|
|
3
|
+
import { getTheme } from '@/themes/registry';
|
|
4
|
+
|
|
5
|
+
interface DocsPageProps {
|
|
6
|
+
slug: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DocsPage({ slug }: DocsPageProps) {
|
|
10
|
+
const { config, tree, page } = usePageContext();
|
|
11
|
+
|
|
12
|
+
if (!page) return null;
|
|
13
|
+
|
|
14
|
+
const { Page } = getTheme(config.theme?.name);
|
|
15
|
+
const pageUrl = config.url ? `${config.url}/${slug.join('/')}` : undefined;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<Head
|
|
20
|
+
title={page.frontmatter.title}
|
|
21
|
+
description={page.frontmatter.description}
|
|
22
|
+
config={config}
|
|
23
|
+
jsonLd={{
|
|
24
|
+
'@context': 'https://schema.org',
|
|
25
|
+
'@type': 'Article',
|
|
26
|
+
headline: page.frontmatter.title,
|
|
27
|
+
description: page.frontmatter.description,
|
|
28
|
+
...(pageUrl && { url: pageUrl })
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
<Page
|
|
32
|
+
page={{
|
|
33
|
+
slug,
|
|
34
|
+
frontmatter: page.frontmatter,
|
|
35
|
+
content: page.content,
|
|
36
|
+
toc: page.toc
|
|
37
|
+
}}
|
|
38
|
+
config={config}
|
|
39
|
+
tree={tree}
|
|
40
|
+
/>
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Flex, Headline, Text } from '@raystack/apsara';
|
|
2
|
+
|
|
3
|
+
export function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<Flex
|
|
6
|
+
direction='column'
|
|
7
|
+
align='center'
|
|
8
|
+
justify='center'
|
|
9
|
+
style={{ minHeight: '60vh' }}
|
|
10
|
+
>
|
|
11
|
+
<Headline size='large' as='h1'>
|
|
12
|
+
404
|
|
13
|
+
</Headline>
|
|
14
|
+
<Text size={3}>Page not found</Text>
|
|
15
|
+
</Flex>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import '@raystack/apsara/normalize.css';
|
|
2
|
+
import '@raystack/apsara/style.css';
|
|
3
|
+
import { ThemeProvider } from '@raystack/apsara';
|
|
4
|
+
import { useLocation } from 'react-router';
|
|
5
|
+
import { Head } from '@/lib/head';
|
|
6
|
+
import { usePageContext } from '@/lib/page-context';
|
|
7
|
+
import { ApiLayout } from '@/pages/ApiLayout';
|
|
8
|
+
import { ApiPage } from '@/pages/ApiPage';
|
|
9
|
+
import { DocsLayout } from '@/pages/DocsLayout';
|
|
10
|
+
import { DocsPage } from '@/pages/DocsPage';
|
|
11
|
+
import type { ChronicleConfig } from '@/types';
|
|
12
|
+
|
|
13
|
+
function resolveRoute(pathname: string) {
|
|
14
|
+
if (pathname.startsWith('/apis')) {
|
|
15
|
+
const slug = pathname
|
|
16
|
+
.replace(/^\/apis\/?/, '')
|
|
17
|
+
.split('/')
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
return { type: 'api' as const, slug };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const slug =
|
|
23
|
+
pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
|
|
24
|
+
return { type: 'docs' as const, slug };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function App() {
|
|
28
|
+
const { pathname } = useLocation();
|
|
29
|
+
const { config } = usePageContext();
|
|
30
|
+
const route = resolveRoute(pathname);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ThemeProvider enableSystem>
|
|
34
|
+
<RootHead config={config} />
|
|
35
|
+
{route.type === 'api' ? (
|
|
36
|
+
<ApiLayout>
|
|
37
|
+
<ApiPage slug={route.slug} />
|
|
38
|
+
</ApiLayout>
|
|
39
|
+
) : (
|
|
40
|
+
<DocsLayout>
|
|
41
|
+
<DocsPage slug={route.slug} />
|
|
42
|
+
</DocsLayout>
|
|
43
|
+
)}
|
|
44
|
+
</ThemeProvider>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function RootHead({ config }: { config: ChronicleConfig }) {
|
|
49
|
+
return (
|
|
50
|
+
<Head
|
|
51
|
+
title={config.title}
|
|
52
|
+
description={config.description}
|
|
53
|
+
config={config}
|
|
54
|
+
jsonLd={
|
|
55
|
+
config.url
|
|
56
|
+
? {
|
|
57
|
+
'@context': 'https://schema.org',
|
|
58
|
+
'@type': 'WebSite',
|
|
59
|
+
name: config.title,
|
|
60
|
+
description: config.description,
|
|
61
|
+
url: config.url
|
|
62
|
+
}
|
|
63
|
+
: undefined
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
+
import { loadConfig } from '@/lib/config';
|
|
3
|
+
import { loadApiSpecs } from '@/lib/openapi';
|
|
4
|
+
|
|
5
|
+
interface ProxyRequest {
|
|
6
|
+
specName: string;
|
|
7
|
+
method: string;
|
|
8
|
+
path: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default defineHandler(async event => {
|
|
14
|
+
if (event.req.method !== 'POST') {
|
|
15
|
+
throw new HTTPError({ status: 405, message: 'Method not allowed' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { specName, method, path, headers, body } =
|
|
19
|
+
(await event.req.json()) as ProxyRequest;
|
|
20
|
+
|
|
21
|
+
if (!specName || !method || !path) {
|
|
22
|
+
throw new HTTPError({
|
|
23
|
+
status: 400,
|
|
24
|
+
message: 'Missing specName, method, or path'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
const specs = await loadApiSpecs(config.api ?? []);
|
|
30
|
+
const spec = specs.find(s => s.name === specName);
|
|
31
|
+
|
|
32
|
+
if (!spec) {
|
|
33
|
+
throw new HTTPError({ status: 404, message: `Unknown spec: ${specName}` });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (/^[a-z]+:\/\//i.test(path) || path.includes('..')) {
|
|
37
|
+
throw new HTTPError({ status: 400, message: 'Invalid path' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const url = spec.server.url + path;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method,
|
|
45
|
+
headers,
|
|
46
|
+
body: body ? JSON.stringify(body) : undefined
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
50
|
+
const responseBody = contentType.includes('application/json')
|
|
51
|
+
? await response.json()
|
|
52
|
+
: await response.text();
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
status: response.status,
|
|
56
|
+
statusText: response.statusText,
|
|
57
|
+
body: responseBody
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message =
|
|
61
|
+
error instanceof Error
|
|
62
|
+
? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}`
|
|
63
|
+
: 'Request failed';
|
|
64
|
+
throw new HTTPError({
|
|
65
|
+
status: 502,
|
|
66
|
+
message: `Could not reach ${url}\n${message}`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
+
import { getPage, extractFrontmatter, getRelativePath } from '@/lib/source';
|
|
3
|
+
|
|
4
|
+
export default defineHandler(async event => {
|
|
5
|
+
const slugParam = event.context.params?.slug ?? '';
|
|
6
|
+
const slug = slugParam ? slugParam.split('/') : [];
|
|
7
|
+
const page = await getPage(slug);
|
|
8
|
+
|
|
9
|
+
if (!page) {
|
|
10
|
+
throw new HTTPError({ status: 404, message: 'Page not found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
|
|
15
|
+
relativePath: getRelativePath(page),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import MiniSearch from 'minisearch';
|
|
2
|
+
import { defineHandler } from 'nitro';
|
|
3
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
4
|
+
import { getSpecSlug } from '@/lib/api-routes';
|
|
5
|
+
import { loadConfig } from '@/lib/config';
|
|
6
|
+
import { loadApiSpecs } from '@/lib/openapi';
|
|
7
|
+
import { getPages, extractFrontmatter } from '@/lib/source';
|
|
8
|
+
|
|
9
|
+
interface SearchDocument {
|
|
10
|
+
id: string;
|
|
11
|
+
url: string;
|
|
12
|
+
title: string;
|
|
13
|
+
content: string;
|
|
14
|
+
type: 'page' | 'api';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let searchIndex: MiniSearch<SearchDocument> | null = null;
|
|
18
|
+
let cachedDocs: SearchDocument[] | null = null;
|
|
19
|
+
|
|
20
|
+
function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
|
|
21
|
+
const index = new MiniSearch<SearchDocument>({
|
|
22
|
+
fields: ['title', 'content'],
|
|
23
|
+
storeFields: ['url', 'title', 'type'],
|
|
24
|
+
searchOptions: {
|
|
25
|
+
boost: { title: 2 },
|
|
26
|
+
fuzzy: 0.2,
|
|
27
|
+
prefix: true
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
index.addAll(docs);
|
|
31
|
+
return index;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function scanContent(): Promise<SearchDocument[]> {
|
|
35
|
+
const pages = await getPages();
|
|
36
|
+
return pages.map(p => {
|
|
37
|
+
const fm = extractFrontmatter(p);
|
|
38
|
+
return {
|
|
39
|
+
id: p.url,
|
|
40
|
+
url: p.url,
|
|
41
|
+
title: fm.title,
|
|
42
|
+
content: fm.description ?? '',
|
|
43
|
+
type: 'page' as const
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function buildApiDocs(): Promise<SearchDocument[]> {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
if (!config.api?.length) return [];
|
|
51
|
+
|
|
52
|
+
const docs: SearchDocument[] = [];
|
|
53
|
+
const specs = await loadApiSpecs(config.api);
|
|
54
|
+
|
|
55
|
+
for (const spec of specs) {
|
|
56
|
+
const specSlug = getSpecSlug(spec);
|
|
57
|
+
const paths = spec.document.paths ?? {};
|
|
58
|
+
for (const [, pathItem] of Object.entries(paths)) {
|
|
59
|
+
if (!pathItem) continue;
|
|
60
|
+
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
|
|
61
|
+
const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
|
|
62
|
+
if (!op?.operationId) continue;
|
|
63
|
+
const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
|
|
64
|
+
docs.push({
|
|
65
|
+
id: url,
|
|
66
|
+
url,
|
|
67
|
+
title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
|
|
68
|
+
content: op.description ?? '',
|
|
69
|
+
type: 'api'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return docs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getDocs(): Promise<SearchDocument[]> {
|
|
79
|
+
if (cachedDocs) return cachedDocs;
|
|
80
|
+
const [contentDocs, apiDocs] = await Promise.all([
|
|
81
|
+
scanContent(),
|
|
82
|
+
buildApiDocs()
|
|
83
|
+
]);
|
|
84
|
+
cachedDocs = [...contentDocs, ...apiDocs];
|
|
85
|
+
return cachedDocs;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function getIndex(): Promise<MiniSearch<SearchDocument>> {
|
|
89
|
+
if (searchIndex) return searchIndex;
|
|
90
|
+
const docs = await getDocs();
|
|
91
|
+
searchIndex = createIndex(docs);
|
|
92
|
+
return searchIndex;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default defineHandler(async event => {
|
|
96
|
+
const query = event.url.searchParams.get('query') ?? '';
|
|
97
|
+
const index = await getIndex();
|
|
98
|
+
|
|
99
|
+
if (!query) {
|
|
100
|
+
const docs = await getDocs();
|
|
101
|
+
return docs
|
|
102
|
+
.filter(d => d.type === 'page')
|
|
103
|
+
.slice(0, 8)
|
|
104
|
+
.map(d => ({
|
|
105
|
+
id: d.id,
|
|
106
|
+
url: d.url,
|
|
107
|
+
type: d.type,
|
|
108
|
+
content: d.title
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return index.search(query).map(r => ({
|
|
113
|
+
id: r.id,
|
|
114
|
+
url: r.url,
|
|
115
|
+
type: r.type,
|
|
116
|
+
content: r.title
|
|
117
|
+
}));
|
|
118
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineHandler } from 'nitro';
|
|
2
|
+
import { loadConfig } from '@/lib/config';
|
|
3
|
+
import { loadApiSpecs } from '@/lib/openapi';
|
|
4
|
+
|
|
5
|
+
export default defineHandler(async () => {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const specs = config.api?.length ? await loadApiSpecs(config.api) : [];
|
|
8
|
+
return specs;
|
|
9
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import { visit } from 'unist-util-visit';
|
|
8
|
+
import type { Heading, Text } from 'mdast';
|
|
9
|
+
import { getSpecSlug } from '@/lib/api-routes';
|
|
10
|
+
import { loadConfig } from '@/lib/config';
|
|
11
|
+
import { loadApiSpecs } from '@/lib/openapi';
|
|
12
|
+
|
|
13
|
+
interface SearchDocument {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
title: string;
|
|
17
|
+
content: string;
|
|
18
|
+
type: 'page' | 'api';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractHeadings(markdown: string): string {
|
|
22
|
+
const tree = unified().use(remarkParse).parse(markdown);
|
|
23
|
+
const headings: string[] = [];
|
|
24
|
+
visit(tree, 'heading', (node: Heading) => {
|
|
25
|
+
const text = node.children
|
|
26
|
+
.filter((child): child is Text => child.type === 'text')
|
|
27
|
+
.map(child => child.value)
|
|
28
|
+
.join('');
|
|
29
|
+
if (text) headings.push(text);
|
|
30
|
+
});
|
|
31
|
+
return headings.join(' ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function scanContent(contentDir: string): Promise<SearchDocument[]> {
|
|
35
|
+
const docs: SearchDocument[] = [];
|
|
36
|
+
|
|
37
|
+
async function scan(dir: string, prefix: string[] = []) {
|
|
38
|
+
try {
|
|
39
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
42
|
+
continue;
|
|
43
|
+
const fullPath = path.join(dir, entry.name);
|
|
44
|
+
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
await scan(fullPath, [...prefix, entry.name]);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
|
|
51
|
+
continue;
|
|
52
|
+
|
|
53
|
+
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
54
|
+
const { data: fm, content } = matter(raw);
|
|
55
|
+
const baseName = entry.name.replace(/\.(mdx|md)$/, '');
|
|
56
|
+
const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
|
|
57
|
+
const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
|
|
58
|
+
|
|
59
|
+
docs.push({
|
|
60
|
+
id: url,
|
|
61
|
+
url,
|
|
62
|
+
title: fm.title ?? baseName,
|
|
63
|
+
content: extractHeadings(content),
|
|
64
|
+
type: 'page'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
/* directory not readable */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await scan(contentDir);
|
|
73
|
+
return docs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function buildApiDocs(): Promise<SearchDocument[]> {
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
if (!config.api?.length) return [];
|
|
79
|
+
|
|
80
|
+
const docs: SearchDocument[] = [];
|
|
81
|
+
const specs = await loadApiSpecs(config.api);
|
|
82
|
+
|
|
83
|
+
for (const spec of specs) {
|
|
84
|
+
const specSlug = getSpecSlug(spec);
|
|
85
|
+
const paths = spec.document.paths ?? {};
|
|
86
|
+
for (const [, pathItem] of Object.entries(paths)) {
|
|
87
|
+
if (!pathItem) continue;
|
|
88
|
+
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
|
|
89
|
+
const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
|
|
90
|
+
if (!op?.operationId) continue;
|
|
91
|
+
const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
|
|
92
|
+
docs.push({
|
|
93
|
+
id: url,
|
|
94
|
+
url,
|
|
95
|
+
title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
|
|
96
|
+
content: op.description ?? '',
|
|
97
|
+
type: 'api'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return docs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function generateSearchIndex(contentDir: string, outDir: string) {
|
|
107
|
+
const [contentDocs, apiDocs] = await Promise.all([
|
|
108
|
+
scanContent(contentDir),
|
|
109
|
+
buildApiDocs()
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const documents = [...contentDocs, ...apiDocs];
|
|
113
|
+
const outPath = path.join(outDir, 'search-index.json');
|
|
114
|
+
await fs.writeFile(outPath, JSON.stringify(documents));
|
|
115
|
+
|
|
116
|
+
return documents.length;
|
|
117
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import '@vitejs/plugin-react/preamble';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
4
|
+
import { BrowserRouter } from 'react-router';
|
|
5
|
+
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
6
|
+
import { mdxComponents } from '@/components/mdx';
|
|
7
|
+
import { PageProvider } from '@/lib/page-context';
|
|
8
|
+
import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
|
|
9
|
+
import type { ApiSpec } from '@/lib/openapi';
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
import { App } from './App';
|
|
12
|
+
|
|
13
|
+
interface EmbeddedData {
|
|
14
|
+
config: ChronicleConfig;
|
|
15
|
+
tree: Root;
|
|
16
|
+
slug: string[];
|
|
17
|
+
frontmatter: Frontmatter;
|
|
18
|
+
relativePath: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
|
|
22
|
+
'../../.content/**/*.{mdx,md}'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
|
|
26
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
27
|
+
const key = relativePath.endsWith('.md')
|
|
28
|
+
? `../../.content/${withoutExt}.md`
|
|
29
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
30
|
+
const loader = contentModules[key];
|
|
31
|
+
if (!loader) return { content: null, toc: [] };
|
|
32
|
+
const mod = await loader();
|
|
33
|
+
const content = mod.default
|
|
34
|
+
? React.createElement(mod.default, { components: mdxComponents })
|
|
35
|
+
: null;
|
|
36
|
+
return { content, toc: mod.toc ?? [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function hydrate() {
|
|
40
|
+
try {
|
|
41
|
+
const embedded = (
|
|
42
|
+
window as unknown as { __PAGE_DATA__?: EmbeddedData }
|
|
43
|
+
).__PAGE_DATA__;
|
|
44
|
+
|
|
45
|
+
const config: ChronicleConfig = embedded?.config ?? {
|
|
46
|
+
title: 'Documentation'
|
|
47
|
+
};
|
|
48
|
+
const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
|
|
49
|
+
const isApiPage =
|
|
50
|
+
window.location.pathname.startsWith('/apis') && !!config.api?.length;
|
|
51
|
+
const apiSpecs: ApiSpec[] = isApiPage
|
|
52
|
+
? await fetch('/api/specs')
|
|
53
|
+
.then(r => r.json())
|
|
54
|
+
.catch(() => [])
|
|
55
|
+
: [];
|
|
56
|
+
|
|
57
|
+
const page = embedded?.relativePath
|
|
58
|
+
? {
|
|
59
|
+
slug: embedded.slug,
|
|
60
|
+
frontmatter: embedded.frontmatter,
|
|
61
|
+
...(await loadMdxModule(embedded.relativePath)),
|
|
62
|
+
}
|
|
63
|
+
: null;
|
|
64
|
+
|
|
65
|
+
hydrateRoot(
|
|
66
|
+
document.getElementById('root') as HTMLElement,
|
|
67
|
+
<BrowserRouter>
|
|
68
|
+
<ReactRouterProvider>
|
|
69
|
+
<PageProvider
|
|
70
|
+
initialConfig={config}
|
|
71
|
+
initialTree={tree}
|
|
72
|
+
initialPage={page}
|
|
73
|
+
initialApiSpecs={apiSpecs}
|
|
74
|
+
loadMdx={loadMdxModule}
|
|
75
|
+
>
|
|
76
|
+
<App />
|
|
77
|
+
</PageProvider>
|
|
78
|
+
</ReactRouterProvider>
|
|
79
|
+
</BrowserRouter>
|
|
80
|
+
);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('Hydration failed:', err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
hydrate();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import '@raystack/apsara/normalize.css';
|
|
2
|
+
import '@raystack/apsara/style.css';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { renderToReadableStream } from 'react-dom/server.edge';
|
|
5
|
+
import { StaticRouter } from 'react-router';
|
|
6
|
+
import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
|
|
7
|
+
import { mdxComponents } from '@/components/mdx';
|
|
8
|
+
import { loadConfig } from '@/lib/config';
|
|
9
|
+
import { loadApiSpecs } from '@/lib/openapi';
|
|
10
|
+
import { PageProvider } from '@/lib/page-context';
|
|
11
|
+
import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath } from '@/lib/source';
|
|
12
|
+
import { App } from './App';
|
|
13
|
+
|
|
14
|
+
import clientAssets from './entry-client?assets=client';
|
|
15
|
+
import serverAssets from './entry-server?assets=ssr';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
async fetch(req: Request) {
|
|
19
|
+
const url = new URL(req.url);
|
|
20
|
+
const pathname = url.pathname;
|
|
21
|
+
const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
|
|
22
|
+
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
const apiSpecs = config.api?.length
|
|
25
|
+
? await loadApiSpecs(config.api).catch(() => [])
|
|
26
|
+
: [];
|
|
27
|
+
|
|
28
|
+
const [tree, page] = await Promise.all([
|
|
29
|
+
getPageTree(),
|
|
30
|
+
getPage(slug),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const relativePath = page ? getRelativePath(page) : null;
|
|
34
|
+
const mdxModule = relativePath ? await loadPageModule(relativePath) : null;
|
|
35
|
+
|
|
36
|
+
const pageData = page
|
|
37
|
+
? {
|
|
38
|
+
slug,
|
|
39
|
+
frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
|
|
40
|
+
content: mdxModule?.default
|
|
41
|
+
? React.createElement(mdxModule.default, { components: mdxComponents })
|
|
42
|
+
: null,
|
|
43
|
+
toc: mdxModule?.toc ?? [],
|
|
44
|
+
}
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
const embeddedData = {
|
|
48
|
+
config,
|
|
49
|
+
tree,
|
|
50
|
+
slug,
|
|
51
|
+
frontmatter: pageData?.frontmatter ?? null,
|
|
52
|
+
relativePath,
|
|
53
|
+
};
|
|
54
|
+
const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c');
|
|
55
|
+
|
|
56
|
+
const assets = clientAssets.merge(serverAssets);
|
|
57
|
+
|
|
58
|
+
const stream = await renderToReadableStream(
|
|
59
|
+
<html lang="en">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charSet="UTF-8" />
|
|
62
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
63
|
+
{assets.css.map((attr: { href: string }) => (
|
|
64
|
+
<link key={attr.href} rel="stylesheet" {...attr} />
|
|
65
|
+
))}
|
|
66
|
+
{assets.js.map((attr: { href: string }) => (
|
|
67
|
+
<link key={attr.href} rel="modulepreload" {...attr} />
|
|
68
|
+
))}
|
|
69
|
+
<script type="module" src={assets.entry} />
|
|
70
|
+
<script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div id="root">
|
|
74
|
+
<StaticRouter location={pathname}>
|
|
75
|
+
<ReactRouterProvider>
|
|
76
|
+
<PageProvider
|
|
77
|
+
initialConfig={config}
|
|
78
|
+
initialTree={tree}
|
|
79
|
+
initialPage={pageData}
|
|
80
|
+
initialApiSpecs={apiSpecs}
|
|
81
|
+
loadMdx={async () => ({ content: null, toc: [] })}
|
|
82
|
+
>
|
|
83
|
+
<App />
|
|
84
|
+
</PageProvider>
|
|
85
|
+
</ReactRouterProvider>
|
|
86
|
+
</StaticRouter>
|
|
87
|
+
</div>
|
|
88
|
+
</body>
|
|
89
|
+
</html>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const isApiRoute = pathname.startsWith('/apis');
|
|
93
|
+
const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
|
|
94
|
+
|
|
95
|
+
return new Response(stream, {
|
|
96
|
+
status,
|
|
97
|
+
headers: { 'Content-Type': 'text/html;charset=utf-8' },
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
+
import { loadConfig } from '@/lib/config';
|
|
3
|
+
import { getPages, extractFrontmatter } from '@/lib/source';
|
|
4
|
+
|
|
5
|
+
export default defineHandler(async event => {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
|
|
8
|
+
if (!config.llms?.enabled) {
|
|
9
|
+
throw new HTTPError({ status: 404, message: 'Not Found' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const pages = await getPages();
|
|
13
|
+
const index = pages.map(p => {
|
|
14
|
+
const fm = extractFrontmatter(p);
|
|
15
|
+
return `- [${fm.title}](${p.url})`;
|
|
16
|
+
}).join('\n');
|
|
17
|
+
const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
|
|
18
|
+
|
|
19
|
+
event.res.headers.set('Content-Type', 'text/plain');
|
|
20
|
+
return body;
|
|
21
|
+
});
|