@raystack/chronicle 0.1.0-canary.a320792 → 0.1.0-canary.ac60f9f
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/dist/cli/index.js +150 -416
- package/package.json +13 -9
- package/src/cli/commands/build.ts +30 -48
- package/src/cli/commands/dev.ts +24 -13
- package/src/cli/commands/init.ts +38 -123
- package/src/cli/commands/serve.ts +35 -50
- package/src/cli/commands/start.ts +20 -16
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +25 -26
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/resolve.ts +7 -3
- package/src/cli/utils/scaffold.ts +14 -16
- package/src/components/mdx/code.tsx +1 -10
- package/src/components/mdx/details.module.css +24 -1
- package/src/components/mdx/details.tsx +3 -2
- package/src/components/mdx/image.tsx +5 -20
- package/src/components/mdx/index.tsx +3 -3
- package/src/components/mdx/link.tsx +24 -20
- package/src/components/ui/footer.tsx +2 -3
- package/src/components/ui/search.tsx +116 -71
- package/src/lib/config.ts +31 -29
- package/src/lib/get-llm-text.ts +10 -0
- package/src/lib/head.tsx +26 -22
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +76 -57
- package/src/lib/source.ts +144 -96
- package/src/pages/ApiLayout.tsx +22 -18
- package/src/pages/ApiPage.tsx +32 -27
- package/src/pages/DocsLayout.tsx +7 -7
- package/src/pages/DocsPage.tsx +11 -11
- package/src/pages/NotFound.tsx +11 -4
- package/src/server/App.tsx +35 -27
- package/src/server/api/apis-proxy.ts +69 -0
- package/src/server/api/health.ts +5 -0
- package/src/server/api/page/[...slug].ts +18 -0
- package/src/server/api/search.ts +170 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +117 -0
- package/src/server/entry-client.tsx +52 -56
- package/src/server/entry-server.tsx +95 -35
- package/src/server/routes/llms.txt.ts +61 -0
- package/src/server/routes/og.tsx +75 -0
- package/src/server/routes/robots.txt.ts +11 -0
- package/src/server/routes/sitemap.xml.ts +39 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +50 -49
- package/src/themes/default/Layout.tsx +69 -41
- package/src/themes/default/Page.module.css +0 -60
- package/src/themes/default/Page.tsx +9 -11
- package/src/themes/default/Toc.tsx +30 -28
- package/src/themes/default/index.ts +7 -9
- package/src/themes/paper/ChapterNav.tsx +59 -39
- package/src/themes/paper/Layout.module.css +1 -1
- package/src/themes/paper/Layout.tsx +24 -12
- package/src/themes/paper/Page.module.css +11 -4
- package/src/themes/paper/Page.tsx +67 -47
- package/src/themes/paper/ReadingProgress.tsx +160 -139
- package/src/themes/paper/index.ts +5 -5
- package/src/themes/registry.ts +7 -7
- package/src/types/globals.d.ts +4 -0
- package/src/cli/__tests__/config.test.ts +0 -25
- package/src/cli/__tests__/scaffold.test.ts +0 -10
- package/src/pages/__tests__/head.test.tsx +0 -57
- package/src/server/__tests__/entry-server.test.tsx +0 -35
- package/src/server/__tests__/handlers.test.ts +0 -77
- package/src/server/__tests__/og.test.ts +0 -23
- package/src/server/__tests__/router.test.ts +0 -72
- package/src/server/__tests__/vite-config.test.ts +0 -25
- package/src/server/dev.ts +0 -156
- package/src/server/entry-prod.ts +0 -127
- package/src/server/handlers/apis-proxy.ts +0 -52
- package/src/server/handlers/health.ts +0 -3
- package/src/server/handlers/llms.ts +0 -58
- package/src/server/handlers/og.ts +0 -87
- package/src/server/handlers/robots.ts +0 -11
- package/src/server/handlers/search.ts +0 -140
- package/src/server/handlers/sitemap.ts +0 -39
- package/src/server/handlers/specs.ts +0 -9
- package/src/server/index.html +0 -12
- package/src/server/prod.ts +0 -18
- package/src/server/router.ts +0 -42
- package/src/themes/default/font.ts +0 -4
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { resolveContentDir } from '../utils/config'
|
|
3
|
-
|
|
4
|
-
describe('resolveContentDir', () => {
|
|
5
|
-
it('returns flag value when provided', () => {
|
|
6
|
-
const result = resolveContentDir('/custom/path')
|
|
7
|
-
expect(result).toBe('/custom/path')
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('returns env var when set', () => {
|
|
11
|
-
const original = process.env.CHRONICLE_CONTENT_DIR
|
|
12
|
-
process.env.CHRONICLE_CONTENT_DIR = '/env/content'
|
|
13
|
-
const result = resolveContentDir()
|
|
14
|
-
expect(result).toContain('env/content')
|
|
15
|
-
process.env.CHRONICLE_CONTENT_DIR = original
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('defaults to content directory', () => {
|
|
19
|
-
const original = process.env.CHRONICLE_CONTENT_DIR
|
|
20
|
-
delete process.env.CHRONICLE_CONTENT_DIR
|
|
21
|
-
const result = resolveContentDir()
|
|
22
|
-
expect(result).toContain('content')
|
|
23
|
-
process.env.CHRONICLE_CONTENT_DIR = original
|
|
24
|
-
})
|
|
25
|
-
})
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { detectPackageManager } from '../utils/scaffold'
|
|
3
|
-
|
|
4
|
-
describe('detectPackageManager', () => {
|
|
5
|
-
it('returns a string', () => {
|
|
6
|
-
const result = detectPackageManager()
|
|
7
|
-
expect(typeof result).toBe('string')
|
|
8
|
-
expect(['npm', 'bun', 'pnpm', 'yarn']).toContain(result)
|
|
9
|
-
})
|
|
10
|
-
})
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { renderToString } from 'react-dom/server'
|
|
3
|
-
import { Head } from '@/lib/head'
|
|
4
|
-
|
|
5
|
-
describe('Head component', () => {
|
|
6
|
-
const baseConfig = {
|
|
7
|
-
title: 'Test Docs',
|
|
8
|
-
theme: { name: 'default' as const },
|
|
9
|
-
search: { enabled: true, placeholder: 'Search...' },
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
it('renders title tag', () => {
|
|
13
|
-
const html = renderToString(
|
|
14
|
-
<Head title="Page Title" config={baseConfig} />
|
|
15
|
-
)
|
|
16
|
-
expect(html).toContain('Page Title | Test Docs')
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('renders description meta tag', () => {
|
|
20
|
-
const html = renderToString(
|
|
21
|
-
<Head title="Page" description="A description" config={baseConfig} />
|
|
22
|
-
)
|
|
23
|
-
expect(html).toContain('A description')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('renders OG tags when config.url is set', () => {
|
|
27
|
-
const html = renderToString(
|
|
28
|
-
<Head
|
|
29
|
-
title="Page"
|
|
30
|
-
description="Desc"
|
|
31
|
-
config={{ ...baseConfig, url: 'https://docs.example.com' }}
|
|
32
|
-
/>
|
|
33
|
-
)
|
|
34
|
-
expect(html).toContain('og:title')
|
|
35
|
-
expect(html).toContain('og:description')
|
|
36
|
-
expect(html).toContain('twitter:card')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('skips OG tags when no url in config', () => {
|
|
40
|
-
const html = renderToString(
|
|
41
|
-
<Head title="Page" config={baseConfig} />
|
|
42
|
-
)
|
|
43
|
-
expect(html).not.toContain('og:title')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('renders JSON-LD script', () => {
|
|
47
|
-
const html = renderToString(
|
|
48
|
-
<Head
|
|
49
|
-
title="Page"
|
|
50
|
-
config={baseConfig}
|
|
51
|
-
jsonLd={{ '@context': 'https://schema.org', '@type': 'Article' }}
|
|
52
|
-
/>
|
|
53
|
-
)
|
|
54
|
-
expect(html).toContain('application/ld+json')
|
|
55
|
-
expect(html).toContain('schema.org')
|
|
56
|
-
})
|
|
57
|
-
})
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { render, type SSRData } from '../entry-server'
|
|
3
|
-
|
|
4
|
-
const mockData: SSRData = {
|
|
5
|
-
config: { title: 'Test Site' },
|
|
6
|
-
tree: { name: 'root', children: [] },
|
|
7
|
-
page: {
|
|
8
|
-
slug: [],
|
|
9
|
-
frontmatter: { title: 'Test' },
|
|
10
|
-
content: null,
|
|
11
|
-
},
|
|
12
|
-
apiSpecs: [],
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('entry-server', () => {
|
|
16
|
-
it('exports a render function', () => {
|
|
17
|
-
expect(typeof render).toBe('function')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('returns an HTML string', () => {
|
|
21
|
-
const html = render('http://localhost:3000/', mockData)
|
|
22
|
-
expect(typeof html).toBe('string')
|
|
23
|
-
expect(html.length).toBeGreaterThan(0)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('renders docs route for root URL', () => {
|
|
27
|
-
const html = render('http://localhost:3000/', mockData)
|
|
28
|
-
expect(html).toBeTruthy()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('renders api route for /apis URL', () => {
|
|
32
|
-
const html = render('http://localhost:3000/apis', mockData)
|
|
33
|
-
expect(html).toBeTruthy()
|
|
34
|
-
})
|
|
35
|
-
})
|
|
@@ -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
|
-
})
|
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
|
-
}
|
package/src/server/entry-prod.ts
DELETED
|
@@ -1,127 +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 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
|
-
}
|
|
@@ -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
|
-
}
|