@raystack/chronicle 0.3.0 → 0.4.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/dist/cli/index.js +289 -9931
- package/package.json +20 -12
- package/src/cli/commands/build.ts +28 -31
- package/src/cli/commands/dev.ts +24 -31
- package/src/cli/commands/init.ts +38 -132
- package/src/cli/commands/serve.ts +36 -55
- package/src/cli/commands/start.ts +20 -31
- 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 +7 -3
- package/src/cli/utils/scaffold.ts +11 -130
- 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 -36
- 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/remark-strip-md-extensions.ts +14 -0
- package/src/lib/source.ts +139 -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 +72 -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 +18 -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 +88 -0
- package/src/server/entry-server.tsx +102 -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 +129 -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 +14 -7
- package/src/types/content.ts +5 -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 -51
- package/src/app/[[...slug]]/layout.tsx +0 -15
- package/src/app/[[...slug]]/page.tsx +0 -106
- 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 -117
- package/src/app/layout.tsx +0 -57
- package/src/app/llms-full.txt/route.ts +0 -18
- package/src/app/llms.txt/route.ts +0 -15
- package/src/app/og/route.tsx +0 -62
- package/src/app/providers.tsx +0 -8
- package/src/app/robots.ts +0 -10
- package/src/app/sitemap.ts +0 -29
- 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
package/src/lib/source.ts
CHANGED
|
@@ -1,67 +1,143 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
13
|
-
return [...items].sort((a, b) => {
|
|
14
|
-
const orderA = a.frontmatter?.order ?? Number.MAX_SAFE_INTEGER
|
|
15
|
-
const orderB = b.frontmatter?.order ?? Number.MAX_SAFE_INTEGER
|
|
16
|
-
return orderA - orderB
|
|
17
|
-
})
|
|
18
|
-
}
|
|
1
|
+
import { loader } from 'fumadocs-core/source';
|
|
2
|
+
import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
|
|
3
|
+
import type { MDXContent } from 'mdx/types';
|
|
4
|
+
import type { TableOfContents } from 'fumadocs-core/toc';
|
|
5
|
+
import type { Frontmatter } from '@/types';
|
|
6
|
+
|
|
7
|
+
const CONTENT_PREFIX = '../../.content/';
|
|
8
|
+
|
|
9
|
+
const frontmatterGlob: Record<string, Record<string, unknown>> = import.meta.glob(
|
|
10
|
+
'../../.content/**/*.{mdx,md}',
|
|
11
|
+
{ eager: true, import: 'frontmatter' }
|
|
12
|
+
);
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
const metaGlob: Record<string, Record<string, unknown>> = import.meta.glob(
|
|
15
|
+
'../../.content/**/meta.json',
|
|
16
|
+
{ eager: true }
|
|
17
|
+
);
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
function buildFiles() {
|
|
20
|
+
const files: {
|
|
21
|
+
type: 'page' | 'meta';
|
|
22
|
+
path: string;
|
|
23
|
+
data: Record<string, unknown>;
|
|
24
|
+
}[] = [];
|
|
25
|
+
|
|
26
|
+
for (const [key, data] of Object.entries(frontmatterGlob)) {
|
|
27
|
+
const originalPath = key.slice(CONTENT_PREFIX.length);
|
|
28
|
+
const relativePath = originalPath.replace(/readme\.(mdx?)$/i, 'index.$1');
|
|
29
|
+
files.push({
|
|
29
30
|
type: 'page',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
31
|
+
path: relativePath,
|
|
32
|
+
data: { ...data, _relativePath: relativePath, _originalPath: originalPath }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const [key, data] of Object.entries(metaGlob)) {
|
|
37
|
+
const relativePath = key.slice(CONTENT_PREFIX.length);
|
|
38
|
+
files.push({ type: 'meta', path: relativePath, data: data ?? {} });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let cachedSource: ReturnType<typeof loader> | null = null;
|
|
45
|
+
|
|
46
|
+
async function getSource() {
|
|
47
|
+
if (cachedSource) return cachedSource;
|
|
48
|
+
const files = buildFiles();
|
|
49
|
+
cachedSource = loader({
|
|
50
|
+
source: { files },
|
|
51
|
+
baseUrl: '/'
|
|
52
|
+
});
|
|
53
|
+
return cachedSource;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { getSource as source };
|
|
57
|
+
|
|
58
|
+
export function invalidate() {
|
|
59
|
+
cachedSource = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
|
|
63
|
+
if (node.type === 'page') return orderMap.get(node.url);
|
|
64
|
+
if (node.type === 'folder' && node.index) return orderMap.get(node.index.url);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
|
|
69
|
+
return [...nodes]
|
|
70
|
+
.map(n =>
|
|
71
|
+
n.type === 'folder'
|
|
72
|
+
? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
|
|
73
|
+
: n
|
|
74
|
+
)
|
|
75
|
+
.sort(
|
|
76
|
+
(a, b) =>
|
|
77
|
+
(getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
|
|
78
|
+
(getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
|
|
83
|
+
const orderMap = new Map<string, number>();
|
|
84
|
+
for (const page of pages) {
|
|
85
|
+
const d = page.data as Record<string, unknown>;
|
|
86
|
+
const order = d.order as number | undefined;
|
|
87
|
+
if (order !== undefined) orderMap.set(page.url, order);
|
|
88
|
+
if (page.url === '/') orderMap.set('/', order ?? 0);
|
|
89
|
+
}
|
|
90
|
+
return { ...tree, children: sortNodes(tree.children, orderMap) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function getPageTree(): Promise<Root> {
|
|
94
|
+
const s = await getSource();
|
|
95
|
+
return sortTreeByOrder(s.pageTree as Root, s.getPages());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function getPages() {
|
|
99
|
+
const s = await getSource();
|
|
100
|
+
return s.getPages();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getPage(slugs?: string[]) {
|
|
104
|
+
const s = await getSource();
|
|
105
|
+
return s.getPage(slugs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter {
|
|
109
|
+
const d = page.data as Record<string, unknown>;
|
|
110
|
+
return {
|
|
111
|
+
title: (d.title as string) ?? fallbackTitle ?? 'Untitled',
|
|
112
|
+
description: d.description as string | undefined,
|
|
113
|
+
order: d.order as number | undefined,
|
|
114
|
+
icon: d.icon as string | undefined,
|
|
115
|
+
lastModified: d.lastModified as string | undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getRelativePath(page: { data: unknown }): string {
|
|
120
|
+
return ((page.data as Record<string, unknown>)._relativePath as string) ?? '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getOriginalPath(page: { data: unknown }): string {
|
|
124
|
+
return ((page.data as Record<string, unknown>)._originalPath as string) ?? '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ssrModules = import.meta.glob<{ default?: MDXContent; toc?: TableOfContents }>(
|
|
128
|
+
'../../.content/**/*.{mdx,md}'
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
export async function loadPageModule(
|
|
132
|
+
relativePath: string
|
|
133
|
+
): Promise<{ default: MDXContent | null; toc: TableOfContents }> {
|
|
134
|
+
if (!relativePath || relativePath.includes('..')) return { default: null, toc: [] };
|
|
135
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
136
|
+
const key = relativePath.endsWith('.md')
|
|
137
|
+
? `../../.content/${withoutExt}.md`
|
|
138
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
139
|
+
const loader = ssrModules[key];
|
|
140
|
+
if (!loader) return { default: null, toc: [] };
|
|
141
|
+
const mod = await loader();
|
|
142
|
+
return { default: mod.default ?? null, toc: mod.toc ?? [] };
|
|
67
143
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { cx } from 'class-variance-authority';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { Search } from '@/components/ui/search';
|
|
4
|
+
import { buildApiPageTree } from '@/lib/api-routes';
|
|
5
|
+
import { usePageContext } from '@/lib/page-context';
|
|
6
|
+
import { getTheme } from '@/themes/registry';
|
|
7
|
+
import styles from './ApiLayout.module.css';
|
|
8
|
+
|
|
9
|
+
interface ApiLayoutProps {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ApiLayout({ children }: ApiLayoutProps) {
|
|
14
|
+
const { config, apiSpecs } = usePageContext();
|
|
15
|
+
const { Layout, className } = getTheme(config.theme?.name);
|
|
16
|
+
const tree = buildApiPageTree(apiSpecs);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Layout
|
|
20
|
+
config={config}
|
|
21
|
+
tree={tree}
|
|
22
|
+
classNames={{
|
|
23
|
+
layout: cx(styles.layout, className),
|
|
24
|
+
body: styles.body,
|
|
25
|
+
sidebar: styles.sidebar,
|
|
26
|
+
content: styles.content
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<Search className={styles.hiddenSearch} />
|
|
30
|
+
{children}
|
|
31
|
+
</Layout>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Flex, Headline, Text } from '@raystack/apsara';
|
|
2
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
import { EndpointPage } from '@/components/api';
|
|
4
|
+
import { findApiOperation } from '@/lib/api-routes';
|
|
5
|
+
import { Head } from '@/lib/head';
|
|
6
|
+
import type { ApiSpec } from '@/lib/openapi';
|
|
7
|
+
import { usePageContext } from '@/lib/page-context';
|
|
8
|
+
|
|
9
|
+
interface ApiPageProps {
|
|
10
|
+
slug: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ApiPage({ slug }: ApiPageProps) {
|
|
14
|
+
const { config, apiSpecs } = usePageContext();
|
|
15
|
+
|
|
16
|
+
if (slug.length === 0) {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<Head
|
|
20
|
+
title='API Reference'
|
|
21
|
+
description={`API documentation for ${config.title}`}
|
|
22
|
+
config={config}
|
|
23
|
+
/>
|
|
24
|
+
<ApiLanding specs={apiSpecs} />
|
|
25
|
+
</>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const match = findApiOperation(apiSpecs, slug);
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
|
|
32
|
+
const operation = match.operation as OpenAPIV3.OperationObject;
|
|
33
|
+
const title =
|
|
34
|
+
operation.summary ?? `${match.method.toUpperCase()} ${match.path}`;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<Head title={title} description={operation.description} config={config} />
|
|
39
|
+
<EndpointPage
|
|
40
|
+
method={match.method}
|
|
41
|
+
path={match.path}
|
|
42
|
+
operation={match.operation}
|
|
43
|
+
serverUrl={match.spec.server.url}
|
|
44
|
+
specName={match.spec.name}
|
|
45
|
+
auth={match.spec.auth}
|
|
46
|
+
/>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ApiLanding({ specs }: { specs: ApiSpec[] }) {
|
|
52
|
+
return (
|
|
53
|
+
<Flex
|
|
54
|
+
direction='column'
|
|
55
|
+
gap='large'
|
|
56
|
+
style={{ padding: 'var(--rs-space-7)' }}
|
|
57
|
+
>
|
|
58
|
+
<Headline size='medium' as='h1'>
|
|
59
|
+
API Reference
|
|
60
|
+
</Headline>
|
|
61
|
+
{specs.map(spec => (
|
|
62
|
+
<Flex key={spec.name} direction='column' gap='small'>
|
|
63
|
+
<Headline size='small' as='h2'>
|
|
64
|
+
{spec.name}
|
|
65
|
+
</Headline>
|
|
66
|
+
{spec.document.info.description && (
|
|
67
|
+
<Text size={3}>{spec.document.info.description}</Text>
|
|
68
|
+
)}
|
|
69
|
+
</Flex>
|
|
70
|
+
))}
|
|
71
|
+
</Flex>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { usePageContext } from '@/lib/page-context';
|
|
3
|
+
import { getTheme } from '@/themes/registry';
|
|
4
|
+
|
|
5
|
+
interface DocsLayoutProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DocsLayout({ children }: DocsLayoutProps) {
|
|
10
|
+
const { config, tree } = usePageContext();
|
|
11
|
+
const { Layout, className } = getTheme(config.theme?.name);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Layout config={config} tree={tree} classNames={{ layout: className }}>
|
|
15
|
+
{children}
|
|
16
|
+
</Layout>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -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,72 @@
|
|
|
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
|
+
import { getThemeConfig } from '@/themes/registry';
|
|
13
|
+
|
|
14
|
+
function resolveRoute(pathname: string) {
|
|
15
|
+
if (pathname.startsWith('/apis')) {
|
|
16
|
+
const slug = pathname
|
|
17
|
+
.replace(/^\/apis\/?/, '')
|
|
18
|
+
.split('/')
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
return { type: 'api' as const, slug };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const slug =
|
|
24
|
+
pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
|
|
25
|
+
return { type: 'docs' as const, slug };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function App() {
|
|
29
|
+
const { pathname } = useLocation();
|
|
30
|
+
const { config } = usePageContext();
|
|
31
|
+
const route = resolveRoute(pathname);
|
|
32
|
+
const themeConfig = getThemeConfig(config.theme?.name);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ThemeProvider
|
|
36
|
+
enableSystem={themeConfig.enableSystem}
|
|
37
|
+
forcedTheme={themeConfig.forcedTheme}
|
|
38
|
+
>
|
|
39
|
+
<RootHead config={config} />
|
|
40
|
+
{route.type === 'api' ? (
|
|
41
|
+
<ApiLayout>
|
|
42
|
+
<ApiPage slug={route.slug} />
|
|
43
|
+
</ApiLayout>
|
|
44
|
+
) : (
|
|
45
|
+
<DocsLayout>
|
|
46
|
+
<DocsPage slug={route.slug} />
|
|
47
|
+
</DocsLayout>
|
|
48
|
+
)}
|
|
49
|
+
</ThemeProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function RootHead({ config }: { config: ChronicleConfig }) {
|
|
54
|
+
return (
|
|
55
|
+
<Head
|
|
56
|
+
title={config.title}
|
|
57
|
+
description={config.description}
|
|
58
|
+
config={config}
|
|
59
|
+
jsonLd={
|
|
60
|
+
config.url
|
|
61
|
+
? {
|
|
62
|
+
'@context': 'https://schema.org',
|
|
63
|
+
'@type': 'WebSite',
|
|
64
|
+
name: config.title,
|
|
65
|
+
description: config.description,
|
|
66
|
+
url: config.url
|
|
67
|
+
}
|
|
68
|
+
: undefined
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -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,18 @@
|
|
|
1
|
+
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
+
import { getPage, extractFrontmatter, getRelativePath, getOriginalPath } 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
|
+
originalPath: getOriginalPath(page),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
@@ -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
|
+
});
|