@raystack/chronicle 0.1.0-canary.a320792 → 0.1.0-canary.ac60f9f
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 +150 -416
- package/package.json +13 -9
- package/src/cli/commands/build.ts +30 -48
- package/src/cli/commands/dev.ts +24 -13
- package/src/cli/commands/init.ts +38 -123
- package/src/cli/commands/serve.ts +35 -50
- package/src/cli/commands/start.ts +20 -16
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +25 -26
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/resolve.ts +7 -3
- package/src/cli/utils/scaffold.ts +14 -16
- package/src/components/mdx/code.tsx +1 -10
- package/src/components/mdx/details.module.css +24 -1
- package/src/components/mdx/details.tsx +3 -2
- package/src/components/mdx/image.tsx +5 -20
- package/src/components/mdx/index.tsx +3 -3
- package/src/components/mdx/link.tsx +24 -20
- package/src/components/ui/footer.tsx +2 -3
- package/src/components/ui/search.tsx +116 -71
- package/src/lib/config.ts +31 -29
- package/src/lib/get-llm-text.ts +10 -0
- package/src/lib/head.tsx +26 -22
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +76 -57
- package/src/lib/source.ts +144 -96
- package/src/pages/ApiLayout.tsx +22 -18
- package/src/pages/ApiPage.tsx +32 -27
- package/src/pages/DocsLayout.tsx +7 -7
- package/src/pages/DocsPage.tsx +11 -11
- package/src/pages/NotFound.tsx +11 -4
- package/src/server/App.tsx +35 -27
- 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 +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 +52 -56
- package/src/server/entry-server.tsx +95 -35
- 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 +39 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +50 -49
- package/src/themes/default/Layout.tsx +69 -41
- package/src/themes/default/Page.module.css +0 -60
- package/src/themes/default/Page.tsx +9 -11
- package/src/themes/default/Toc.tsx +30 -28
- package/src/themes/default/index.ts +7 -9
- package/src/themes/paper/ChapterNav.tsx +59 -39
- package/src/themes/paper/Layout.module.css +1 -1
- package/src/themes/paper/Layout.tsx +24 -12
- package/src/themes/paper/Page.module.css +11 -4
- package/src/themes/paper/Page.tsx +67 -47
- 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/globals.d.ts +4 -0
- package/src/cli/__tests__/config.test.ts +0 -25
- package/src/cli/__tests__/scaffold.test.ts +0 -10
- package/src/pages/__tests__/head.test.tsx +0 -57
- package/src/server/__tests__/entry-server.test.tsx +0 -35
- package/src/server/__tests__/handlers.test.ts +0 -77
- package/src/server/__tests__/og.test.ts +0 -23
- package/src/server/__tests__/router.test.ts +0 -72
- package/src/server/__tests__/vite-config.test.ts +0 -25
- package/src/server/dev.ts +0 -156
- package/src/server/entry-prod.ts +0 -127
- package/src/server/handlers/apis-proxy.ts +0 -52
- package/src/server/handlers/health.ts +0 -3
- package/src/server/handlers/llms.ts +0 -58
- package/src/server/handlers/og.ts +0 -87
- package/src/server/handlers/robots.ts +0 -11
- package/src/server/handlers/search.ts +0 -140
- package/src/server/handlers/sitemap.ts +0 -39
- package/src/server/handlers/specs.ts +0 -9
- package/src/server/index.html +0 -12
- package/src/server/prod.ts +0 -18
- package/src/server/router.ts +0 -42
- package/src/themes/default/font.ts +0 -4
package/src/lib/head.tsx
CHANGED
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
import type { ChronicleConfig } from '@/types'
|
|
1
|
+
import type { ChronicleConfig } from '@/types';
|
|
2
2
|
|
|
3
3
|
export interface HeadProps {
|
|
4
|
-
title: string
|
|
5
|
-
description?: string
|
|
6
|
-
config: ChronicleConfig
|
|
7
|
-
jsonLd?: Record<string, unknown
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
config: ChronicleConfig;
|
|
7
|
+
jsonLd?: Record<string, unknown>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
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)
|
|
11
|
+
const fullTitle = `${title} | ${config.title}`;
|
|
12
|
+
const ogParams = new URLSearchParams({ title });
|
|
13
|
+
if (description) ogParams.set('description', description);
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<>
|
|
17
17
|
<title>{fullTitle}</title>
|
|
18
|
-
{description && <meta name=
|
|
18
|
+
{description && <meta name='description' content={description} />}
|
|
19
19
|
|
|
20
20
|
{config.url && (
|
|
21
21
|
<>
|
|
22
|
-
<meta property=
|
|
23
|
-
{description &&
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<meta property=
|
|
27
|
-
<meta property=
|
|
28
|
-
<meta property=
|
|
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' />
|
|
29
31
|
|
|
30
|
-
<meta name=
|
|
31
|
-
<meta name=
|
|
32
|
-
{description &&
|
|
33
|
-
|
|
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()}`} />
|
|
34
38
|
</>
|
|
35
39
|
)}
|
|
36
40
|
|
|
37
41
|
{jsonLd && (
|
|
38
42
|
<script
|
|
39
|
-
type=
|
|
43
|
+
type='application/ld+json'
|
|
40
44
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd, null, 2) }}
|
|
41
45
|
/>
|
|
42
46
|
)}
|
|
43
47
|
</>
|
|
44
|
-
)
|
|
48
|
+
);
|
|
45
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
|
|
package/src/lib/page-context.tsx
CHANGED
|
@@ -1,95 +1,114 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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, PageTree } from '@/types';
|
|
8
12
|
|
|
9
13
|
interface PageData {
|
|
10
|
-
slug: string[]
|
|
11
|
-
frontmatter: Frontmatter
|
|
12
|
-
content: ReactNode
|
|
14
|
+
slug: string[];
|
|
15
|
+
frontmatter: Frontmatter;
|
|
16
|
+
content: ReactNode;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
interface PageContextValue {
|
|
16
|
-
config: ChronicleConfig
|
|
17
|
-
tree: PageTree
|
|
18
|
-
page: PageData | null
|
|
19
|
-
apiSpecs: ApiSpec[]
|
|
20
|
+
config: ChronicleConfig;
|
|
21
|
+
tree: PageTree;
|
|
22
|
+
page: PageData | null;
|
|
23
|
+
apiSpecs: ApiSpec[];
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
const PageContext = createContext<PageContextValue | null>(null)
|
|
26
|
+
const PageContext = createContext<PageContextValue | null>(null);
|
|
23
27
|
|
|
24
28
|
export function usePageContext(): PageContextValue {
|
|
25
|
-
const ctx = useContext(PageContext)
|
|
29
|
+
const ctx = useContext(PageContext);
|
|
26
30
|
if (!ctx) {
|
|
27
|
-
console.error('usePageContext: no context found!')
|
|
31
|
+
console.error('usePageContext: no context found!');
|
|
28
32
|
return {
|
|
29
33
|
config: { title: 'Documentation' },
|
|
30
34
|
tree: { name: 'root', children: [] },
|
|
31
35
|
page: null,
|
|
32
|
-
apiSpecs: []
|
|
33
|
-
}
|
|
36
|
+
apiSpecs: []
|
|
37
|
+
};
|
|
34
38
|
}
|
|
35
|
-
return ctx
|
|
39
|
+
return ctx;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
interface PageProviderProps {
|
|
39
|
-
initialConfig: ChronicleConfig
|
|
40
|
-
initialTree: PageTree
|
|
41
|
-
initialPage: PageData | null
|
|
42
|
-
initialApiSpecs: ApiSpec[]
|
|
43
|
-
children: ReactNode
|
|
43
|
+
initialConfig: ChronicleConfig;
|
|
44
|
+
initialTree: PageTree;
|
|
45
|
+
initialPage: PageData | null;
|
|
46
|
+
initialApiSpecs: ApiSpec[];
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadMdxComponent(relativePath: string): Promise<ReactNode> {
|
|
51
|
+
const mod = await import(/* @vite-ignore */ `/.content/${relativePath}`);
|
|
52
|
+
return mod.default
|
|
53
|
+
? React.createElement(mod.default, { components: mdxComponents })
|
|
54
|
+
: null;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
export function PageProvider({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
export function PageProvider({
|
|
58
|
+
initialConfig,
|
|
59
|
+
initialTree,
|
|
60
|
+
initialPage,
|
|
61
|
+
initialApiSpecs,
|
|
62
|
+
children
|
|
63
|
+
}: PageProviderProps) {
|
|
64
|
+
const { pathname } = useLocation();
|
|
65
|
+
const [tree] = useState<PageTree>(initialTree);
|
|
66
|
+
const [page, setPage] = useState<PageData | null>(initialPage);
|
|
67
|
+
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs);
|
|
68
|
+
const [currentPath, setCurrentPath] = useState(pathname);
|
|
52
69
|
|
|
53
70
|
useEffect(() => {
|
|
54
|
-
if (pathname === currentPath) return
|
|
55
|
-
setCurrentPath(pathname)
|
|
71
|
+
if (pathname === currentPath) return;
|
|
72
|
+
setCurrentPath(pathname);
|
|
56
73
|
|
|
57
|
-
|
|
74
|
+
const cancelled = { current: false };
|
|
58
75
|
|
|
59
76
|
if (pathname.startsWith('/apis')) {
|
|
60
|
-
// Fetch API specs if not already loaded
|
|
61
77
|
if (apiSpecs.length === 0) {
|
|
62
78
|
fetch('/api/specs')
|
|
63
|
-
.then(
|
|
64
|
-
.then(
|
|
65
|
-
|
|
79
|
+
.then(res => res.json())
|
|
80
|
+
.then(specs => {
|
|
81
|
+
if (!cancelled.current) setApiSpecs(specs);
|
|
82
|
+
})
|
|
83
|
+
.catch(() => {});
|
|
66
84
|
}
|
|
67
|
-
return () => { cancelled = true }
|
|
85
|
+
return () => { cancelled.current = true; };
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
const slug = pathname === '/'
|
|
89
|
+
? []
|
|
90
|
+
: pathname.slice(1).split('/').filter(Boolean);
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
if (cancelled || !sourcePage) return
|
|
92
|
+
const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`;
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
frontmatter:
|
|
83
|
-
content: component ? React.createElement(component, { components: mdxComponents }) : null,
|
|
94
|
+
fetch(apiPath)
|
|
95
|
+
.then(res => res.json())
|
|
96
|
+
.then(async (data: { frontmatter: Frontmatter; relativePath: string }) => {
|
|
97
|
+
if (cancelled.current) return;
|
|
98
|
+
const content = await loadMdxComponent(data.relativePath);
|
|
99
|
+
if (cancelled.current) return;
|
|
100
|
+
setPage({ slug, frontmatter: data.frontmatter, content });
|
|
84
101
|
})
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return () => { cancelled = true }
|
|
88
|
-
}, [pathname])
|
|
102
|
+
.catch(() => {});
|
|
103
|
+
|
|
104
|
+
return () => { cancelled.current = true; };
|
|
105
|
+
}, [pathname]);
|
|
89
106
|
|
|
90
107
|
return (
|
|
91
|
-
<PageContext.Provider
|
|
108
|
+
<PageContext.Provider
|
|
109
|
+
value={{ config: initialConfig, tree, page, apiSpecs }}
|
|
110
|
+
>
|
|
92
111
|
{children}
|
|
93
112
|
</PageContext.Provider>
|
|
94
|
-
)
|
|
113
|
+
);
|
|
95
114
|
}
|
package/src/lib/source.ts
CHANGED
|
@@ -1,138 +1,186 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
const loaders: Record<string, () => Promise<{ default: MDXContent }>> = import.meta.glob(
|
|
10
|
-
'@content/**/*.{mdx,md}'
|
|
11
|
-
)
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loader } from 'fumadocs-core/source';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import type { MDXContent } from 'mdx/types';
|
|
6
|
+
import type { Frontmatter, PageTree, PageTreeItem } from '@/types';
|
|
12
7
|
|
|
13
8
|
export interface SourcePage {
|
|
14
|
-
url: string
|
|
15
|
-
slugs: string[]
|
|
16
|
-
filePath: string
|
|
17
|
-
frontmatter: Frontmatter
|
|
9
|
+
url: string;
|
|
10
|
+
slugs: string[];
|
|
11
|
+
filePath: string;
|
|
12
|
+
frontmatter: Frontmatter;
|
|
18
13
|
}
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (keys.length === 0) return ''
|
|
23
|
-
const dirs = keys.map((k) => k.split('/').slice(0, -1)) // drop filename
|
|
24
|
-
const first = dirs[0]
|
|
25
|
-
let depth = 0
|
|
26
|
-
for (let i = 0; i < first.length; i++) {
|
|
27
|
-
if (dirs.every((d) => d[i] === first[i])) {
|
|
28
|
-
depth = i + 1
|
|
29
|
-
} else {
|
|
30
|
-
break
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return first.slice(0, depth).join('/') + '/'
|
|
15
|
+
function getContentDir(): string {
|
|
16
|
+
return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
|
|
34
17
|
}
|
|
35
18
|
|
|
36
|
-
|
|
19
|
+
async function scanFiles(contentDir: string) {
|
|
20
|
+
const files: {
|
|
21
|
+
type: 'page' | 'meta';
|
|
22
|
+
path: string;
|
|
23
|
+
data: Record<string, unknown>;
|
|
24
|
+
}[] = [];
|
|
25
|
+
|
|
26
|
+
async function scan(dir: string, prefix: string[] = []) {
|
|
27
|
+
try {
|
|
28
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
31
|
+
continue;
|
|
32
|
+
const fullPath = path.join(dir, entry.name);
|
|
33
|
+
const relativePath = [...prefix, entry.name].join('/');
|
|
34
|
+
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
await scan(fullPath, [...prefix, entry.name]);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
|
|
41
|
+
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
42
|
+
const { data } = matter(raw);
|
|
43
|
+
files.push({
|
|
44
|
+
type: 'page',
|
|
45
|
+
path: relativePath,
|
|
46
|
+
data: { ...data, _absolutePath: fullPath }
|
|
47
|
+
});
|
|
48
|
+
} else if (entry.name === 'meta.json' || entry.name === 'meta.yaml') {
|
|
49
|
+
const raw = await fs.readFile(fullPath, 'utf-8');
|
|
50
|
+
const data = entry.name.endsWith('.json')
|
|
51
|
+
? JSON.parse(raw)
|
|
52
|
+
: matter(raw).data;
|
|
53
|
+
files.push({ type: 'meta', path: relativePath, data });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
/* directory not readable */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
37
60
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const withoutExt = relative.replace(/\.(mdx|md)$/, '')
|
|
41
|
-
const parts = withoutExt.split('/').filter(Boolean)
|
|
42
|
-
if (parts[parts.length - 1] === 'index') parts.pop()
|
|
43
|
-
return parts
|
|
61
|
+
await scan(contentDir);
|
|
62
|
+
return files;
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
let cachedSource: ReturnType<typeof loader> | null = null;
|
|
66
|
+
let cachedPages: SourcePage[] | null = null;
|
|
67
|
+
|
|
68
|
+
async function getSource() {
|
|
69
|
+
if (cachedSource) return cachedSource;
|
|
70
|
+
const contentDir = getContentDir();
|
|
71
|
+
const files = await scanFiles(contentDir);
|
|
72
|
+
cachedSource = loader({
|
|
73
|
+
source: { files },
|
|
74
|
+
baseUrl: '/'
|
|
75
|
+
});
|
|
76
|
+
return cachedSource;
|
|
48
77
|
}
|
|
49
78
|
|
|
50
|
-
|
|
79
|
+
export function invalidate() {
|
|
80
|
+
cachedSource = null;
|
|
81
|
+
cachedPages = null;
|
|
82
|
+
}
|
|
51
83
|
|
|
52
84
|
export async function getPages(): Promise<SourcePage[]> {
|
|
53
|
-
if (cachedPages) return cachedPages
|
|
85
|
+
if (cachedPages) return cachedPages;
|
|
54
86
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
87
|
+
const s = await getSource();
|
|
88
|
+
cachedPages = s.getPages().map(page => {
|
|
89
|
+
const data = page.data as Record<string, unknown>;
|
|
58
90
|
return {
|
|
59
|
-
url:
|
|
60
|
-
slugs,
|
|
61
|
-
filePath,
|
|
91
|
+
url: page.url,
|
|
92
|
+
slugs: page.slugs,
|
|
93
|
+
filePath: (data._absolutePath as string) ?? '',
|
|
62
94
|
frontmatter: {
|
|
63
|
-
title:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
title:
|
|
96
|
+
(data.title as string) ??
|
|
97
|
+
page.slugs[page.slugs.length - 1] ??
|
|
98
|
+
'Untitled',
|
|
99
|
+
description: data.description as string | undefined,
|
|
100
|
+
order: data.order as number | undefined,
|
|
101
|
+
icon: data.icon as string | undefined,
|
|
102
|
+
lastModified: data.lastModified as string | undefined
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
});
|
|
71
106
|
|
|
72
|
-
return cachedPages
|
|
107
|
+
return cachedPages;
|
|
73
108
|
}
|
|
74
109
|
|
|
75
110
|
export async function getPage(slug?: string[]): Promise<SourcePage | null> {
|
|
76
|
-
const pages = await getPages()
|
|
77
|
-
const targetUrl = !slug || slug.length === 0 ? '/' :
|
|
78
|
-
return pages.find(
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function loadPageComponent(page: SourcePage): Promise<MDXContent | null> {
|
|
82
|
-
const loader = loaders[page.filePath]
|
|
83
|
-
if (!loader) return null
|
|
84
|
-
const mod = await loader()
|
|
85
|
-
return mod.default
|
|
111
|
+
const pages = await getPages();
|
|
112
|
+
const targetUrl = !slug || slug.length === 0 ? '/' : `/${slug.join('/')}`;
|
|
113
|
+
return pages.find(p => p.url === targetUrl) ?? null;
|
|
86
114
|
}
|
|
87
115
|
|
|
88
|
-
export function
|
|
89
|
-
|
|
116
|
+
export async function loadPageComponent(
|
|
117
|
+
page: SourcePage
|
|
118
|
+
): Promise<MDXContent | null> {
|
|
119
|
+
if (!page.filePath) return null;
|
|
120
|
+
try {
|
|
121
|
+
await fs.access(page.filePath);
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const contentDir = getContentDir();
|
|
126
|
+
const relativePath = path.relative(contentDir, page.filePath);
|
|
127
|
+
const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
|
|
128
|
+
const mod = relativePath.endsWith('.md')
|
|
129
|
+
? await import(`@content/${withoutExt}.md`)
|
|
130
|
+
: await import(`@content/${withoutExt}.mdx`);
|
|
131
|
+
return mod.default;
|
|
90
132
|
}
|
|
91
133
|
|
|
92
134
|
export async function buildPageTree(): Promise<PageTree> {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
const s = await getSource();
|
|
136
|
+
const pages = s.getPages();
|
|
137
|
+
const folders = new Map<string, PageTreeItem[]>();
|
|
138
|
+
const rootPages: PageTreeItem[] = [];
|
|
139
|
+
|
|
140
|
+
for (const page of pages) {
|
|
141
|
+
const data = page.data as Record<string, unknown>;
|
|
142
|
+
const isIndex = page.url === '/';
|
|
99
143
|
const item: PageTreeItem = {
|
|
100
144
|
type: 'page',
|
|
101
|
-
name: page.
|
|
145
|
+
name: (data.title as string) ?? page.slugs.join('/') ?? 'Untitled',
|
|
102
146
|
url: page.url,
|
|
103
|
-
order:
|
|
104
|
-
}
|
|
147
|
+
order: (data.order as number | undefined) ?? (isIndex ? 0 : undefined)
|
|
148
|
+
};
|
|
105
149
|
|
|
106
150
|
if (page.slugs.length > 1) {
|
|
107
|
-
const folder = page.slugs[0]
|
|
151
|
+
const folder = page.slugs[0];
|
|
108
152
|
if (!folders.has(folder)) {
|
|
109
|
-
folders.set(folder, [])
|
|
153
|
+
folders.set(folder, []);
|
|
110
154
|
}
|
|
111
|
-
folders.get(folder)?.push(item)
|
|
155
|
+
folders.get(folder)?.push(item);
|
|
112
156
|
} else {
|
|
113
|
-
rootPages.push(item)
|
|
157
|
+
rootPages.push(item);
|
|
114
158
|
}
|
|
115
|
-
}
|
|
159
|
+
}
|
|
116
160
|
|
|
117
161
|
const sortByOrder = (items: PageTreeItem[]) =>
|
|
118
|
-
items.sort(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
162
|
+
items.sort(
|
|
163
|
+
(a, b) =>
|
|
164
|
+
(a.order ?? Number.MAX_SAFE_INTEGER) -
|
|
165
|
+
(b.order ?? Number.MAX_SAFE_INTEGER)
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const children: PageTreeItem[] = sortByOrder(rootPages);
|
|
169
|
+
|
|
170
|
+
const folderItems: PageTreeItem[] = [];
|
|
171
|
+
for (const [folder, items] of folders) {
|
|
172
|
+
const sorted = sortByOrder(items);
|
|
173
|
+
const indexPage = items.find(item => item.url === `/${folder}`);
|
|
174
|
+
const folderOrder = indexPage?.order ?? sorted[0]?.order;
|
|
127
175
|
folderItems.push({
|
|
128
176
|
type: 'folder',
|
|
129
|
-
name: folder.charAt(0).toUpperCase()
|
|
177
|
+
name: `${folder.charAt(0).toUpperCase()}${folder.slice(1)}`,
|
|
130
178
|
order: folderOrder,
|
|
131
|
-
children: sorted
|
|
132
|
-
})
|
|
133
|
-
}
|
|
179
|
+
children: sorted
|
|
180
|
+
});
|
|
181
|
+
}
|
|
134
182
|
|
|
135
|
-
children.push(...sortByOrder(folderItems))
|
|
183
|
+
children.push(...sortByOrder(folderItems));
|
|
136
184
|
|
|
137
|
-
return { name: 'root', children }
|
|
185
|
+
return { name: 'root', children };
|
|
138
186
|
}
|
package/src/pages/ApiLayout.tsx
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { buildApiPageTree } from '@/lib/api-routes'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import styles from './ApiLayout.module.css'
|
|
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
8
|
|
|
9
9
|
interface ApiLayoutProps {
|
|
10
|
-
children: ReactNode
|
|
10
|
+
children: ReactNode;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function ApiLayout({ children }: ApiLayoutProps) {
|
|
14
|
-
const { config, apiSpecs } = usePageContext()
|
|
15
|
-
const { Layout, className } = getTheme(config.theme?.name)
|
|
16
|
-
const tree = buildApiPageTree(apiSpecs)
|
|
14
|
+
const { config, apiSpecs } = usePageContext();
|
|
15
|
+
const { Layout, className } = getTheme(config.theme?.name);
|
|
16
|
+
const tree = buildApiPageTree(apiSpecs);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<Layout
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
>
|
|
25
29
|
<Search className={styles.hiddenSearch} />
|
|
26
30
|
{children}
|
|
27
31
|
</Layout>
|
|
28
|
-
)
|
|
32
|
+
);
|
|
29
33
|
}
|