@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
package/src/lib/api-routes.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
2
2
|
import slugify from 'slugify'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Root, Node, Item, Folder } from 'fumadocs-core/page-tree'
|
|
4
4
|
import type { ApiSpec } from './openapi'
|
|
5
5
|
|
|
6
6
|
export function getSpecSlug(spec: ApiSpec): string {
|
|
@@ -56,16 +56,15 @@ export function findApiOperation(specs: ApiSpec[], slug: string[]): ApiRouteMatc
|
|
|
56
56
|
return null
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function buildApiPageTree(specs: ApiSpec[]):
|
|
60
|
-
const children:
|
|
59
|
+
export function buildApiPageTree(specs: ApiSpec[]): Root {
|
|
60
|
+
const children: Node[] = []
|
|
61
61
|
|
|
62
62
|
for (const spec of specs) {
|
|
63
63
|
const specSlug = getSpecSlug(spec)
|
|
64
64
|
const paths = spec.document.paths ?? {}
|
|
65
65
|
const tags = spec.document.tags ?? []
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
const opsByTag = new Map<string, PageTreeItem[]>()
|
|
67
|
+
const opsByTag = new Map<string, Item[]>()
|
|
69
68
|
const tagDisplayName = new Map<string, string>()
|
|
70
69
|
|
|
71
70
|
for (const [, pathItem] of Object.entries(paths)) {
|
|
@@ -90,7 +89,6 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
// Use doc.tags display names where available
|
|
94
92
|
for (const t of tags) {
|
|
95
93
|
const key = t.name.toLowerCase()
|
|
96
94
|
if (opsByTag.has(key)) {
|
|
@@ -98,7 +96,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
const tagFolders:
|
|
99
|
+
const tagFolders: Folder[] = Array.from(opsByTag.entries()).map(([key, ops]) => ({
|
|
102
100
|
type: 'folder' as const,
|
|
103
101
|
name: tagDisplayName.get(key) ?? key,
|
|
104
102
|
icon: 'rectangle-stack',
|
|
@@ -110,7 +108,7 @@ export function buildApiPageTree(specs: ApiSpec[]): PageTree {
|
|
|
110
108
|
type: 'folder',
|
|
111
109
|
name: spec.name,
|
|
112
110
|
children: tagFolders,
|
|
113
|
-
})
|
|
111
|
+
} as Folder)
|
|
114
112
|
} else {
|
|
115
113
|
children.push(...tagFolders)
|
|
116
114
|
}
|
package/src/lib/config.ts
CHANGED
|
@@ -1,55 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { parse } from 'yaml'
|
|
4
|
-
import type { ChronicleConfig } from '@/types'
|
|
5
|
-
|
|
6
|
-
const CONFIG_FILE = 'chronicle.yaml'
|
|
1
|
+
import { parse } from 'yaml';
|
|
2
|
+
import type { ChronicleConfig } from '@/types';
|
|
7
3
|
|
|
8
4
|
const defaultConfig: ChronicleConfig = {
|
|
9
5
|
title: 'Documentation',
|
|
10
6
|
theme: { name: 'default' },
|
|
11
|
-
search: { enabled: true, placeholder: 'Search...' }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function resolveConfigPath(): string | null {
|
|
15
|
-
// Check project root via env var
|
|
16
|
-
const projectRoot = process.env.CHRONICLE_PROJECT_ROOT
|
|
17
|
-
if (projectRoot) {
|
|
18
|
-
const rootPath = path.join(projectRoot, CONFIG_FILE)
|
|
19
|
-
if (fs.existsSync(rootPath)) return rootPath
|
|
20
|
-
}
|
|
21
|
-
// Check cwd
|
|
22
|
-
const cwdPath = path.join(process.cwd(), CONFIG_FILE)
|
|
23
|
-
if (fs.existsSync(cwdPath)) return cwdPath
|
|
24
|
-
// Check content dir
|
|
25
|
-
const contentDir = process.env.CHRONICLE_CONTENT_DIR
|
|
26
|
-
if (contentDir) {
|
|
27
|
-
const contentPath = path.join(contentDir, CONFIG_FILE)
|
|
28
|
-
if (fs.existsSync(contentPath)) return contentPath
|
|
29
|
-
}
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
7
|
+
search: { enabled: true, placeholder: 'Search...' }
|
|
8
|
+
};
|
|
32
9
|
|
|
33
10
|
export function loadConfig(): ChronicleConfig {
|
|
34
|
-
const
|
|
11
|
+
const raw = typeof __CHRONICLE_CONFIG_RAW__ !== 'undefined' ? __CHRONICLE_CONFIG_RAW__ : null;
|
|
35
12
|
|
|
36
|
-
if (!
|
|
37
|
-
return defaultConfig
|
|
13
|
+
if (!raw) {
|
|
14
|
+
return defaultConfig;
|
|
38
15
|
}
|
|
39
16
|
|
|
40
|
-
const
|
|
41
|
-
const userConfig = parse(raw) as Partial<ChronicleConfig>
|
|
17
|
+
const userConfig = parse(raw) as Partial<ChronicleConfig>;
|
|
42
18
|
|
|
43
19
|
return {
|
|
44
20
|
...defaultConfig,
|
|
45
21
|
...userConfig,
|
|
46
22
|
theme: {
|
|
47
23
|
name: userConfig.theme?.name ?? defaultConfig.theme!.name,
|
|
48
|
-
colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors }
|
|
24
|
+
colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors }
|
|
49
25
|
},
|
|
50
26
|
search: { ...defaultConfig.search, ...userConfig.search },
|
|
51
27
|
footer: userConfig.footer,
|
|
52
28
|
api: userConfig.api,
|
|
53
29
|
llms: { enabled: false, ...userConfig.llms },
|
|
54
|
-
|
|
55
|
-
}
|
|
30
|
+
analytics: { enabled: false, ...userConfig.analytics }
|
|
31
|
+
};
|
|
32
|
+
}
|
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,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useState
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useLocation } from 'react-router';
|
|
9
|
+
import type { ApiSpec } from '@/lib/openapi';
|
|
10
|
+
import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
|
|
11
|
+
|
|
12
|
+
export type MdxLoader = (relativePath: string) => Promise<{ content: ReactNode; toc: TableOfContents }>;
|
|
13
|
+
|
|
14
|
+
interface PageData {
|
|
15
|
+
slug: string[];
|
|
16
|
+
frontmatter: Frontmatter;
|
|
17
|
+
content: ReactNode;
|
|
18
|
+
toc: TableOfContents;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PageContextValue {
|
|
22
|
+
config: ChronicleConfig;
|
|
23
|
+
tree: Root;
|
|
24
|
+
page: PageData | null;
|
|
25
|
+
apiSpecs: ApiSpec[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PageContext = createContext<PageContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
export function usePageContext(): PageContextValue {
|
|
31
|
+
const ctx = useContext(PageContext);
|
|
32
|
+
if (!ctx) {
|
|
33
|
+
console.error('usePageContext: no context found!');
|
|
34
|
+
return {
|
|
35
|
+
config: { title: 'Documentation' },
|
|
36
|
+
tree: { name: 'root', children: [] } as Root,
|
|
37
|
+
page: null,
|
|
38
|
+
apiSpecs: []
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return ctx;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PageProviderProps {
|
|
45
|
+
initialConfig: ChronicleConfig;
|
|
46
|
+
initialTree: Root;
|
|
47
|
+
initialPage: PageData | null;
|
|
48
|
+
initialApiSpecs: ApiSpec[];
|
|
49
|
+
loadMdx: MdxLoader;
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function PageProvider({
|
|
54
|
+
initialConfig,
|
|
55
|
+
initialTree,
|
|
56
|
+
initialPage,
|
|
57
|
+
initialApiSpecs,
|
|
58
|
+
loadMdx,
|
|
59
|
+
children
|
|
60
|
+
}: PageProviderProps) {
|
|
61
|
+
const { pathname } = useLocation();
|
|
62
|
+
const [tree] = useState<Root>(initialTree);
|
|
63
|
+
const [page, setPage] = useState<PageData | null>(initialPage);
|
|
64
|
+
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
|
|
65
|
+
const [currentPath, setCurrentPath] = useState(pathname);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (pathname === currentPath) return;
|
|
69
|
+
setCurrentPath(pathname);
|
|
70
|
+
|
|
71
|
+
const cancelled = { current: false };
|
|
72
|
+
|
|
73
|
+
if (pathname.startsWith('/apis')) {
|
|
74
|
+
if (apiSpecs.length === 0) {
|
|
75
|
+
fetch('/api/specs')
|
|
76
|
+
.then(res => res.json())
|
|
77
|
+
.then(specs => {
|
|
78
|
+
if (!cancelled.current) setApiSpecs(specs);
|
|
79
|
+
})
|
|
80
|
+
.catch(() => {});
|
|
81
|
+
}
|
|
82
|
+
return () => { cancelled.current = true; };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const slug = pathname === '/'
|
|
86
|
+
? []
|
|
87
|
+
: pathname.slice(1).split('/').filter(Boolean);
|
|
88
|
+
|
|
89
|
+
const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`;
|
|
90
|
+
|
|
91
|
+
fetch(apiPath)
|
|
92
|
+
.then(res => res.json())
|
|
93
|
+
.then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
|
|
94
|
+
if (cancelled.current) return;
|
|
95
|
+
const { content, toc } = await loadMdx(data.relativePath);
|
|
96
|
+
if (cancelled.current) return;
|
|
97
|
+
setPage({ slug, frontmatter: data.frontmatter, content, toc });
|
|
98
|
+
})
|
|
99
|
+
.catch(() => {});
|
|
100
|
+
|
|
101
|
+
return () => { cancelled.current = true; };
|
|
102
|
+
}, [pathname]);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<PageContext.Provider
|
|
106
|
+
value={{ config: initialConfig, tree, page, apiSpecs }}
|
|
107
|
+
>
|
|
108
|
+
{children}
|
|
109
|
+
</PageContext.Provider>
|
|
110
|
+
);
|
|
111
|
+
}
|
package/src/lib/source.ts
CHANGED
|
@@ -1,67 +1,138 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
);
|
|
13
|
+
|
|
14
|
+
const metaGlob: Record<string, Record<string, unknown>> = import.meta.glob(
|
|
15
|
+
'../../.content/**/meta.json',
|
|
16
|
+
{ eager: true }
|
|
17
|
+
);
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
function buildFiles() {
|
|
20
|
+
const files: {
|
|
21
|
+
type: 'page' | 'meta';
|
|
22
|
+
path: string;
|
|
23
|
+
data: Record<string, unknown>;
|
|
24
|
+
}[] = [];
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const item: PageTreeItem = {
|
|
26
|
+
for (const [key, data] of Object.entries(frontmatterGlob)) {
|
|
27
|
+
const relativePath = key.slice(CONTENT_PREFIX.length);
|
|
28
|
+
files.push({
|
|
29
29
|
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
|
-
|
|
30
|
+
path: relativePath,
|
|
31
|
+
data: { ...data, _relativePath: relativePath }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const [key, data] of Object.entries(metaGlob)) {
|
|
36
|
+
const relativePath = key.slice(CONTENT_PREFIX.length);
|
|
37
|
+
files.push({ type: 'meta', path: relativePath, data: data ?? {} });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let cachedSource: ReturnType<typeof loader> | null = null;
|
|
44
|
+
|
|
45
|
+
async function getSource() {
|
|
46
|
+
if (cachedSource) return cachedSource;
|
|
47
|
+
const files = buildFiles();
|
|
48
|
+
cachedSource = loader({
|
|
49
|
+
source: { files },
|
|
50
|
+
baseUrl: '/'
|
|
51
|
+
});
|
|
52
|
+
return cachedSource;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { getSource as source };
|
|
56
|
+
|
|
57
|
+
export function invalidate() {
|
|
58
|
+
cachedSource = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
|
|
62
|
+
if (node.type === 'page') return orderMap.get(node.url);
|
|
63
|
+
if (node.type === 'folder' && node.index) return orderMap.get(node.index.url);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sortNodes(nodes: Node[], orderMap: Map<string, number>): Node[] {
|
|
68
|
+
return [...nodes]
|
|
69
|
+
.map(n =>
|
|
70
|
+
n.type === 'folder'
|
|
71
|
+
? ({ ...n, children: sortNodes(n.children, orderMap) } as Folder)
|
|
72
|
+
: n
|
|
73
|
+
)
|
|
74
|
+
.sort(
|
|
75
|
+
(a, b) =>
|
|
76
|
+
(getOrder(a, orderMap) ?? Number.MAX_SAFE_INTEGER) -
|
|
77
|
+
(getOrder(b, orderMap) ?? Number.MAX_SAFE_INTEGER)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): Root {
|
|
82
|
+
const orderMap = new Map<string, number>();
|
|
83
|
+
for (const page of pages) {
|
|
84
|
+
const d = page.data as Record<string, unknown>;
|
|
85
|
+
const order = d.order as number | undefined;
|
|
86
|
+
if (order !== undefined) orderMap.set(page.url, order);
|
|
87
|
+
if (page.url === '/') orderMap.set('/', order ?? 0);
|
|
88
|
+
}
|
|
89
|
+
return { ...tree, children: sortNodes(tree.children, orderMap) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function getPageTree(): Promise<Root> {
|
|
93
|
+
const s = await getSource();
|
|
94
|
+
return sortTreeByOrder(s.pageTree as Root, s.getPages());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function getPages() {
|
|
98
|
+
const s = await getSource();
|
|
99
|
+
return s.getPages();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function getPage(slugs?: string[]) {
|
|
103
|
+
const s = await getSource();
|
|
104
|
+
return s.getPage(slugs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter {
|
|
108
|
+
const d = page.data as Record<string, unknown>;
|
|
109
|
+
return {
|
|
110
|
+
title: (d.title as string) ?? fallbackTitle ?? 'Untitled',
|
|
111
|
+
description: d.description as string | undefined,
|
|
112
|
+
order: d.order as number | undefined,
|
|
113
|
+
icon: d.icon as string | undefined,
|
|
114
|
+
lastModified: d.lastModified as string | undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getRelativePath(page: { data: unknown }): string {
|
|
119
|
+
return ((page.data as Record<string, unknown>)._relativePath as string) ?? '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ssrModules = import.meta.glob<{ default?: MDXContent; toc?: TableOfContents }>(
|
|
123
|
+
'../../.content/**/*.{mdx,md}'
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
export async function loadPageModule(
|
|
127
|
+
relativePath: string
|
|
128
|
+
): Promise<{ default: MDXContent | null; toc: TableOfContents }> {
|
|
129
|
+
if (!relativePath || relativePath.includes('..')) return { default: null, toc: [] };
|
|
130
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
131
|
+
const key = relativePath.endsWith('.md')
|
|
132
|
+
? `../../.content/${withoutExt}.md`
|
|
133
|
+
: `../../.content/${withoutExt}.mdx`;
|
|
134
|
+
const loader = ssrModules[key];
|
|
135
|
+
if (!loader) return { default: null, toc: [] };
|
|
136
|
+
const mod = await loader();
|
|
137
|
+
return { default: mod.default ?? null, toc: mod.toc ?? [] };
|
|
67
138
|
}
|
|
@@ -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
|
+
}
|