@raystack/chronicle 0.1.0-canary.a320792 → 0.1.0-canary.a638730

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 CHANGED
@@ -221,18 +221,124 @@ var init_dev = __esm(() => {
221
221
  init_vite_config();
222
222
  });
223
223
 
224
+ // src/server/adapters/vercel.ts
225
+ var exports_vercel = {};
226
+ __export(exports_vercel, {
227
+ buildVercelOutput: () => buildVercelOutput
228
+ });
229
+ import path7 from "path";
230
+ import fs3 from "fs/promises";
231
+ import { existsSync } from "fs";
232
+ import chalk5 from "chalk";
233
+ async function buildVercelOutput(options) {
234
+ const { distDir, contentDir, projectRoot } = options;
235
+ const outputDir = path7.resolve(projectRoot, ".vercel/output");
236
+ console.log(chalk5.gray("Generating Vercel output..."));
237
+ await fs3.rm(outputDir, { recursive: true, force: true });
238
+ const staticDir = path7.resolve(outputDir, "static");
239
+ const funcDir = path7.resolve(outputDir, "functions/index.func");
240
+ await fs3.mkdir(staticDir, { recursive: true });
241
+ await fs3.mkdir(funcDir, { recursive: true });
242
+ const clientDir = path7.resolve(distDir, "client");
243
+ await copyDir(clientDir, staticDir);
244
+ console.log(chalk5.gray(" Copied client assets to static/"));
245
+ if (existsSync(contentDir)) {
246
+ await copyContentAssets(contentDir, staticDir);
247
+ console.log(chalk5.gray(" Copied content assets to static/"));
248
+ }
249
+ const serverDir = path7.resolve(distDir, "server");
250
+ await copyDir(serverDir, funcDir);
251
+ console.log(chalk5.gray(" Copied server bundle to functions/"));
252
+ const templateSrc = path7.resolve(clientDir, "src/server/index.html");
253
+ await fs3.copyFile(templateSrc, path7.resolve(funcDir, "index.html"));
254
+ await fs3.writeFile(path7.resolve(funcDir, "package.json"), JSON.stringify({ type: "module" }, null, 2));
255
+ await fs3.writeFile(path7.resolve(funcDir, ".vc-config.json"), JSON.stringify({
256
+ runtime: "nodejs22.x",
257
+ handler: "entry-vercel.js",
258
+ launcherType: "Nodejs"
259
+ }, null, 2));
260
+ await fs3.writeFile(path7.resolve(outputDir, "config.json"), JSON.stringify({
261
+ version: 3,
262
+ routes: [
263
+ { handle: "filesystem" },
264
+ { src: "/(.*)", dest: "/index" }
265
+ ]
266
+ }, null, 2));
267
+ console.log(chalk5.green("Vercel output generated →"), outputDir);
268
+ }
269
+ async function copyDir(src, dest) {
270
+ await fs3.mkdir(dest, { recursive: true });
271
+ const entries = await fs3.readdir(src, { withFileTypes: true });
272
+ for (const entry of entries) {
273
+ const srcPath = path7.join(src, entry.name);
274
+ const destPath = path7.join(dest, entry.name);
275
+ if (entry.isDirectory()) {
276
+ await copyDir(srcPath, destPath);
277
+ } else {
278
+ await fs3.copyFile(srcPath, destPath);
279
+ }
280
+ }
281
+ }
282
+ async function copyContentAssets(contentDir, staticDir) {
283
+ const entries = await fs3.readdir(contentDir, { withFileTypes: true });
284
+ for (const entry of entries) {
285
+ const srcPath = path7.join(contentDir, entry.name);
286
+ if (entry.isDirectory()) {
287
+ const destSubDir = path7.join(staticDir, entry.name);
288
+ await copyContentAssetsRecursive(srcPath, destSubDir);
289
+ } else {
290
+ const ext = path7.extname(entry.name).toLowerCase();
291
+ if (CONTENT_EXTENSIONS.has(ext)) {
292
+ await fs3.copyFile(srcPath, path7.join(staticDir, entry.name));
293
+ }
294
+ }
295
+ }
296
+ }
297
+ async function copyContentAssetsRecursive(srcDir, destDir) {
298
+ const entries = await fs3.readdir(srcDir, { withFileTypes: true });
299
+ for (const entry of entries) {
300
+ const srcPath = path7.join(srcDir, entry.name);
301
+ if (entry.isDirectory()) {
302
+ await copyContentAssetsRecursive(srcPath, path7.join(destDir, entry.name));
303
+ } else {
304
+ const ext = path7.extname(entry.name).toLowerCase();
305
+ if (CONTENT_EXTENSIONS.has(ext)) {
306
+ await fs3.mkdir(destDir, { recursive: true });
307
+ await fs3.copyFile(srcPath, path7.join(destDir, entry.name));
308
+ }
309
+ }
310
+ }
311
+ }
312
+ var CONTENT_EXTENSIONS;
313
+ var init_vercel = __esm(() => {
314
+ CONTENT_EXTENSIONS = new Set([
315
+ ".png",
316
+ ".jpg",
317
+ ".jpeg",
318
+ ".gif",
319
+ ".svg",
320
+ ".webp",
321
+ ".ico",
322
+ ".pdf",
323
+ ".json",
324
+ ".yaml",
325
+ ".yml",
326
+ ".txt"
327
+ ]);
328
+ });
329
+
224
330
  // src/server/prod.ts
225
331
  var exports_prod = {};
226
332
  __export(exports_prod, {
227
333
  startProdServer: () => startProdServer
228
334
  });
229
- import path8 from "path";
230
- import chalk6 from "chalk";
335
+ import path9 from "path";
336
+ import chalk7 from "chalk";
231
337
  async function startProdServer(options) {
232
338
  const { port, distDir } = options;
233
- const serverEntry = path8.resolve(distDir, "server/entry-prod.js");
339
+ const serverEntry = path9.resolve(distDir, "server/entry-prod.js");
234
340
  const { startServer } = await import(serverEntry);
235
- console.log(chalk6.cyan("Starting production server..."));
341
+ console.log(chalk7.cyan("Starting production server..."));
236
342
  return startServer({ port, distDir });
237
343
  }
238
344
  var init_prod = () => {};
@@ -442,78 +548,87 @@ var devCommand = new Command2("dev").description("Start development server").opt
442
548
 
443
549
  // src/cli/commands/build.ts
444
550
  import { Command as Command3 } from "commander";
445
- import path7 from "path";
446
- import chalk5 from "chalk";
447
- var buildCommand = new Command3("build").description("Build for production").option("-c, --content <path>", "Content directory").option("-o, --outDir <path>", "Output directory", "dist").action(async (options) => {
551
+ import path8 from "path";
552
+ import chalk6 from "chalk";
553
+ var buildCommand = new Command3("build").description("Build for production").option("-c, --content <path>", "Content directory").option("-o, --outDir <path>", "Output directory", "dist").option("--adapter <adapter>", "Deploy adapter (vercel)").action(async (options) => {
448
554
  const contentDir = resolveContentDir(options.content);
449
- const outDir = path7.resolve(options.outDir);
555
+ const outDir = path8.resolve(options.outDir);
450
556
  process.env.CHRONICLE_PROJECT_ROOT = process.cwd();
451
557
  process.env.CHRONICLE_CONTENT_DIR = contentDir;
452
- console.log(chalk5.cyan("Building for production..."));
558
+ console.log(chalk6.cyan("Building for production..."));
453
559
  const { build } = await import("vite");
454
560
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
455
561
  const baseConfig = await createViteConfig2({ root: PACKAGE_ROOT, contentDir });
456
- console.log(chalk5.gray("Building client..."));
562
+ console.log(chalk6.gray("Building client..."));
457
563
  await build({
458
564
  ...baseConfig,
459
565
  build: {
460
- outDir: path7.join(outDir, "client"),
566
+ outDir: path8.join(outDir, "client"),
461
567
  ssrManifest: true,
462
568
  rolldownOptions: {
463
- input: path7.resolve(PACKAGE_ROOT, "src/server/index.html")
569
+ input: path8.resolve(PACKAGE_ROOT, "src/server/index.html")
464
570
  }
465
571
  }
466
572
  });
467
- console.log(chalk5.gray("Building server..."));
573
+ const serverEntry = options.adapter === "vercel" ? path8.resolve(PACKAGE_ROOT, "src/server/entry-vercel.ts") : path8.resolve(PACKAGE_ROOT, "src/server/entry-prod.ts");
574
+ console.log(chalk6.gray("Building server..."));
468
575
  await build({
469
576
  ...baseConfig,
470
577
  ssr: {
471
578
  noExternal: true
472
579
  },
473
580
  build: {
474
- outDir: path7.join(outDir, "server"),
475
- ssr: path7.resolve(PACKAGE_ROOT, "src/server/entry-prod.ts")
581
+ outDir: path8.join(outDir, "server"),
582
+ ssr: serverEntry
476
583
  }
477
584
  });
478
- console.log(chalk5.green("Build complete →"), outDir);
585
+ console.log(chalk6.green("Build complete →"), outDir);
586
+ if (options.adapter === "vercel") {
587
+ const { buildVercelOutput: buildVercelOutput2 } = await Promise.resolve().then(() => (init_vercel(), exports_vercel));
588
+ await buildVercelOutput2({
589
+ distDir: outDir,
590
+ contentDir,
591
+ projectRoot: process.cwd()
592
+ });
593
+ }
479
594
  });
480
595
 
481
596
  // src/cli/commands/start.ts
482
597
  import { Command as Command4 } from "commander";
483
- import path9 from "path";
484
- import chalk7 from "chalk";
598
+ import path10 from "path";
599
+ import chalk8 from "chalk";
485
600
  var startCommand = new Command4("start").description("Start production server").option("-p, --port <port>", "Port number", "3000").option("-c, --content <path>", "Content directory").option("-d, --dist <path>", "Dist directory", "dist").action(async (options) => {
486
601
  const contentDir = resolveContentDir(options.content);
487
602
  const port = parseInt(options.port, 10);
488
- const distDir = path9.resolve(options.dist);
603
+ const distDir = path10.resolve(options.dist);
489
604
  process.env.CHRONICLE_PROJECT_ROOT = process.cwd();
490
605
  process.env.CHRONICLE_CONTENT_DIR = contentDir;
491
- console.log(chalk7.cyan("Starting production server..."));
606
+ console.log(chalk8.cyan("Starting production server..."));
492
607
  const { startProdServer: startProdServer2 } = await Promise.resolve().then(() => (init_prod(), exports_prod));
493
608
  await startProdServer2({ port, root: PACKAGE_ROOT, distDir });
494
609
  });
495
610
 
496
611
  // src/cli/commands/serve.ts
497
612
  import { Command as Command5 } from "commander";
498
- import path10 from "path";
499
- import chalk8 from "chalk";
613
+ import path11 from "path";
614
+ import chalk9 from "chalk";
500
615
  var serveCommand = new Command5("serve").description("Build and start production server").option("-p, --port <port>", "Port number", "3000").option("-c, --content <path>", "Content directory").option("-o, --outDir <path>", "Output directory", "dist").action(async (options) => {
501
616
  const contentDir = resolveContentDir(options.content);
502
617
  const port = parseInt(options.port, 10);
503
- const outDir = path10.resolve(options.outDir);
618
+ const outDir = path11.resolve(options.outDir);
504
619
  process.env.CHRONICLE_PROJECT_ROOT = process.cwd();
505
620
  process.env.CHRONICLE_CONTENT_DIR = contentDir;
506
- console.log(chalk8.cyan("Building for production..."));
621
+ console.log(chalk9.cyan("Building for production..."));
507
622
  const { build } = await import("vite");
508
623
  const { createViteConfig: createViteConfig2 } = await Promise.resolve().then(() => (init_vite_config(), exports_vite_config));
509
624
  const baseConfig = await createViteConfig2({ root: PACKAGE_ROOT, contentDir });
510
625
  await build({
511
626
  ...baseConfig,
512
627
  build: {
513
- outDir: path10.join(outDir, "client"),
628
+ outDir: path11.join(outDir, "client"),
514
629
  ssrManifest: true,
515
630
  rolldownOptions: {
516
- input: path10.resolve(PACKAGE_ROOT, "src/server/index.html")
631
+ input: path11.resolve(PACKAGE_ROOT, "src/server/index.html")
517
632
  }
518
633
  }
519
634
  });
@@ -523,11 +638,11 @@ var serveCommand = new Command5("serve").description("Build and start production
523
638
  noExternal: true
524
639
  },
525
640
  build: {
526
- outDir: path10.join(outDir, "server"),
527
- ssr: path10.resolve(PACKAGE_ROOT, "src/server/entry-prod.ts")
641
+ outDir: path11.join(outDir, "server"),
642
+ ssr: path11.resolve(PACKAGE_ROOT, "src/server/entry-prod.ts")
528
643
  }
529
644
  });
530
- console.log(chalk8.cyan("Starting production server..."));
645
+ console.log(chalk9.cyan("Starting production server..."));
531
646
  const { startProdServer: startProdServer2 } = await Promise.resolve().then(() => (init_prod(), exports_prod));
532
647
  await startProdServer2({ port, root: PACKAGE_ROOT, distDir: outDir });
533
648
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.1.0-canary.a320792",
3
+ "version": "0.1.0-canary.a638730",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -8,6 +8,7 @@ export const buildCommand = new Command('build')
8
8
  .description('Build for production')
9
9
  .option('-c, --content <path>', 'Content directory')
10
10
  .option('-o, --outDir <path>', 'Output directory', 'dist')
11
+ .option('--adapter <adapter>', 'Deploy adapter (vercel)')
11
12
  .action(async (options) => {
12
13
  const contentDir = resolveContentDir(options.content)
13
14
  const outDir = path.resolve(options.outDir)
@@ -35,7 +36,11 @@ export const buildCommand = new Command('build')
35
36
  },
36
37
  })
37
38
 
38
- // Build server bundle (noExternal: true to bundle all deps for portability)
39
+ // Build server bundle
40
+ const serverEntry = options.adapter === 'vercel'
41
+ ? path.resolve(PACKAGE_ROOT, 'src/server/entry-vercel.ts')
42
+ : path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts')
43
+
39
44
  console.log(chalk.gray('Building server...'))
40
45
  await build({
41
46
  ...baseConfig,
@@ -44,9 +49,19 @@ export const buildCommand = new Command('build')
44
49
  },
45
50
  build: {
46
51
  outDir: path.join(outDir, 'server'),
47
- ssr: path.resolve(PACKAGE_ROOT, 'src/server/entry-prod.ts'),
52
+ ssr: serverEntry,
48
53
  },
49
54
  })
50
55
 
51
56
  console.log(chalk.green('Build complete →'), outDir)
57
+
58
+ // Run Vercel adapter post-build
59
+ if (options.adapter === 'vercel') {
60
+ const { buildVercelOutput } = await import('@/server/adapters/vercel')
61
+ await buildVercelOutput({
62
+ distDir: outDir,
63
+ contentDir,
64
+ projectRoot: process.cwd(),
65
+ })
66
+ }
52
67
  })
@@ -0,0 +1,133 @@
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: 'nodejs22.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
+ }
@@ -3,16 +3,22 @@ import { createServer } from 'http'
3
3
  import { readFileSync, createReadStream } from 'fs'
4
4
  import fsPromises from 'fs/promises'
5
5
  import path from 'path'
6
- import React from 'react'
7
6
  import { render } from './entry-server'
8
7
  import { matchRoute } from './router'
9
8
  import { loadConfig } from '@/lib/config'
10
9
  import { loadApiSpecs } from '@/lib/openapi'
11
10
  import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
12
- import { mdxComponents } from '@/components/mdx'
11
+ import { handleRequest } from './request-handler'
13
12
 
14
13
  export { render, matchRoute, loadConfig, loadApiSpecs, getPage, loadPageComponent, buildPageTree }
15
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
+
16
22
  export async function startServer(options: { port: number; distDir: string }) {
17
23
  const { port, distDir } = options
18
24
 
@@ -23,19 +29,17 @@ export async function startServer(options: { port: number; distDir: string }) {
23
29
  const sirv = (await import('sirv')).default
24
30
  const assets = sirv(clientDir, { gzip: true })
25
31
 
32
+ const baseUrl = `http://localhost:${port}`
33
+
26
34
  const server = createServer(async (req, res) => {
27
35
  const url = req.url || '/'
28
36
 
29
37
  try {
30
- // API routes
31
- const routeHandler = matchRoute(new URL(url, `http://localhost:${port}`).href)
38
+ // API routes — handled by shared request handler
39
+ const routeHandler = matchRoute(new URL(url, baseUrl).href)
32
40
  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)
41
+ const response = await routeHandler(new Request(new URL(url, baseUrl)))
42
+ await writeResponse(res, response)
39
43
  return
40
44
  }
41
45
 
@@ -67,43 +71,9 @@ export async function startServer(options: { port: number; distDir: string }) {
67
71
  })
68
72
  if (assetHandled) return
69
73
 
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)
74
+ // SSR render — handled by shared request handler
75
+ const response = await handleRequest(url, { template, baseUrl })
76
+ await writeResponse(res, response)
107
77
  } catch (e) {
108
78
  console.error(e)
109
79
  res.statusCode = 500
@@ -0,0 +1,26 @@
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 path from 'path'
5
+ import { handleRequest } from './request-handler'
6
+
7
+ const templatePath = path.resolve(__dirname, 'index.html')
8
+ const template = readFileSync(templatePath, 'utf-8')
9
+
10
+ export default async function handler(req: IncomingMessage, res: ServerResponse) {
11
+ const url = req.url || '/'
12
+ const baseUrl = `https://${req.headers.host || 'localhost'}`
13
+
14
+ try {
15
+ const response = await handleRequest(url, { template, baseUrl })
16
+
17
+ res.statusCode = response.status
18
+ response.headers.forEach((value: string, key: string) => res.setHeader(key, value))
19
+ const body = await response.text()
20
+ res.end(body)
21
+ } catch (e) {
22
+ console.error(e)
23
+ res.statusCode = 500
24
+ res.end((e as Error).message)
25
+ }
26
+ }
@@ -0,0 +1,63 @@
1
+ // Shared request handler for API routes + SSR rendering
2
+ // Used by entry-prod.ts (Node) and entry-vercel.ts (Vercel)
3
+ import React from 'react'
4
+ import { render } from './entry-server'
5
+ import { matchRoute } from './router'
6
+ import { loadConfig } from '@/lib/config'
7
+ import { loadApiSpecs } from '@/lib/openapi'
8
+ import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
9
+ import { mdxComponents } from '@/components/mdx'
10
+
11
+ export interface RequestHandlerOptions {
12
+ template: string
13
+ baseUrl: string
14
+ }
15
+
16
+ export async function handleRequest(url: string, options: RequestHandlerOptions): Promise<Response> {
17
+ const { template, baseUrl } = options
18
+ const fullUrl = new URL(url, baseUrl).href
19
+
20
+ // API routes
21
+ const routeHandler = matchRoute(fullUrl)
22
+ if (routeHandler) {
23
+ return routeHandler(new Request(fullUrl))
24
+ }
25
+
26
+ // SSR render
27
+ const pathname = new URL(url, baseUrl).pathname
28
+ const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
29
+
30
+ const config = loadConfig()
31
+ const apiSpecs = config.api?.length ? loadApiSpecs(config.api) : []
32
+
33
+ const [tree, sourcePage] = await Promise.all([
34
+ buildPageTree(),
35
+ getPage(slug),
36
+ ])
37
+
38
+ let pageData = null
39
+ let embeddedData: any = { config, tree, slug, frontmatter: null, filePath: null }
40
+
41
+ if (sourcePage) {
42
+ const component = await loadPageComponent(sourcePage)
43
+ pageData = {
44
+ slug,
45
+ frontmatter: sourcePage.frontmatter,
46
+ content: component ? React.createElement(component, { components: mdxComponents }) : null,
47
+ }
48
+ embeddedData.frontmatter = sourcePage.frontmatter
49
+ embeddedData.filePath = sourcePage.filePath
50
+ }
51
+
52
+ const html = render(url, { config, tree, page: pageData, apiSpecs })
53
+
54
+ const dataScript = `<script>window.__PAGE_DATA__ = ${JSON.stringify(embeddedData)}</script>`
55
+ const finalHtml = template
56
+ .replace('<!--head-outlet-->', `<!--head-outlet-->${dataScript}`)
57
+ .replace('<!--ssr-outlet-->', html)
58
+
59
+ return new Response(finalHtml, {
60
+ status: 200,
61
+ headers: { 'Content-Type': 'text/html' },
62
+ })
63
+ }