@raystack/chronicle 0.1.0-canary.6b53016 → 0.1.0-canary.847a058
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 +265 -9912
- 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 +39 -125
- 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/image.tsx +5 -34
- package/src/components/mdx/link.tsx +18 -15
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/components/ui/search.module.css +7 -0
- package/src/components/ui/search.tsx +65 -49
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/config.ts +31 -28
- package/src/lib/head.tsx +49 -0
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +117 -0
- package/src/lib/source.ts +134 -56
- 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 +26 -0
- package/src/server/api/search.ts +170 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +117 -0
- package/src/server/entry-client.tsx +76 -0
- package/src/server/entry-server.tsx +104 -0
- package/src/server/routes/llms.txt.ts +61 -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 +102 -0
- package/src/themes/default/Layout.tsx +78 -48
- package/src/themes/default/Page.module.css +5 -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
package/src/lib/config.ts
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { parse } from 'yaml'
|
|
4
|
-
import type { ChronicleConfig } from '@/types'
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import type { ChronicleConfig } from '@/types';
|
|
5
5
|
|
|
6
|
-
const CONFIG_FILE = 'chronicle.yaml'
|
|
6
|
+
const CONFIG_FILE = 'chronicle.yaml';
|
|
7
7
|
|
|
8
8
|
const defaultConfig: ChronicleConfig = {
|
|
9
9
|
title: 'Documentation',
|
|
10
10
|
theme: { name: 'default' },
|
|
11
|
-
search: { enabled: true, placeholder: 'Search...' }
|
|
12
|
-
}
|
|
11
|
+
search: { enabled: true, placeholder: 'Search...' }
|
|
12
|
+
};
|
|
13
13
|
|
|
14
14
|
function resolveConfigPath(): string | null {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
const projectRoot =
|
|
16
|
+
typeof __CHRONICLE_PROJECT_ROOT__ !== 'undefined'
|
|
17
|
+
? __CHRONICLE_PROJECT_ROOT__
|
|
18
|
+
: process.cwd();
|
|
19
|
+
|
|
20
|
+
const rootPath = path.join(projectRoot, CONFIG_FILE);
|
|
21
|
+
if (fs.existsSync(rootPath)) return rootPath;
|
|
22
|
+
|
|
23
|
+
const contentDir =
|
|
24
|
+
typeof __CHRONICLE_CONTENT_DIR__ !== 'undefined'
|
|
25
|
+
? __CHRONICLE_CONTENT_DIR__
|
|
26
|
+
: undefined;
|
|
26
27
|
if (contentDir) {
|
|
27
|
-
const contentPath = path.join(contentDir, CONFIG_FILE)
|
|
28
|
-
if (fs.existsSync(contentPath)) return contentPath
|
|
28
|
+
const contentPath = path.join(contentDir, CONFIG_FILE);
|
|
29
|
+
if (fs.existsSync(contentPath)) return contentPath;
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
return null;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function loadConfig(): ChronicleConfig {
|
|
34
|
-
const configPath = resolveConfigPath()
|
|
36
|
+
const configPath = resolveConfigPath();
|
|
35
37
|
|
|
36
38
|
if (!configPath) {
|
|
37
|
-
return defaultConfig
|
|
39
|
+
return defaultConfig;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
const raw = fs.readFileSync(configPath, 'utf-8')
|
|
41
|
-
const userConfig = parse(raw) as Partial<ChronicleConfig
|
|
42
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
43
|
+
const userConfig = parse(raw) as Partial<ChronicleConfig>;
|
|
42
44
|
|
|
43
45
|
return {
|
|
44
46
|
...defaultConfig,
|
|
45
47
|
...userConfig,
|
|
46
48
|
theme: {
|
|
47
49
|
name: userConfig.theme?.name ?? defaultConfig.theme!.name,
|
|
48
|
-
colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors }
|
|
50
|
+
colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors }
|
|
49
51
|
},
|
|
50
52
|
search: { ...defaultConfig.search, ...userConfig.search },
|
|
51
53
|
footer: userConfig.footer,
|
|
52
54
|
api: userConfig.api,
|
|
53
55
|
llms: { enabled: false, ...userConfig.llms },
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
+
analytics: { enabled: false, ...userConfig.analytics }
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/lib/head.tsx
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ChronicleConfig } from '@/types';
|
|
2
|
+
|
|
3
|
+
export interface HeadProps {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
config: ChronicleConfig;
|
|
7
|
+
jsonLd?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Head({ title, description, config, jsonLd }: HeadProps) {
|
|
11
|
+
const fullTitle = `${title} | ${config.title}`;
|
|
12
|
+
const ogParams = new URLSearchParams({ title });
|
|
13
|
+
if (description) ogParams.set('description', description);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<title>{fullTitle}</title>
|
|
18
|
+
{description && <meta name='description' content={description} />}
|
|
19
|
+
|
|
20
|
+
{config.url && (
|
|
21
|
+
<>
|
|
22
|
+
<meta property='og:title' content={title} />
|
|
23
|
+
{description && (
|
|
24
|
+
<meta property='og:description' content={description} />
|
|
25
|
+
)}
|
|
26
|
+
<meta property='og:site_name' content={config.title} />
|
|
27
|
+
<meta property='og:type' content='website' />
|
|
28
|
+
<meta property='og:image' content={`/og?${ogParams.toString()}`} />
|
|
29
|
+
<meta property='og:image:width' content='1200' />
|
|
30
|
+
<meta property='og:image:height' content='630' />
|
|
31
|
+
|
|
32
|
+
<meta name='twitter:card' content='summary_large_image' />
|
|
33
|
+
<meta name='twitter:title' content={title} />
|
|
34
|
+
{description && (
|
|
35
|
+
<meta name='twitter:description' content={description} />
|
|
36
|
+
)}
|
|
37
|
+
<meta name='twitter:image' content={`/og?${ogParams.toString()}`} />
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{jsonLd && (
|
|
42
|
+
<script
|
|
43
|
+
type='application/ld+json'
|
|
44
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd, null, 2) }}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
package/src/lib/openapi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
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'
|
|
@@ -17,14 +17,14 @@ export interface ApiSpec {
|
|
|
17
17
|
export type { SchemaField } from './schema'
|
|
18
18
|
export { flattenSchema } from './schema'
|
|
19
19
|
|
|
20
|
-
export function loadApiSpecs(apiConfigs: ApiConfig[]): ApiSpec[] {
|
|
21
|
-
const
|
|
22
|
-
return apiConfigs.map((config) => loadApiSpec(config,
|
|
20
|
+
export async function loadApiSpecs(apiConfigs: ApiConfig[]): Promise<ApiSpec[]> {
|
|
21
|
+
const projectRoot = typeof __CHRONICLE_PROJECT_ROOT__ !== 'undefined' ? __CHRONICLE_PROJECT_ROOT__ : process.cwd()
|
|
22
|
+
return Promise.all(apiConfigs.map((config) => loadApiSpec(config, projectRoot)))
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function loadApiSpec(config: ApiConfig,
|
|
26
|
-
const specPath = path.resolve(
|
|
27
|
-
const raw = fs.
|
|
25
|
+
export async function loadApiSpec(config: ApiConfig, projectRoot: string): Promise<ApiSpec> {
|
|
26
|
+
const specPath = path.resolve(projectRoot, config.spec)
|
|
27
|
+
const raw = await fs.readFile(specPath, 'utf-8')
|
|
28
28
|
const isYaml = specPath.endsWith('.yaml') || specPath.endsWith('.yml')
|
|
29
29
|
const doc = (isYaml ? parseYaml(raw) : JSON.parse(raw)) as OpenAPIV2.Document | OpenAPIV3.Document
|
|
30
30
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useLocation } from 'react-router';
|
|
9
|
+
import { mdxComponents } from '@/components/mdx';
|
|
10
|
+
import type { ApiSpec } from '@/lib/openapi';
|
|
11
|
+
import type { ChronicleConfig, Frontmatter, Root } from '@/types';
|
|
12
|
+
|
|
13
|
+
interface PageData {
|
|
14
|
+
slug: string[];
|
|
15
|
+
frontmatter: Frontmatter;
|
|
16
|
+
content: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PageContextValue {
|
|
20
|
+
config: ChronicleConfig;
|
|
21
|
+
tree: Root;
|
|
22
|
+
page: PageData | null;
|
|
23
|
+
apiSpecs: ApiSpec[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PageContext = createContext<PageContextValue | null>(null);
|
|
27
|
+
|
|
28
|
+
export function usePageContext(): PageContextValue {
|
|
29
|
+
const ctx = useContext(PageContext);
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
console.error('usePageContext: no context found!');
|
|
32
|
+
return {
|
|
33
|
+
config: { title: 'Documentation' },
|
|
34
|
+
tree: { name: 'root', children: [] } as Root,
|
|
35
|
+
page: null,
|
|
36
|
+
apiSpecs: []
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return ctx;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PageProviderProps {
|
|
43
|
+
initialConfig: ChronicleConfig;
|
|
44
|
+
initialTree: Root;
|
|
45
|
+
initialPage: PageData | null;
|
|
46
|
+
initialApiSpecs: ApiSpec[];
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
|
|
51
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
52
|
+
const mod = relativePath.endsWith('.md')
|
|
53
|
+
? await import(`../../.content/${withoutExt}.md`)
|
|
54
|
+
: await import(`../../.content/${withoutExt}.mdx`);
|
|
55
|
+
return mod.default
|
|
56
|
+
? React.createElement(mod.default, { components: mdxComponents })
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function PageProvider({
|
|
61
|
+
initialConfig,
|
|
62
|
+
initialTree,
|
|
63
|
+
initialPage,
|
|
64
|
+
initialApiSpecs,
|
|
65
|
+
children
|
|
66
|
+
}: PageProviderProps) {
|
|
67
|
+
const { pathname } = useLocation();
|
|
68
|
+
const [tree] = useState<Root>(initialTree);
|
|
69
|
+
const [page, setPage] = useState<PageData | null>(initialPage);
|
|
70
|
+
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
|
|
71
|
+
const [currentPath, setCurrentPath] = useState(pathname);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (pathname === currentPath) return;
|
|
75
|
+
setCurrentPath(pathname);
|
|
76
|
+
|
|
77
|
+
const cancelled = { current: false };
|
|
78
|
+
|
|
79
|
+
if (pathname.startsWith('/apis')) {
|
|
80
|
+
if (apiSpecs.length === 0) {
|
|
81
|
+
fetch('/api/specs')
|
|
82
|
+
.then(res => res.json())
|
|
83
|
+
.then(specs => {
|
|
84
|
+
if (!cancelled.current) setApiSpecs(specs);
|
|
85
|
+
})
|
|
86
|
+
.catch(() => {});
|
|
87
|
+
}
|
|
88
|
+
return () => { cancelled.current = true; };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const slug = pathname === '/'
|
|
92
|
+
? []
|
|
93
|
+
: pathname.slice(1).split('/').filter(Boolean);
|
|
94
|
+
|
|
95
|
+
const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`;
|
|
96
|
+
|
|
97
|
+
fetch(apiPath)
|
|
98
|
+
.then(res => res.json())
|
|
99
|
+
.then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
|
|
100
|
+
if (cancelled.current) return;
|
|
101
|
+
const content = await loadMdxComponent(data.relativePath);
|
|
102
|
+
if (cancelled.current) return;
|
|
103
|
+
setPage({ slug, frontmatter: data.frontmatter, content });
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {});
|
|
106
|
+
|
|
107
|
+
return () => { cancelled.current = true; };
|
|
108
|
+
}, [pathname]);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<PageContext.Provider
|
|
112
|
+
value={{ config: initialConfig, tree, page, apiSpecs }}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</PageContext.Provider>
|
|
116
|
+
);
|
|
117
|
+
}
|
package/src/lib/source.ts
CHANGED
|
@@ -1,67 +1,145 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export function sortByOrder<T extends { frontmatter?: Frontmatter }>(
|
|
11
|
-
items: T[]
|
|
12
|
-
): T[] {
|
|
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
|
-
})
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loader } from 'fumadocs-core/source';
|
|
4
|
+
import type { Root, Node, Folder } from 'fumadocs-core/page-tree';
|
|
5
|
+
import matter from 'gray-matter';
|
|
6
|
+
import type { MDXContent } from 'mdx/types';
|
|
7
|
+
|
|
8
|
+
function getContentDir(): string {
|
|
9
|
+
return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
12
|
+
async function scanFiles(contentDir: string) {
|
|
13
|
+
const files: {
|
|
14
|
+
type: 'page' | 'meta';
|
|
15
|
+
path: string;
|
|
16
|
+
data: Record<string, unknown>;
|
|
17
|
+
}[] = [];
|
|
18
|
+
|
|
19
|
+
async function scan(dir: string, prefix: string[] = []) {
|
|
20
|
+
try {
|
|
21
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
24
|
+
continue;
|
|
25
|
+
const fullPath = path.join(dir, entry.name);
|
|
26
|
+
const relativePath = [...prefix, entry.name].join('/');
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
await scan(fullPath, [...prefix, entry.name]);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
|
|
34
|
+
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
35
|
+
const { data } = matter(raw);
|
|
36
|
+
files.push({
|
|
37
|
+
type: 'page',
|
|
38
|
+
path: relativePath,
|
|
39
|
+
data: { ...data, _relativePath: relativePath }
|
|
40
|
+
});
|
|
41
|
+
} else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
|
|
42
|
+
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
43
|
+
const data = entry.name.endsWith('.json')
|
|
44
|
+
? JSON.parse(raw)
|
|
45
|
+
: matter(raw).data;
|
|
46
|
+
files.push({ type: 'meta', path: relativePath, data });
|
|
47
|
+
}
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
rootPages.push(item)
|
|
49
|
+
} catch {
|
|
50
|
+
/* directory not readable */
|
|
43
51
|
}
|
|
44
|
-
}
|
|
52
|
+
}
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
await scan(contentDir);
|
|
55
|
+
return files;
|
|
56
|
+
}
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
let cachedSource: ReturnType<typeof loader> | null = null;
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
62
|
-
})
|
|
60
|
+
async function getSource() {
|
|
61
|
+
if (cachedSource) return cachedSource;
|
|
62
|
+
const contentDir = getContentDir();
|
|
63
|
+
const files = await scanFiles(contentDir);
|
|
64
|
+
cachedSource = loader({
|
|
65
|
+
source: { files },
|
|
66
|
+
baseUrl: '/'
|
|
67
|
+
});
|
|
68
|
+
return cachedSource;
|
|
69
|
+
}
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
export { getSource as source };
|
|
72
|
+
|
|
73
|
+
export function invalidate() {
|
|
74
|
+
cachedSource = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
|
|
78
|
+
if (node.type === 'page') return orderMap.get(node.url);
|
|
79
|
+
if (node.type === 'folder') {
|
|
80
|
+
if (node.index) return orderMap.get(node.index.url);
|
|
81
|
+
for (const child of node.children) {
|
|
82
|
+
const o = getOrder(child, orderMap);
|
|
83
|
+
if (o !== undefined) return o;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
|
|
90
|
+
return [...nodes]
|
|
91
|
+
.map(n =>
|
|
92
|
+
n.type === 'folder'
|
|
93
|
+
? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
|
|
94
|
+
: n
|
|
95
|
+
)
|
|
96
|
+
.sort(
|
|
97
|
+
(a, b) =>
|
|
98
|
+
(getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
|
|
99
|
+
(getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
|
|
104
|
+
const orderMap = new Map<string, number>();
|
|
105
|
+
for (const page of pages) {
|
|
106
|
+
const d = page.data as Record<string, unknown>;
|
|
107
|
+
const order = d.order as number | undefined;
|
|
108
|
+
if (order !== undefined) orderMap.set(page.url, order);
|
|
109
|
+
if (page.url === '/') orderMap.set('/', order ?? 0);
|
|
110
|
+
}
|
|
111
|
+
return { ...tree, children: sortNodes(tree.children, orderMap) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getPageTree(): Promise<Root> {
|
|
115
|
+
const s = await getSource();
|
|
116
|
+
return sortTreeByOrder(s.pageTree as Root, s.getPages());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getPages() {
|
|
120
|
+
const s = await getSource();
|
|
121
|
+
return s.getPages();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function getPage(slugs?: string[]) {
|
|
125
|
+
const s = await getSource();
|
|
126
|
+
return s.getPage(slugs);
|
|
127
|
+
}
|
|
65
128
|
|
|
66
|
-
|
|
129
|
+
export async function loadPageComponent(
|
|
130
|
+
relativePath: string
|
|
131
|
+
): Promise<MDXContent | null> {
|
|
132
|
+
if (!relativePath) return null;
|
|
133
|
+
const contentDir = getContentDir();
|
|
134
|
+
const fullPath = path.join(contentDir, relativePath);
|
|
135
|
+
try {
|
|
136
|
+
await fs.access(fullPath);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
141
|
+
const mod = relativePath.endsWith('.md')
|
|
142
|
+
? await import(`../../.content/${withoutExt}.md`)
|
|
143
|
+
: await import(`../../.content/${withoutExt}.mdx`);
|
|
144
|
+
return mod.default;
|
|
67
145
|
}
|
|
@@ -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: []
|
|
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
|
+
}
|