@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,77 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { handleHealth } from '../handlers/health'
3
- import { handleRobots } from '../handlers/robots'
4
- import { handleSitemap } from '../handlers/sitemap'
5
- import { handleApisProxy } from '../handlers/apis-proxy'
6
- import { handleLlms } from '../handlers/llms'
7
-
8
- describe('handleHealth', () => {
9
- it('returns 200 with status ok', async () => {
10
- const response = handleHealth()
11
- expect(response.status).toBe(200)
12
- const body = await response.json()
13
- expect(body).toEqual({ status: 'ok' })
14
- })
15
- })
16
-
17
- describe('handleRobots', () => {
18
- it('returns text/plain content type', async () => {
19
- const response = handleRobots()
20
- expect(response.headers.get('content-type')).toBe('text/plain')
21
- })
22
-
23
- it('includes User-agent directive', async () => {
24
- const response = handleRobots()
25
- const text = await response.text()
26
- expect(text).toContain('User-agent: *')
27
- expect(text).toContain('Allow: /')
28
- })
29
- })
30
-
31
- describe('handleSitemap', () => {
32
- it('returns application/xml content type', async () => {
33
- const response = await handleSitemap()
34
- expect(response.headers.get('content-type')).toBe('application/xml')
35
- })
36
-
37
- it('returns valid XML structure', async () => {
38
- const response = await handleSitemap()
39
- const xml = await response.text()
40
- expect(xml).toContain('<urlset')
41
- })
42
- })
43
-
44
- describe('handleApisProxy', () => {
45
- it('returns 405 for GET requests', async () => {
46
- const req = new Request('http://localhost:3000/api/apis-proxy', { method: 'GET' })
47
- const response = await handleApisProxy(req)
48
- expect(response.status).toBe(405)
49
- })
50
-
51
- it('returns 400 for POST without required fields', async () => {
52
- const req = new Request('http://localhost:3000/api/apis-proxy', {
53
- method: 'POST',
54
- body: JSON.stringify({}),
55
- headers: { 'Content-Type': 'application/json' },
56
- })
57
- const response = await handleApisProxy(req)
58
- expect(response.status).toBe(400)
59
- })
60
-
61
- it('returns 404 for unknown spec', async () => {
62
- const req = new Request('http://localhost:3000/api/apis-proxy', {
63
- method: 'POST',
64
- body: JSON.stringify({ specName: 'nonexistent', method: 'GET', path: '/test' }),
65
- headers: { 'Content-Type': 'application/json' },
66
- })
67
- const response = await handleApisProxy(req)
68
- expect(response.status).toBe(404)
69
- })
70
- })
71
-
72
- describe('handleLlms', () => {
73
- it('returns 404 when llms not enabled', async () => {
74
- const response = await handleLlms()
75
- expect(response.status).toBe(404)
76
- })
77
- })
@@ -1,23 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { handleOg } from '../handlers/og'
3
-
4
- // OG handler requires network to fetch fonts, skip in CI-like environments
5
- describe.skipIf(!process.env.TEST_OG)('handleOg', () => {
6
- it('returns SVG content type', async () => {
7
- const req = new Request('http://localhost:3000/og?title=Test')
8
- const response = await handleOg(req)
9
- expect(response.headers.get('content-type')).toBe('image/svg+xml')
10
- })
11
-
12
- it('returns cache-control header', async () => {
13
- const req = new Request('http://localhost:3000/og?title=Test')
14
- const response = await handleOg(req)
15
- expect(response.headers.get('cache-control')).toContain('max-age')
16
- })
17
- })
18
-
19
- describe('handleOg export', () => {
20
- it('exports a function', () => {
21
- expect(typeof handleOg).toBe('function')
22
- })
23
- })
@@ -1,72 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { matchRoute } from '../router'
3
-
4
- describe('router', () => {
5
- it('matches /api/health route', () => {
6
- const handler = matchRoute('http://localhost:3000/api/health')
7
- expect(handler).not.toBeNull()
8
- })
9
-
10
- it('returns ok for /api/health', async () => {
11
- const handler = matchRoute('http://localhost:3000/api/health')
12
- const response = await handler!(new Request('http://localhost:3000/api/health'))
13
- expect(response.status).toBe(200)
14
- const body = await response.json()
15
- expect(body).toEqual({ status: 'ok' })
16
- })
17
-
18
- it('matches /api/search route', () => {
19
- const handler = matchRoute('http://localhost:3000/api/search')
20
- expect(handler).not.toBeNull()
21
- })
22
-
23
- it('matches /robots.txt route', () => {
24
- const handler = matchRoute('http://localhost:3000/robots.txt')
25
- expect(handler).not.toBeNull()
26
- })
27
-
28
- it('returns robots.txt with correct content', async () => {
29
- const handler = matchRoute('http://localhost:3000/robots.txt')
30
- const response = await handler!(new Request('http://localhost:3000/robots.txt'))
31
- expect(response.headers.get('content-type')).toBe('text/plain')
32
- const text = await response.text()
33
- expect(text).toContain('User-agent')
34
- })
35
-
36
- it('matches /sitemap.xml route', () => {
37
- const handler = matchRoute('http://localhost:3000/sitemap.xml')
38
- expect(handler).not.toBeNull()
39
- })
40
-
41
- it('returns sitemap.xml with xml content type', async () => {
42
- const handler = matchRoute('http://localhost:3000/sitemap.xml')
43
- const response = await handler!(new Request('http://localhost:3000/sitemap.xml'))
44
- expect(response.headers.get('content-type')).toBe('application/xml')
45
- })
46
-
47
- it('returns null for unknown routes', () => {
48
- const handler = matchRoute('http://localhost:3000/some/random/path')
49
- expect(handler).toBeNull()
50
- })
51
-
52
- it('matches /llms.txt route', () => {
53
- const handler = matchRoute('http://localhost:3000/llms.txt')
54
- expect(handler).not.toBeNull()
55
- })
56
-
57
- it('matches /og route', () => {
58
- const handler = matchRoute('http://localhost:3000/og')
59
- expect(handler).not.toBeNull()
60
- })
61
-
62
- it('matches /api/apis-proxy route', () => {
63
- const handler = matchRoute('http://localhost:3000/api/apis-proxy')
64
- expect(handler).not.toBeNull()
65
- })
66
-
67
- it('returns 405 for non-POST to apis-proxy', async () => {
68
- const handler = matchRoute('http://localhost:3000/api/apis-proxy')
69
- const response = await handler!(new Request('http://localhost:3000/api/apis-proxy'))
70
- expect(response.status).toBe(405)
71
- })
72
- })
@@ -1,25 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { createViteConfig } from '../vite-config'
3
-
4
- describe('createViteConfig', () => {
5
- it('returns a valid vite config object', async () => {
6
- const config = await createViteConfig({
7
- root: '/tmp/test',
8
- contentDir: '/tmp/test/content',
9
- isDev: true,
10
- })
11
-
12
- expect(config.root).toBe('/tmp/test')
13
- expect(config.configFile).toBe(false)
14
- })
15
-
16
- it('accepts isDev option', async () => {
17
- const config = await createViteConfig({
18
- root: '/tmp/test',
19
- contentDir: '/tmp/test/content',
20
- isDev: false,
21
- })
22
-
23
- expect(config.root).toBe('/tmp/test')
24
- })
25
- })
@@ -1,133 +0,0 @@
1
- import path from 'path'
2
- import fs from 'fs/promises'
3
- import { existsSync } from 'fs'
4
- import chalk from 'chalk'
5
-
6
- interface VercelAdapterOptions {
7
- distDir: string
8
- contentDir: string
9
- projectRoot: string
10
- }
11
-
12
- const CONTENT_EXTENSIONS = new Set([
13
- '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
14
- '.pdf', '.json', '.yaml', '.yml', '.txt',
15
- ])
16
-
17
- export async function buildVercelOutput(options: VercelAdapterOptions) {
18
- const { distDir, contentDir, projectRoot } = options
19
- const outputDir = path.resolve(projectRoot, '.vercel/output')
20
-
21
- console.log(chalk.gray('Generating Vercel output...'))
22
-
23
- // Clean previous output
24
- await fs.rm(outputDir, { recursive: true, force: true })
25
-
26
- // Create output directories
27
- const staticDir = path.resolve(outputDir, 'static')
28
- const funcDir = path.resolve(outputDir, 'functions/index.func')
29
- await fs.mkdir(staticDir, { recursive: true })
30
- await fs.mkdir(funcDir, { recursive: true })
31
-
32
- // 1. Copy client assets → .vercel/output/static/
33
- const clientDir = path.resolve(distDir, 'client')
34
- await copyDir(clientDir, staticDir)
35
- console.log(chalk.gray(' Copied client assets to static/'))
36
-
37
- // 2. Copy content dir assets (images, etc.) → .vercel/output/static/
38
- if (existsSync(contentDir)) {
39
- await copyContentAssets(contentDir, staticDir)
40
- console.log(chalk.gray(' Copied content assets to static/'))
41
- }
42
-
43
- // 3. Copy server bundle → .vercel/output/functions/index.func/
44
- const serverDir = path.resolve(distDir, 'server')
45
- await copyDir(serverDir, funcDir)
46
- console.log(chalk.gray(' Copied server bundle to functions/'))
47
-
48
- // 4. Copy HTML template into function dir (not accessible from static/ at runtime)
49
- const templateSrc = path.resolve(clientDir, 'src/server/index.html')
50
- await fs.copyFile(templateSrc, path.resolve(funcDir, 'index.html'))
51
-
52
- // 5. Write package.json for ESM support
53
- await fs.writeFile(
54
- path.resolve(funcDir, 'package.json'),
55
- JSON.stringify({ type: 'module' }, null, 2),
56
- )
57
-
58
- // 6. Write .vc-config.json
59
- await fs.writeFile(
60
- path.resolve(funcDir, '.vc-config.json'),
61
- JSON.stringify({
62
- runtime: 'nodejs24.x',
63
- handler: 'entry-vercel.js',
64
- launcherType: 'Nodejs',
65
- }, null, 2),
66
- )
67
-
68
- // 7. Write config.json
69
- await fs.writeFile(
70
- path.resolve(outputDir, 'config.json'),
71
- JSON.stringify({
72
- version: 3,
73
- routes: [
74
- { handle: 'filesystem' },
75
- { src: '/(.*)', dest: '/index' },
76
- ],
77
- }, null, 2),
78
- )
79
-
80
- console.log(chalk.green('Vercel output generated →'), outputDir)
81
- }
82
-
83
- async function copyDir(src: string, dest: string) {
84
- await fs.mkdir(dest, { recursive: true })
85
- const entries = await fs.readdir(src, { withFileTypes: true })
86
-
87
- for (const entry of entries) {
88
- const srcPath = path.join(src, entry.name)
89
- const destPath = path.join(dest, entry.name)
90
-
91
- if (entry.isDirectory()) {
92
- await copyDir(srcPath, destPath)
93
- } else {
94
- await fs.copyFile(srcPath, destPath)
95
- }
96
- }
97
- }
98
-
99
- async function copyContentAssets(contentDir: string, staticDir: string) {
100
- const entries = await fs.readdir(contentDir, { withFileTypes: true })
101
-
102
- for (const entry of entries) {
103
- const srcPath = path.join(contentDir, entry.name)
104
-
105
- if (entry.isDirectory()) {
106
- const destSubDir = path.join(staticDir, entry.name)
107
- await copyContentAssetsRecursive(srcPath, destSubDir)
108
- } else {
109
- const ext = path.extname(entry.name).toLowerCase()
110
- if (CONTENT_EXTENSIONS.has(ext)) {
111
- await fs.copyFile(srcPath, path.join(staticDir, entry.name))
112
- }
113
- }
114
- }
115
- }
116
-
117
- async function copyContentAssetsRecursive(srcDir: string, destDir: string) {
118
- const entries = await fs.readdir(srcDir, { withFileTypes: true })
119
-
120
- for (const entry of entries) {
121
- const srcPath = path.join(srcDir, entry.name)
122
-
123
- if (entry.isDirectory()) {
124
- await copyContentAssetsRecursive(srcPath, path.join(destDir, entry.name))
125
- } else {
126
- const ext = path.extname(entry.name).toLowerCase()
127
- if (CONTENT_EXTENSIONS.has(ext)) {
128
- await fs.mkdir(destDir, { recursive: true })
129
- await fs.copyFile(srcPath, path.join(destDir, entry.name))
130
- }
131
- }
132
- }
133
- }
package/src/server/dev.ts DELETED
@@ -1,156 +0,0 @@
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
- }
@@ -1,97 +0,0 @@
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 { render } from './entry-server'
7
- import { matchRoute } from './router'
8
- import { loadConfig } from '@/lib/config'
9
- import { loadApiSpecs } from '@/lib/openapi'
10
- import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
11
- import { handleRequest } from './request-handler'
12
-
13
- export { render, matchRoute, loadConfig, loadApiSpecs, getPage, loadPageComponent, buildPageTree }
14
-
15
- async function writeResponse(res: import('http').ServerResponse, response: Response) {
16
- res.statusCode = response.status
17
- response.headers.forEach((value: string, key: string) => res.setHeader(key, value))
18
- const body = await response.text()
19
- res.end(body)
20
- }
21
-
22
- export async function startServer(options: { port: number; distDir: string }) {
23
- const { port, distDir } = options
24
-
25
- const clientDir = path.resolve(distDir, 'client')
26
- const templatePath = path.resolve(clientDir, 'src/server/index.html')
27
- const template = readFileSync(templatePath, 'utf-8')
28
-
29
- const sirv = (await import('sirv')).default
30
- const assets = sirv(clientDir, { gzip: true })
31
-
32
- const baseUrl = `http://localhost:${port}`
33
-
34
- const server = createServer(async (req, res) => {
35
- const url = req.url || '/'
36
-
37
- try {
38
- // API routes — handled by shared request handler
39
- const routeHandler = matchRoute(new URL(url, baseUrl).href)
40
- if (routeHandler) {
41
- const response = await routeHandler(new Request(new URL(url, baseUrl)))
42
- await writeResponse(res, response)
43
- return
44
- }
45
-
46
- // Serve static files from content dir (skip .md/.mdx)
47
- const contentDir = process.env.CHRONICLE_CONTENT_DIR || process.cwd()
48
- const contentFile = path.join(contentDir, decodeURIComponent(url.split('?')[0]))
49
- if (!url.endsWith('.md') && !url.endsWith('.mdx')) {
50
- try {
51
- const stat = await fsPromises.stat(contentFile)
52
- if (stat.isFile()) {
53
- const ext = path.extname(contentFile).toLowerCase()
54
- const mimeTypes: Record<string, string> = {
55
- '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
56
- '.gif': 'image/gif', '.svg': 'image/svg+xml', '.webp': 'image/webp',
57
- '.ico': 'image/x-icon', '.pdf': 'application/pdf', '.json': 'application/json',
58
- '.yaml': 'text/yaml', '.yml': 'text/yaml', '.txt': 'text/plain',
59
- }
60
- res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream')
61
- createReadStream(contentFile).pipe(res)
62
- return
63
- }
64
- } catch { /* fall through */ }
65
- }
66
-
67
- // Static assets from dist/client
68
- const assetHandled = await new Promise<boolean>((resolve) => {
69
- assets(req, res, () => resolve(false))
70
- res.on('close', () => resolve(true))
71
- })
72
- if (assetHandled) return
73
-
74
- // SSR render — handled by shared request handler
75
- const response = await handleRequest(url, { template, baseUrl })
76
- await writeResponse(res, response)
77
- } catch (e) {
78
- console.error(e)
79
- res.statusCode = 500
80
- res.end((e as Error).message)
81
- }
82
- })
83
-
84
- server.listen(port, () => {
85
- console.log(`\n Chronicle production server running at:`)
86
- console.log(` http://localhost:${port}\n`)
87
- })
88
-
89
- const shutdown = () => {
90
- server.close()
91
- process.exit(0)
92
- }
93
- process.on('SIGINT', shutdown)
94
- process.on('SIGTERM', shutdown)
95
-
96
- return server
97
- }
@@ -1,28 +0,0 @@
1
- // Vercel serverless function entry — built by Vite, deployed as catch-all function
2
- import type { IncomingMessage, ServerResponse } from 'http'
3
- import { readFileSync } from 'fs'
4
- import { fileURLToPath } from 'url'
5
- import path from 'path'
6
- import { handleRequest } from './request-handler'
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
- const templatePath = path.resolve(__dirname, 'index.html')
10
- const template = readFileSync(templatePath, 'utf-8')
11
-
12
- export default async function handler(req: IncomingMessage, res: ServerResponse) {
13
- const url = req.url || '/'
14
- const baseUrl = `https://${req.headers.host || 'localhost'}`
15
-
16
- try {
17
- const response = await handleRequest(url, { template, baseUrl })
18
-
19
- res.statusCode = response.status
20
- response.headers.forEach((value: string, key: string) => res.setHeader(key, value))
21
- const body = await response.text()
22
- res.end(body)
23
- } catch (e) {
24
- console.error(e)
25
- res.statusCode = 500
26
- res.end((e as Error).message)
27
- }
28
- }
@@ -1,52 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
1
- export function handleHealth(): Response {
2
- return Response.json({ status: 'ok' })
3
- }