@raystack/chronicle 0.1.0-canary.5a2be79 → 0.1.0-canary.6511afe
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 +224 -9916
- 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/search.module.css +7 -0
- package/src/components/ui/search.tsx +65 -49
- 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 +114 -0
- package/src/lib/source.ts +164 -45
- 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 +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 +70 -0
- package/src/server/entry-server.tsx +95 -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 +39 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +83 -0
- package/src/themes/default/Layout.tsx +69 -42
- 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 +60 -41
- 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 -48
- 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 +1 -0
- package/src/types/globals.d.ts +4 -0
- 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/cli/utils/resolve.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import { fileURLToPath } from 'url'
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// After bundling: dist/cli/index.js → ../.. = package root
|
|
5
|
+
// After install: node_modules/@raystack/chronicle/dist/cli/index.js → ../.. = package root
|
|
6
|
+
export const PACKAGE_ROOT = path.resolve(
|
|
7
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
'..',
|
|
9
|
+
'..'
|
|
10
|
+
);
|
|
@@ -1,131 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import chalk from 'chalk'
|
|
6
|
-
import { PACKAGE_ROOT } from './resolve'
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PACKAGE_ROOT } from './resolve';
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
export async function linkContent(contentDir: string): Promise<void> {
|
|
6
|
+
const linkPath = path.join(PACKAGE_ROOT, '.content');
|
|
7
|
+
const target = path.resolve(contentDir);
|
|
9
8
|
|
|
10
|
-
function copyRecursive(src: string, dest: string) {
|
|
11
|
-
const stat = fs.statSync(src)
|
|
12
|
-
if (stat.isDirectory()) {
|
|
13
|
-
fs.mkdirSync(dest, { recursive: true })
|
|
14
|
-
for (const entry of fs.readdirSync(src)) {
|
|
15
|
-
copyRecursive(path.join(src, entry), path.join(dest, entry))
|
|
16
|
-
}
|
|
17
|
-
} else {
|
|
18
|
-
fs.copyFileSync(src, dest)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function ensureRemoved(targetPath: string) {
|
|
23
9
|
try {
|
|
24
|
-
fs.
|
|
25
|
-
|
|
10
|
+
const existing = await fs.readlink(linkPath);
|
|
11
|
+
if (existing === target) return;
|
|
12
|
+
await fs.unlink(linkPath);
|
|
26
13
|
} catch {
|
|
27
|
-
//
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function detectPackageManager(): string {
|
|
32
|
-
if (process.env.npm_config_user_agent) {
|
|
33
|
-
return process.env.npm_config_user_agent.split('/')[0]
|
|
34
|
-
}
|
|
35
|
-
const cwd = process.cwd()
|
|
36
|
-
if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) return 'bun'
|
|
37
|
-
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
38
|
-
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
|
|
39
|
-
return 'npm'
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function generateNextConfig(scaffoldPath: string) {
|
|
43
|
-
const config = `import { createMDX } from 'fumadocs-mdx/next'
|
|
44
|
-
|
|
45
|
-
const withMDX = createMDX()
|
|
46
|
-
|
|
47
|
-
/** @type {import('next').NextConfig} */
|
|
48
|
-
const nextConfig = {
|
|
49
|
-
reactStrictMode: true,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default withMDX(nextConfig)
|
|
53
|
-
`
|
|
54
|
-
fs.writeFileSync(path.join(scaffoldPath, 'next.config.mjs'), config)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function createPackageJson(): Record<string, unknown> {
|
|
58
|
-
return {
|
|
59
|
-
name: 'chronicle-docs',
|
|
60
|
-
private: true,
|
|
61
|
-
dependencies: {
|
|
62
|
-
'@raystack/chronicle': 'latest',
|
|
63
|
-
},
|
|
64
|
-
devDependencies: {
|
|
65
|
-
'@raystack/tools-config': '0.56.0',
|
|
66
|
-
'openapi-types': '^12.1.3',
|
|
67
|
-
typescript: '5.9.3',
|
|
68
|
-
'@types/react': '^19.2.10',
|
|
69
|
-
'@types/node': '^25.1.0',
|
|
70
|
-
},
|
|
14
|
+
// link doesn't exist
|
|
71
15
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function ensureDeps() {
|
|
75
|
-
const cwd = process.cwd()
|
|
76
|
-
const cwdPkgJson = path.join(cwd, 'package.json')
|
|
77
|
-
const cwdNodeModules = path.join(cwd, 'node_modules')
|
|
78
|
-
|
|
79
|
-
if (fs.existsSync(cwdPkgJson) && fs.existsSync(cwdNodeModules)) {
|
|
80
|
-
// Case 1: existing project with deps installed
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Case 2: no package.json — create in cwd and install
|
|
85
|
-
if (!fs.existsSync(cwdPkgJson)) {
|
|
86
|
-
fs.writeFileSync(cwdPkgJson, JSON.stringify(createPackageJson(), null, 2) + '\n')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!fs.existsSync(cwdNodeModules)) {
|
|
90
|
-
const pm = detectPackageManager()
|
|
91
|
-
console.log(chalk.cyan(`Installing dependencies with ${pm}...`))
|
|
92
|
-
execSync(`${pm} install`, { cwd, stdio: 'inherit' })
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function resolveNextCli(): string {
|
|
97
|
-
const cwdRequire = createRequire(path.join(process.cwd(), 'package.json'))
|
|
98
|
-
return cwdRequire.resolve('next/dist/bin/next')
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function scaffoldDir(contentDir: string): string {
|
|
102
|
-
const scaffoldPath = path.join(process.cwd(), '.chronicle')
|
|
103
|
-
|
|
104
|
-
// Create .chronicle/ if not exists
|
|
105
|
-
if (!fs.existsSync(scaffoldPath)) {
|
|
106
|
-
fs.mkdirSync(scaffoldPath, { recursive: true })
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Copy package files
|
|
110
|
-
for (const name of COPY_FILES) {
|
|
111
|
-
const src = path.join(PACKAGE_ROOT, name)
|
|
112
|
-
const dest = path.join(scaffoldPath, name)
|
|
113
|
-
ensureRemoved(dest)
|
|
114
|
-
copyRecursive(src, dest)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Generate next.config.mjs
|
|
118
|
-
generateNextConfig(scaffoldPath)
|
|
119
|
-
|
|
120
|
-
// Symlink content dir
|
|
121
|
-
const contentLink = path.join(scaffoldPath, 'content')
|
|
122
|
-
ensureRemoved(contentLink)
|
|
123
|
-
fs.symlinkSync(path.resolve(contentDir), contentLink)
|
|
124
|
-
|
|
125
|
-
// Ensure dependencies are available
|
|
126
|
-
ensureDeps()
|
|
127
|
-
|
|
128
|
-
console.log(chalk.gray(`Scaffold: ${scaffoldPath}`))
|
|
129
16
|
|
|
130
|
-
|
|
17
|
+
await fs.symlink(target, linkPath);
|
|
131
18
|
}
|
|
@@ -1,38 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import type { ComponentProps } from 'react'
|
|
3
|
+
type ImageProps = ComponentProps<'img'>;
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
src
|
|
8
|
-
width?: number | string
|
|
9
|
-
height?: number | string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function Image({ src, alt, width, height, ...props }: ImageProps) {
|
|
13
|
-
if (!src || typeof src !== 'string') return null
|
|
14
|
-
|
|
15
|
-
const isExternal = src.startsWith('http://') || src.startsWith('https://')
|
|
16
|
-
|
|
17
|
-
if (isExternal) {
|
|
18
|
-
return (
|
|
19
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
20
|
-
<img
|
|
21
|
-
src={src}
|
|
22
|
-
alt={alt ?? ''}
|
|
23
|
-
width={width}
|
|
24
|
-
height={height}
|
|
25
|
-
{...props}
|
|
26
|
-
/>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
5
|
+
export function Image({ src, alt, ...props }: ImageProps) {
|
|
6
|
+
if (!src) return null;
|
|
29
7
|
|
|
30
|
-
return
|
|
31
|
-
<NextImage
|
|
32
|
-
src={src}
|
|
33
|
-
alt={alt ?? ''}
|
|
34
|
-
width={typeof width === 'string' ? parseInt(width, 10) : (width ?? 800)}
|
|
35
|
-
height={typeof height === 'string' ? parseInt(height, 10) : (height ?? 400)}
|
|
36
|
-
/>
|
|
37
|
-
)
|
|
8
|
+
return <img src={src} alt={alt ?? ''} loading='lazy' {...props} />;
|
|
38
9
|
}
|
|
@@ -1,38 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
import { Link as ApsaraLink } from '@raystack/apsara';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { Link as RouterLink } from 'react-router';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
import { Link as ApsaraLink } from '@raystack/apsara'
|
|
5
|
-
import type { ComponentProps } from 'react'
|
|
6
|
-
|
|
7
|
-
type LinkProps = ComponentProps<'a'>
|
|
5
|
+
type LinkProps = ComponentProps<'a'>;
|
|
8
6
|
|
|
9
7
|
export function Link({ href, children, ...props }: LinkProps) {
|
|
10
8
|
if (!href) {
|
|
11
|
-
return <span {...props}>{children}</span
|
|
9
|
+
return <span {...props}>{children}</span>;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
const isExternal = href.startsWith('http://') || href.startsWith('https://')
|
|
15
|
-
const isAnchor = href.startsWith('#')
|
|
12
|
+
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
13
|
+
const isAnchor = href.startsWith('#');
|
|
16
14
|
|
|
17
15
|
if (isAnchor) {
|
|
18
16
|
return (
|
|
19
17
|
<ApsaraLink href={href} {...props}>
|
|
20
18
|
{children}
|
|
21
19
|
</ApsaraLink>
|
|
22
|
-
)
|
|
20
|
+
);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
if (isExternal) {
|
|
26
24
|
return (
|
|
27
|
-
<ApsaraLink
|
|
25
|
+
<ApsaraLink
|
|
26
|
+
href={href}
|
|
27
|
+
target='_blank'
|
|
28
|
+
rel='noopener noreferrer'
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
28
31
|
{children}
|
|
29
32
|
</ApsaraLink>
|
|
30
|
-
)
|
|
33
|
+
);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
return (
|
|
34
|
-
<
|
|
37
|
+
<RouterLink to={href} className={props.className}>
|
|
35
38
|
{children}
|
|
36
|
-
</
|
|
37
|
-
)
|
|
39
|
+
</RouterLink>
|
|
40
|
+
);
|
|
38
41
|
}
|
|
@@ -102,3 +102,10 @@
|
|
|
102
102
|
.item[data-selected="true"] .icon {
|
|
103
103
|
color: var(--rs-color-foreground-accent-primary-hover);
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
.pageText :global(mark),
|
|
107
|
+
.headingText :global(mark) {
|
|
108
|
+
background: transparent;
|
|
109
|
+
color: var(--rs-color-foreground-accent-primary);
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import { isMacOs } from "react-device-detect";
|
|
11
|
-
import { MethodBadge } from "@/components/api/method-badge";
|
|
12
|
-
import styles from "./search.module.css";
|
|
1
|
+
import { DocumentIcon, HashtagIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { Button, Command, Dialog, Text } from '@raystack/apsara';
|
|
3
|
+
import { cx } from 'class-variance-authority';
|
|
4
|
+
import type { SortedResult } from 'fumadocs-core/search';
|
|
5
|
+
import { useDocsSearch } from 'fumadocs-core/search/client';
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
import { useNavigate } from 'react-router';
|
|
8
|
+
import { MethodBadge } from '@/components/api/method-badge';
|
|
9
|
+
import styles from './search.module.css';
|
|
13
10
|
|
|
14
11
|
function SearchShortcutKey({ className }: { className?: string }) {
|
|
15
|
-
const [key, setKey] = useState(
|
|
12
|
+
const [key, setKey] = useState('⌘');
|
|
16
13
|
|
|
17
14
|
useEffect(() => {
|
|
18
|
-
|
|
15
|
+
const isMac = navigator.platform?.toUpperCase().includes('MAC');
|
|
16
|
+
setKey(isMac ? '⌘' : 'Ctrl');
|
|
19
17
|
}, []);
|
|
20
18
|
|
|
21
19
|
return (
|
|
@@ -26,50 +24,50 @@ function SearchShortcutKey({ className }: { className?: string }) {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
interface SearchProps {
|
|
29
|
-
className?: string
|
|
27
|
+
className?: string;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
export function Search({ className }: SearchProps) {
|
|
33
31
|
const [open, setOpen] = useState(false);
|
|
34
|
-
const
|
|
32
|
+
const navigate = useNavigate();
|
|
35
33
|
|
|
36
34
|
const { search, setSearch, query } = useDocsSearch({
|
|
37
|
-
type:
|
|
38
|
-
api:
|
|
35
|
+
type: 'fetch',
|
|
36
|
+
api: '/api/search',
|
|
39
37
|
delayMs: 100,
|
|
40
|
-
allowEmpty: true
|
|
38
|
+
allowEmpty: true
|
|
41
39
|
});
|
|
42
40
|
|
|
43
41
|
const onSelect = useCallback(
|
|
44
42
|
(url: string) => {
|
|
45
43
|
setOpen(false);
|
|
46
|
-
|
|
44
|
+
navigate(url);
|
|
47
45
|
},
|
|
48
|
-
[
|
|
46
|
+
[navigate]
|
|
49
47
|
);
|
|
50
48
|
|
|
51
49
|
useEffect(() => {
|
|
52
50
|
const down = (e: KeyboardEvent) => {
|
|
53
|
-
if (e.key ===
|
|
51
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
54
52
|
e.preventDefault();
|
|
55
|
-
setOpen(
|
|
53
|
+
setOpen(open => !open);
|
|
56
54
|
}
|
|
57
55
|
};
|
|
58
56
|
|
|
59
|
-
document.addEventListener(
|
|
60
|
-
return () => document.removeEventListener(
|
|
57
|
+
document.addEventListener('keydown', down);
|
|
58
|
+
return () => document.removeEventListener('keydown', down);
|
|
61
59
|
}, []);
|
|
62
60
|
|
|
63
61
|
const results = deduplicateByUrl(
|
|
64
|
-
query.data ===
|
|
62
|
+
query.data === 'empty' ? [] : (query.data ?? [])
|
|
65
63
|
);
|
|
66
64
|
|
|
67
65
|
return (
|
|
68
66
|
<>
|
|
69
67
|
<Button
|
|
70
|
-
variant=
|
|
71
|
-
color=
|
|
72
|
-
size=
|
|
68
|
+
variant='outline'
|
|
69
|
+
color='neutral'
|
|
70
|
+
size='small'
|
|
73
71
|
onClick={() => setOpen(true)}
|
|
74
72
|
className={cx(styles.trigger, className)}
|
|
75
73
|
trailingIcon={<SearchShortcutKey className={styles.kbd} />}
|
|
@@ -84,7 +82,7 @@ export function Search({ className }: SearchProps) {
|
|
|
84
82
|
</Dialog.Title>
|
|
85
83
|
<Command loop>
|
|
86
84
|
<Command.Input
|
|
87
|
-
placeholder=
|
|
85
|
+
placeholder='Search'
|
|
88
86
|
value={search}
|
|
89
87
|
onValueChange={setSearch}
|
|
90
88
|
className={styles.input}
|
|
@@ -100,7 +98,7 @@ export function Search({ className }: SearchProps) {
|
|
|
100
98
|
{!query.isLoading &&
|
|
101
99
|
search.length === 0 &&
|
|
102
100
|
results.length > 0 && (
|
|
103
|
-
<Command.Group heading=
|
|
101
|
+
<Command.Group heading='Suggestions'>
|
|
104
102
|
{results.slice(0, 8).map((result: SortedResult) => (
|
|
105
103
|
<Command.Item
|
|
106
104
|
key={result.id}
|
|
@@ -111,7 +109,9 @@ export function Search({ className }: SearchProps) {
|
|
|
111
109
|
<div className={styles.itemContent}>
|
|
112
110
|
{getResultIcon(result)}
|
|
113
111
|
<Text className={styles.pageText}>
|
|
114
|
-
|
|
112
|
+
<HighlightedText
|
|
113
|
+
html={stripMethod(result.content)}
|
|
114
|
+
/>
|
|
115
115
|
</Text>
|
|
116
116
|
</div>
|
|
117
117
|
</Command.Item>
|
|
@@ -129,10 +129,12 @@ export function Search({ className }: SearchProps) {
|
|
|
129
129
|
<div className={styles.itemContent}>
|
|
130
130
|
{getResultIcon(result)}
|
|
131
131
|
<div className={styles.resultText}>
|
|
132
|
-
{result.type ===
|
|
132
|
+
{result.type === 'heading' ? (
|
|
133
133
|
<>
|
|
134
134
|
<Text className={styles.headingText}>
|
|
135
|
-
|
|
135
|
+
<HighlightedText
|
|
136
|
+
html={stripMethod(result.content)}
|
|
137
|
+
/>
|
|
136
138
|
</Text>
|
|
137
139
|
<Text className={styles.separator}>-</Text>
|
|
138
140
|
<Text className={styles.pageText}>
|
|
@@ -141,7 +143,9 @@ export function Search({ className }: SearchProps) {
|
|
|
141
143
|
</>
|
|
142
144
|
) : (
|
|
143
145
|
<Text className={styles.pageText}>
|
|
144
|
-
|
|
146
|
+
<HighlightedText
|
|
147
|
+
html={stripMethod(result.content)}
|
|
148
|
+
/>
|
|
145
149
|
</Text>
|
|
146
150
|
)}
|
|
147
151
|
</div>
|
|
@@ -158,45 +162,57 @@ export function Search({ className }: SearchProps) {
|
|
|
158
162
|
|
|
159
163
|
function deduplicateByUrl(results: SortedResult[]): SortedResult[] {
|
|
160
164
|
const seen = new Set<string>();
|
|
161
|
-
return results.filter(
|
|
162
|
-
const base = r.url.split(
|
|
165
|
+
return results.filter(r => {
|
|
166
|
+
const base = r.url.split('#')[0];
|
|
163
167
|
if (seen.has(base)) return false;
|
|
164
168
|
seen.add(base);
|
|
165
169
|
return true;
|
|
166
170
|
});
|
|
167
171
|
}
|
|
168
172
|
|
|
169
|
-
const API_METHODS = new Set([
|
|
173
|
+
const API_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']);
|
|
170
174
|
|
|
171
175
|
function extractMethod(content: string): string | null {
|
|
172
|
-
const first = content.split(
|
|
176
|
+
const first = content.split(' ')[0];
|
|
173
177
|
return API_METHODS.has(first) ? first : null;
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
function stripMethod(content: string): string {
|
|
177
|
-
const first = content.split(
|
|
181
|
+
const first = content.split(' ')[0];
|
|
178
182
|
return API_METHODS.has(first) ? content.slice(first.length + 1) : content;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
function HighlightedText({
|
|
186
|
+
html,
|
|
187
|
+
className
|
|
188
|
+
}: {
|
|
189
|
+
html: string;
|
|
190
|
+
className?: string;
|
|
191
|
+
}) {
|
|
192
|
+
return (
|
|
193
|
+
<span className={className} dangerouslySetInnerHTML={{ __html: html }} />
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
function getResultIcon(result: SortedResult): React.ReactNode {
|
|
182
|
-
if (!result.url.startsWith(
|
|
183
|
-
return result.type ===
|
|
198
|
+
if (!result.url.startsWith('/apis/')) {
|
|
199
|
+
return result.type === 'page' ? (
|
|
184
200
|
<DocumentIcon className={styles.icon} />
|
|
185
201
|
) : (
|
|
186
202
|
<HashtagIcon className={styles.icon} />
|
|
187
203
|
);
|
|
188
204
|
}
|
|
189
205
|
const method = extractMethod(result.content);
|
|
190
|
-
return method ? <MethodBadge method={method} size=
|
|
206
|
+
return method ? <MethodBadge method={method} size='micro' /> : null;
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
function getPageTitle(url: string): string {
|
|
194
|
-
const path = url.split(
|
|
195
|
-
const segments = path.split(
|
|
210
|
+
const path = url.split('#')[0];
|
|
211
|
+
const segments = path.split('/').filter(Boolean);
|
|
196
212
|
const lastSegment = segments[segments.length - 1];
|
|
197
|
-
if (!lastSegment) return
|
|
213
|
+
if (!lastSegment) return 'Home';
|
|
198
214
|
return lastSegment
|
|
199
|
-
.split(
|
|
200
|
-
.map(
|
|
201
|
-
.join(
|
|
215
|
+
.split('-')
|
|
216
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
217
|
+
.join(' ');
|
|
202
218
|
}
|
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
|
|