@raystack/chronicle 0.3.0 → 0.5.0

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 (84) hide show
  1. package/dist/cli/index.js +425 -9937
  2. package/package.json +19 -10
  3. package/src/cli/commands/build.ts +33 -31
  4. package/src/cli/commands/dev.ts +23 -31
  5. package/src/cli/commands/init.ts +38 -132
  6. package/src/cli/commands/serve.ts +41 -55
  7. package/src/cli/commands/start.ts +20 -31
  8. package/src/cli/index.ts +14 -14
  9. package/src/cli/utils/config.ts +58 -30
  10. package/src/cli/utils/index.ts +3 -3
  11. package/src/cli/utils/resolve.ts +7 -3
  12. package/src/cli/utils/scaffold.ts +11 -130
  13. package/src/components/mdx/code.tsx +10 -1
  14. package/src/components/mdx/details.module.css +1 -26
  15. package/src/components/mdx/details.tsx +2 -3
  16. package/src/components/mdx/image.tsx +5 -34
  17. package/src/components/mdx/index.tsx +15 -1
  18. package/src/components/mdx/link.tsx +18 -15
  19. package/src/components/ui/breadcrumbs.tsx +8 -42
  20. package/src/components/ui/search.tsx +63 -51
  21. package/src/lib/api-routes.ts +6 -8
  22. package/src/lib/config.ts +12 -36
  23. package/src/lib/head.tsx +49 -0
  24. package/src/lib/openapi.ts +8 -8
  25. package/src/lib/page-context.tsx +111 -0
  26. package/src/lib/remark-strip-md-extensions.ts +14 -0
  27. package/src/lib/source.ts +139 -63
  28. package/src/pages/ApiLayout.tsx +33 -0
  29. package/src/pages/ApiPage.tsx +73 -0
  30. package/src/pages/DocsLayout.tsx +18 -0
  31. package/src/pages/DocsPage.tsx +43 -0
  32. package/src/pages/NotFound.tsx +17 -0
  33. package/src/server/App.tsx +72 -0
  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 +18 -0
  37. package/src/server/api/search.ts +118 -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 +88 -0
  41. package/src/server/entry-server.tsx +102 -0
  42. package/src/server/routes/llms.txt.ts +21 -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 +133 -0
  48. package/src/themes/default/Layout.tsx +78 -48
  49. package/src/themes/default/Page.module.css +44 -0
  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 +64 -45
  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 -63
  58. package/src/themes/paper/ReadingProgress.tsx +160 -139
  59. package/src/themes/paper/index.ts +5 -5
  60. package/src/themes/registry.ts +14 -7
  61. package/src/types/config.ts +86 -67
  62. package/src/types/content.ts +5 -21
  63. package/src/types/globals.d.ts +4 -0
  64. package/src/types/theme.ts +4 -3
  65. package/tsconfig.json +2 -3
  66. package/next.config.mjs +0 -10
  67. package/source.config.ts +0 -51
  68. package/src/app/[[...slug]]/layout.tsx +0 -15
  69. package/src/app/[[...slug]]/page.tsx +0 -106
  70. package/src/app/api/apis-proxy/route.ts +0 -59
  71. package/src/app/api/health/route.ts +0 -3
  72. package/src/app/api/search/route.ts +0 -90
  73. package/src/app/apis/[[...slug]]/layout.tsx +0 -26
  74. package/src/app/apis/[[...slug]]/page.tsx +0 -117
  75. package/src/app/layout.tsx +0 -57
  76. package/src/app/llms-full.txt/route.ts +0 -18
  77. package/src/app/llms.txt/route.ts +0 -15
  78. package/src/app/og/route.tsx +0 -62
  79. package/src/app/providers.tsx +0 -8
  80. package/src/app/robots.ts +0 -10
  81. package/src/app/sitemap.ts +0 -29
  82. package/src/cli/utils/process.ts +0 -7
  83. package/src/themes/default/font.ts +0 -6
  84. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
@@ -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
+ });
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import matter from 'gray-matter';
4
+ import type { OpenAPIV3 } from 'openapi-types';
5
+ import remarkParse from 'remark-parse';
6
+ import { unified } from 'unified';
7
+ import { visit } from 'unist-util-visit';
8
+ import type { Heading, Text } from 'mdast';
9
+ import { getSpecSlug } from '@/lib/api-routes';
10
+ import { loadConfig } from '@/lib/config';
11
+ import { loadApiSpecs } from '@/lib/openapi';
12
+
13
+ interface SearchDocument {
14
+ id: string;
15
+ url: string;
16
+ title: string;
17
+ content: string;
18
+ type: 'page' | 'api';
19
+ }
20
+
21
+ function extractHeadings(markdown: string): string {
22
+ const tree = unified().use(remarkParse).parse(markdown);
23
+ const headings: string[] = [];
24
+ visit(tree, 'heading', (node: Heading) => {
25
+ const text = node.children
26
+ .filter((child): child is Text => child.type === 'text')
27
+ .map(child => child.value)
28
+ .join('');
29
+ if (text) headings.push(text);
30
+ });
31
+ return headings.join(' ');
32
+ }
33
+
34
+ async function scanContent(contentDir: string): Promise<SearchDocument[]> {
35
+ const docs: SearchDocument[] = [];
36
+
37
+ async function scan(dir: string, prefix: string[] = []) {
38
+ try {
39
+ const entries = await fs.readdir(dir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
42
+ continue;
43
+ const fullPath = path.join(dir, entry.name);
44
+
45
+ if (entry.isDirectory()) {
46
+ await scan(fullPath, [...prefix, entry.name]);
47
+ continue;
48
+ }
49
+
50
+ if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md'))
51
+ continue;
52
+
53
+ const raw = await fs.readFile(fullPath, 'utf-8');
54
+ const { data: fm, content } = matter(raw);
55
+ const baseName = entry.name.replace(/\.(mdx|md)$/, '');
56
+ const slugs = baseName === 'index' ? prefix : [...prefix, baseName];
57
+ const url = slugs.length === 0 ? '/' : `/${slugs.join('/')}`;
58
+
59
+ docs.push({
60
+ id: url,
61
+ url,
62
+ title: fm.title ?? baseName,
63
+ content: extractHeadings(content),
64
+ type: 'page'
65
+ });
66
+ }
67
+ } catch {
68
+ /* directory not readable */
69
+ }
70
+ }
71
+
72
+ await scan(contentDir);
73
+ return docs;
74
+ }
75
+
76
+ async function buildApiDocs(): Promise<SearchDocument[]> {
77
+ const config = loadConfig();
78
+ if (!config.api?.length) return [];
79
+
80
+ const docs: SearchDocument[] = [];
81
+ const specs = await loadApiSpecs(config.api);
82
+
83
+ for (const spec of specs) {
84
+ const specSlug = getSpecSlug(spec);
85
+ const paths = spec.document.paths ?? {};
86
+ for (const [, pathItem] of Object.entries(paths)) {
87
+ if (!pathItem) continue;
88
+ for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
89
+ const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
90
+ if (!op?.operationId) continue;
91
+ const url = `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`;
92
+ docs.push({
93
+ id: url,
94
+ url,
95
+ title: `${method.toUpperCase()} ${op.summary ?? op.operationId}`,
96
+ content: op.description ?? '',
97
+ type: 'api'
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ return docs;
104
+ }
105
+
106
+ export async function generateSearchIndex(contentDir: string, outDir: string) {
107
+ const [contentDocs, apiDocs] = await Promise.all([
108
+ scanContent(contentDir),
109
+ buildApiDocs()
110
+ ]);
111
+
112
+ const documents = [...contentDocs, ...apiDocs];
113
+ const outPath = path.join(outDir, 'search-index.json');
114
+ await fs.writeFile(outPath, JSON.stringify(documents));
115
+
116
+ return documents.length;
117
+ }
@@ -0,0 +1,88 @@
1
+ import '@vitejs/plugin-react/preamble';
2
+ import React from 'react';
3
+ import { hydrateRoot } from 'react-dom/client';
4
+ import { BrowserRouter } from 'react-router';
5
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
6
+ import { mdxComponents } from '@/components/mdx';
7
+ import { PageProvider } from '@/lib/page-context';
8
+ import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types';
9
+ import type { ApiSpec } from '@/lib/openapi';
10
+ import type { ReactNode } from 'react';
11
+ import { App } from './App';
12
+
13
+ interface EmbeddedData {
14
+ config: ChronicleConfig;
15
+ tree: Root;
16
+ slug: string[];
17
+ frontmatter: Frontmatter;
18
+ relativePath: string;
19
+ originalPath?: string;
20
+ }
21
+
22
+ const contentModules = import.meta.glob<{ default?: React.ComponentType<any>; toc?: TableOfContents }>(
23
+ '../../.content/**/*.{mdx,md}'
24
+ );
25
+
26
+ async function loadMdxModule(relativePath: string): Promise<{ content: ReactNode; toc: TableOfContents }> {
27
+ const withoutExt = relativePath.replace(/\.(mdx|md)$/, '');
28
+ const key = relativePath.endsWith('.md')
29
+ ? `../../.content/${withoutExt}.md`
30
+ : `../../.content/${withoutExt}.mdx`;
31
+ const loader = contentModules[key];
32
+ if (!loader) return { content: null, toc: [] };
33
+ const mod = await loader();
34
+ const content = mod.default
35
+ ? React.createElement(mod.default, { components: mdxComponents })
36
+ : null;
37
+ return { content, toc: mod.toc ?? [] };
38
+ }
39
+
40
+ async function hydrate() {
41
+ try {
42
+ const embedded = (
43
+ window as unknown as { __PAGE_DATA__?: EmbeddedData }
44
+ ).__PAGE_DATA__;
45
+
46
+ const config: ChronicleConfig = embedded?.config ?? {
47
+ title: 'Documentation'
48
+ };
49
+ const tree: Root = embedded?.tree ?? { name: 'root', children: [] };
50
+ const isApiPage =
51
+ window.location.pathname.startsWith('/apis') && !!config.api?.length;
52
+ const apiSpecs: ApiSpec[] = isApiPage
53
+ ? await fetch('/api/specs')
54
+ .then(r => r.json())
55
+ .catch(() => [])
56
+ : [];
57
+
58
+ const mdxPath = embedded?.originalPath || embedded?.relativePath;
59
+ const page = mdxPath
60
+ ? {
61
+ slug: embedded!.slug,
62
+ frontmatter: embedded!.frontmatter,
63
+ ...(await loadMdxModule(mdxPath)),
64
+ }
65
+ : null;
66
+
67
+ hydrateRoot(
68
+ document.getElementById('root') as HTMLElement,
69
+ <BrowserRouter>
70
+ <ReactRouterProvider>
71
+ <PageProvider
72
+ initialConfig={config}
73
+ initialTree={tree}
74
+ initialPage={page}
75
+ initialApiSpecs={apiSpecs}
76
+ loadMdx={loadMdxModule}
77
+ >
78
+ <App />
79
+ </PageProvider>
80
+ </ReactRouterProvider>
81
+ </BrowserRouter>
82
+ );
83
+ } catch (err) {
84
+ console.error('Hydration failed:', err);
85
+ }
86
+ }
87
+
88
+ hydrate();
@@ -0,0 +1,102 @@
1
+ import '@raystack/apsara/normalize.css';
2
+ import '@raystack/apsara/style.css';
3
+ import React from 'react';
4
+ import { renderToReadableStream } from 'react-dom/server.edge';
5
+ import { StaticRouter } from 'react-router';
6
+ import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
7
+ import { mdxComponents } from '@/components/mdx';
8
+ import { loadConfig } from '@/lib/config';
9
+ import { loadApiSpecs } from '@/lib/openapi';
10
+ import { PageProvider } from '@/lib/page-context';
11
+ import { getPageTree, getPage, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
12
+ import { App } from './App';
13
+
14
+ import clientAssets from './entry-client?assets=client';
15
+ import serverAssets from './entry-server?assets=ssr';
16
+
17
+ export default {
18
+ async fetch(req: Request) {
19
+ const url = new URL(req.url);
20
+ const pathname = url.pathname;
21
+ const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean);
22
+
23
+ const config = loadConfig();
24
+ const apiSpecs = config.api?.length
25
+ ? await loadApiSpecs(config.api).catch(() => [])
26
+ : [];
27
+
28
+ const [tree, page] = await Promise.all([
29
+ getPageTree(),
30
+ getPage(slug),
31
+ ]);
32
+
33
+ const relativePath = page ? getRelativePath(page) : null;
34
+ const originalPath = page ? getOriginalPath(page) : null;
35
+ const mdxModule = (originalPath || relativePath) ? await loadPageModule(originalPath || relativePath!) : null;
36
+
37
+ const pageData = page
38
+ ? {
39
+ slug,
40
+ frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
41
+ content: mdxModule?.default
42
+ ? React.createElement(mdxModule.default, { components: mdxComponents })
43
+ : null,
44
+ toc: mdxModule?.toc ?? [],
45
+ }
46
+ : null;
47
+
48
+ const embeddedData = {
49
+ config,
50
+ tree,
51
+ slug,
52
+ frontmatter: pageData?.frontmatter ?? null,
53
+ relativePath,
54
+ originalPath,
55
+ };
56
+ const safeJson = JSON.stringify(embeddedData).replace(/</g, '\\u003c');
57
+
58
+ const assets = clientAssets.merge(serverAssets);
59
+
60
+ const stream = await renderToReadableStream(
61
+ <html lang="en">
62
+ <head>
63
+ <meta charSet="UTF-8" />
64
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
65
+ {assets.css.map((attr: { href: string }) => (
66
+ <link key={attr.href} rel="stylesheet" {...attr} />
67
+ ))}
68
+ {assets.js.map((attr: { href: string }) => (
69
+ <link key={attr.href} rel="modulepreload" {...attr} />
70
+ ))}
71
+ <script type="module" src={assets.entry} />
72
+ <script dangerouslySetInnerHTML={{ __html: `window.__PAGE_DATA__ = ${safeJson}` }} />
73
+ </head>
74
+ <body>
75
+ <div id="root">
76
+ <StaticRouter location={pathname}>
77
+ <ReactRouterProvider>
78
+ <PageProvider
79
+ initialConfig={config}
80
+ initialTree={tree}
81
+ initialPage={pageData}
82
+ initialApiSpecs={apiSpecs}
83
+ loadMdx={async () => ({ content: null, toc: [] })}
84
+ >
85
+ <App />
86
+ </PageProvider>
87
+ </ReactRouterProvider>
88
+ </StaticRouter>
89
+ </div>
90
+ </body>
91
+ </html>,
92
+ );
93
+
94
+ const isApiRoute = pathname.startsWith('/apis');
95
+ const status = !page && !isApiRoute && slug.length > 0 ? 404 : 200;
96
+
97
+ return new Response(stream, {
98
+ status,
99
+ headers: { 'Content-Type': 'text/html;charset=utf-8' },
100
+ });
101
+ },
102
+ };
@@ -0,0 +1,21 @@
1
+ import { defineHandler, HTTPError } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+ import { getPages, extractFrontmatter } from '@/lib/source';
4
+
5
+ export default defineHandler(async event => {
6
+ const config = loadConfig();
7
+
8
+ if (!config.llms?.enabled) {
9
+ throw new HTTPError({ status: 404, message: 'Not Found' });
10
+ }
11
+
12
+ const pages = await getPages();
13
+ const index = pages.map(p => {
14
+ const fm = extractFrontmatter(p);
15
+ return `- [${fm.title}](${p.url})`;
16
+ }).join('\n');
17
+ const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`;
18
+
19
+ event.res.headers.set('Content-Type', 'text/plain');
20
+ return body;
21
+ });
@@ -0,0 +1,75 @@
1
+ import { defineHandler } from 'nitro';
2
+ import React from 'react';
3
+ import satori from 'satori';
4
+ import { loadConfig } from '@/lib/config';
5
+
6
+ let fontData: ArrayBuffer | null = null;
7
+
8
+ async function loadFont(): Promise<ArrayBuffer> {
9
+ if (fontData) return fontData;
10
+
11
+ try {
12
+ const response = await fetch(
13
+ 'https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2'
14
+ );
15
+ fontData = await response.arrayBuffer();
16
+ } catch {
17
+ fontData = new ArrayBuffer(0);
18
+ }
19
+
20
+ return fontData;
21
+ }
22
+
23
+ export default defineHandler(async event => {
24
+ const config = loadConfig();
25
+ const title = event.url.searchParams.get('title') ?? config.title;
26
+ const description = event.url.searchParams.get('description') ?? '';
27
+ const siteName = config.title;
28
+
29
+ const font = await loadFont();
30
+
31
+ const svg = await satori(
32
+ <div
33
+ style={{
34
+ height: '100%',
35
+ width: '100%',
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ justifyContent: 'center',
39
+ padding: '60px 80px',
40
+ backgroundColor: '#0a0a0a',
41
+ color: '#fafafa',
42
+ }}
43
+ >
44
+ <div style={{ fontSize: 24, color: '#888', marginBottom: 16 }}>
45
+ {siteName}
46
+ </div>
47
+ <div
48
+ style={{
49
+ fontSize: 56,
50
+ fontWeight: 700,
51
+ lineHeight: 1.2,
52
+ marginBottom: 24,
53
+ }}
54
+ >
55
+ {title}
56
+ </div>
57
+ {description && (
58
+ <div style={{ fontSize: 24, color: '#999', lineHeight: 1.4 }}>
59
+ {description}
60
+ </div>
61
+ )}
62
+ </div>,
63
+ {
64
+ width: 1200,
65
+ height: 630,
66
+ fonts: [
67
+ { name: 'Inter', data: font, weight: 400, style: 'normal' as const },
68
+ ],
69
+ },
70
+ );
71
+
72
+ event.res.headers.set('Content-Type', 'image/svg+xml');
73
+ event.res.headers.set('Cache-Control', 'public, max-age=86400');
74
+ return svg;
75
+ });
@@ -0,0 +1,11 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { loadConfig } from '@/lib/config';
3
+
4
+ export default defineHandler(event => {
5
+ const config = loadConfig();
6
+ const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : '';
7
+ const body = `User-agent: *\nAllow: /${sitemap}`;
8
+
9
+ event.res.headers.set('Content-Type', 'text/plain');
10
+ return body;
11
+ });
@@ -0,0 +1,40 @@
1
+ import { defineHandler } from 'nitro';
2
+ import { buildApiRoutes } from '@/lib/api-routes';
3
+ import { loadConfig } from '@/lib/config';
4
+ import { loadApiSpecs } from '@/lib/openapi';
5
+ import { getPages } from '@/lib/source';
6
+
7
+ export default defineHandler(async event => {
8
+ const config = loadConfig();
9
+
10
+ if (!config.url) {
11
+ event.res.headers.set('Content-Type', 'application/xml');
12
+ return '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>';
13
+ }
14
+
15
+ const baseUrl = config.url.replace(/\/$/, '');
16
+
17
+ const pages = await getPages();
18
+ const docPages = pages.map(page => {
19
+ const data = page.data as Record<string, unknown>;
20
+ const lastmod = data.lastModified
21
+ ? `<lastmod>${new Date(data.lastModified as string).toISOString()}</lastmod>`
22
+ : '';
23
+ return `<url><loc>${baseUrl}/${page.slugs.join('/')}</loc>${lastmod}</url>`;
24
+ });
25
+
26
+ const apiPages = config.api?.length
27
+ ? buildApiRoutes(await loadApiSpecs(config.api)).map(
28
+ route => `<url><loc>${baseUrl}/apis/${route.slug.join('/')}</loc></url>`
29
+ )
30
+ : [];
31
+
32
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
33
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
34
+ <url><loc>${baseUrl}</loc></url>
35
+ ${[...docPages, ...apiPages].join('\n')}
36
+ </urlset>`;
37
+
38
+ event.res.headers.set('Content-Type', 'application/xml');
39
+ return xml;
40
+ });
@@ -0,0 +1,17 @@
1
+ import path from 'node:path';
2
+
3
+ /**
4
+ * Resolve a URL path within a base directory, preventing path traversal.
5
+ * Returns null if the resolved path escapes the base directory.
6
+ */
7
+ export function safePath(baseDir: string, urlPath: string): string | null {
8
+ const decoded = decodeURIComponent(urlPath.split('?')[0]);
9
+ const resolved = path.resolve(baseDir, '.' + decoded);
10
+ if (
11
+ !resolved.startsWith(path.resolve(baseDir) + path.sep) &&
12
+ resolved !== path.resolve(baseDir)
13
+ ) {
14
+ return null;
15
+ }
16
+ return resolved;
17
+ }
@@ -0,0 +1,133 @@
1
+ import react from '@vitejs/plugin-react';
2
+ import { remarkDirectiveAdmonition, remarkMdxMermaid } from 'fumadocs-core/mdx-plugins';
3
+ import { defineConfig as defineFumadocsConfig } from 'fumadocs-mdx/config';
4
+ import mdx from 'fumadocs-mdx/vite';
5
+ import { nitro } from 'nitro/vite';
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import remarkDirective from 'remark-directive';
9
+ import { type InlineConfig } from 'vite';
10
+ import remarkStripMdExtensions from '../lib/remark-strip-md-extensions';
11
+ import remarkUnusedDirectives from '../lib/remark-unused-directives';
12
+
13
+ function resolveOutputDir(projectRoot: string, preset?: string): string {
14
+ if (preset === 'vercel' || preset === 'vercel-static') return path.resolve(projectRoot, '.vercel/output');
15
+ return path.resolve(projectRoot, '.output');
16
+ }
17
+
18
+ export interface ViteConfigOptions {
19
+ packageRoot: string;
20
+ projectRoot: string;
21
+ contentDir: string;
22
+ configPath?: string;
23
+ preset?: string;
24
+ }
25
+
26
+ async function readChronicleConfig(projectRoot: string, configPath?: string): Promise<string | null> {
27
+ if (configPath) {
28
+ try {
29
+ return await fs.readFile(configPath, 'utf-8');
30
+ } catch (error) {
31
+ throw new Error(`Failed to read config file '${configPath}': ${(error as Error).message}`);
32
+ }
33
+ }
34
+ try {
35
+ return await fs.readFile(path.join(projectRoot, 'chronicle.yaml'), 'utf-8');
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ export async function createViteConfig(
42
+ options: ViteConfigOptions
43
+ ): Promise<InlineConfig> {
44
+ const { packageRoot, projectRoot, contentDir, configPath, preset } = options;
45
+ const rawConfig = await readChronicleConfig(projectRoot, configPath);
46
+
47
+ return {
48
+ root: packageRoot,
49
+ configFile: false,
50
+ plugins: [
51
+ nitro({
52
+ serverDir: path.resolve(packageRoot, 'src/server'),
53
+ ...(preset && { preset }),
54
+ }),
55
+ mdx({
56
+ default: defineFumadocsConfig({
57
+ mdxOptions: {
58
+ remarkPlugins: [
59
+ remarkDirective,
60
+ [remarkDirectiveAdmonition, {
61
+ tags: {
62
+ CalloutContainer: 'Callout',
63
+ CalloutTitle: 'CalloutTitle',
64
+ CalloutDescription: 'CalloutDescription',
65
+ },
66
+ types: {
67
+ note: 'accent',
68
+ tip: 'accent',
69
+ info: 'accent',
70
+ warn: 'attention',
71
+ warning: 'attention',
72
+ danger: 'alert',
73
+ caution: 'alert',
74
+ success: 'success',
75
+ },
76
+ }],
77
+ remarkUnusedDirectives,
78
+ remarkStripMdExtensions,
79
+ remarkMdxMermaid,
80
+ ],
81
+ },
82
+ }),
83
+ }, { index: false }),
84
+ react()
85
+ ],
86
+ resolve: {
87
+ alias: {
88
+ '@': path.resolve(packageRoot, 'src'),
89
+ },
90
+ conditions: ['module-sync', 'import', 'node'],
91
+ dedupe: [
92
+ 'react',
93
+ 'react-dom',
94
+ 'react/jsx-runtime',
95
+ 'react/jsx-dev-runtime',
96
+ 'react-router',
97
+ ]
98
+ },
99
+ server: {
100
+ fs: {
101
+ allow: [packageRoot, projectRoot, contentDir]
102
+ }
103
+ },
104
+ define: {
105
+ __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
106
+ __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
107
+ __CHRONICLE_CONFIG_RAW__: JSON.stringify(rawConfig),
108
+ },
109
+ css: {
110
+ modules: {
111
+ localsConvention: 'camelCase'
112
+ }
113
+ },
114
+ ssr: {
115
+ noExternal: ['@raystack/apsara', 'dayjs', 'fumadocs-core']
116
+ },
117
+ environments: {
118
+ client: {
119
+ build: {
120
+ rollupOptions: {
121
+ input: path.resolve(packageRoot, 'src/server/entry-client.tsx')
122
+ }
123
+ }
124
+ }
125
+ },
126
+ nitro: {
127
+ logLevel: 2,
128
+ output: {
129
+ dir: resolveOutputDir(projectRoot, preset),
130
+ },
131
+ },
132
+ };
133
+ }