@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.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
package/src/ssg/index.ts
ADDED
|
@@ -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, "&")
|
|
115
|
+
.replace(/</g, "<")
|
|
116
|
+
.replace(/>/g, ">");
|
|
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>© ${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 };
|