@raystack/chronicle 0.1.0-canary.5a730d4 → 0.1.0-canary.67113f8

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 (85) hide show
  1. package/dist/cli/index.js +217 -412
  2. package/package.json +13 -10
  3. package/src/cli/commands/build.ts +30 -48
  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 -72
  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/mdx-loader.ts +21 -0
  25. package/src/lib/openapi.ts +8 -8
  26. package/src/lib/page-context.tsx +74 -58
  27. package/src/lib/source.ts +136 -114
  28. package/src/pages/ApiLayout.tsx +22 -18
  29. package/src/pages/ApiPage.tsx +32 -27
  30. package/src/pages/DocsLayout.tsx +7 -7
  31. package/src/pages/DocsPage.tsx +11 -11
  32. package/src/pages/NotFound.tsx +11 -4
  33. package/src/server/App.tsx +35 -27
  34. package/src/server/api/apis-proxy.ts +69 -0
  35. package/src/server/api/health.ts +5 -0
  36. package/src/server/api/page/[...slug].ts +17 -0
  37. package/src/server/api/search.ts +170 -0
  38. package/src/server/api/specs.ts +9 -0
  39. package/src/server/build-search-index.ts +117 -0
  40. package/src/server/entry-client.tsx +47 -55
  41. package/src/server/entry-server.tsx +100 -35
  42. package/src/server/routes/llms.txt.ts +61 -0
  43. package/src/server/routes/og.tsx +75 -0
  44. package/src/server/routes/robots.txt.ts +11 -0
  45. package/src/server/routes/sitemap.xml.ts +40 -0
  46. package/src/server/utils/safe-path.ts +17 -0
  47. package/src/server/vite-config.ts +90 -44
  48. package/src/themes/default/Layout.tsx +78 -47
  49. package/src/themes/default/Page.module.css +0 -12
  50. package/src/themes/default/Page.tsx +9 -11
  51. package/src/themes/default/Toc.tsx +25 -39
  52. package/src/themes/default/index.ts +7 -9
  53. package/src/themes/paper/ChapterNav.tsx +63 -43
  54. package/src/themes/paper/Layout.module.css +1 -1
  55. package/src/themes/paper/Layout.tsx +24 -12
  56. package/src/themes/paper/Page.module.css +16 -4
  57. package/src/themes/paper/Page.tsx +56 -62
  58. package/src/themes/paper/ReadingProgress.tsx +160 -139
  59. package/src/themes/paper/index.ts +5 -5
  60. package/src/themes/registry.ts +7 -7
  61. package/src/types/content.ts +5 -21
  62. package/src/types/globals.d.ts +3 -0
  63. package/src/types/theme.ts +4 -3
  64. package/src/cli/__tests__/config.test.ts +0 -25
  65. package/src/cli/__tests__/scaffold.test.ts +0 -10
  66. package/src/pages/__tests__/head.test.tsx +0 -57
  67. package/src/server/__tests__/entry-server.test.tsx +0 -35
  68. package/src/server/__tests__/handlers.test.ts +0 -77
  69. package/src/server/__tests__/og.test.ts +0 -23
  70. package/src/server/__tests__/router.test.ts +0 -72
  71. package/src/server/__tests__/vite-config.test.ts +0 -25
  72. package/src/server/dev.ts +0 -156
  73. package/src/server/entry-prod.ts +0 -127
  74. package/src/server/handlers/apis-proxy.ts +0 -52
  75. package/src/server/handlers/health.ts +0 -3
  76. package/src/server/handlers/llms.ts +0 -58
  77. package/src/server/handlers/og.ts +0 -87
  78. package/src/server/handlers/robots.ts +0 -11
  79. package/src/server/handlers/search.ts +0 -140
  80. package/src/server/handlers/sitemap.ts +0 -39
  81. package/src/server/handlers/specs.ts +0 -9
  82. package/src/server/index.html +0 -12
  83. package/src/server/prod.ts +0 -18
  84. package/src/server/router.ts +0 -42
  85. package/src/themes/default/font.ts +0 -4
package/src/lib/config.ts CHANGED
@@ -1,56 +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
- // 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
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
- return null
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
- analytics: { enabled: false, ...userConfig.analytics },
55
- }
56
- }
56
+ analytics: { enabled: false, ...userConfig.analytics }
57
+ };
58
+ }
@@ -0,0 +1,10 @@
1
+ import { source } from '@/lib/source'
2
+ import type { InferPageType } from 'fumadocs-core/source'
3
+
4
+ export async function getLLMText(page: InferPageType<typeof source>) {
5
+ const processed = await page.data.getText('processed')
6
+
7
+ return `# ${page.data.title} (${page.url})
8
+
9
+ ${processed}`
10
+ }
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="description" content={description} />}
18
+ {description && <meta name='description' content={description} />}
19
19
 
20
20
  {config.url && (
21
21
  <>
22
- <meta property="og:title" content={title} />
23
- {description && <meta property="og:description" content={description} />}
24
- <meta property="og:site_name" content={config.title} />
25
- <meta property="og:type" content="website" />
26
- <meta property="og:image" content={`/og?${ogParams.toString()}`} />
27
- <meta property="og:image:width" content="1200" />
28
- <meta property="og:image:height" content="630" />
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="twitter:card" content="summary_large_image" />
31
- <meta name="twitter:title" content={title} />
32
- {description && <meta name="twitter:description" content={description} />}
33
- <meta name="twitter:image" content={`/og?${ogParams.toString()}`} />
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="application/ld+json"
43
+ type='application/ld+json'
40
44
  dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd, null, 2) }}
41
45
  />
42
46
  )}
43
47
  </>
44
- )
48
+ );
45
49
  }
@@ -0,0 +1,21 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import type { TableOfContents } from 'fumadocs-core/toc';
3
+ import { mdxComponents } from '@/components/mdx';
4
+
5
+ const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
6
+ '../../.content/**/*.{mdx,md}'
7
+ );
8
+
9
+ export async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
10
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
11
+ const key = relativePath.endsWith('.md')
12
+ ? `../../.content/${withoutExt}.md`
13
+ : `../../.content/${withoutExt}.mdx`;
14
+ const loader = contentModules[key];
15
+ if (!loader) return { content: null, toc: [] };
16
+ const mod = await loader();
17
+ const content = mod.default
18
+ ? React.createElement(mod.default, { components: mdxComponents })
19
+ : null;
20
+ return { content, toc: mod.toc ?? [] };
21
+ }
@@ -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 contentDir = process.env.CHRONICLE_CONTENT_DIR ?? process.cwd()
22
- return apiConfigs.map((config) => loadApiSpec(config, contentDir))
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, contentDir: string): ApiSpec {
26
- const specPath = path.resolve(contentDir, config.spec)
27
- const raw = fs.readFileSync(specPath, 'utf-8')
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
 
@@ -1,95 +1,111 @@
1
- import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
2
- import { useLocation } from 'react-router-dom'
3
- import type { ChronicleConfig, Frontmatter, PageTree } from '@/types'
4
- import type { ApiSpec } from '@/lib/openapi'
5
- import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
6
- import { mdxComponents } from '@/components/mdx'
7
- import React from 'react'
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 }>;
8
13
 
9
14
  interface PageData {
10
- slug: string[]
11
- frontmatter: Frontmatter
12
- content: ReactNode
15
+ slug: string[];
16
+ frontmatter: Frontmatter;
17
+ content: ReactNode;
18
+ toc: TableOfContents;
13
19
  }
14
20
 
15
21
  interface PageContextValue {
16
- config: ChronicleConfig
17
- tree: PageTree
18
- page: PageData | null
19
- apiSpecs: ApiSpec[]
22
+ config: ChronicleConfig;
23
+ tree: Root;
24
+ page: PageData | null;
25
+ apiSpecs: ApiSpec[];
20
26
  }
21
27
 
22
- const PageContext = createContext<PageContextValue | null>(null)
28
+ const PageContext = createContext<PageContextValue | null>(null);
23
29
 
24
30
  export function usePageContext(): PageContextValue {
25
- const ctx = useContext(PageContext)
31
+ const ctx = useContext(PageContext);
26
32
  if (!ctx) {
27
- console.error('usePageContext: no context found!')
33
+ console.error('usePageContext: no context found!');
28
34
  return {
29
35
  config: { title: 'Documentation' },
30
- tree: { name: 'root', children: [] },
36
+ tree: { name: 'root', children: [] } as Root,
31
37
  page: null,
32
- apiSpecs: [],
33
- }
38
+ apiSpecs: []
39
+ };
34
40
  }
35
- return ctx
41
+ return ctx;
36
42
  }
37
43
 
38
44
  interface PageProviderProps {
39
- initialConfig: ChronicleConfig
40
- initialTree: PageTree
41
- initialPage: PageData | null
42
- initialApiSpecs: ApiSpec[]
43
- children: ReactNode
45
+ initialConfig: ChronicleConfig;
46
+ initialTree: Root;
47
+ initialPage: PageData | null;
48
+ initialApiSpecs: ApiSpec[];
49
+ loadMdx: MdxLoader;
50
+ children: ReactNode;
44
51
  }
45
52
 
46
- export function PageProvider({ initialConfig, initialTree, initialPage, initialApiSpecs, children }: PageProviderProps) {
47
- const { pathname } = useLocation()
48
- const [tree, setTree] = useState<PageTree>(initialTree)
49
- const [page, setPage] = useState<PageData | null>(initialPage)
50
- const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs)
51
- const [currentPath, setCurrentPath] = useState(pathname)
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);
52
66
 
53
67
  useEffect(() => {
54
- if (pathname === currentPath) return
55
- setCurrentPath(pathname)
68
+ if (pathname === currentPath) return;
69
+ setCurrentPath(pathname);
56
70
 
57
- let cancelled = false
71
+ const cancelled = { current: false };
58
72
 
59
73
  if (pathname.startsWith('/apis')) {
60
- // Fetch API specs if not already loaded
61
74
  if (apiSpecs.length === 0) {
62
75
  fetch('/api/specs')
63
- .then((res) => res.json())
64
- .then((specs) => { if (!cancelled) setApiSpecs(specs) })
65
- .catch(() => {})
76
+ .then(res => res.json())
77
+ .then(specs => {
78
+ if (!cancelled.current) setApiSpecs(specs);
79
+ })
80
+ .catch(() => {});
66
81
  }
67
- return () => { cancelled = true }
82
+ return () => { cancelled.current = true; };
68
83
  }
69
84
 
70
- async function load() {
71
- const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
72
-
73
- const [sourcePage, newTree] = await Promise.all([getPage(slug), buildPageTree()])
74
- if (cancelled || !sourcePage) return
85
+ const slug = pathname === '/'
86
+ ? []
87
+ : pathname.slice(1).split('/').filter(Boolean);
75
88
 
76
- const component = await loadPageComponent(sourcePage)
77
- if (cancelled) return
89
+ const apiPath = slug.length === 0 ? '/api/page/' : `/api/page/${slug.join('/')}`;
78
90
 
79
- setTree(newTree)
80
- setPage({
81
- slug,
82
- frontmatter: sourcePage.frontmatter,
83
- content: component ? React.createElement(component, { components: mdxComponents }) : null,
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 });
84
98
  })
85
- }
86
- load()
87
- return () => { cancelled = true }
88
- }, [pathname])
99
+ .catch(() => {});
100
+
101
+ return () => { cancelled.current = true; };
102
+ }, [pathname]);
89
103
 
90
104
  return (
91
- <PageContext.Provider value={{ config: initialConfig, tree, page, apiSpecs }}>
105
+ <PageContext.Provider
106
+ value={{ config: initialConfig, tree, page, apiSpecs }}
107
+ >
92
108
  {children}
93
109
  </PageContext.Provider>
94
- )
110
+ );
95
111
  }