@raystack/chronicle 0.1.0-canary.111b55a → 0.1.0-canary.1e5fdae

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.
Files changed (87) hide show
  1. package/dist/cli/index.js +212 -833
  2. package/package.json +13 -9
  3. package/src/cli/commands/build.ts +30 -70
  4. package/src/cli/commands/dev.ts +24 -13
  5. package/src/cli/commands/init.ts +38 -123
  6. package/src/cli/commands/serve.ts +35 -50
  7. package/src/cli/commands/start.ts +20 -16
  8. package/src/cli/index.ts +14 -14
  9. package/src/cli/utils/config.ts +25 -26
  10. package/src/cli/utils/index.ts +3 -2
  11. package/src/cli/utils/resolve.ts +7 -3
  12. package/src/cli/utils/scaffold.ts +14 -16
  13. package/src/components/mdx/details.module.css +0 -2
  14. package/src/components/mdx/image.tsx +5 -20
  15. package/src/components/mdx/index.tsx +18 -4
  16. package/src/components/mdx/link.tsx +24 -20
  17. package/src/components/ui/breadcrumbs.tsx +8 -42
  18. package/src/components/ui/footer.tsx +2 -3
  19. package/src/components/ui/search.tsx +116 -71
  20. package/src/lib/api-routes.ts +6 -8
  21. package/src/lib/config.ts +31 -29
  22. package/src/lib/get-llm-text.ts +10 -0
  23. package/src/lib/head.tsx +26 -22
  24. package/src/lib/openapi.ts +8 -8
  25. package/src/lib/page-context.tsx +74 -58
  26. package/src/lib/source.ts +136 -114
  27. package/src/pages/ApiLayout.tsx +22 -18
  28. package/src/pages/ApiPage.tsx +32 -27
  29. package/src/pages/DocsLayout.tsx +7 -7
  30. package/src/pages/DocsPage.tsx +11 -11
  31. package/src/pages/NotFound.tsx +11 -4
  32. package/src/server/App.tsx +35 -27
  33. package/src/server/api/apis-proxy.ts +69 -0
  34. package/src/server/api/health.ts +5 -0
  35. package/src/server/api/page/[...slug].ts +17 -0
  36. package/src/server/api/search.ts +170 -0
  37. package/src/server/api/specs.ts +9 -0
  38. package/src/server/build-search-index.ts +78 -68
  39. package/src/server/entry-client.tsx +67 -55
  40. package/src/server/entry-server.tsx +100 -35
  41. package/src/server/routes/llms.txt.ts +61 -0
  42. package/src/server/routes/og.tsx +75 -0
  43. package/src/server/routes/robots.txt.ts +11 -0
  44. package/src/server/routes/sitemap.xml.ts +40 -0
  45. package/src/server/utils/safe-path.ts +17 -0
  46. package/src/server/vite-config.ts +87 -47
  47. package/src/themes/default/Layout.tsx +78 -47
  48. package/src/themes/default/Page.module.css +0 -16
  49. package/src/themes/default/Page.tsx +9 -11
  50. package/src/themes/default/Toc.tsx +25 -39
  51. package/src/themes/default/index.ts +7 -9
  52. package/src/themes/paper/ChapterNav.tsx +63 -43
  53. package/src/themes/paper/Layout.module.css +1 -1
  54. package/src/themes/paper/Layout.tsx +24 -12
  55. package/src/themes/paper/Page.module.css +16 -4
  56. package/src/themes/paper/Page.tsx +56 -62
  57. package/src/themes/paper/ReadingProgress.tsx +160 -139
  58. package/src/themes/paper/index.ts +5 -5
  59. package/src/themes/registry.ts +7 -7
  60. package/src/types/content.ts +5 -21
  61. package/src/types/globals.d.ts +3 -0
  62. package/src/types/theme.ts +4 -3
  63. package/src/cli/__tests__/config.test.ts +0 -25
  64. package/src/cli/__tests__/scaffold.test.ts +0 -10
  65. package/src/pages/__tests__/head.test.tsx +0 -57
  66. package/src/server/__tests__/entry-server.test.tsx +0 -35
  67. package/src/server/__tests__/handlers.test.ts +0 -77
  68. package/src/server/__tests__/og.test.ts +0 -23
  69. package/src/server/__tests__/router.test.ts +0 -72
  70. package/src/server/__tests__/vite-config.test.ts +0 -25
  71. package/src/server/adapters/vercel.ts +0 -133
  72. package/src/server/dev.ts +0 -156
  73. package/src/server/entry-prod.ts +0 -97
  74. package/src/server/entry-vercel.ts +0 -28
  75. package/src/server/handlers/apis-proxy.ts +0 -52
  76. package/src/server/handlers/health.ts +0 -3
  77. package/src/server/handlers/llms.ts +0 -58
  78. package/src/server/handlers/og.ts +0 -87
  79. package/src/server/handlers/robots.ts +0 -11
  80. package/src/server/handlers/search.ts +0 -172
  81. package/src/server/handlers/sitemap.ts +0 -39
  82. package/src/server/handlers/specs.ts +0 -9
  83. package/src/server/index.html +0 -12
  84. package/src/server/prod.ts +0 -18
  85. package/src/server/request-handler.ts +0 -63
  86. package/src/server/router.ts +0 -42
  87. package/src/themes/default/font.ts +0 -4
@@ -1,29 +1,33 @@
1
- import type { ReactNode } from 'react'
2
- import { cx } from 'class-variance-authority'
3
- import { usePageContext } from '@/lib/page-context'
4
- import { buildApiPageTree } from '@/lib/api-routes'
5
- import { getTheme } from '@/themes/registry'
6
- import { Search } from '@/components/ui/search'
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 config={config} tree={tree} classNames={{
20
- layout: cx(styles.layout, className),
21
- body: styles.body,
22
- sidebar: styles.sidebar,
23
- content: styles.content,
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
  }
@@ -1,44 +1,41 @@
1
- import type { OpenAPIV3 } from 'openapi-types'
2
- import { Flex, Headline, Text } from '@raystack/apsara'
3
- import { usePageContext } from '@/lib/page-context'
4
- import { findApiOperation } from '@/lib/api-routes'
5
- import { EndpointPage } from '@/components/api'
6
- import { Head } from '@/lib/head'
7
- import type { ApiSpec } from '@/lib/openapi'
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
8
 
9
9
  interface ApiPageProps {
10
- slug: string[]
10
+ slug: string[];
11
11
  }
12
12
 
13
13
  export function ApiPage({ slug }: ApiPageProps) {
14
- const { config, apiSpecs } = usePageContext()
14
+ const { config, apiSpecs } = usePageContext();
15
15
 
16
16
  if (slug.length === 0) {
17
17
  return (
18
18
  <>
19
19
  <Head
20
- title="API Reference"
20
+ title='API Reference'
21
21
  description={`API documentation for ${config.title}`}
22
22
  config={config}
23
23
  />
24
24
  <ApiLanding specs={apiSpecs} />
25
25
  </>
26
- )
26
+ );
27
27
  }
28
28
 
29
- const match = findApiOperation(apiSpecs, slug)
30
- if (!match) return null
29
+ const match = findApiOperation(apiSpecs, slug);
30
+ if (!match) return null;
31
31
 
32
- const operation = match.operation as OpenAPIV3.OperationObject
33
- const title = operation.summary ?? `${match.method.toUpperCase()} ${match.path}`
32
+ const operation = match.operation as OpenAPIV3.OperationObject;
33
+ const title =
34
+ operation.summary ?? `${match.method.toUpperCase()} ${match.path}`;
34
35
 
35
36
  return (
36
37
  <>
37
- <Head
38
- title={title}
39
- description={operation.description}
40
- config={config}
41
- />
38
+ <Head title={title} description={operation.description} config={config} />
42
39
  <EndpointPage
43
40
  method={match.method}
44
41
  path={match.path}
@@ -48,21 +45,29 @@ export function ApiPage({ slug }: ApiPageProps) {
48
45
  auth={match.spec.auth}
49
46
  />
50
47
  </>
51
- )
48
+ );
52
49
  }
53
50
 
54
51
  function ApiLanding({ specs }: { specs: ApiSpec[] }) {
55
52
  return (
56
- <Flex direction="column" gap="large" style={{ padding: 'var(--rs-space-7)' }}>
57
- <Headline size="medium" as="h1">API Reference</Headline>
58
- {specs.map((spec) => (
59
- <Flex key={spec.name} direction="column" gap="small">
60
- <Headline size="small" as="h2">{spec.name}</Headline>
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>
61
66
  {spec.document.info.description && (
62
67
  <Text size={3}>{spec.document.info.description}</Text>
63
68
  )}
64
69
  </Flex>
65
70
  ))}
66
71
  </Flex>
67
- )
72
+ );
68
73
  }
@@ -1,18 +1,18 @@
1
- import type { ReactNode } from 'react'
2
- import { usePageContext } from '@/lib/page-context'
3
- import { getTheme } from '@/themes/registry'
1
+ import type { ReactNode } from 'react';
2
+ import { usePageContext } from '@/lib/page-context';
3
+ import { getTheme } from '@/themes/registry';
4
4
 
5
5
  interface DocsLayoutProps {
6
- children: ReactNode
6
+ children: ReactNode;
7
7
  }
8
8
 
9
9
  export function DocsLayout({ children }: DocsLayoutProps) {
10
- const { config, tree } = usePageContext()
11
- const { Layout, className } = getTheme(config.theme?.name)
10
+ const { config, tree } = usePageContext();
11
+ const { Layout, className } = getTheme(config.theme?.name);
12
12
 
13
13
  return (
14
14
  <Layout config={config} tree={tree} classNames={{ layout: className }}>
15
15
  {children}
16
16
  </Layout>
17
- )
17
+ );
18
18
  }
@@ -1,18 +1,18 @@
1
- import { usePageContext } from '@/lib/page-context'
2
- import { getTheme } from '@/themes/registry'
3
- import { Head } from '@/lib/head'
1
+ import { Head } from '@/lib/head';
2
+ import { usePageContext } from '@/lib/page-context';
3
+ import { getTheme } from '@/themes/registry';
4
4
 
5
5
  interface DocsPageProps {
6
- slug: string[]
6
+ slug: string[];
7
7
  }
8
8
 
9
9
  export function DocsPage({ slug }: DocsPageProps) {
10
- const { config, tree, page } = usePageContext()
10
+ const { config, tree, page } = usePageContext();
11
11
 
12
- if (!page) return null
12
+ if (!page) return null;
13
13
 
14
- const { Page } = getTheme(config.theme?.name)
15
- const pageUrl = config.url ? `${config.url}/${slug.join('/')}` : undefined
14
+ const { Page } = getTheme(config.theme?.name);
15
+ const pageUrl = config.url ? `${config.url}/${slug.join('/')}` : undefined;
16
16
 
17
17
  return (
18
18
  <>
@@ -25,7 +25,7 @@ export function DocsPage({ slug }: DocsPageProps) {
25
25
  '@type': 'Article',
26
26
  headline: page.frontmatter.title,
27
27
  description: page.frontmatter.description,
28
- ...(pageUrl && { url: pageUrl }),
28
+ ...(pageUrl && { url: pageUrl })
29
29
  }}
30
30
  />
31
31
  <Page
@@ -33,11 +33,11 @@ export function DocsPage({ slug }: DocsPageProps) {
33
33
  slug,
34
34
  frontmatter: page.frontmatter,
35
35
  content: page.content,
36
- toc: [],
36
+ toc: page.toc
37
37
  }}
38
38
  config={config}
39
39
  tree={tree}
40
40
  />
41
41
  </>
42
- )
42
+ );
43
43
  }
@@ -1,10 +1,17 @@
1
- import { Flex, Headline, Text } from '@raystack/apsara'
1
+ import { Flex, Headline, Text } from '@raystack/apsara';
2
2
 
3
3
  export function NotFound() {
4
4
  return (
5
- <Flex direction="column" align="center" justify="center" style={{ minHeight: '60vh' }}>
6
- <Headline size="large" as="h1">404</Headline>
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>
7
14
  <Text size={3}>Page not found</Text>
8
15
  </Flex>
9
- )
16
+ );
10
17
  }
@@ -1,29 +1,33 @@
1
- import '@raystack/apsara/normalize.css'
2
- import '@raystack/apsara/style.css'
3
- import { ThemeProvider } from '@raystack/apsara'
4
- import { useLocation } from 'react-router-dom'
5
- import { usePageContext } from '@/lib/page-context'
6
- import { Head } from '@/lib/head'
7
- import { DocsLayout } from '@/pages/DocsLayout'
8
- import { DocsPage } from '@/pages/DocsPage'
9
- import { ApiLayout } from '@/pages/ApiLayout'
10
- import { ApiPage } from '@/pages/ApiPage'
11
- import type { ChronicleConfig } from '@/types'
1
+ import '@raystack/apsara/normalize.css';
2
+ import '@raystack/apsara/style.css';
3
+ import { ThemeProvider } from '@raystack/apsara';
4
+ import { useLocation } from 'react-router';
5
+ import { Head } from '@/lib/head';
6
+ import { usePageContext } from '@/lib/page-context';
7
+ import { ApiLayout } from '@/pages/ApiLayout';
8
+ import { ApiPage } from '@/pages/ApiPage';
9
+ import { DocsLayout } from '@/pages/DocsLayout';
10
+ import { DocsPage } from '@/pages/DocsPage';
11
+ import type { ChronicleConfig } from '@/types';
12
12
 
13
13
  function resolveRoute(pathname: string) {
14
14
  if (pathname.startsWith('/apis')) {
15
- const slug = pathname.replace(/^\/apis\/?/, '').split('/').filter(Boolean)
16
- return { type: 'api' as const, slug }
15
+ const slug = pathname
16
+ .replace(/^\/apis\/?/, '')
17
+ .split('/')
18
+ .filter(Boolean);
19
+ return { type: 'api' as const, slug };
17
20
  }
18
21
 
19
- const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
20
- return { type: 'docs' as const, slug }
22
+ const slug =
23
+ pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
24
+ return { type: 'docs' as const, slug };
21
25
  }
22
26
 
23
27
  export function App() {
24
- const { pathname } = useLocation()
25
- const { config } = usePageContext()
26
- const route = resolveRoute(pathname)
28
+ const { pathname } = useLocation();
29
+ const { config } = usePageContext();
30
+ const route = resolveRoute(pathname);
27
31
 
28
32
  return (
29
33
  <ThemeProvider enableSystem>
@@ -38,7 +42,7 @@ export function App() {
38
42
  </DocsLayout>
39
43
  )}
40
44
  </ThemeProvider>
41
- )
45
+ );
42
46
  }
43
47
 
44
48
  function RootHead({ config }: { config: ChronicleConfig }) {
@@ -47,13 +51,17 @@ function RootHead({ config }: { config: ChronicleConfig }) {
47
51
  title={config.title}
48
52
  description={config.description}
49
53
  config={config}
50
- jsonLd={config.url ? {
51
- '@context': 'https://schema.org',
52
- '@type': 'WebSite',
53
- name: config.title,
54
- description: config.description,
55
- url: config.url,
56
- } : undefined}
54
+ jsonLd={
55
+ config.url
56
+ ? {
57
+ '@context': 'https://schema.org',
58
+ '@type': 'WebSite',
59
+ name: config.title,
60
+ description: config.description,
61
+ url: config.url
62
+ }
63
+ : undefined
64
+ }
57
65
  />
58
- )
66
+ );
59
67
  }
@@ -0,0 +1,69 @@
1
+ import { defineHandler, HTTPError } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+ import { loadApiSpecs } from '@/lib/openapi';
4
+
5
+ interface ProxyRequest {
6
+ specName: string;
7
+ method: string;
8
+ path: string;
9
+ headers?: Record<string, string>;
10
+ body?: unknown;
11
+ }
12
+
13
+ export default defineHandler(async event => {
14
+ if (event.req.method !== 'POST') {
15
+ throw new HTTPError({ status: 405, message: 'Method not allowed' });
16
+ }
17
+
18
+ const { specName, method, path, headers, body } =
19
+ (await event.req.json()) as ProxyRequest;
20
+
21
+ if (!specName || !method || !path) {
22
+ throw new HTTPError({
23
+ status: 400,
24
+ message: 'Missing specName, method, or path'
25
+ });
26
+ }
27
+
28
+ const config = loadConfig();
29
+ const specs = await loadApiSpecs(config.api ?? []);
30
+ const spec = specs.find(s => s.name === specName);
31
+
32
+ if (!spec) {
33
+ throw new HTTPError({ status: 404, message: `Unknown spec: ${specName}` });
34
+ }
35
+
36
+ if (/^[a-z]+:\/\//i.test(path) || path.includes('..')) {
37
+ throw new HTTPError({ status: 400, message: 'Invalid path' });
38
+ }
39
+
40
+ const url = spec.server.url + path;
41
+
42
+ try {
43
+ const response = await fetch(url, {
44
+ method,
45
+ headers,
46
+ body: body ? JSON.stringify(body) : undefined
47
+ });
48
+
49
+ const contentType = response.headers.get('content-type') ?? '';
50
+ const responseBody = contentType.includes('application/json')
51
+ ? await response.json()
52
+ : await response.text();
53
+
54
+ return {
55
+ status: response.status,
56
+ statusText: response.statusText,
57
+ body: responseBody
58
+ };
59
+ } catch (error) {
60
+ const message =
61
+ error instanceof Error
62
+ ? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}`
63
+ : 'Request failed';
64
+ throw new HTTPError({
65
+ status: 502,
66
+ message: `Could not reach ${url}\n${message}`
67
+ });
68
+ }
69
+ });
@@ -0,0 +1,5 @@
1
+ import { defineHandler } from 'nitro';
2
+
3
+ export default defineHandler(() => {
4
+ return { status: 'ok' };
5
+ });
@@ -0,0 +1,17 @@
1
+ import { defineHandler, HTTPError } from 'nitro';
2
+ import { getPage, extractFrontmatter, getRelativePath } from '@/lib/source';
3
+
4
+ export default defineHandler(async event => {
5
+ const slugParam = event.context.params?.slug ?? '';
6
+ const slug = slugParam ? slugParam.split('/') : [];
7
+ const page = await getPage(slug);
8
+
9
+ if (!page) {
10
+ throw new HTTPError({ status: 404, message: 'Page not found' });
11
+ }
12
+
13
+ return {
14
+ frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
15
+ relativePath: getRelativePath(page),
16
+ };
17
+ });
@@ -0,0 +1,170 @@
1
+ import fs from 'node:fs/promises';
2
+ import matter from 'gray-matter';
3
+ import MiniSearch from 'minisearch';
4
+ import { defineHandler } from 'nitro';
5
+ import type { OpenAPIV3 } from 'openapi-types';
6
+ import path from 'node:path';
7
+ import { getSpecSlug } from '@/lib/api-routes';
8
+ import { loadConfig } from '@/lib/config';
9
+ import { loadApiSpecs } from '@/lib/openapi';
10
+
11
+ interface SearchDocument {
12
+ id: string;
13
+ url: string;
14
+ title: string;
15
+ content: string;
16
+ type: 'page' | 'api';
17
+ }
18
+
19
+ let searchIndex: MiniSearch<SearchDocument> | null = null;
20
+ let cachedDocs: SearchDocument[] | null = null;
21
+
22
+ function createIndex(docs: SearchDocument[]): MiniSearch<SearchDocument> {
23
+ const index = new MiniSearch<SearchDocument>({
24
+ fields: ['title', 'content'],
25
+ storeFields: ['url', 'title', 'type'],
26
+ searchOptions: {
27
+ boost: { title: 2 },
28
+ fuzzy: 0.2,
29
+ prefix: true
30
+ }
31
+ });
32
+ index.addAll(docs);
33
+ return index;
34
+ }
35
+
36
+ async function loadPrebuiltIndex(): Promise<SearchDocument[] | null> {
37
+ try {
38
+ const indexPath = path.resolve(__dirname, 'search-index.json');
39
+ const raw = await fs.readFile(indexPath, 'utf-8');
40
+ return JSON.parse(raw);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function getContentDir(): string {
47
+ return __CHRONICLE_CONTENT_DIR__ || path.join(process.cwd(), 'content');
48
+ }
49
+
50
+ async function scanContent(): Promise<SearchDocument[]> {
51
+ const contentDir = getContentDir();
52
+ const docs: SearchDocument[] = [];
53
+
54
+ async function scan(dir: string, prefix: string[] = []) {
55
+ try {
56
+ const entries = await fs.readdir(dir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
59
+ continue;
60
+ const fullPath = path.join(dir, entry.name);
61
+
62
+ if (entry.isDirectory()) {
63
+ await scan(fullPath, [...prefix, entry.name]);
64
+ continue;
65
+ }
66
+
67
+ if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
68
+ continue;
69
+
70
+ const raw = await fs.readFile(fullPath, 'utf-8');
71
+ const { data: fm, content } = matter(raw);
72
+ const baseName = entry.name.replace(/\.(mdx|md)$/, '');
73
+ const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
74
+ const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
75
+
76
+ docs.push({
77
+ id: url,
78
+ url,
79
+ title: fm.title ?? baseName,
80
+ content: content.slice(0, 5000),
81
+ type: 'page'
82
+ });
83
+ }
84
+ } catch {
85
+ /* directory not readable */
86
+ }
87
+ }
88
+
89
+ await scan(contentDir);
90
+ return docs;
91
+ }
92
+
93
+ async function buildApiDocs(): Promise<SearchDocument[]> {
94
+ const config = loadConfig();
95
+ if (!config.api?.length) return [];
96
+
97
+ const docs: SearchDocument[] = [];
98
+ const specs = await loadApiSpecs(config.api);
99
+
100
+ for (const spec of specs) {
101
+ const specSlug = getSpecSlug(spec);
102
+ const paths = spec.document.paths ?? {};
103
+ for (const [, pathItem] of Object.entries(paths)) {
104
+ if (!pathItem) continue;
105
+ for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
106
+ const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
107
+ if (!op?.operationId) continue;
108
+ const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
109
+ docs.push({
110
+ id: url,
111
+ url,
112
+ title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
113
+ content: op.description ?? '',
114
+ type: 'api'
115
+ });
116
+ }
117
+ }
118
+ }
119
+
120
+ return docs;
121
+ }
122
+
123
+ async function loadDocuments(): Promise<SearchDocument[]> {
124
+ const prebuilt = await loadPrebuiltIndex();
125
+ if (prebuilt) return prebuilt;
126
+
127
+ const [contentDocs, apiDocs] = await Promise.all([
128
+ scanContent(),
129
+ buildApiDocs()
130
+ ]);
131
+ return [...contentDocs, ...apiDocs];
132
+ }
133
+
134
+ async function getDocs(): Promise<SearchDocument[]> {
135
+ if (cachedDocs) return cachedDocs;
136
+ cachedDocs = await loadDocuments();
137
+ return cachedDocs;
138
+ }
139
+
140
+ async function getIndex(): Promise<MiniSearch<SearchDocument>> {
141
+ if (searchIndex) return searchIndex;
142
+ const docs = await getDocs();
143
+ searchIndex = createIndex(docs);
144
+ return searchIndex;
145
+ }
146
+
147
+ export default defineHandler(async event => {
148
+ const query = event.url.searchParams.get('query') ?? '';
149
+ const index = await getIndex();
150
+
151
+ if (!query) {
152
+ const docs = await getDocs();
153
+ return docs
154
+ .filter(d => d.type === 'page')
155
+ .slice(0, 8)
156
+ .map(d => ({
157
+ id: d.id,
158
+ url: d.url,
159
+ type: d.type,
160
+ content: d.title
161
+ }));
162
+ }
163
+
164
+ return index.search(query).map(r => ({
165
+ id: r.id,
166
+ url: r.url,
167
+ type: r.type,
168
+ content: r.title
169
+ }));
170
+ });
@@ -0,0 +1,9 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+ import { loadApiSpecs } from '@/lib/openapi';
4
+
5
+ export default defineHandler(async () => {
6
+ const config = loadConfig();
7
+ const specs = config.api?.length ? await loadApiSpecs(config.api) : [];
8
+ return specs;
9
+ });