@raystack/chronicle 0.1.0-canary.1f5227c
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/bin/chronicle.js +2 -0
- package/dist/cli/index.js +543 -0
- package/package.json +68 -0
- package/src/cli/__tests__/config.test.ts +25 -0
- package/src/cli/__tests__/scaffold.test.ts +10 -0
- package/src/cli/commands/build.ts +52 -0
- package/src/cli/commands/dev.ts +21 -0
- package/src/cli/commands/init.ts +154 -0
- package/src/cli/commands/serve.ts +55 -0
- package/src/cli/commands/start.ts +24 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/config.ts +43 -0
- package/src/cli/utils/index.ts +2 -0
- package/src/cli/utils/resolve.ts +6 -0
- package/src/cli/utils/scaffold.ts +20 -0
- package/src/components/api/code-snippets.module.css +7 -0
- package/src/components/api/code-snippets.tsx +76 -0
- package/src/components/api/endpoint-page.module.css +58 -0
- package/src/components/api/endpoint-page.tsx +283 -0
- package/src/components/api/field-row.module.css +126 -0
- package/src/components/api/field-row.tsx +204 -0
- package/src/components/api/field-section.module.css +24 -0
- package/src/components/api/field-section.tsx +100 -0
- package/src/components/api/index.ts +8 -0
- package/src/components/api/json-editor.module.css +9 -0
- package/src/components/api/json-editor.tsx +61 -0
- package/src/components/api/key-value-editor.module.css +13 -0
- package/src/components/api/key-value-editor.tsx +62 -0
- package/src/components/api/method-badge.module.css +4 -0
- package/src/components/api/method-badge.tsx +29 -0
- package/src/components/api/response-panel.module.css +8 -0
- package/src/components/api/response-panel.tsx +44 -0
- package/src/components/common/breadcrumb.tsx +3 -0
- package/src/components/common/button.tsx +3 -0
- package/src/components/common/callout.module.css +7 -0
- package/src/components/common/callout.tsx +27 -0
- package/src/components/common/code-block.tsx +3 -0
- package/src/components/common/dialog.tsx +3 -0
- package/src/components/common/index.ts +10 -0
- package/src/components/common/input-field.tsx +3 -0
- package/src/components/common/sidebar.tsx +3 -0
- package/src/components/common/switch.tsx +3 -0
- package/src/components/common/table.tsx +3 -0
- package/src/components/common/tabs.tsx +3 -0
- package/src/components/mdx/code.module.css +42 -0
- package/src/components/mdx/code.tsx +36 -0
- package/src/components/mdx/details.module.css +14 -0
- package/src/components/mdx/details.tsx +17 -0
- package/src/components/mdx/image.tsx +24 -0
- package/src/components/mdx/index.tsx +35 -0
- package/src/components/mdx/link.tsx +37 -0
- package/src/components/mdx/mermaid.module.css +9 -0
- package/src/components/mdx/mermaid.tsx +37 -0
- package/src/components/mdx/paragraph.module.css +8 -0
- package/src/components/mdx/paragraph.tsx +19 -0
- package/src/components/mdx/table.tsx +40 -0
- package/src/components/ui/breadcrumbs.tsx +72 -0
- package/src/components/ui/client-theme-switcher.tsx +18 -0
- package/src/components/ui/footer.module.css +27 -0
- package/src/components/ui/footer.tsx +31 -0
- package/src/components/ui/search.module.css +111 -0
- package/src/components/ui/search.tsx +174 -0
- package/src/lib/api-routes.ts +120 -0
- package/src/lib/config.ts +56 -0
- package/src/lib/head.tsx +45 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/openapi.ts +188 -0
- package/src/lib/page-context.tsx +95 -0
- package/src/lib/remark-unused-directives.ts +30 -0
- package/src/lib/schema.ts +99 -0
- package/src/lib/snippet-generators.ts +87 -0
- package/src/lib/source.ts +138 -0
- package/src/pages/ApiLayout.module.css +22 -0
- package/src/pages/ApiLayout.tsx +29 -0
- package/src/pages/ApiPage.tsx +68 -0
- package/src/pages/DocsLayout.tsx +18 -0
- package/src/pages/DocsPage.tsx +43 -0
- package/src/pages/NotFound.tsx +10 -0
- package/src/pages/__tests__/head.test.tsx +57 -0
- package/src/server/App.tsx +59 -0
- package/src/server/__tests__/entry-server.test.tsx +35 -0
- package/src/server/__tests__/handlers.test.ts +77 -0
- package/src/server/__tests__/og.test.ts +23 -0
- package/src/server/__tests__/router.test.ts +72 -0
- package/src/server/__tests__/vite-config.test.ts +25 -0
- package/src/server/dev.ts +156 -0
- package/src/server/entry-client.tsx +74 -0
- package/src/server/entry-prod.ts +127 -0
- package/src/server/entry-server.tsx +35 -0
- package/src/server/handlers/apis-proxy.ts +52 -0
- package/src/server/handlers/health.ts +3 -0
- package/src/server/handlers/llms.ts +58 -0
- package/src/server/handlers/og.ts +87 -0
- package/src/server/handlers/robots.ts +11 -0
- package/src/server/handlers/search.ts +140 -0
- package/src/server/handlers/sitemap.ts +39 -0
- package/src/server/handlers/specs.ts +9 -0
- package/src/server/index.html +12 -0
- package/src/server/prod.ts +18 -0
- package/src/server/router.ts +42 -0
- package/src/server/vite-config.ts +71 -0
- package/src/themes/default/Layout.module.css +81 -0
- package/src/themes/default/Layout.tsx +132 -0
- package/src/themes/default/Page.module.css +102 -0
- package/src/themes/default/Page.tsx +21 -0
- package/src/themes/default/Toc.module.css +48 -0
- package/src/themes/default/Toc.tsx +66 -0
- package/src/themes/default/font.ts +4 -0
- package/src/themes/default/index.ts +13 -0
- package/src/themes/paper/ChapterNav.module.css +71 -0
- package/src/themes/paper/ChapterNav.tsx +95 -0
- package/src/themes/paper/Layout.module.css +33 -0
- package/src/themes/paper/Layout.tsx +25 -0
- package/src/themes/paper/Page.module.css +174 -0
- package/src/themes/paper/Page.tsx +106 -0
- package/src/themes/paper/ReadingProgress.module.css +132 -0
- package/src/themes/paper/ReadingProgress.tsx +294 -0
- package/src/themes/paper/index.ts +8 -0
- package/src/themes/registry.ts +14 -0
- package/src/types/config.ts +80 -0
- package/src/types/content.ts +36 -0
- package/src/types/index.ts +3 -0
- package/src/types/theme.ts +22 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createServer as createViteServer } from 'vite'
|
|
2
|
+
import { createServer } from 'http'
|
|
3
|
+
import fsPromises from 'fs/promises'
|
|
4
|
+
import { createReadStream } from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
import { createViteConfig } from './vite-config'
|
|
8
|
+
|
|
9
|
+
export interface DevServerOptions {
|
|
10
|
+
port: number
|
|
11
|
+
root: string
|
|
12
|
+
contentDir: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function startDevServer(options: DevServerOptions) {
|
|
16
|
+
const { port, root, contentDir } = options
|
|
17
|
+
|
|
18
|
+
const viteConfig = await createViteConfig({ root, contentDir, isDev: true })
|
|
19
|
+
const vite = await createViteServer({
|
|
20
|
+
...viteConfig,
|
|
21
|
+
server: { middlewareMode: true },
|
|
22
|
+
appType: 'custom',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const templatePath = path.resolve(root, 'src/server/index.html')
|
|
26
|
+
|
|
27
|
+
const server = createServer(async (req, res) => {
|
|
28
|
+
const url = req.url || '/'
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Let Vite handle its own requests (HMR, modules)
|
|
32
|
+
if (url.startsWith('/@') || url.startsWith('/__vite') || url.startsWith('/node_modules/')) {
|
|
33
|
+
vite.middlewares(req, res, () => {
|
|
34
|
+
res.statusCode = 404
|
|
35
|
+
res.end()
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Serve static files from content dir (skip .md/.mdx)
|
|
41
|
+
const contentFile = path.join(contentDir, decodeURIComponent(url.split('?')[0]))
|
|
42
|
+
if (!url.endsWith('.md') && !url.endsWith('.mdx')) {
|
|
43
|
+
try {
|
|
44
|
+
const stat = await fsPromises.stat(contentFile)
|
|
45
|
+
if (stat.isFile()) {
|
|
46
|
+
const ext = path.extname(contentFile).toLowerCase()
|
|
47
|
+
const mimeTypes: Record<string, string> = {
|
|
48
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
49
|
+
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp',
|
|
50
|
+
'.ico': 'image/x-icon', '.pdf': 'application/pdf', '.json': 'application/json',
|
|
51
|
+
'.yaml': 'text/yaml', '.yml': 'text/yaml', '.txt': 'text/plain',
|
|
52
|
+
}
|
|
53
|
+
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream')
|
|
54
|
+
createReadStream(contentFile).pipe(res)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
} catch { /* fall through to SSR */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Let Vite handle JS/CSS/TS module requests and other static assets
|
|
61
|
+
if (/\.(js|ts|tsx|css|map)(\?|$)/.test(url)) {
|
|
62
|
+
vite.middlewares(req, res, () => {
|
|
63
|
+
res.statusCode = 404
|
|
64
|
+
res.end()
|
|
65
|
+
})
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check API/static routes (load through Vite SSR for import.meta.glob support)
|
|
70
|
+
const { matchRoute } = await vite.ssrLoadModule(path.resolve(root, 'src/server/router.ts'))
|
|
71
|
+
const routeHandler = matchRoute(new URL(url, `http://localhost:${port}`).href)
|
|
72
|
+
if (routeHandler) {
|
|
73
|
+
const request = new Request(new URL(url, `http://localhost:${port}`))
|
|
74
|
+
const response = await routeHandler(request)
|
|
75
|
+
res.statusCode = response.status
|
|
76
|
+
response.headers.forEach((value: string, key: string) => res.setHeader(key, value))
|
|
77
|
+
const body = await response.text()
|
|
78
|
+
res.end(body)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Resolve page data before SSR render
|
|
83
|
+
const pathname = new URL(url, `http://localhost:${port}`).pathname
|
|
84
|
+
const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
|
|
85
|
+
|
|
86
|
+
const source = await vite.ssrLoadModule(path.resolve(root, 'src/lib/source.ts'))
|
|
87
|
+
const { mdxComponents } = await vite.ssrLoadModule(path.resolve(root, 'src/components/mdx/index.tsx'))
|
|
88
|
+
const { loadConfig } = await vite.ssrLoadModule(path.resolve(root, 'src/lib/config.ts'))
|
|
89
|
+
|
|
90
|
+
const config = loadConfig()
|
|
91
|
+
|
|
92
|
+
const { loadApiSpecs } = await vite.ssrLoadModule(path.resolve(root, 'src/lib/openapi.ts'))
|
|
93
|
+
const apiSpecs = config.api?.length ? loadApiSpecs(config.api) : []
|
|
94
|
+
|
|
95
|
+
const [tree, sourcePage] = await Promise.all([
|
|
96
|
+
source.buildPageTree(),
|
|
97
|
+
source.getPage(slug),
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
let pageData = null
|
|
101
|
+
// Don't embed apiSpecs — too large. Client fetches via /api/specs
|
|
102
|
+
let embeddedData: any = { config, tree, slug, frontmatter: null, filePath: null }
|
|
103
|
+
|
|
104
|
+
if (sourcePage) {
|
|
105
|
+
const component = await source.loadPageComponent(sourcePage)
|
|
106
|
+
const React = await import('react')
|
|
107
|
+
const MDXBody = component
|
|
108
|
+
pageData = {
|
|
109
|
+
slug,
|
|
110
|
+
frontmatter: sourcePage.frontmatter,
|
|
111
|
+
content: MDXBody ? React.createElement(MDXBody, { components: mdxComponents }) : null,
|
|
112
|
+
}
|
|
113
|
+
embeddedData.frontmatter = sourcePage.frontmatter
|
|
114
|
+
embeddedData.filePath = sourcePage.filePath
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// SSR render
|
|
118
|
+
let template = await fsPromises.readFile(templatePath, 'utf-8')
|
|
119
|
+
template = await vite.transformIndexHtml(url, template)
|
|
120
|
+
|
|
121
|
+
// Embed page data for client hydration
|
|
122
|
+
const dataScript = `<script>window.__PAGE_DATA__ = ${JSON.stringify(embeddedData)}</script>`
|
|
123
|
+
template = template.replace('<!--head-outlet-->', `<!--head-outlet-->${dataScript}`)
|
|
124
|
+
|
|
125
|
+
const { render } = await vite.ssrLoadModule(path.resolve(root, 'src/server/entry-server.tsx'))
|
|
126
|
+
|
|
127
|
+
const html = render(url, { config, tree, page: pageData, apiSpecs })
|
|
128
|
+
const finalHtml = template.replace('<!--ssr-outlet-->', html)
|
|
129
|
+
|
|
130
|
+
res.setHeader('Content-Type', 'text/html')
|
|
131
|
+
res.statusCode = 200
|
|
132
|
+
res.end(finalHtml)
|
|
133
|
+
} catch (e) {
|
|
134
|
+
vite.ssrFixStacktrace(e as Error)
|
|
135
|
+
console.error(e)
|
|
136
|
+
res.statusCode = 500
|
|
137
|
+
res.end((e as Error).message)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
server.listen(port, () => {
|
|
142
|
+
console.log(chalk.cyan(`\n Chronicle dev server running at:`))
|
|
143
|
+
console.log(chalk.green(` http://localhost:${port}\n`))
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Graceful shutdown
|
|
147
|
+
const shutdown = () => {
|
|
148
|
+
vite.close()
|
|
149
|
+
server.close()
|
|
150
|
+
process.exit(0)
|
|
151
|
+
}
|
|
152
|
+
process.on('SIGINT', shutdown)
|
|
153
|
+
process.on('SIGTERM', shutdown)
|
|
154
|
+
|
|
155
|
+
return { server, vite }
|
|
156
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { hydrateRoot } from 'react-dom/client'
|
|
2
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
3
|
+
import { PageProvider } from '@/lib/page-context'
|
|
4
|
+
import { App } from './App'
|
|
5
|
+
import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
|
|
6
|
+
import { mdxComponents } from '@/components/mdx'
|
|
7
|
+
import type { ChronicleConfig, PageTree } from '@/types'
|
|
8
|
+
import type { ApiSpec } from '@/lib/openapi'
|
|
9
|
+
import type { ReactNode } from 'react'
|
|
10
|
+
import React from 'react'
|
|
11
|
+
|
|
12
|
+
interface EmbeddedData {
|
|
13
|
+
config: ChronicleConfig
|
|
14
|
+
tree: PageTree
|
|
15
|
+
slug: string[]
|
|
16
|
+
frontmatter: { title: string; description?: string; order?: number }
|
|
17
|
+
filePath: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function hydrate() {
|
|
21
|
+
try {
|
|
22
|
+
const embedded: EmbeddedData | undefined = (window as any).__PAGE_DATA__
|
|
23
|
+
|
|
24
|
+
let config: ChronicleConfig = { title: 'Documentation' }
|
|
25
|
+
let tree: PageTree = { name: 'root', children: [] }
|
|
26
|
+
let page: { slug: string[]; frontmatter: any; content: ReactNode } | null = null
|
|
27
|
+
let apiSpecs: ApiSpec[] = []
|
|
28
|
+
|
|
29
|
+
if (embedded) {
|
|
30
|
+
config = embedded.config
|
|
31
|
+
tree = embedded.tree
|
|
32
|
+
|
|
33
|
+
// Fetch API specs if on /apis route
|
|
34
|
+
const isApiRoute = window.location.pathname.startsWith('/apis')
|
|
35
|
+
if (isApiRoute && config.api?.length) {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch('/api/specs')
|
|
38
|
+
apiSpecs = await res.json()
|
|
39
|
+
} catch { /* will load on demand */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sourcePage = await getPage(embedded.slug)
|
|
43
|
+
if (sourcePage) {
|
|
44
|
+
const component = await loadPageComponent(sourcePage)
|
|
45
|
+
page = {
|
|
46
|
+
slug: embedded.slug,
|
|
47
|
+
frontmatter: embedded.frontmatter,
|
|
48
|
+
content: component ? React.createElement(component, { components: mdxComponents }) : null,
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
page = {
|
|
52
|
+
slug: embedded.slug,
|
|
53
|
+
frontmatter: embedded.frontmatter,
|
|
54
|
+
content: null,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
tree = await buildPageTree()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
hydrateRoot(
|
|
62
|
+
document.getElementById('root') as HTMLElement,
|
|
63
|
+
<BrowserRouter>
|
|
64
|
+
<PageProvider initialConfig={config} initialTree={tree} initialPage={page} initialApiSpecs={apiSpecs}>
|
|
65
|
+
<App />
|
|
66
|
+
</PageProvider>
|
|
67
|
+
</BrowserRouter>,
|
|
68
|
+
)
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('Hydration failed:', err)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
hydrate()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Production server entry — built by Vite, loaded by prod.ts at runtime
|
|
2
|
+
import { createServer } from 'http'
|
|
3
|
+
import { readFileSync, createReadStream } from 'fs'
|
|
4
|
+
import fsPromises from 'fs/promises'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import React from 'react'
|
|
7
|
+
import { render } from './entry-server'
|
|
8
|
+
import { matchRoute } from './router'
|
|
9
|
+
import { loadConfig } from '@/lib/config'
|
|
10
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
11
|
+
import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
|
|
12
|
+
import { mdxComponents } from '@/components/mdx'
|
|
13
|
+
|
|
14
|
+
export { render, matchRoute, loadConfig, loadApiSpecs, getPage, loadPageComponent, buildPageTree }
|
|
15
|
+
|
|
16
|
+
export async function startServer(options: { port: number; distDir: string }) {
|
|
17
|
+
const { port, distDir } = options
|
|
18
|
+
|
|
19
|
+
const clientDir = path.resolve(distDir, 'client')
|
|
20
|
+
const templatePath = path.resolve(clientDir, 'src/server/index.html')
|
|
21
|
+
const template = readFileSync(templatePath, 'utf-8')
|
|
22
|
+
|
|
23
|
+
const sirv = (await import('sirv')).default
|
|
24
|
+
const assets = sirv(clientDir, { gzip: true })
|
|
25
|
+
|
|
26
|
+
const server = createServer(async (req, res) => {
|
|
27
|
+
const url = req.url || '/'
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// API routes
|
|
31
|
+
const routeHandler = matchRoute(new URL(url, `http://localhost:${port}`).href)
|
|
32
|
+
if (routeHandler) {
|
|
33
|
+
const request = new Request(new URL(url, `http://localhost:${port}`))
|
|
34
|
+
const response = await routeHandler(request)
|
|
35
|
+
res.statusCode = response.status
|
|
36
|
+
response.headers.forEach((value: string, key: string) => res.setHeader(key, value))
|
|
37
|
+
const body = await response.text()
|
|
38
|
+
res.end(body)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Serve static files from content dir (skip .md/.mdx)
|
|
43
|
+
const contentDir = process.env.CHRONICLE_CONTENT_DIR || process.cwd()
|
|
44
|
+
const contentFile = path.join(contentDir, decodeURIComponent(url.split('?')[0]))
|
|
45
|
+
if (!url.endsWith('.md') && !url.endsWith('.mdx')) {
|
|
46
|
+
try {
|
|
47
|
+
const stat = await fsPromises.stat(contentFile)
|
|
48
|
+
if (stat.isFile()) {
|
|
49
|
+
const ext = path.extname(contentFile).toLowerCase()
|
|
50
|
+
const mimeTypes: Record<string, string> = {
|
|
51
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
52
|
+
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp',
|
|
53
|
+
'.ico': 'image/x-icon', '.pdf': 'application/pdf', '.json': 'application/json',
|
|
54
|
+
'.yaml': 'text/yaml', '.yml': 'text/yaml', '.txt': 'text/plain',
|
|
55
|
+
}
|
|
56
|
+
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream')
|
|
57
|
+
createReadStream(contentFile).pipe(res)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
} catch { /* fall through */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Static assets from dist/client
|
|
64
|
+
const assetHandled = await new Promise<boolean>((resolve) => {
|
|
65
|
+
assets(req, res, () => resolve(false))
|
|
66
|
+
res.on('close', () => resolve(true))
|
|
67
|
+
})
|
|
68
|
+
if (assetHandled) return
|
|
69
|
+
|
|
70
|
+
// Resolve page data
|
|
71
|
+
const pathname = new URL(url, `http://localhost:${port}`).pathname
|
|
72
|
+
const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
|
|
73
|
+
|
|
74
|
+
const config = loadConfig()
|
|
75
|
+
const apiSpecs = config.api?.length ? loadApiSpecs(config.api) : []
|
|
76
|
+
|
|
77
|
+
const [tree, sourcePage] = await Promise.all([
|
|
78
|
+
buildPageTree(),
|
|
79
|
+
getPage(slug),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
let pageData = null
|
|
83
|
+
let embeddedData: any = { config, tree, slug, frontmatter: null, filePath: null }
|
|
84
|
+
|
|
85
|
+
if (sourcePage) {
|
|
86
|
+
const component = await loadPageComponent(sourcePage)
|
|
87
|
+
pageData = {
|
|
88
|
+
slug,
|
|
89
|
+
frontmatter: sourcePage.frontmatter,
|
|
90
|
+
content: component ? React.createElement(component, { components: mdxComponents }) : null,
|
|
91
|
+
}
|
|
92
|
+
embeddedData.frontmatter = sourcePage.frontmatter
|
|
93
|
+
embeddedData.filePath = sourcePage.filePath
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// SSR render
|
|
97
|
+
const html = render(url, { config, tree, page: pageData, apiSpecs })
|
|
98
|
+
|
|
99
|
+
const dataScript = `<script>window.__PAGE_DATA__ = ${JSON.stringify(embeddedData)}</script>`
|
|
100
|
+
const finalHtml = template
|
|
101
|
+
.replace('<!--head-outlet-->', `<!--head-outlet-->${dataScript}`)
|
|
102
|
+
.replace('<!--ssr-outlet-->', html)
|
|
103
|
+
|
|
104
|
+
res.setHeader('Content-Type', 'text/html')
|
|
105
|
+
res.statusCode = 200
|
|
106
|
+
res.end(finalHtml)
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error(e)
|
|
109
|
+
res.statusCode = 500
|
|
110
|
+
res.end((e as Error).message)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
server.listen(port, () => {
|
|
115
|
+
console.log(`\n Chronicle production server running at:`)
|
|
116
|
+
console.log(` http://localhost:${port}\n`)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const shutdown = () => {
|
|
120
|
+
server.close()
|
|
121
|
+
process.exit(0)
|
|
122
|
+
}
|
|
123
|
+
process.on('SIGINT', shutdown)
|
|
124
|
+
process.on('SIGTERM', shutdown)
|
|
125
|
+
|
|
126
|
+
return server
|
|
127
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { renderToString } from 'react-dom/server'
|
|
2
|
+
import { StaticRouter } from 'react-router-dom'
|
|
3
|
+
import { PageProvider } from '@/lib/page-context'
|
|
4
|
+
import { App } from './App'
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import type { ChronicleConfig, Frontmatter, PageTree } from '@/types'
|
|
7
|
+
import type { ApiSpec } from '@/lib/openapi'
|
|
8
|
+
|
|
9
|
+
export interface SSRData {
|
|
10
|
+
config: ChronicleConfig
|
|
11
|
+
tree: PageTree
|
|
12
|
+
page: {
|
|
13
|
+
slug: string[]
|
|
14
|
+
frontmatter: Frontmatter
|
|
15
|
+
content: ReactNode
|
|
16
|
+
} | null
|
|
17
|
+
apiSpecs: ApiSpec[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function render(url: string, data: SSRData): string {
|
|
21
|
+
const pathname = new URL(url, 'http://localhost').pathname
|
|
22
|
+
|
|
23
|
+
return renderToString(
|
|
24
|
+
<StaticRouter location={pathname}>
|
|
25
|
+
<PageProvider
|
|
26
|
+
initialConfig={data.config}
|
|
27
|
+
initialTree={data.tree}
|
|
28
|
+
initialPage={data.page}
|
|
29
|
+
initialApiSpecs={data.apiSpecs}
|
|
30
|
+
>
|
|
31
|
+
<App />
|
|
32
|
+
</PageProvider>
|
|
33
|
+
</StaticRouter>,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { loadConfig } from '@/lib/config'
|
|
2
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
3
|
+
|
|
4
|
+
export async function handleApisProxy(req: Request): Promise<Response> {
|
|
5
|
+
if (req.method !== 'POST') {
|
|
6
|
+
return Response.json({ error: 'Method not allowed' }, { status: 405 })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { specName, method, path, headers, body } = await req.json()
|
|
10
|
+
|
|
11
|
+
if (!specName || !method || !path) {
|
|
12
|
+
return Response.json({ error: 'Missing specName, method, or path' }, { status: 400 })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const config = loadConfig()
|
|
16
|
+
const specs = loadApiSpecs(config.api ?? [])
|
|
17
|
+
const spec = specs.find((s) => s.name === specName)
|
|
18
|
+
|
|
19
|
+
if (!spec) {
|
|
20
|
+
return Response.json({ error: `Unknown spec: ${specName}` }, { status: 404 })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const url = spec.server.url + path
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method,
|
|
28
|
+
headers,
|
|
29
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
33
|
+
const responseBody = contentType.includes('application/json')
|
|
34
|
+
? await response.json()
|
|
35
|
+
: await response.text()
|
|
36
|
+
|
|
37
|
+
return Response.json({
|
|
38
|
+
status: response.status,
|
|
39
|
+
statusText: response.statusText,
|
|
40
|
+
body: responseBody,
|
|
41
|
+
}, { status: response.status })
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error
|
|
44
|
+
? `${error.message}${error.cause ? `: ${(error.cause as Error).message}` : ''}`
|
|
45
|
+
: 'Request failed'
|
|
46
|
+
return Response.json({
|
|
47
|
+
status: 502,
|
|
48
|
+
statusText: 'Bad Gateway',
|
|
49
|
+
body: `Could not reach ${url}\n${message}`,
|
|
50
|
+
}, { status: 502 })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import matter from 'gray-matter'
|
|
4
|
+
import { loadConfig } from '@/lib/config'
|
|
5
|
+
|
|
6
|
+
function getContentDir(): string {
|
|
7
|
+
return process.env.CHRONICLE_CONTENT_DIR || path.join(process.cwd(), 'content')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function scanPages(): Promise<{ title: string; url: string }[]> {
|
|
11
|
+
const contentDir = getContentDir()
|
|
12
|
+
const pages: { title: string; url: string }[] = []
|
|
13
|
+
|
|
14
|
+
async function scan(dir: string, prefix: string[] = []) {
|
|
15
|
+
let entries
|
|
16
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }) }
|
|
17
|
+
catch { return }
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
21
|
+
const fullPath = path.join(dir, entry.name)
|
|
22
|
+
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
await scan(fullPath, [...prefix, entry.name])
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!entry.name.endsWith('.mdx') && !entry.name.endsWith('.md')) continue
|
|
29
|
+
|
|
30
|
+
const raw = await fs.readFile(fullPath, 'utf-8')
|
|
31
|
+
const { data: fm } = matter(raw)
|
|
32
|
+
const baseName = entry.name.replace(/\.(mdx|md)$/, '')
|
|
33
|
+
const slugs = baseName === 'index' ? prefix : [...prefix, baseName]
|
|
34
|
+
const url = slugs.length === 0 ? '/' : '/' + slugs.join('/')
|
|
35
|
+
|
|
36
|
+
pages.push({ title: fm.title ?? baseName, url })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await scan(contentDir)
|
|
41
|
+
return pages
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function handleLlms(): Promise<Response> {
|
|
45
|
+
const config = loadConfig()
|
|
46
|
+
|
|
47
|
+
if (!config.llms?.enabled) {
|
|
48
|
+
return new Response('Not Found', { status: 404 })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const pages = await scanPages()
|
|
52
|
+
const index = pages.map((p) => `- [${p.title}](${p.url})`).join('\n')
|
|
53
|
+
const body = `# ${config.title}\n\n${config.description ?? ''}\n\n${index}`
|
|
54
|
+
|
|
55
|
+
return new Response(body, {
|
|
56
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import satori from 'satori'
|
|
2
|
+
import { loadConfig } from '@/lib/config'
|
|
3
|
+
|
|
4
|
+
let fontData: ArrayBuffer | null = null
|
|
5
|
+
|
|
6
|
+
async function loadFont(): Promise<ArrayBuffer> {
|
|
7
|
+
if (fontData) return fontData
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2')
|
|
11
|
+
fontData = await response.arrayBuffer()
|
|
12
|
+
} catch {
|
|
13
|
+
// Fallback: create minimal valid font buffer
|
|
14
|
+
fontData = new ArrayBuffer(0)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return fontData
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function handleOg(req: Request): Promise<Response> {
|
|
21
|
+
const url = new URL(req.url)
|
|
22
|
+
const title = url.searchParams.get('title') ?? loadConfig().title
|
|
23
|
+
const description = url.searchParams.get('description') ?? ''
|
|
24
|
+
const siteName = loadConfig().title
|
|
25
|
+
|
|
26
|
+
const font = await loadFont()
|
|
27
|
+
|
|
28
|
+
const svg = await satori(
|
|
29
|
+
{
|
|
30
|
+
type: 'div',
|
|
31
|
+
props: {
|
|
32
|
+
style: {
|
|
33
|
+
height: '100%',
|
|
34
|
+
width: '100%',
|
|
35
|
+
display: 'flex',
|
|
36
|
+
flexDirection: 'column',
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
padding: '60px 80px',
|
|
39
|
+
backgroundColor: '#0a0a0a',
|
|
40
|
+
color: '#fafafa',
|
|
41
|
+
},
|
|
42
|
+
children: [
|
|
43
|
+
{
|
|
44
|
+
type: 'div',
|
|
45
|
+
props: {
|
|
46
|
+
style: { fontSize: 24, color: '#888', marginBottom: 16 },
|
|
47
|
+
children: siteName,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'div',
|
|
52
|
+
props: {
|
|
53
|
+
style: { fontSize: 56, fontWeight: 700, lineHeight: 1.2, marginBottom: 24 },
|
|
54
|
+
children: title,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
...(description ? [{
|
|
58
|
+
type: 'div',
|
|
59
|
+
props: {
|
|
60
|
+
style: { fontSize: 24, color: '#999', lineHeight: 1.4 },
|
|
61
|
+
children: description,
|
|
62
|
+
},
|
|
63
|
+
}] : []),
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
width: 1200,
|
|
69
|
+
height: 630,
|
|
70
|
+
fonts: [
|
|
71
|
+
{
|
|
72
|
+
name: 'Inter',
|
|
73
|
+
data: font,
|
|
74
|
+
weight: 400,
|
|
75
|
+
style: 'normal' as const,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return new Response(svg, {
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'image/svg+xml',
|
|
84
|
+
'Cache-Control': 'public, max-age=86400',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { loadConfig } from '@/lib/config'
|
|
2
|
+
|
|
3
|
+
export function handleRobots(): Response {
|
|
4
|
+
const config = loadConfig()
|
|
5
|
+
const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : ''
|
|
6
|
+
const body = `User-agent: *\nAllow: /${sitemap}`
|
|
7
|
+
|
|
8
|
+
return new Response(body, {
|
|
9
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
10
|
+
})
|
|
11
|
+
}
|