@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,699 @@
1
+ /**
2
+ * Server-Side Rendering (SSR) Implementation
3
+ *
4
+ * Provides a unified SSR system that supports:
5
+ * - React, Vue, Svelte, and Solid frameworks
6
+ * - Streaming SSR for faster TTFB
7
+ * - Client-side hydration
8
+ * - Server-side data fetching
9
+ * - Head management (title, meta, links)
10
+ */
11
+
12
+ import { createLogger, type Logger } from "../logger/index.js";
13
+ import type {
14
+ SSRConfig,
15
+ PartialSSRConfig,
16
+ SSRContext,
17
+ SSRElement,
18
+ SSRPage,
19
+ RenderResult,
20
+ SSRHydrationData,
21
+ SSRRenderOptions,
22
+ BuildManifest,
23
+ FrontendFramework,
24
+ FrameworkSSRRenderer,
25
+ PreloadLink,
26
+ } from "./types.js";
27
+ import { createReactSSRRenderer, type ReactSSRRenderer } from "./ssr/react.js";
28
+ import { createVueSSRRenderer, type VueSSRRenderer } from "./ssr/vue.js";
29
+ import { createSvelteSSRRenderer, type SvelteSSRRenderer } from "./ssr/svelte.js";
30
+ import { createSolidSSRRenderer, type SolidSSRRenderer } from "./ssr/solid.js";
31
+
32
+ // ============= Constants =============
33
+
34
+ const DEFAULT_MAX_TIMEOUT = 5000;
35
+ const DEFAULT_STREAMING = true;
36
+ const DEFAULT_BUFFER_INITIAL_STREAM = true;
37
+
38
+ // ============= SSR Renderer Class =============
39
+
40
+ /**
41
+ * Main SSR Renderer class
42
+ *
43
+ * Provides server-side rendering for all supported frameworks
44
+ * with streaming support and client hydration.
45
+ */
46
+ export class SSRRenderer {
47
+ private config: SSRConfig;
48
+ private logger: Logger;
49
+ private frameworkRenderer: FrameworkSSRRenderer | null = null;
50
+ private pageCache: Map<string, SSRPage> = new Map();
51
+
52
+ constructor(config: PartialSSRConfig) {
53
+ this.config = this.normalizeConfig(config);
54
+ this.logger = createLogger({
55
+ level: "debug",
56
+ pretty: true,
57
+ context: { component: "SSRRenderer" },
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Normalize partial config to full config with defaults
63
+ */
64
+ private normalizeConfig(config: PartialSSRConfig): SSRConfig {
65
+ return {
66
+ entry: config.entry,
67
+ clientEntry: config.clientEntry,
68
+ clientManifest: config.clientManifest,
69
+ streaming: config.streaming ?? DEFAULT_STREAMING,
70
+ maxTimeout: config.maxTimeout ?? DEFAULT_MAX_TIMEOUT,
71
+ bufferInitialStream: config.bufferInitialStream ?? DEFAULT_BUFFER_INITIAL_STREAM,
72
+ framework: config.framework,
73
+ rootDir: config.rootDir,
74
+ template: config.template,
75
+ templateFn: config.templateFn,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Initialize the framework-specific renderer
81
+ */
82
+ async init(): Promise<void> {
83
+ this.frameworkRenderer = await this.createFrameworkRenderer(this.config.framework);
84
+ this.logger.info(`SSR initialized for framework: ${this.config.framework}`);
85
+ }
86
+
87
+ /**
88
+ * Create framework-specific renderer
89
+ */
90
+ private async createFrameworkRenderer(framework: FrontendFramework): Promise<FrameworkSSRRenderer> {
91
+ switch (framework) {
92
+ case "react":
93
+ const reactRenderer = createReactSSRRenderer();
94
+ await reactRenderer.init();
95
+ return reactRenderer;
96
+ case "vue":
97
+ const vueRenderer = createVueSSRRenderer();
98
+ await vueRenderer.init();
99
+ return vueRenderer;
100
+ case "svelte":
101
+ const svelteRenderer = createSvelteSSRRenderer();
102
+ await svelteRenderer.init();
103
+ return svelteRenderer;
104
+ case "solid":
105
+ const solidRenderer = createSolidSSRRenderer();
106
+ await solidRenderer.init();
107
+ return solidRenderer;
108
+ default:
109
+ throw new Error(`Unsupported framework: ${framework}`);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Render a page to HTML
115
+ */
116
+ async render(url: string, request: Request): Promise<RenderResult> {
117
+ if (!this.frameworkRenderer) {
118
+ await this.init();
119
+ }
120
+
121
+ const context = this.createContext(url, request);
122
+ const startTime = Date.now();
123
+
124
+ try {
125
+ // Load the page module
126
+ const page = await this.loadPage(url);
127
+
128
+ // Run server-side data fetching if available
129
+ if (page.getServerSideProps) {
130
+ this.logger.debug(`Fetching server-side props for: ${url}`);
131
+ const props = await page.getServerSideProps(context);
132
+ context.data = { ...context.data, ...props };
133
+ }
134
+
135
+ // Render the page
136
+ const component = this.frameworkRenderer!.createComponent(page, context);
137
+ const html = await this.frameworkRenderer!.renderToString(component, context);
138
+
139
+ // Get head elements
140
+ const headElements = this.frameworkRenderer!.getHeadElements(context);
141
+ const head = this.renderHeadElements(headElements);
142
+
143
+ // Collect loaded modules
144
+ const modules = Array.from(context.modules);
145
+
146
+ const duration = Date.now() - startTime;
147
+ this.logger.debug(`Rendered ${url} in ${duration}ms`);
148
+
149
+ return {
150
+ html,
151
+ head,
152
+ body: "",
153
+ data: context.data,
154
+ modules,
155
+ status: context.status,
156
+ };
157
+ } catch (error) {
158
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
159
+ this.logger.error(`SSR render failed for ${url}:`, error);
160
+
161
+ return {
162
+ html: this.renderErrorPage(error),
163
+ head: "<title>Error</title>",
164
+ body: "",
165
+ data: {},
166
+ modules: [],
167
+ status: 500,
168
+ };
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Render a page to a stream
174
+ */
175
+ renderToStream(url: string, request: Request): ReadableStream<Uint8Array> {
176
+ const encoder = new TextEncoder();
177
+ let context: SSRContext;
178
+ let frameworkRenderer = this.frameworkRenderer;
179
+
180
+ return new ReadableStream<Uint8Array>({
181
+ start: async (controller) => {
182
+ try {
183
+ if (!frameworkRenderer) {
184
+ await this.init();
185
+ frameworkRenderer = this.frameworkRenderer;
186
+ }
187
+
188
+ context = this.createContext(url, request);
189
+
190
+ // Load the page module
191
+ const page = await this.loadPage(url);
192
+
193
+ // Run server-side data fetching if available
194
+ if (page.getServerSideProps) {
195
+ const props = await page.getServerSideProps(context);
196
+ context.data = { ...context.data, ...props };
197
+ }
198
+
199
+ // Send HTML preamble
200
+ const preamble = this.renderPreamble(context);
201
+ controller.enqueue(encoder.encode(preamble));
202
+
203
+ // Create component and render to stream
204
+ const component = frameworkRenderer!.createComponent(page, context);
205
+ const htmlStream = frameworkRenderer!.renderToStream(component, context);
206
+
207
+ const reader = htmlStream.getReader();
208
+ while (true) {
209
+ const { done, value } = await reader.read();
210
+ if (done) break;
211
+ controller.enqueue(value);
212
+ }
213
+
214
+ // Send HTML footer with hydration data
215
+ const footer = this.renderFooter(context);
216
+ controller.enqueue(encoder.encode(footer));
217
+ controller.close();
218
+ } catch (error) {
219
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
220
+ this.logger.error(`SSR stream failed for ${url}:`, error);
221
+ controller.enqueue(encoder.encode(this.renderErrorPage(error)));
222
+ controller.close();
223
+ }
224
+ },
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Render with options
230
+ */
231
+ async renderWithOptions(options: SSRRenderOptions): Promise<RenderResult> {
232
+ const { url, request, params = {}, props = {}, skipStreaming = false } = options;
233
+
234
+ if (!skipStreaming && this.config.streaming) {
235
+ // For streaming, we need to buffer the result
236
+ const stream = this.renderToStream(url, request);
237
+ const reader = stream.getReader();
238
+ const chunks: Uint8Array[] = [];
239
+
240
+ while (true) {
241
+ const { done, value } = await reader.read();
242
+ if (done) break;
243
+ chunks.push(value);
244
+ }
245
+
246
+ const html = new TextDecoder().decode(
247
+ chunks.reduce((acc, chunk) => {
248
+ const combined = new Uint8Array(acc.length + chunk.length);
249
+ combined.set(acc);
250
+ combined.set(chunk, acc.length);
251
+ return combined;
252
+ }, new Uint8Array())
253
+ );
254
+
255
+ return {
256
+ html,
257
+ head: "",
258
+ body: "",
259
+ data: props,
260
+ modules: [],
261
+ status: 200,
262
+ };
263
+ }
264
+
265
+ return this.render(url, request);
266
+ }
267
+
268
+ /**
269
+ * Get client hydration script
270
+ */
271
+ getHydrationScript(): string {
272
+ const framework = this.config.framework;
273
+ const clientEntry = this.config.clientEntry;
274
+
275
+ // Framework-specific hydration code
276
+ const hydrationScripts: Record<FrontendFramework, string> = {
277
+ react: `
278
+ (function() {
279
+ const data = JSON.parse(document.getElementById('__DATA__').textContent);
280
+ window.__SSR_DATA__ = data;
281
+ import('${clientEntry}').then(({ hydrate }) => {
282
+ hydrate(document.getElementById('app'), data);
283
+ });
284
+ })();`,
285
+ vue: `
286
+ (function() {
287
+ const data = JSON.parse(document.getElementById('__DATA__').textContent);
288
+ window.__SSR_DATA__ = data;
289
+ import('${clientEntry}').then(({ createApp }) => {
290
+ createApp(data).mount('#app', true);
291
+ });
292
+ })();`,
293
+ svelte: `
294
+ (function() {
295
+ const data = JSON.parse(document.getElementById('__DATA__').textContent);
296
+ window.__SSR_DATA__ = data;
297
+ import('${clientEntry}').then(({ mount }) => {
298
+ mount(document.getElementById('app'), { props: data.props, hydrate: true });
299
+ });
300
+ })();`,
301
+ solid: `
302
+ (function() {
303
+ const data = JSON.parse(document.getElementById('__DATA__').textContent);
304
+ window.__SSR_DATA__ = data;
305
+ import('${clientEntry}').then({ hydrate } => {
306
+ hydrate(document.getElementById('app'));
307
+ });
308
+ })();`,
309
+ };
310
+
311
+ return `<script type="module">${hydrationScripts[framework]}</script>`;
312
+ }
313
+
314
+ /**
315
+ * Get preload links for modules
316
+ */
317
+ getPreloadLinks(modules: string[]): string {
318
+ const manifest = this.config.clientManifest;
319
+ const links: string[] = [];
320
+ const seen = new Set<string>();
321
+
322
+ for (const module of modules) {
323
+ if (seen.has(module)) continue;
324
+ seen.add(module);
325
+
326
+ const fileInfo = manifest.files[module];
327
+ if (!fileInfo) continue;
328
+
329
+ if (fileInfo.type === "js") {
330
+ links.push(`<link rel="modulepreload" href="/${module}">`);
331
+ }
332
+
333
+ // Also preload dependencies
334
+ if (fileInfo.imports) {
335
+ for (const dep of fileInfo.imports) {
336
+ if (!seen.has(dep)) {
337
+ links.push(`<link rel="modulepreload" href="/${dep}">`);
338
+ seen.add(dep);
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ return links.join("\n");
345
+ }
346
+
347
+ /**
348
+ * Get CSS links for modules
349
+ */
350
+ getCSSLinks(modules: string[]): string {
351
+ const manifest = this.config.clientManifest;
352
+ const links: string[] = [];
353
+ const seen = new Set<string>();
354
+
355
+ // Get CSS for each entry point
356
+ for (const [entry, cssFiles] of Object.entries(manifest.css)) {
357
+ for (const cssFile of cssFiles) {
358
+ if (!seen.has(cssFile)) {
359
+ links.push(`<link rel="stylesheet" href="/${cssFile}">`);
360
+ seen.add(cssFile);
361
+ }
362
+ }
363
+ }
364
+
365
+ // Also check module-specific CSS
366
+ for (const module of modules) {
367
+ const fileInfo = manifest.files[module];
368
+ if (fileInfo?.type === "css" && !seen.has(module)) {
369
+ links.push(`<link rel="stylesheet" href="/${module}">`);
370
+ seen.add(module);
371
+ }
372
+ }
373
+
374
+ return links.join("\n");
375
+ }
376
+
377
+ /**
378
+ * Create SSR context
379
+ */
380
+ private createContext(url: string, request: Request): SSRContext {
381
+ const parsedUrl = new URL(url, request.url);
382
+
383
+ return {
384
+ url: parsedUrl.href,
385
+ request,
386
+ headers: new Headers(),
387
+ status: 200,
388
+ head: [],
389
+ body: [],
390
+ data: {},
391
+ modules: new Set(),
392
+ pathname: parsedUrl.pathname,
393
+ query: parsedUrl.searchParams,
394
+ params: {},
395
+ };
396
+ }
397
+
398
+ /**
399
+ * Load page module
400
+ */
401
+ private async loadPage(url: string): Promise<SSRPage> {
402
+ // Check cache first
403
+ const cached = this.pageCache.get(url);
404
+ if (cached) return cached;
405
+
406
+ // Dynamic import of the server entry
407
+ try {
408
+ const entryPath = this.config.entry.startsWith("/")
409
+ ? this.config.entry
410
+ : `${this.config.rootDir || "."}/${this.config.entry}`;
411
+
412
+ const module = await import(entryPath);
413
+ const page: SSRPage = module.default || module;
414
+
415
+ // Cache for future requests
416
+ this.pageCache.set(url, page);
417
+
418
+ return page;
419
+ } catch (error) {
420
+ this.logger.error(`Failed to load page: ${url}`, error);
421
+ throw new Error(`Page not found: ${url}`);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Render head elements to string
427
+ */
428
+ private renderHeadElements(elements: SSRElement[]): string {
429
+ return elements.map(this.ssrElementToString).join("\n");
430
+ }
431
+
432
+ /**
433
+ * Convert SSR element to HTML string
434
+ */
435
+ private ssrElementToString(element: SSRElement): string {
436
+ if (element.tag === "#text") {
437
+ return this.escapeHtml(element.innerHTML || "");
438
+ }
439
+
440
+ const attrs = Object.entries(element.attrs)
441
+ .map(([key, value]) => `${key}="${this.escapeHtml(value)}"`)
442
+ .join(" ");
443
+
444
+ const openTag = attrs ? `<${element.tag} ${attrs}>` : `<${element.tag}>`;
445
+
446
+ if (element.innerHTML) {
447
+ return `${openTag}${element.innerHTML}</${element.tag}>`;
448
+ }
449
+
450
+ if (element.children && element.children.length > 0) {
451
+ const children = element.children.map(this.ssrElementToString.bind(this)).join("");
452
+ return `${openTag}${children}</${element.tag}>`;
453
+ }
454
+
455
+ // Self-closing tags
456
+ const voidElements = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
457
+ if (voidElements.includes(element.tag)) {
458
+ return attrs ? `<${element.tag} ${attrs}>` : `<${element.tag}>`;
459
+ }
460
+
461
+ return `${openTag}</${element.tag}>`;
462
+ }
463
+
464
+ /**
465
+ * Escape HTML special characters
466
+ */
467
+ private escapeHtml(str: string): string {
468
+ return str
469
+ .replace(/&/g, "\x26amp;")
470
+ .replace(/</g, "\x26lt;")
471
+ .replace(/>/g, "\x26gt;")
472
+ .replace(/"/g, "\x26quot;")
473
+ .replace(/'/g, "&#39;");
474
+ }
475
+
476
+ /**
477
+ * Render HTML preamble (opening tags)
478
+ */
479
+ private renderPreamble(context: SSRContext): string {
480
+ const manifest = this.config.clientManifest;
481
+ const preloadLinks = this.getPreloadLinks([]);
482
+ const cssLinks = this.getCSSLinks([]);
483
+
484
+ return `<!DOCTYPE html>
485
+ <html>
486
+ <head>
487
+ <meta charset="utf-8">
488
+ <meta name="viewport" content="width=device-width, initial-scale=1">
489
+ ${preloadLinks}
490
+ ${cssLinks}
491
+ </head>
492
+ <body>
493
+ <div id="app">`;
494
+ }
495
+
496
+ /**
497
+ * Render HTML footer (closing tags and scripts)
498
+ */
499
+ private renderFooter(context: SSRContext): string {
500
+ const hydrationData: SSRHydrationData = {
501
+ props: context.data,
502
+ url: context.url,
503
+ params: context.params,
504
+ query: Object.fromEntries(context.query),
505
+ framework: this.config.framework,
506
+ };
507
+
508
+ const dataScript = `<script type="application/json" id="__DATA__">${JSON.stringify(hydrationData)}</script>`;
509
+ const hydrationScript = this.getHydrationScript();
510
+
511
+ return `</div>
512
+ ${dataScript}
513
+ ${hydrationScript}
514
+ </body>
515
+ </html>`;
516
+ }
517
+
518
+ /**
519
+ * Render error page
520
+ */
521
+ private renderErrorPage(error: unknown): string {
522
+ const message = error instanceof Error ? error.message : "Unknown error";
523
+ const stack = error instanceof Error && process.env.NODE_ENV !== "production"
524
+ ? `<pre>${this.escapeHtml(error.stack || "")}</pre>`
525
+ : "";
526
+
527
+ return `<!DOCTYPE html>
528
+ <html>
529
+ <head>
530
+ <title>Error</title>
531
+ <style>
532
+ body { font-family: system-ui, sans-serif; padding: 2rem; }
533
+ h1 { color: #dc2626; }
534
+ pre { background: #f5f5f5; padding: 1rem; overflow: auto; }
535
+ </style>
536
+ </head>
537
+ <body>
538
+ <h1>Server Error</h1>
539
+ <p>${this.escapeHtml(message)}</p>
540
+ ${stack}
541
+ </body>
542
+ </html>`;
543
+ }
544
+
545
+ /**
546
+ * Clear page cache
547
+ */
548
+ clearCache(): void {
549
+ this.pageCache.clear();
550
+ this.logger.debug("Page cache cleared");
551
+ }
552
+
553
+ /**
554
+ * Get configuration
555
+ */
556
+ getConfig(): SSRConfig {
557
+ return { ...this.config };
558
+ }
559
+
560
+ /**
561
+ * Check if streaming is enabled
562
+ */
563
+ isStreamingEnabled(): boolean {
564
+ return this.config.streaming;
565
+ }
566
+
567
+ /**
568
+ * Get framework
569
+ */
570
+ getFramework(): FrontendFramework {
571
+ return this.config.framework;
572
+ }
573
+ }
574
+
575
+ // ============= Factory Function =============
576
+
577
+ /**
578
+ * Create an SSR renderer
579
+ */
580
+ export function createSSRRenderer(config: PartialSSRConfig): SSRRenderer {
581
+ return new SSRRenderer(config);
582
+ }
583
+
584
+ // ============= Utility Functions =============
585
+
586
+ /**
587
+ * Create SSR context from request
588
+ */
589
+ export function createSSRContext(
590
+ request: Request,
591
+ params: Record<string, string> = {}
592
+ ): SSRContext {
593
+ const url = new URL(request.url);
594
+
595
+ return {
596
+ url: request.url,
597
+ request,
598
+ headers: new Headers(),
599
+ status: 200,
600
+ head: [],
601
+ body: [],
602
+ data: {},
603
+ modules: new Set(),
604
+ pathname: url.pathname,
605
+ query: url.searchParams,
606
+ params,
607
+ };
608
+ }
609
+
610
+ /**
611
+ * Serialize data for hydration
612
+ */
613
+ export function serializeHydrationData(data: SSRHydrationData): string {
614
+ return JSON.stringify(data);
615
+ }
616
+
617
+ /**
618
+ * Deserialize hydration data
619
+ */
620
+ export function deserializeHydrationData(json: string): SSRHydrationData {
621
+ return JSON.parse(json);
622
+ }
623
+
624
+ /**
625
+ * Generate HTML template
626
+ */
627
+ export function generateHTML(options: {
628
+ head: string;
629
+ body: string;
630
+ data: Record<string, unknown>;
631
+ scripts: string[];
632
+ styles: string[];
633
+ }): string {
634
+ const { head, body, data, scripts, styles } = options;
635
+
636
+ return `<!DOCTYPE html>
637
+ <html>
638
+ <head>
639
+ <meta charset="utf-8">
640
+ <meta name="viewport" content="width=device-width, initial-scale=1">
641
+ ${styles.join("\n")}
642
+ ${head}
643
+ </head>
644
+ <body>
645
+ <div id="app">${body}</div>
646
+ <script type="application/json" id="__DATA__">${JSON.stringify(data)}</script>
647
+ ${scripts.join("\n")}
648
+ </body>
649
+ </html>`;
650
+ }
651
+
652
+ /**
653
+ * Create preload link tag
654
+ */
655
+ export function createPreloadLink(link: PreloadLink): string {
656
+ const attrs = Object.entries(link.attrs || {})
657
+ .map(([key, value]) => `${key}="${value}"`)
658
+ .join(" ");
659
+
660
+ if (link.as) {
661
+ return `<link rel="${link.rel}" href="${link.href}" as="${link.as}" ${attrs}>`;
662
+ }
663
+
664
+ return `<link rel="${link.rel}" href="${link.href}" ${attrs}>`;
665
+ }
666
+
667
+ /**
668
+ * Merge multiple head element arrays
669
+ */
670
+ export function mergeHeadElements(...arrays: SSRElement[][]): SSRElement[] {
671
+ const seen = new Set<string>();
672
+ const result: SSRElement[] = [];
673
+
674
+ for (const arr of arrays) {
675
+ for (const element of arr) {
676
+ // Create a key based on tag and identifying attributes
677
+ const key = element.tag === "title"
678
+ ? "title"
679
+ : element.tag === "meta" && element.attrs.name
680
+ ? `meta:${element.attrs.name}`
681
+ : element.tag === "meta" && element.attrs.property
682
+ ? `meta:${element.attrs.property}`
683
+ : JSON.stringify(element);
684
+
685
+ if (!seen.has(key)) {
686
+ seen.add(key);
687
+ result.push(element);
688
+ }
689
+ }
690
+ }
691
+
692
+ return result;
693
+ }
694
+
695
+ // Re-export framework renderers
696
+ export { createReactSSRRenderer, type ReactSSRRenderer } from "./ssr/react.js";
697
+ export { createVueSSRRenderer, type VueSSRRenderer } from "./ssr/vue.js";
698
+ export { createSvelteSSRRenderer, type SvelteSSRRenderer } from "./ssr/svelte.js";
699
+ export { createSolidSSRRenderer, type SolidSSRRenderer } from "./ssr/solid.js";