@buenojs/bueno 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,631 @@
1
+ /**
2
+ * Static Site Generation (SSG) Module
3
+ *
4
+ * Provides markdown-to-HTML conversion with frontmatter support,
5
+ * template system, and static site generation capabilities.
6
+ */
7
+
8
+ import { Context } from "../context";
9
+ import { Router } from "../router";
10
+
11
+ export interface Frontmatter {
12
+ title?: string;
13
+ description?: string;
14
+ layout?: string;
15
+ date?: string;
16
+ slug?: string;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export interface Page {
21
+ path: string;
22
+ content: string;
23
+ html: string;
24
+ frontmatter: Frontmatter;
25
+ raw: string;
26
+ }
27
+
28
+ export interface SSGConfig {
29
+ contentDir: string;
30
+ outputDir: string;
31
+ publicDir?: string;
32
+ layoutsDir?: string;
33
+ defaultLayout?: string;
34
+ baseUrl?: string;
35
+ minify?: boolean;
36
+ }
37
+
38
+ export interface LayoutContext {
39
+ title?: string;
40
+ description?: string;
41
+ content: string;
42
+ page: Page;
43
+ site: SiteConfig;
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ export interface SiteConfig {
48
+ title: string;
49
+ description: string;
50
+ baseUrl: string;
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n/;
55
+ const MARKDOWN_CODE_BLOCK = /```(\w+)?\n([\s\S]*?)```/g;
56
+ const MARKDOWN_INLINE_CODE = /`([^`]+)`/g;
57
+ const MARKDOWN_HEADERS = /^(#{1,6})\s+(.+)$/gm;
58
+ const MARKDOWN_BOLD = /\*\*(.+?)\*\*/g;
59
+ const MARKDOWN_ITALIC = /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g;
60
+ const MARKDOWN_LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
61
+ const MARKDOWN_IMAGE = /!\[([^\]]*)\]\(([^)]+)\)/g;
62
+ const MARKDOWN_LIST = /^(\s*)[-*+]\s+(.+)$/gm;
63
+ const MARKDOWN_ORDERED_LIST = /^(\s*)(\d+)\.\s+(.+)$/gm;
64
+ const MARKDOWN_BLOCKQUOTE = /^>\s+(.+)$/gm;
65
+ const MARKDOWN_HR = /^---$/gm;
66
+ const MARKDOWN_PARAGRAPH = /\n\n/g;
67
+ const MARKDOWN_TABLE_ROW = /^\|(.+)\|$/;
68
+ const MARKDOWN_TABLE_DIVIDER = /^\|[-:\s|]+\|$/;
69
+
70
+ function parseFrontmatter(content: string): {
71
+ frontmatter: Frontmatter;
72
+ body: string;
73
+ } {
74
+ const match = content.match(FRONTMATTER_REGEX);
75
+
76
+ if (!match) {
77
+ return { frontmatter: {}, body: content };
78
+ }
79
+
80
+ const frontmatterRaw = match[1];
81
+ const body = content.slice(match[0].length);
82
+ const frontmatter: Frontmatter = {};
83
+
84
+ for (const line of frontmatterRaw.split("\n")) {
85
+ const colonIndex = line.indexOf(":");
86
+ if (colonIndex > 0) {
87
+ const key = line.slice(0, colonIndex).trim();
88
+ let value: unknown = line.slice(colonIndex + 1).trim();
89
+
90
+ if (typeof value === "string") {
91
+ if (
92
+ (value.startsWith('"') && value.endsWith('"')) ||
93
+ (value.startsWith("'") && value.endsWith("'"))
94
+ ) {
95
+ value = value.slice(1, -1);
96
+ } else if (value === "true") {
97
+ value = true;
98
+ } else if (value === "false") {
99
+ value = false;
100
+ } else if (!Number.isNaN(Number(value))) {
101
+ value = Number(value);
102
+ }
103
+ }
104
+
105
+ frontmatter[key] = value;
106
+ }
107
+ }
108
+
109
+ return { frontmatter, body };
110
+ }
111
+
112
+ function escapeHtml(text: string): string {
113
+ return text
114
+ .replace(/&/g, "&amp;")
115
+ .replace(/</g, "&lt;")
116
+ .replace(/>/g, "&gt;");
117
+ }
118
+ function parseTables(html: string): string {
119
+ const lines = html.split("\n");
120
+ const result: string[] = [];
121
+ let inTable = false;
122
+ let tableLines: string[] = [];
123
+
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const line = lines[i];
126
+
127
+ if (MARKDOWN_TABLE_ROW.test(line)) {
128
+ if (!inTable) {
129
+ inTable = true;
130
+ tableLines = [];
131
+ }
132
+ tableLines.push(line);
133
+ } else {
134
+ if (inTable) {
135
+ // End of table, convert to HTML
136
+ result.push(convertTableToHtml(tableLines));
137
+ inTable = false;
138
+ tableLines = [];
139
+ }
140
+ result.push(line);
141
+ }
142
+ }
143
+
144
+ // Handle table at end of content
145
+ if (inTable && tableLines.length > 0) {
146
+ result.push(convertTableToHtml(tableLines));
147
+ }
148
+
149
+ return result.join("\n");
150
+ }
151
+
152
+ function convertTableToHtml(tableLines: string[]): string {
153
+ if (tableLines.length < 2) {
154
+ return tableLines.join("\n");
155
+ }
156
+
157
+ // First row is header
158
+ const headerLine = tableLines[0];
159
+ const dividerLine = tableLines[1];
160
+ const bodyLines = tableLines.slice(2);
161
+
162
+ // Parse header
163
+ const headers = parseTableRow(headerLine);
164
+
165
+ // Parse body rows
166
+ const bodyRows = bodyLines.map(parseTableRow);
167
+
168
+ // Build HTML
169
+ let html = '<div class="overflow-x-auto"><table>\n';
170
+
171
+ // Header
172
+ html += "<thead>\n<tr>\n";
173
+ for (const header of headers) {
174
+ html += `<th>${header.trim()}</th>\n`;
175
+ }
176
+ html += "</tr>\n</thead>\n";
177
+
178
+ // Body
179
+ if (bodyRows.length > 0) {
180
+ html += "<tbody>\n";
181
+ for (const row of bodyRows) {
182
+ html += "<tr>\n";
183
+ for (const cell of row) {
184
+ html += `<td>${cell.trim()}</td>\n`;
185
+ }
186
+ html += "</tr>\n";
187
+ }
188
+ html += "</tbody>\n";
189
+ }
190
+
191
+ html += "</table></div>";
192
+
193
+ return html;
194
+ }
195
+
196
+ function parseTableRow(line: string): string[] {
197
+ // Remove leading and trailing |
198
+ const content = line.replace(/^\|/, "").replace(/\|$/, "");
199
+ // Split by | and trim each cell
200
+ return content.split("|").map((cell) => cell.trim());
201
+ }
202
+
203
+
204
+ function parseMarkdown(markdown: string): string {
205
+ let html = markdown;
206
+
207
+ const codeBlocks: Array<{ placeholder: string; html: string }> = [];
208
+ let codeIndex = 0;
209
+
210
+ html = html.replace(MARKDOWN_CODE_BLOCK, (_, lang, code) => {
211
+ const placeholder = `__CODE_BLOCK_${codeIndex}__`;
212
+ const escapedCode = escapeHtml(code.trim());
213
+ const langClass = lang ? ` class="language-${lang}"` : "";
214
+ codeBlocks.push({
215
+ placeholder,
216
+ html: `<pre><code${langClass}>${escapedCode}</code></pre>`,
217
+ });
218
+ codeIndex++;
219
+ return placeholder;
220
+ });
221
+
222
+ const inlineCodes: Array<{ placeholder: string; html: string }> = [];
223
+ let inlineIndex = 0;
224
+
225
+ html = html.replace(MARKDOWN_INLINE_CODE, (_, code) => {
226
+ const placeholder = `__INLINE_CODE_${inlineIndex}__`;
227
+ inlineCodes.push({
228
+ placeholder,
229
+ html: `<code>${escapeHtml(code)}</code>`,
230
+ });
231
+ inlineIndex++;
232
+ return placeholder;
233
+ });
234
+
235
+ html = html.replace(MARKDOWN_IMAGE, '<img src="$2" alt="$1" />');
236
+ html = html.replace(MARKDOWN_LINK, '<a href="$2">$1</a>');
237
+
238
+ html = html.replace(MARKDOWN_HEADERS, (_, hashes, text) => {
239
+ const level = hashes.length;
240
+ const trimmedText = text.trim();
241
+ // Generate slug from heading text
242
+ const slug = trimmedText
243
+ .toLowerCase()
244
+ .replace(/[^a-z0-9]+/g, '-')
245
+ .replace(/^-|-$/g, '');
246
+ return `<h${level} id="${slug}">${trimmedText}</h${level}>`;
247
+ });
248
+
249
+ html = html.replace(MARKDOWN_BOLD, "<strong>$1</strong>");
250
+ html = html.replace(MARKDOWN_ITALIC, "<em>$1</em>");
251
+
252
+ html = html.replace(MARKDOWN_BLOCKQUOTE, "<blockquote>$1</blockquote>");
253
+
254
+ html = html.replace(MARKDOWN_HR, "<hr />");
255
+
256
+ const listItems: Array<{
257
+ indent: number;
258
+ type: "ul" | "ol";
259
+ items: string[];
260
+ }> = [];
261
+ let currentList: {
262
+ indent: number;
263
+ type: "ul" | "ol";
264
+ items: string[];
265
+ } | null = null;
266
+
267
+ const lines = html.split("\n");
268
+ const processedLines: string[] = [];
269
+
270
+ for (const line of lines) {
271
+ const ulMatch = line.match(/^(\s*)[-*+]\s+(.+)$/);
272
+ const olMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
273
+
274
+ if (ulMatch || olMatch) {
275
+ const match = ulMatch || olMatch;
276
+ const indent = match?.[1].length ?? 0;
277
+ const type = ulMatch ? "ul" : "ol";
278
+ const text = ulMatch ? ulMatch[2] : (olMatch?.[3] ?? "");
279
+
280
+ if (
281
+ currentList &&
282
+ currentList.indent === indent &&
283
+ currentList.type === type
284
+ ) {
285
+ currentList.items.push(text);
286
+ } else {
287
+ if (currentList) {
288
+ const tag = currentList.type;
289
+ processedLines.push(
290
+ `<${tag}><li>${currentList.items.join("</li><li>")}</li></${tag}>`,
291
+ );
292
+ }
293
+ currentList = { indent: indent ?? 0, type, items: [text] };
294
+ }
295
+ } else {
296
+ if (currentList) {
297
+ const tag = currentList.type;
298
+ processedLines.push(
299
+ `<${tag}><li>${currentList.items.join("</li><li>")}</li></${tag}>`,
300
+ );
301
+ currentList = null;
302
+ }
303
+ processedLines.push(line);
304
+ }
305
+ }
306
+
307
+ if (currentList) {
308
+ const tag = currentList.type;
309
+ processedLines.push(
310
+ `<${tag}><li>${currentList.items.join("</li><li>")}</li></${tag}>`,
311
+ );
312
+ }
313
+
314
+ html = processedLines.join("\n");
315
+
316
+ // Parse tables
317
+ html = parseTables(html);
318
+
319
+ html = html
320
+ .split(MARKDOWN_PARAGRAPH.source)
321
+ .map((p) => {
322
+ p = p.trim();
323
+ if (!p) return "";
324
+ if (p.startsWith("<")) return p;
325
+ return `<p>${p}</p>`;
326
+ })
327
+ .join("\n");
328
+
329
+ for (const { placeholder, html: codeHtml } of codeBlocks) {
330
+ html = html.replace(placeholder, codeHtml);
331
+ }
332
+
333
+ for (const { placeholder, html: codeHtml } of inlineCodes) {
334
+ html = html.replace(placeholder, codeHtml);
335
+ }
336
+
337
+ return html;
338
+ }
339
+
340
+ export class SSG {
341
+ private config: SSGConfig;
342
+ private pages: Map<string, Page> = new Map();
343
+ private layouts: Map<string, (ctx: LayoutContext) => string> = new Map();
344
+ private siteConfig: SiteConfig;
345
+ private router: Router;
346
+
347
+ constructor(config: SSGConfig, siteConfig?: Partial<SiteConfig>) {
348
+ this.config = config;
349
+ this.siteConfig = {
350
+ title: "Bueno Documentation",
351
+ description: "A Bun-Native Full-Stack Framework",
352
+ baseUrl: config.baseUrl || "/",
353
+ ...siteConfig,
354
+ };
355
+ this.router = new Router();
356
+
357
+ this.registerDefaultLayouts();
358
+ }
359
+
360
+ private registerDefaultLayouts(): void {
361
+ this.layouts.set(
362
+ "default",
363
+ (ctx) => `<!DOCTYPE html>
364
+ <html lang="en">
365
+ <head>
366
+ <meta charset="UTF-8">
367
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
368
+ <title>${ctx.title || ctx.page.frontmatter.title || ctx.site.title}</title>
369
+ <meta name="description" content="${ctx.description || ctx.page.frontmatter.description || ctx.site.description}">
370
+ <link rel="stylesheet" href="${ctx.site.baseUrl}style.css">
371
+ </head>
372
+ <body>
373
+ <nav>
374
+ <a href="${ctx.site.baseUrl}">Home</a>
375
+ <a href="${ctx.site.baseUrl}docs">Docs</a>
376
+ <a href="${ctx.site.baseUrl}api">API</a>
377
+ </nav>
378
+ <main>
379
+ ${ctx.content}
380
+ </main>
381
+ <footer>
382
+ <p>&copy; ${new Date().getFullYear()} Bueno Framework</p>
383
+ </footer>
384
+ </body>
385
+ </html>`,
386
+ );
387
+
388
+ this.layouts.set(
389
+ "docs",
390
+ (ctx) => `<!DOCTYPE html>
391
+ <html lang="en">
392
+ <head>
393
+ <meta charset="UTF-8">
394
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
395
+ <title>${ctx.page.frontmatter.title || "Docs"} | ${ctx.site.title}</title>
396
+ <meta name="description" content="${ctx.page.frontmatter.description || ctx.site.description}">
397
+ <link rel="stylesheet" href="${ctx.site.baseUrl}style.css">
398
+ </head>
399
+ <body class="docs-page">
400
+ <aside class="sidebar">
401
+ <div class="logo">
402
+ <a href="${ctx.site.baseUrl}">Bueno</a>
403
+ </div>
404
+ <nav class="sidebar-nav">
405
+ <a href="${ctx.site.baseUrl}docs/getting-started">Getting Started</a>
406
+ <a href="${ctx.site.baseUrl}docs/router">Router</a>
407
+ <a href="${ctx.site.baseUrl}docs/context">Context</a>
408
+ <a href="${ctx.site.baseUrl}docs/middleware">Middleware</a>
409
+ <a href="${ctx.site.baseUrl}docs/validation">Validation</a>
410
+ <a href="${ctx.site.baseUrl}docs/database">Database</a>
411
+ <a href="${ctx.site.baseUrl}docs/rpc">RPC Client</a>
412
+ </nav>
413
+ </aside>
414
+ <main class="content">
415
+ <article>
416
+ <h1>${ctx.page.frontmatter.title || ""}</h1>
417
+ ${ctx.content}
418
+ </article>
419
+ </main>
420
+ </body>
421
+ </html>`,
422
+ );
423
+ }
424
+
425
+ registerLayout(name: string, render: (ctx: LayoutContext) => string): void {
426
+ this.layouts.set(name, render);
427
+ }
428
+
429
+ async loadContent(): Promise<void> {
430
+ const contentDir = this.config.contentDir;
431
+
432
+ try {
433
+ const fs = require("node:fs");
434
+ if (!fs.existsSync(contentDir)) {
435
+ console.warn(`Content directory not found: ${contentDir}`);
436
+ return;
437
+ }
438
+ } catch {
439
+ console.warn(`Content directory not found: ${contentDir}`);
440
+ return;
441
+ }
442
+
443
+ await this.scanDirectory(contentDir, "");
444
+ }
445
+
446
+ private async scanDirectory(
447
+ dirPath: string,
448
+ relativePath: string,
449
+ ): Promise<void> {
450
+ const glob = new Bun.Glob("**/*.{md,markdown}");
451
+
452
+ for await (const file of glob.scan(dirPath)) {
453
+ const filePath = `${dirPath}/${file}`;
454
+ await this.processFile(filePath, file);
455
+ }
456
+ }
457
+
458
+ private async processFile(
459
+ filePath: string,
460
+ relativePath: string,
461
+ ): Promise<void> {
462
+ const file = Bun.file(filePath);
463
+ const content = await file.text();
464
+
465
+ const { frontmatter, body } = parseFrontmatter(content);
466
+ const html = parseMarkdown(body);
467
+
468
+ let pagePath = relativePath
469
+ .replace(/\.(md|markdown)$/, "")
470
+ .replace(/\\/g, "/");
471
+
472
+ if (pagePath.endsWith("index")) {
473
+ pagePath = pagePath.replace(/\/?index$/, "") || "/";
474
+ }
475
+
476
+ if (!pagePath.startsWith("/")) {
477
+ pagePath = `/${pagePath}`;
478
+ }
479
+
480
+ const page: Page = {
481
+ path: pagePath,
482
+ content: body,
483
+ html,
484
+ frontmatter,
485
+ raw: content,
486
+ };
487
+
488
+ this.pages.set(pagePath, page);
489
+ }
490
+
491
+ renderPage(page: Page): string {
492
+ const layoutName =
493
+ page.frontmatter.layout || this.config.defaultLayout || "default";
494
+ const layout = this.layouts.get(layoutName);
495
+
496
+ if (!layout) {
497
+ console.warn(`Layout not found: ${layoutName}, using default`);
498
+ return this.layouts.get("default")?.({
499
+ content: page.html,
500
+ page,
501
+ site: this.siteConfig,
502
+ }) ?? "";
503
+ }
504
+
505
+ return layout({
506
+ title: page.frontmatter.title,
507
+ description: page.frontmatter.description,
508
+ content: page.html,
509
+ page,
510
+ site: this.siteConfig,
511
+ });
512
+ }
513
+
514
+ async build(): Promise<void> {
515
+ await this.loadContent();
516
+
517
+ const outputDir = this.config.outputDir;
518
+
519
+ await Bun.$`mkdir -p ${outputDir}`.quiet();
520
+
521
+ for (const [path, page] of this.pages) {
522
+ const html = this.renderPage(page);
523
+ const outputPath =
524
+ path === "/"
525
+ ? `${outputDir}/index.html`
526
+ : `${outputDir}${path}/index.html`;
527
+
528
+ const outputDirPath = outputPath.replace(/\/index\.html$/, "");
529
+ await Bun.$`mkdir -p ${outputDirPath}`.quiet();
530
+
531
+ await Bun.write(outputPath, html);
532
+ console.log(`Generated: ${path}`);
533
+ }
534
+
535
+ if (this.config.publicDir) {
536
+ await this.copyPublicDir();
537
+ }
538
+
539
+ console.log(`\nBuild complete: ${this.pages.size} pages generated`);
540
+ }
541
+
542
+ private async copyPublicDir(): Promise<void> {
543
+ const publicDir = this.config.publicDir;
544
+ const outputDir = this.config.outputDir;
545
+
546
+ try {
547
+ const glob = new Bun.Glob("**/*");
548
+
549
+ for await (const file of glob.scan(publicDir!)) {
550
+ const srcPath = `${publicDir}/${file}`;
551
+ const destPath = `${outputDir}/${file}`;
552
+
553
+ const destDir = destPath.substring(0, destPath.lastIndexOf("/"));
554
+ await Bun.$`mkdir -p ${destDir}`.quiet();
555
+
556
+ await Bun.write(destPath, Bun.file(srcPath));
557
+ }
558
+ } catch (e) {
559
+ console.warn(`Failed to copy public directory: ${e}`);
560
+ }
561
+ }
562
+
563
+ createRouter(): Router {
564
+ for (const [path, page] of this.pages) {
565
+ this.router.get(path, (ctx) => {
566
+ const html = this.renderPage(page);
567
+ return (ctx as Context).html(html);
568
+ });
569
+ }
570
+
571
+ if (this.config.publicDir) {
572
+ this.router.all("/*", async (ctx) => {
573
+ const context = ctx as Context;
574
+ const publicPath = `${this.config.publicDir}${context.path}`;
575
+ const file = Bun.file(publicPath);
576
+
577
+ if (await file.exists()) {
578
+ return new Response(file);
579
+ }
580
+
581
+ return context.notFound();
582
+ });
583
+ }
584
+
585
+ return this.router;
586
+ }
587
+
588
+ async serve(port = 3000): Promise<void> {
589
+ await this.loadContent();
590
+ const router = this.createRouter();
591
+
592
+ Bun.serve({
593
+ port,
594
+ fetch: async (request: Request): Promise<Response> => {
595
+ const url = new URL(request.url);
596
+ const match = router.match(request.method as "GET", url.pathname);
597
+
598
+ if (!match) {
599
+ return new Response("Not Found", { status: 404 });
600
+ }
601
+
602
+ const context = new Context(request, match.params);
603
+
604
+ if (match.middleware && match.middleware.length > 0) {
605
+ console.warn("Middleware not yet supported in SSG dev server");
606
+ }
607
+
608
+ return match.handler(context) as Response;
609
+ },
610
+ });
611
+
612
+ console.log(`SSG dev server running at http://localhost:${port}`);
613
+ }
614
+
615
+ getPages(): Page[] {
616
+ return Array.from(this.pages.values());
617
+ }
618
+
619
+ getPage(path: string): Page | undefined {
620
+ return this.pages.get(path);
621
+ }
622
+ }
623
+
624
+ export function createSSG(
625
+ config: SSGConfig,
626
+ siteConfig?: Partial<SiteConfig>,
627
+ ): SSG {
628
+ return new SSG(config, siteConfig);
629
+ }
630
+
631
+ export { parseMarkdown, parseFrontmatter };