@useavalon/avalon 0.1.10 → 0.1.12

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 (250) hide show
  1. package/README.md +54 -54
  2. package/dist/mod.js +1 -0
  3. package/dist/src/build/integration-bundler-plugin.js +1 -0
  4. package/dist/src/build/integration-config.js +1 -0
  5. package/dist/src/build/integration-detection-plugin.js +1 -0
  6. package/dist/src/build/integration-resolver-plugin.js +1 -0
  7. package/dist/src/build/island-manifest.js +1 -0
  8. package/dist/src/build/island-types-generator.js +5 -0
  9. package/dist/src/build/mdx-island-transform.js +2 -0
  10. package/dist/src/build/mdx-plugin.js +1 -0
  11. package/dist/src/build/page-island-transform.js +3 -0
  12. package/dist/src/build/prop-extractors/index.js +1 -0
  13. package/dist/src/build/prop-extractors/lit.js +1 -0
  14. package/dist/src/build/prop-extractors/qwik.js +1 -0
  15. package/dist/src/build/prop-extractors/solid.js +1 -0
  16. package/dist/src/build/prop-extractors/svelte.js +1 -0
  17. package/dist/src/build/prop-extractors/vue.js +1 -0
  18. package/dist/src/build/sidecar-file-manager.js +1 -0
  19. package/dist/src/build/sidecar-renderer.js +6 -0
  20. package/dist/src/client/adapters/index.js +1 -0
  21. package/dist/src/client/components.js +1 -0
  22. package/dist/src/client/css-hmr-handler.js +1 -0
  23. package/dist/src/client/framework-adapter.js +13 -0
  24. package/dist/src/client/hmr-coordinator.js +1 -0
  25. package/dist/src/client/hmr-error-overlay.js +214 -0
  26. package/dist/src/client/main.js +39 -0
  27. package/{src → dist/src}/client/types/framework-runtime.d.ts +68 -68
  28. package/{src → dist/src}/client/types/vite-hmr.d.ts +46 -46
  29. package/dist/src/client/types/vite-virtual-modules.d.ts +70 -0
  30. package/dist/src/components/Image.js +1 -0
  31. package/dist/src/components/IslandErrorBoundary.js +1 -0
  32. package/dist/src/components/LayoutDataErrorBoundary.js +1 -0
  33. package/dist/src/components/LayoutErrorBoundary.js +1 -0
  34. package/dist/src/components/PersistentIsland.js +1 -0
  35. package/dist/src/components/StreamingErrorBoundary.js +1 -0
  36. package/dist/src/components/StreamingLayout.js +29 -0
  37. package/dist/src/core/components/component-analyzer.js +1 -0
  38. package/dist/src/core/components/component-detection.js +5 -0
  39. package/dist/src/core/components/enhanced-framework-detector.js +1 -0
  40. package/dist/src/core/components/framework-registry.js +1 -0
  41. package/dist/src/core/content/mdx-processor.js +1 -0
  42. package/dist/src/core/integrations/index.js +1 -0
  43. package/dist/src/core/integrations/loader.js +1 -0
  44. package/dist/src/core/integrations/registry.js +1 -0
  45. package/dist/src/core/islands/island-persistence.js +1 -0
  46. package/dist/src/core/islands/island-state-serializer.js +1 -0
  47. package/dist/src/core/islands/persistent-island-context.js +1 -0
  48. package/dist/src/core/islands/use-persistent-state.js +1 -0
  49. package/dist/src/core/layout/enhanced-layout-resolver.js +1 -0
  50. package/dist/src/core/layout/layout-cache-manager.js +1 -0
  51. package/dist/src/core/layout/layout-composer.js +1 -0
  52. package/dist/src/core/layout/layout-data-loader.js +1 -0
  53. package/dist/src/core/layout/layout-discovery.js +1 -0
  54. package/dist/src/core/layout/layout-matcher.js +1 -0
  55. package/dist/src/core/layout/layout-types.js +1 -0
  56. package/dist/src/core/modules/framework-module-resolver.js +1 -0
  57. package/dist/src/islands/component-analysis.js +1 -0
  58. package/dist/src/islands/css-utils.js +17 -0
  59. package/dist/src/islands/discovery/index.js +1 -0
  60. package/dist/src/islands/discovery/registry.js +1 -0
  61. package/dist/src/islands/discovery/resolver.js +2 -0
  62. package/dist/src/islands/discovery/scanner.js +1 -0
  63. package/dist/src/islands/discovery/types.js +1 -0
  64. package/dist/src/islands/discovery/validator.js +18 -0
  65. package/dist/src/islands/discovery/watcher.js +1 -0
  66. package/dist/src/islands/framework-detection.js +1 -0
  67. package/dist/src/islands/integration-loader.js +1 -0
  68. package/dist/src/islands/island.js +1 -0
  69. package/dist/src/islands/render-cache.js +1 -0
  70. package/dist/src/islands/types.js +1 -0
  71. package/dist/src/islands/universal-css-collector.js +5 -0
  72. package/dist/src/islands/universal-head-collector.js +2 -0
  73. package/{src → dist/src}/layout-system.d.ts +592 -592
  74. package/dist/src/layout-system.js +1 -0
  75. package/dist/src/middleware/discovery.js +1 -0
  76. package/dist/src/middleware/executor.js +1 -0
  77. package/dist/src/middleware/index.js +1 -0
  78. package/dist/src/middleware/types.js +1 -0
  79. package/dist/src/nitro/build-config.js +1 -0
  80. package/dist/src/nitro/config.js +1 -0
  81. package/dist/src/nitro/error-handler.js +198 -0
  82. package/dist/src/nitro/index.js +1 -0
  83. package/dist/src/nitro/island-manifest.js +2 -0
  84. package/dist/src/nitro/middleware-adapter.js +1 -0
  85. package/dist/src/nitro/renderer.js +183 -0
  86. package/dist/src/nitro/route-discovery.js +1 -0
  87. package/dist/src/nitro/types.js +1 -0
  88. package/dist/src/render/collect-css.js +3 -0
  89. package/{src/render/error-pages.ts → dist/src/render/error-pages.js} +7 -38
  90. package/dist/src/render/isolated-ssr-renderer.js +1 -0
  91. package/dist/src/render/ssr.js +90 -0
  92. package/dist/src/schemas/api.js +1 -0
  93. package/dist/src/schemas/core.js +1 -0
  94. package/dist/src/schemas/index.js +1 -0
  95. package/dist/src/schemas/layout.js +1 -0
  96. package/dist/src/schemas/routing/index.js +1 -0
  97. package/dist/src/schemas/routing.js +1 -0
  98. package/dist/src/types/as-island.js +1 -0
  99. package/{src → dist/src}/types/image.d.ts +106 -106
  100. package/{src → dist/src}/types/index.d.ts +22 -22
  101. package/{src → dist/src}/types/island-jsx.d.ts +33 -33
  102. package/{src → dist/src}/types/island-prop.d.ts +20 -20
  103. package/dist/src/types/layout.js +1 -0
  104. package/{src → dist/src}/types/mdx.d.ts +6 -6
  105. package/dist/src/types/routing.js +1 -0
  106. package/dist/src/types/types.js +1 -0
  107. package/{src → dist/src}/types/urlpattern.d.ts +49 -49
  108. package/{src → dist/src}/types/vite-env.d.ts +11 -11
  109. package/dist/src/utils/dev-logger.js +12 -0
  110. package/dist/src/utils/fs.js +1 -0
  111. package/dist/src/vite-plugin/auto-discover.js +1 -0
  112. package/dist/src/vite-plugin/config.js +1 -0
  113. package/dist/src/vite-plugin/errors.js +1 -0
  114. package/dist/src/vite-plugin/image-optimization.js +45 -0
  115. package/dist/src/vite-plugin/integration-activator.js +1 -0
  116. package/dist/src/vite-plugin/island-sidecar-plugin.js +1 -0
  117. package/dist/src/vite-plugin/module-discovery.js +1 -0
  118. package/dist/src/vite-plugin/nitro-integration.js +42 -0
  119. package/dist/src/vite-plugin/plugin.js +1 -0
  120. package/dist/src/vite-plugin/types.js +1 -0
  121. package/dist/src/vite-plugin/validation.js +2 -0
  122. package/package.json +57 -26
  123. package/mod.ts +0 -302
  124. package/src/build/integration-bundler-plugin.ts +0 -116
  125. package/src/build/integration-config.ts +0 -168
  126. package/src/build/integration-detection-plugin.ts +0 -117
  127. package/src/build/integration-resolver-plugin.ts +0 -90
  128. package/src/build/island-manifest.ts +0 -269
  129. package/src/build/island-types-generator.ts +0 -476
  130. package/src/build/mdx-island-transform.ts +0 -464
  131. package/src/build/mdx-plugin.ts +0 -98
  132. package/src/build/page-island-transform.ts +0 -598
  133. package/src/build/prop-extractors/index.ts +0 -21
  134. package/src/build/prop-extractors/lit.ts +0 -140
  135. package/src/build/prop-extractors/qwik.ts +0 -16
  136. package/src/build/prop-extractors/solid.ts +0 -125
  137. package/src/build/prop-extractors/svelte.ts +0 -194
  138. package/src/build/prop-extractors/vue.ts +0 -111
  139. package/src/build/sidecar-file-manager.ts +0 -104
  140. package/src/build/sidecar-renderer.ts +0 -30
  141. package/src/client/adapters/index.js +0 -12
  142. package/src/client/adapters/index.ts +0 -13
  143. package/src/client/adapters/lit-adapter.js +0 -467
  144. package/src/client/adapters/lit-adapter.ts +0 -654
  145. package/src/client/adapters/preact-adapter.js +0 -223
  146. package/src/client/adapters/preact-adapter.ts +0 -331
  147. package/src/client/adapters/qwik-adapter.js +0 -259
  148. package/src/client/adapters/qwik-adapter.ts +0 -345
  149. package/src/client/adapters/react-adapter.js +0 -220
  150. package/src/client/adapters/react-adapter.ts +0 -353
  151. package/src/client/adapters/solid-adapter.js +0 -295
  152. package/src/client/adapters/solid-adapter.ts +0 -451
  153. package/src/client/adapters/svelte-adapter.js +0 -368
  154. package/src/client/adapters/svelte-adapter.ts +0 -524
  155. package/src/client/adapters/vue-adapter.js +0 -278
  156. package/src/client/adapters/vue-adapter.ts +0 -467
  157. package/src/client/components.js +0 -23
  158. package/src/client/components.ts +0 -35
  159. package/src/client/css-hmr-handler.js +0 -263
  160. package/src/client/css-hmr-handler.ts +0 -344
  161. package/src/client/framework-adapter.js +0 -283
  162. package/src/client/framework-adapter.ts +0 -462
  163. package/src/client/hmr-coordinator.js +0 -274
  164. package/src/client/hmr-coordinator.ts +0 -396
  165. package/src/client/hmr-error-overlay.js +0 -533
  166. package/src/client/main.js +0 -816
  167. package/src/client/types/vite-virtual-modules.d.ts +0 -60
  168. package/src/components/Image.tsx +0 -123
  169. package/src/components/IslandErrorBoundary.tsx +0 -145
  170. package/src/components/LayoutDataErrorBoundary.tsx +0 -141
  171. package/src/components/LayoutErrorBoundary.tsx +0 -127
  172. package/src/components/PersistentIsland.tsx +0 -52
  173. package/src/components/StreamingErrorBoundary.tsx +0 -233
  174. package/src/components/StreamingLayout.tsx +0 -538
  175. package/src/core/components/component-analyzer.ts +0 -192
  176. package/src/core/components/component-detection.ts +0 -508
  177. package/src/core/components/enhanced-framework-detector.ts +0 -500
  178. package/src/core/components/framework-registry.ts +0 -563
  179. package/src/core/content/mdx-processor.ts +0 -46
  180. package/src/core/integrations/index.ts +0 -19
  181. package/src/core/integrations/loader.ts +0 -125
  182. package/src/core/integrations/registry.ts +0 -175
  183. package/src/core/islands/island-persistence.ts +0 -325
  184. package/src/core/islands/island-state-serializer.ts +0 -258
  185. package/src/core/islands/persistent-island-context.tsx +0 -80
  186. package/src/core/islands/use-persistent-state.ts +0 -68
  187. package/src/core/layout/enhanced-layout-resolver.ts +0 -322
  188. package/src/core/layout/layout-cache-manager.ts +0 -485
  189. package/src/core/layout/layout-composer.ts +0 -357
  190. package/src/core/layout/layout-data-loader.ts +0 -516
  191. package/src/core/layout/layout-discovery.ts +0 -243
  192. package/src/core/layout/layout-matcher.ts +0 -299
  193. package/src/core/layout/layout-types.ts +0 -110
  194. package/src/core/modules/framework-module-resolver.ts +0 -273
  195. package/src/islands/component-analysis.ts +0 -213
  196. package/src/islands/css-utils.ts +0 -565
  197. package/src/islands/discovery/index.ts +0 -80
  198. package/src/islands/discovery/registry.ts +0 -340
  199. package/src/islands/discovery/resolver.ts +0 -477
  200. package/src/islands/discovery/scanner.ts +0 -386
  201. package/src/islands/discovery/types.ts +0 -117
  202. package/src/islands/discovery/validator.ts +0 -544
  203. package/src/islands/discovery/watcher.ts +0 -368
  204. package/src/islands/framework-detection.ts +0 -428
  205. package/src/islands/integration-loader.ts +0 -490
  206. package/src/islands/island.tsx +0 -565
  207. package/src/islands/render-cache.ts +0 -550
  208. package/src/islands/types.ts +0 -80
  209. package/src/islands/universal-css-collector.ts +0 -157
  210. package/src/islands/universal-head-collector.ts +0 -137
  211. package/src/layout-system.ts +0 -218
  212. package/src/middleware/discovery.ts +0 -268
  213. package/src/middleware/executor.ts +0 -315
  214. package/src/middleware/index.ts +0 -76
  215. package/src/middleware/types.ts +0 -99
  216. package/src/nitro/build-config.ts +0 -576
  217. package/src/nitro/config.ts +0 -483
  218. package/src/nitro/error-handler.ts +0 -636
  219. package/src/nitro/index.ts +0 -173
  220. package/src/nitro/island-manifest.ts +0 -584
  221. package/src/nitro/middleware-adapter.ts +0 -260
  222. package/src/nitro/renderer.ts +0 -1471
  223. package/src/nitro/route-discovery.ts +0 -439
  224. package/src/nitro/types.ts +0 -321
  225. package/src/render/collect-css.ts +0 -198
  226. package/src/render/isolated-ssr-renderer.ts +0 -654
  227. package/src/render/ssr.ts +0 -1030
  228. package/src/schemas/api.ts +0 -30
  229. package/src/schemas/core.ts +0 -64
  230. package/src/schemas/index.ts +0 -212
  231. package/src/schemas/layout.ts +0 -279
  232. package/src/schemas/routing/index.ts +0 -38
  233. package/src/schemas/routing.ts +0 -376
  234. package/src/types/as-island.ts +0 -20
  235. package/src/types/layout.ts +0 -285
  236. package/src/types/routing.ts +0 -555
  237. package/src/types/types.ts +0 -5
  238. package/src/utils/dev-logger.ts +0 -299
  239. package/src/utils/fs.ts +0 -151
  240. package/src/vite-plugin/auto-discover.ts +0 -551
  241. package/src/vite-plugin/config.ts +0 -266
  242. package/src/vite-plugin/errors.ts +0 -127
  243. package/src/vite-plugin/image-optimization.ts +0 -156
  244. package/src/vite-plugin/integration-activator.ts +0 -126
  245. package/src/vite-plugin/island-sidecar-plugin.ts +0 -176
  246. package/src/vite-plugin/module-discovery.ts +0 -189
  247. package/src/vite-plugin/nitro-integration.ts +0 -1354
  248. package/src/vite-plugin/plugin.ts +0 -401
  249. package/src/vite-plugin/types.ts +0 -327
  250. package/src/vite-plugin/validation.ts +0 -228
@@ -1,1471 +0,0 @@
1
- /**
2
- * Nitro SSR Renderer Handler for Avalon
3
- *
4
- * This module provides the main SSR renderer for Nitro integration.
5
- * It handles page rendering using Avalon's existing SSR pipeline while
6
- * integrating with Nitro's h3 event handling system.
7
- *
8
- * The renderer acts as a catch-all handler for Nitro - it receives requests
9
- * that don't match any API routes or static files, and renders the appropriate
10
- * page using Avalon's SSR pipeline.
11
- *
12
- * Key design principle: This renderer relies on Nitro's built-in file-system
13
- * routing for route matching. Custom route matching logic has been removed
14
- * in favor of Nitro's native capabilities.
15
- *
16
- * Middleware Integration:
17
- * - Global middleware runs first (handled by Nitro's middleware/ directory)
18
- * - Route-scoped middleware runs after global middleware, before page rendering
19
- * - If global middleware terminates, route-scoped middleware does not run
20
- *
21
- * Custom Error Pages:
22
- * - Supports custom 404 page (src/pages/404.tsx)
23
- * - Supports custom 500 page (src/pages/500.tsx)
24
- * - Supports generic error page (src/pages/_error.tsx)
25
- *
26
- * Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 5.1, 5.3, 9.1, 9.2, 9.3, 9.4, 10.5
27
- */
28
-
29
- import type {
30
- NitroRenderContext,
31
- SSRRenderOptions,
32
- SSRRenderResult,
33
- PageModule,
34
- AvalonRuntimeConfig,
35
- HttpError,
36
- } from './types.ts';
37
- import type { H3Event } from 'h3';
38
- import { getRequestURL as h3GetRequestURL } from 'h3';
39
- import { createNotFoundError, isHttpError } from './types.ts';
40
- import type { MiddlewareRoute } from '../middleware/types.ts';
41
- import { discoverScopedMiddleware, executeScopedMiddleware } from '../middleware/index.ts';
42
- import {
43
- handleRenderError as handleRenderErrorWithCustomPages,
44
- discoverErrorPages,
45
- type ErrorHandlerOptions,
46
- } from './error-handler.ts';
47
- import { h } from 'preact';
48
- import preactRenderToString from 'preact-render-to-string';
49
-
50
- /**
51
- * Resolved page route information
52
- */
53
- export interface ResolvedPageRoute {
54
- /** File path to the page module */
55
- filePath: string;
56
- /** Route pattern that matched */
57
- pattern: string;
58
- /** Extracted route parameters */
59
- params: Record<string, string>;
60
- /** Layout files to apply (outermost first) */
61
- layouts?: string[];
62
- }
63
-
64
- /**
65
- * Render handler options
66
- *
67
- * Simplified for Nitro's catch-all pattern - route resolution is now
68
- * handled by Nitro's file-system routing, so custom resolvers are optional
69
- * and primarily used for development/testing scenarios.
70
- */
71
- export interface RenderHandlerOptions {
72
- /** Avalon runtime configuration */
73
- avalonConfig: AvalonRuntimeConfig;
74
- /** Whether running in development mode */
75
- isDev?: boolean;
76
- /** Vite dev server URL for development */
77
- viteServerUrl?: string;
78
- /**
79
- * Custom page resolver function (optional)
80
- * In production, Nitro handles route resolution via file-system routing.
81
- * This is primarily used for development with Vite's SSR module loading.
82
- */
83
- resolvePageRoute?: (pathname: string, pagesDir: string) => Promise<ResolvedPageRoute | null>;
84
- /**
85
- * Custom page module loader (optional)
86
- * In production, modules are loaded from the build output.
87
- * In development, Vite's ssrLoadModule is used.
88
- */
89
- loadPageModule?: (filePath: string) => Promise<PageModule>;
90
- /** Custom layout resolver */
91
- resolveLayouts?: (routePath: string, config: AvalonRuntimeConfig) => Promise<string[]>;
92
- /**
93
- * Enable custom error pages (404.tsx, 500.tsx, _error.tsx)
94
- * When enabled, the renderer will look for custom error pages in the pages directory
95
- * Requirements: 10.5
96
- */
97
- enableCustomErrorPages?: boolean;
98
- }
99
-
100
- /**
101
- * Creates a render context from an H3 event
102
- *
103
- * @param event - The H3 event from Nitro
104
- * @param params - Route parameters extracted from the URL
105
- * @returns NitroRenderContext for use in rendering
106
- */
107
- export function createRenderContext(event: H3Event, params: Record<string, string> = {}): NitroRenderContext {
108
- const url = getRequestURL(event);
109
-
110
- return {
111
- url,
112
- params,
113
- query: Object.fromEntries(url.searchParams),
114
- request: toRequest(event),
115
- event,
116
- };
117
- }
118
-
119
- /**
120
- * Gets the request URL from an H3 event
121
- */
122
- export function getRequestURL(event: H3Event): URL {
123
- // Use h3's getRequestURL for h3 v2 compatibility
124
- const protocol = 'http';
125
- const host = 'localhost';
126
- return new URL(h3GetRequestURL(event).pathname, `${protocol}://${host}`);
127
- }
128
-
129
- /**
130
- * Converts an H3 event to a standard Request object
131
- */
132
- export function toRequest(event: H3Event): Request {
133
- const url = getRequestURL(event);
134
- return new Request(url, {
135
- method: event.req.method,
136
- headers: getRequestHeaders(event),
137
- });
138
- }
139
-
140
- /**
141
- * Gets request headers from an H3 event
142
- */
143
- export function getRequestHeaders(event: H3Event): Headers {
144
- const headers = new Headers();
145
- // In a real Nitro environment, headers would come from event.node.req.headers
146
- // This is a placeholder implementation
147
- return headers;
148
- }
149
-
150
- /**
151
- * Sets a response header on an H3 event
152
- */
153
- export function setResponseHeader(event: H3Event, name: string, value: string): void {
154
- // In a real Nitro environment, this would use h3's setResponseHeader
155
- // Store in event context for now
156
- if (!event.context.responseHeaders) {
157
- event.context.responseHeaders = {};
158
- }
159
- (event.context.responseHeaders as Record<string, string>)[name] = value;
160
- }
161
-
162
- /**
163
- * Creates an error response
164
- */
165
- export function createErrorResponse(error: Error | HttpError, isDev: boolean): Response {
166
- const statusCode = isHttpError(error) ? error.statusCode : 500;
167
-
168
- if (isDev) {
169
- // Development: include full error details
170
- const errorHtml = generateDevErrorPage(error, statusCode);
171
- return new Response(errorHtml, {
172
- status: statusCode,
173
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
174
- });
175
- }
176
-
177
- // Production: generic error page
178
- const errorHtml = generateProdErrorPage(statusCode);
179
- return new Response(errorHtml, {
180
- status: statusCode,
181
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
182
- });
183
- }
184
-
185
- /**
186
- * Generates a development error page with full details
187
- */
188
- function generateDevErrorPage(error: Error, statusCode: number): string {
189
- return `<!DOCTYPE html>
190
- <html lang="en">
191
- <head>
192
- <meta charset="utf-8">
193
- <meta name="viewport" content="width=device-width, initial-scale=1">
194
- <title>Error ${statusCode}</title>
195
- <style>
196
- body {
197
- font-family: system-ui, -apple-system, sans-serif;
198
- margin: 0;
199
- padding: 40px;
200
- background: #1a1a1a;
201
- color: #fff;
202
- }
203
- .error-container {
204
- max-width: 800px;
205
- margin: 0 auto;
206
- background: #2d2d2d;
207
- padding: 40px;
208
- border-radius: 8px;
209
- border-left: 4px solid #ff6b6b;
210
- }
211
- h1 {
212
- color: #ff6b6b;
213
- margin-top: 0;
214
- font-size: 24px;
215
- }
216
- .status-code {
217
- font-size: 48px;
218
- font-weight: bold;
219
- color: #ff6b6b;
220
- margin-bottom: 10px;
221
- }
222
- .message {
223
- font-size: 18px;
224
- color: #ccc;
225
- margin-bottom: 20px;
226
- }
227
- pre {
228
- background: #1a1a1a;
229
- padding: 20px;
230
- border-radius: 4px;
231
- overflow-x: auto;
232
- font-size: 14px;
233
- line-height: 1.5;
234
- color: #e0e0e0;
235
- }
236
- .stack-title {
237
- color: #888;
238
- font-size: 12px;
239
- text-transform: uppercase;
240
- margin-bottom: 10px;
241
- }
242
- </style>
243
- </head>
244
- <body>
245
- <div class="error-container">
246
- <div class="status-code">${statusCode}</div>
247
- <h1>${getStatusText(statusCode)}</h1>
248
- <p class="message">${escapeHtml(error.message)}</p>
249
- ${
250
- error.stack
251
- ? `
252
- <div class="stack-title">Stack Trace</div>
253
- <pre>${escapeHtml(error.stack)}</pre>
254
- `
255
- : ''
256
- }
257
- </div>
258
- </body>
259
- </html>`;
260
- }
261
-
262
- /**
263
- * Generates a production error page without sensitive details
264
- */
265
- function generateProdErrorPage(statusCode: number): string {
266
- return `<!DOCTYPE html>
267
- <html lang="en">
268
- <head>
269
- <meta charset="utf-8">
270
- <meta name="viewport" content="width=device-width, initial-scale=1">
271
- <title>Error ${statusCode}</title>
272
- <style>
273
- body {
274
- font-family: system-ui, -apple-system, sans-serif;
275
- margin: 0;
276
- padding: 40px;
277
- background: #f5f5f5;
278
- display: flex;
279
- align-items: center;
280
- justify-content: center;
281
- min-height: 100vh;
282
- box-sizing: border-box;
283
- }
284
- .error-container {
285
- text-align: center;
286
- max-width: 400px;
287
- }
288
- .status-code {
289
- font-size: 72px;
290
- font-weight: bold;
291
- color: #333;
292
- margin-bottom: 10px;
293
- }
294
- h1 {
295
- color: #666;
296
- font-size: 24px;
297
- margin: 0 0 20px 0;
298
- }
299
- p {
300
- color: #888;
301
- margin: 0;
302
- }
303
- a {
304
- color: #0066cc;
305
- text-decoration: none;
306
- }
307
- a:hover {
308
- text-decoration: underline;
309
- }
310
- </style>
311
- </head>
312
- <body>
313
- <div class="error-container">
314
- <div class="status-code">${statusCode}</div>
315
- <h1>${getStatusText(statusCode)}</h1>
316
- <p><a href="/">Return to home</a></p>
317
- </div>
318
- </body>
319
- </html>`;
320
- }
321
-
322
- /**
323
- * Gets the status text for an HTTP status code
324
- */
325
- function getStatusText(statusCode: number): string {
326
- const statusTexts: Record<number, string> = {
327
- 400: 'Bad Request',
328
- 401: 'Unauthorized',
329
- 403: 'Forbidden',
330
- 404: 'Page Not Found',
331
- 405: 'Method Not Allowed',
332
- 500: 'Internal Server Error',
333
- 502: 'Bad Gateway',
334
- 503: 'Service Unavailable',
335
- };
336
- return statusTexts[statusCode] || 'Error';
337
- }
338
-
339
- /**
340
- * Escapes HTML special characters
341
- */
342
- function escapeHtml(str: string): string {
343
- return str
344
- .replaceAll('&', '&amp;')
345
- .replaceAll('<', '&lt;')
346
- .replaceAll('>', '&gt;')
347
- .replaceAll('"', '&quot;')
348
- .replaceAll("'", '&#039;');
349
- }
350
-
351
- /**
352
- * Island hydration marker information
353
- */
354
- export interface IslandMarker {
355
- /** Framework identifier (react, vue, svelte, etc.) */
356
- framework: string;
357
- /** Source path to the island module */
358
- src: string;
359
- /** Serialized props for the island */
360
- props?: string;
361
- /** Hydration strategy (load, idle, visible, media) */
362
- hydrate?: string;
363
- }
364
-
365
- /**
366
- * Extracts island markers from HTML
367
- * Requirements: 9.1, 9.2, 9.3
368
- *
369
- * @param html - The rendered HTML string
370
- * @returns Array of island markers found in the HTML
371
- */
372
- export function extractIslandMarkers(html: string): IslandMarker[] {
373
- const markers: IslandMarker[] = [];
374
-
375
- // Match island elements with data-framework attribute
376
- const islandRegex = /<[^>]*data-framework="([^"]+)"[^>]*>/g;
377
- let match;
378
-
379
- while ((match = islandRegex.exec(html)) !== null) {
380
- const fullMatch = match[0];
381
- const framework = match[1];
382
-
383
- // Extract data-src
384
- const srcMatch = /data-src="([^"]+)"/.exec(fullMatch);
385
- const src = srcMatch ? srcMatch[1] : '';
386
-
387
- // Extract data-props
388
- const propsMatch = /data-props="([^"]*)"/.exec(fullMatch);
389
- const props = propsMatch ? propsMatch[1] : undefined;
390
-
391
- // Extract data-hydrate (hydration strategy)
392
- const hydrateMatch = /data-hydrate="([^"]+)"/.exec(fullMatch);
393
- const hydrate = hydrateMatch ? hydrateMatch[1] : undefined;
394
-
395
- markers.push({
396
- framework,
397
- src,
398
- props,
399
- hydrate,
400
- });
401
- }
402
-
403
- return markers;
404
- }
405
-
406
- /**
407
- * Ensures all required hydration markers are present on an island element
408
- * Requirements: 9.1, 9.2, 9.3
409
- *
410
- * @param element - The island element HTML string
411
- * @param marker - The island marker data to ensure
412
- * @returns The element with all required markers
413
- */
414
- export function ensureHydrationMarkers(element: string, marker: Partial<IslandMarker>): string {
415
- let result = element;
416
-
417
- // Ensure data-framework is present
418
- if (marker.framework && !result.includes('data-framework=')) {
419
- result = result.replace(/>/, ` data-framework="${marker.framework}">`);
420
- }
421
-
422
- // Ensure data-src is present
423
- if (marker.src && !result.includes('data-src=')) {
424
- result = result.replace(/>/, ` data-src="${marker.src}">`);
425
- }
426
-
427
- // Ensure data-props is present (even if empty)
428
- if (marker.props !== undefined && !result.includes('data-props=')) {
429
- result = result.replace(/>/, ` data-props="${marker.props}">`);
430
- }
431
-
432
- return result;
433
- }
434
-
435
- /**
436
- * Injects the client hydration script into HTML
437
- * Requirements: 2.6, 9.4
438
- *
439
- * This function:
440
- * 1. Checks if there are islands that need hydration
441
- * 2. Injects the client script before </body> if not already present
442
- * 3. Supports both development and production script paths
443
- *
444
- * @param html - The rendered HTML string
445
- * @param isDev - Whether running in development mode
446
- * @param options - Additional injection options
447
- * @returns HTML with hydration script injected
448
- */
449
- export function injectHydrationScript(
450
- html: string,
451
- isDev: boolean,
452
- options: {
453
- /** Custom script path override */
454
- scriptPath?: string;
455
- /** Additional scripts to inject */
456
- additionalScripts?: string[];
457
- /** Whether to force injection even without islands */
458
- forceInject?: boolean;
459
- } = {},
460
- ): string {
461
- // Check if there are any islands that need hydration
462
- const hasIslands = html.includes('data-framework=') || html.includes('data-src=');
463
-
464
- if (!hasIslands && !options.forceInject) {
465
- // No islands found, no need to inject hydration script
466
- return html;
467
- }
468
-
469
- // Check if the client script is already included
470
- const existingScripts = ['/src/client/main.js', '/dist/client.js', 'client/main.js'];
471
-
472
- if (existingScripts.some(script => html.includes(script))) {
473
- return html;
474
- }
475
-
476
- // Determine the script path based on environment or override
477
- const scriptPath = options.scriptPath || (isDev ? '/src/client/main.js' : '/dist/client.js');
478
-
479
- // Build the script tags
480
- const scripts: string[] = [];
481
-
482
- // Main hydration script
483
- scripts.push(`<script type="module" src="${scriptPath}"></script>`);
484
-
485
- // Additional scripts if provided
486
- if (options.additionalScripts) {
487
- scripts.push(...options.additionalScripts);
488
- }
489
-
490
- const scriptBlock = scripts.join('\n');
491
-
492
- // Inject before closing </body> tag
493
- if (html.includes('</body>')) {
494
- return html.replace('</body>', `${scriptBlock}\n</body>`);
495
- }
496
-
497
- // Fallback: append to the end
498
- return html + scriptBlock;
499
- }
500
-
501
- /**
502
- * Validates that hydration markers are present in the HTML
503
- * Requirements: 2.3, 9.1, 9.2, 9.3
504
- *
505
- * @param html - The rendered HTML string
506
- * @returns Object with validation results
507
- */
508
- export function validateHydrationMarkers(html: string): {
509
- hasFrameworkAttr: boolean;
510
- hasSrcAttr: boolean;
511
- hasPropsAttr: boolean;
512
- islandCount: number;
513
- islands: IslandMarker[];
514
- hasClientScript: boolean;
515
- isValid: boolean;
516
- } {
517
- // Extract all island markers
518
- const islands = extractIslandMarkers(html);
519
-
520
- // Count islands with each attribute type
521
- const frameworkMatches = html.match(/data-framework="[^"]+"/g) || [];
522
- const srcMatches = html.match(/data-src="[^"]+"/g) || [];
523
- const propsMatches = html.match(/data-props="[^"]*"/g) || [];
524
-
525
- // Check for client script
526
- const hasClientScript =
527
- html.includes('/src/client/main.js') || html.includes('/dist/client.js') || html.includes('client/main.js');
528
-
529
- // Validation: all islands should have framework and src attributes
530
- const allIslandsValid = islands.every(island => island.framework && island.src);
531
-
532
- // Overall validity: if there are islands, they should be valid and have client script
533
- const isValid = islands.length === 0 || (allIslandsValid && hasClientScript);
534
-
535
- return {
536
- hasFrameworkAttr: frameworkMatches.length > 0,
537
- hasSrcAttr: srcMatches.length > 0,
538
- hasPropsAttr: propsMatches.length > 0,
539
- islandCount: frameworkMatches.length,
540
- islands,
541
- hasClientScript,
542
- isValid,
543
- };
544
- }
545
-
546
- /**
547
- * Processes HTML to ensure all hydration requirements are met
548
- * Requirements: 2.3, 2.6, 9.1, 9.2, 9.3, 9.4
549
- *
550
- * This is a convenience function that:
551
- * 1. Validates existing hydration markers
552
- * 2. Injects the client script if needed
553
- * 3. Returns the processed HTML
554
- *
555
- * @param html - The rendered HTML string
556
- * @param isDev - Whether running in development mode
557
- * @returns Processed HTML with all hydration requirements met
558
- */
559
- export function processHydrationRequirements(
560
- html: string,
561
- isDev: boolean,
562
- ): {
563
- html: string;
564
- validation: ReturnType<typeof validateHydrationMarkers>;
565
- } {
566
- // First, inject the hydration script
567
- const processedHtml = injectHydrationScript(html, isDev);
568
-
569
- // Then validate the result
570
- const validation = validateHydrationMarkers(processedHtml);
571
-
572
- return {
573
- html: processedHtml,
574
- validation,
575
- };
576
- }
577
-
578
- /**
579
- * Renders a page to HTML string (non-streaming)
580
- * Requirements: 2.1, 2.2
581
- *
582
- * @param pageModule - The page module to render
583
- * @param context - The render context
584
- * @param options - Render options
585
- * @returns SSR render result
586
- */
587
- export async function renderPage(
588
- pageModule: PageModule,
589
- context: NitroRenderContext,
590
- options: SSRRenderOptions = {},
591
- ): Promise<SSRRenderResult> {
592
- try {
593
- // Get page props if getServerSideProps is defined
594
- let pageProps: Record<string, unknown> = {};
595
- if (pageModule.getServerSideProps) {
596
- pageProps = await pageModule.getServerSideProps(context);
597
- }
598
-
599
- // The actual rendering would integrate with Avalon's existing renderToHtml
600
- // For now, we return a placeholder that shows the structure
601
- const html = await renderPageComponent(pageModule, pageProps, context, options);
602
-
603
- return {
604
- html,
605
- statusCode: 200,
606
- headers: {
607
- 'Content-Type': 'text/html; charset=utf-8',
608
- },
609
- };
610
- } catch (error) {
611
- console.error('[SSR Error]', error);
612
-
613
- if (options.onError && error instanceof Error) {
614
- options.onError(error);
615
- }
616
-
617
- throw error;
618
- }
619
- }
620
-
621
- /**
622
- * Renders a page component to HTML
623
- * This is a placeholder that would integrate with Avalon's existing SSR pipeline
624
- */
625
- async function renderPageComponent(
626
- pageModule: PageModule,
627
- pageProps: Record<string, unknown>,
628
- context: NitroRenderContext,
629
- _options: SSRRenderOptions,
630
- ): Promise<string> {
631
- const Component = pageModule.default as (props?: Record<string, unknown>) => unknown;
632
- const metadata = pageModule.metadata || {};
633
-
634
- // Call the page component (supports async components)
635
- let vnode: unknown;
636
- try {
637
- const result = Component(pageProps);
638
- vnode = result instanceof Promise ? await result : result;
639
- } catch (err) {
640
- console.error('[renderer] Error calling page component:', err);
641
- vnode = h('div', null, 'Error rendering page');
642
- }
643
-
644
- // Render the vnode to HTML string using Preact SSR
645
- let pageHtml: string;
646
- try {
647
- pageHtml = preactRenderToString(vnode as any);
648
- } catch (err) {
649
- console.error('[renderer] Error in preactRenderToString:', err);
650
- pageHtml = '<div>Error rendering page</div>';
651
- }
652
-
653
- return `<!DOCTYPE html>
654
- <html lang="en">
655
- <head>
656
- <meta charset="utf-8">
657
- <meta name="viewport" content="width=device-width, initial-scale=1">
658
- <title>${escapeHtml(String(metadata.title || 'Avalon App'))}</title>
659
- ${metadata.description ? `<meta name="description" content="${escapeHtml(String(metadata.description))}">` : ''}
660
- </head>
661
- <body>
662
- <div id="app">
663
- ${pageHtml}
664
- </div>
665
- <script type="module" src="/src/client/main.js"></script>
666
- </body>
667
- </html>`;
668
- }
669
-
670
- /**
671
- * Streaming render state for tracking progress
672
- */
673
- interface StreamingRenderState {
674
- shellSent: boolean;
675
- contentSent: boolean;
676
- closed: boolean;
677
- error: Error | null;
678
- }
679
-
680
- /**
681
- * Extended streaming options with additional callbacks
682
- */
683
- export interface StreamingSSROptions extends SSRRenderOptions {
684
- /** Callback when shell rendering fails before streaming starts */
685
- onShellError?: (error: Error) => void;
686
- /** Timeout for shell ready in milliseconds */
687
- shellReadyTimeout?: number;
688
- /** Timeout for all content ready in milliseconds */
689
- allReadyTimeout?: number;
690
- }
691
-
692
- /**
693
- * Renders a page to a streaming response
694
- * Requirements: 2.4
695
- *
696
- * This function implements streaming SSR with proper shell/content separation:
697
- * 1. Shell (DOCTYPE, html, head, body opening) is sent first
698
- * 2. onShellReady callback is invoked when shell is ready
699
- * 3. Page content is streamed progressively
700
- * 4. onAllReady callback is invoked when all content is complete
701
- *
702
- * @param pageModule - The page module to render
703
- * @param context - The render context
704
- * @param options - Render options including streaming callbacks
705
- * @returns ReadableStream of HTML chunks
706
- */
707
- export async function renderPageStream(
708
- pageModule: PageModule,
709
- context: NitroRenderContext,
710
- options: StreamingSSROptions = {},
711
- ): Promise<ReadableStream<Uint8Array>> {
712
- const encoder = new TextEncoder();
713
- let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
714
-
715
- const state: StreamingRenderState = {
716
- shellSent: false,
717
- contentSent: false,
718
- closed: false,
719
- error: null,
720
- };
721
-
722
- // Set up timeouts if specified
723
- const shellTimeout = options.shellReadyTimeout;
724
- const allReadyTimeout = options.allReadyTimeout;
725
- let shellTimeoutId: ReturnType<typeof setTimeout> | null = null;
726
- let allReadyTimeoutId: ReturnType<typeof setTimeout> | null = null;
727
-
728
- const clearTimeouts = () => {
729
- if (shellTimeoutId) {
730
- clearTimeout(shellTimeoutId);
731
- shellTimeoutId = null;
732
- }
733
- if (allReadyTimeoutId) {
734
- clearTimeout(allReadyTimeoutId);
735
- allReadyTimeoutId = null;
736
- }
737
- };
738
-
739
- function handleStreamError(
740
- err: Error,
741
- isShellError: boolean,
742
- state: StreamingRenderState,
743
- ctrl: ReadableStreamDefaultController<Uint8Array> | null,
744
- encoder: TextEncoder,
745
- clearFn: () => void,
746
- opts: StreamingSSROptions,
747
- ) {
748
- state.error = err;
749
- clearFn();
750
-
751
- console.error('[Streaming Error]', {
752
- message: err.message,
753
- stack: err.stack,
754
- shellSent: state.shellSent,
755
- isShellError,
756
- timestamp: new Date().toISOString(),
757
- });
758
-
759
- // Call appropriate error callback
760
- if (isShellError && opts.onShellError) {
761
- opts.onShellError(err);
762
- }
763
- if (opts.onError) {
764
- opts.onError(err);
765
- }
766
-
767
- if (!state.closed && ctrl) {
768
- if (state.shellSent) {
769
- // Inject error boundary into the stream
770
- const errorBoundary = generateStreamingErrorBoundary(err);
771
- ctrl.enqueue(encoder.encode(errorBoundary));
772
-
773
- // Close the HTML document gracefully
774
- const footer = generateStreamingFooter();
775
- ctrl.enqueue(encoder.encode(footer));
776
- } else {
777
- // Send complete error page if shell hasn't been sent
778
- const errorHtml = generateDevErrorPage(err, 500);
779
- ctrl.enqueue(encoder.encode(errorHtml));
780
- }
781
-
782
- state.closed = true;
783
- ctrl.close();
784
- }
785
- }
786
-
787
- async function executeStreamingRender(ctrl: ReadableStreamDefaultController<Uint8Array>) {
788
- controller = ctrl;
789
-
790
- // Get page props if getServerSideProps is defined
791
- let pageProps: Record<string, unknown> = {};
792
- if (pageModule.getServerSideProps) {
793
- pageProps = await pageModule.getServerSideProps(context);
794
- }
795
-
796
- const metadata = pageModule.metadata || {};
797
-
798
- // Generate the shell (DOCTYPE, html, head, body opening)
799
- const shell = generateStreamingShell(metadata, context);
800
-
801
- // Send the shell
802
- if (!state.closed) {
803
- ctrl.enqueue(encoder.encode(shell));
804
- state.shellSent = true;
805
-
806
- // Clear shell timeout
807
- if (shellTimeoutId) {
808
- clearTimeout(shellTimeoutId);
809
- shellTimeoutId = null;
810
- }
811
-
812
- // Notify that shell is ready
813
- if (options.onShellReady) {
814
- options.onShellReady();
815
- }
816
- }
817
-
818
- // Set up all ready timeout
819
- if (allReadyTimeout && allReadyTimeout > 0) {
820
- allReadyTimeoutId = setTimeout(() => {
821
- if (!state.contentSent && !state.closed) {
822
- const timeoutError = new Error(`All ready timeout after ${allReadyTimeout}ms`);
823
- handleStreamError(timeoutError, false, state, controller, encoder, clearTimeouts, options);
824
- }
825
- }, allReadyTimeout);
826
- }
827
-
828
- // Send the page content
829
- if (!state.closed) {
830
- const content = generateStreamingContent(pageModule, pageProps);
831
- ctrl.enqueue(encoder.encode(content));
832
- state.contentSent = true;
833
- }
834
-
835
- // Send the footer (closing body and html tags)
836
- if (!state.closed) {
837
- const footer = generateStreamingFooter();
838
- ctrl.enqueue(encoder.encode(footer));
839
- }
840
-
841
- // Clear all ready timeout
842
- clearTimeouts();
843
-
844
- // Notify that all content is ready
845
- if (options.onAllReady && !state.closed) {
846
- options.onAllReady();
847
- }
848
-
849
- if (!state.closed) {
850
- state.closed = true;
851
- ctrl.close();
852
- }
853
- }
854
-
855
- const stream = new ReadableStream<Uint8Array>({
856
- async start(ctrl) {
857
- controller = ctrl;
858
-
859
- // Set up shell timeout
860
- if (shellTimeout && shellTimeout > 0) {
861
- shellTimeoutId = setTimeout(() => {
862
- if (!state.shellSent && !state.closed) {
863
- const timeoutError = new Error(`Shell ready timeout after ${shellTimeout}ms`);
864
- handleStreamError(timeoutError, true, state, controller, encoder, clearTimeouts, options);
865
- }
866
- }, shellTimeout);
867
- }
868
-
869
- try {
870
- await executeStreamingRender(ctrl);
871
- } catch (error) {
872
- handleStreamError(
873
- error instanceof Error ? error : new Error(String(error)),
874
- !state.shellSent,
875
- state,
876
- controller,
877
- encoder,
878
- clearTimeouts,
879
- options,
880
- );
881
- }
882
- },
883
-
884
- cancel() {
885
- clearTimeouts();
886
- if (!state.closed && controller) {
887
- state.closed = true;
888
- try {
889
- controller.close();
890
- } catch {
891
- // Already closed
892
- }
893
- }
894
- },
895
- });
896
-
897
- return stream;
898
- }
899
-
900
- /**
901
- * Generates the streaming shell (DOCTYPE, html, head, body opening)
902
- */
903
- function generateStreamingShell(
904
- metadata: { title?: string; description?: string },
905
- _context: NitroRenderContext,
906
- ): string {
907
- return `<!DOCTYPE html>
908
- <html lang="en">
909
- <head>
910
- <meta charset="utf-8">
911
- <meta name="viewport" content="width=device-width, initial-scale=1">
912
- <title>${escapeHtml(String(metadata.title || 'Avalon App'))}</title>
913
- ${metadata.description ? `<meta name="description" content="${escapeHtml(String(metadata.description))}">` : ''}
914
- </head>
915
- <body>
916
- `;
917
- }
918
-
919
- /**
920
- * Generates the streaming content
921
- */
922
- function generateStreamingContent(pageModule: PageModule, pageProps: Record<string, unknown>): string {
923
- const componentName = (pageModule.default as { name?: string })?.name || 'Page';
924
- return ` <div id="app" data-page="${escapeHtml(String(componentName))}" data-props='${escapeHtml(JSON.stringify(pageProps))}'>
925
- <!-- Page content rendered by Avalon SSR pipeline -->
926
- </div>
927
- `;
928
- }
929
-
930
- /**
931
- * Generates the streaming footer (closing body and html tags)
932
- */
933
- function generateStreamingFooter(): string {
934
- return ` </body>
935
- </html>`;
936
- }
937
-
938
- /**
939
- * Generates an error boundary for mid-stream errors
940
- */
941
- function generateStreamingErrorBoundary(error: Error): string {
942
- const isDev = process.env.NODE_ENV !== 'production';
943
-
944
- const stackHtml = error.stack
945
- ? `<pre style="
946
- background: #f5f5f5;
947
- padding: 10px;
948
- border-radius: 4px;
949
- overflow-x: auto;
950
- font-size: 12px;
951
- margin-top: 10px;
952
- ">${escapeHtml(error.stack)}</pre>`
953
- : '';
954
-
955
- return `
956
- <div class="streaming-error-boundary" data-error-boundary="true" style="
957
- background: #fff3cd;
958
- border: 2px solid #ffc107;
959
- border-radius: 8px;
960
- padding: 20px;
961
- margin: 20px 0;
962
- font-family: system-ui, -apple-system, sans-serif;
963
- ">
964
- <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
965
- <span style="font-size: 24px;">⚠️</span>
966
- <h3 style="margin: 0; color: #856404;">Streaming Error</h3>
967
- </div>
968
- <p style="margin: 10px 0; color: #856404;">
969
- An error occurred while streaming this page.
970
- </p>
971
- ${
972
- isDev
973
- ? `
974
- <details style="margin-top: 15px;">
975
- <summary style="cursor: pointer; color: #856404; font-weight: bold;">
976
- Error Details (Development Mode)
977
- </summary>
978
- <div style="margin-top: 10px;">
979
- <p><strong>Error:</strong> ${escapeHtml(error.message)}</p>
980
- ${stackHtml}
981
- </div>
982
- </details>
983
- `
984
- : ''
985
- }
986
- </div>
987
- `;
988
- }
989
-
990
- /**
991
- * Creates a streaming response with proper headers
992
- * Requirements: 2.4
993
- *
994
- * @param stream - The ReadableStream to wrap
995
- * @param options - Additional response options
996
- * @returns Response object with streaming body
997
- */
998
- export function createStreamingResponse(
999
- stream: ReadableStream<Uint8Array>,
1000
- options: {
1001
- status?: number;
1002
- headers?: Record<string, string>;
1003
- } = {},
1004
- ): Response {
1005
- const headers = new Headers({
1006
- 'Content-Type': 'text/html; charset=utf-8',
1007
- 'Transfer-Encoding': 'chunked',
1008
- ...options.headers,
1009
- });
1010
-
1011
- return new Response(stream, {
1012
- status: options.status || 200,
1013
- headers,
1014
- });
1015
- }
1016
-
1017
- /**
1018
- * Creates a scoped middleware getter that discovers and caches middleware routes.
1019
- * Shared between createNitroRenderer and createNitroCatchAllRenderer.
1020
- */
1021
- function createScopedMiddlewareGetter(
1022
- routesRef: { value: MiddlewareRoute[] | null },
1023
- srcDir: string,
1024
- isDev: boolean,
1025
- ): () => Promise<MiddlewareRoute[]> {
1026
- return async () => {
1027
- routesRef.value ??= await discoverScopedMiddleware({
1028
- baseDir: srcDir,
1029
- devMode: isDev,
1030
- });
1031
- return routesRef.value;
1032
- };
1033
- }
1034
-
1035
- /**
1036
- * Creates an error handler with custom error page support.
1037
- * Shared between createNitroRenderer and createNitroCatchAllRenderer.
1038
- */
1039
- function createErrorHandler(
1040
- enableCustomErrorPages: boolean,
1041
- errorHandlerOptions: ErrorHandlerOptions,
1042
- isDev: boolean,
1043
- ): (error: Error | HttpError, event: H3Event) => Promise<Response> {
1044
- return async (error, event) => {
1045
- if (enableCustomErrorPages) {
1046
- return handleRenderErrorWithCustomPages(error, event, errorHandlerOptions);
1047
- }
1048
- return createErrorResponse(error, isDev);
1049
- };
1050
- }
1051
-
1052
- /**
1053
- * Creates the main Nitro renderer handler
1054
- *
1055
- * This is the catch-all handler for Nitro that renders pages not matched
1056
- * by API routes or static files. It integrates with Nitro's routing system:
1057
- *
1058
- * 1. Nitro's file-system routing handles API routes (api/ directory)
1059
- * 2. Nitro's static asset handling serves files from public/
1060
- * 3. This renderer catches all remaining requests for SSR page rendering
1061
- *
1062
- * Middleware execution order:
1063
- * 1. Global middleware (from middleware/ directory) - handled by Nitro
1064
- * 2. Route-scoped middleware (from _middleware.ts files) - handled here
1065
- * 3. Page rendering
1066
- *
1067
- * If global middleware terminates the chain, this handler is not called.
1068
- * If route-scoped middleware terminates, page rendering is skipped.
1069
- *
1070
- * The renderer relies on Nitro's event context for route information when
1071
- * available, falling back to pathname-based resolution for development.
1072
- *
1073
- * Requirements: 2.1, 2.2, 2.4, 5.1, 5.3, 10.5
1074
- *
1075
- * @param options - Render handler options
1076
- * @returns Handler function for Nitro
1077
- */
1078
- export function createNitroRenderer(options: RenderHandlerOptions) {
1079
- const { avalonConfig, isDev = false, enableCustomErrorPages = true } = options;
1080
-
1081
- // Middleware routes cache - discovered once at startup
1082
- let scopedMiddlewareRoutes: MiddlewareRoute[] | null = null;
1083
-
1084
- // Error handler options for custom error pages
1085
- const errorHandlerOptions: ErrorHandlerOptions = {
1086
- isDev,
1087
- avalonConfig,
1088
- loadPageModule: options.loadPageModule,
1089
- pagesDir: avalonConfig.pagesDir,
1090
- };
1091
-
1092
- // Pre-discover error pages if custom error pages are enabled
1093
- if (enableCustomErrorPages) {
1094
- discoverErrorPages(errorHandlerOptions).catch(err => {
1095
- console.warn('[renderer] Failed to discover error pages:', err);
1096
- });
1097
- }
1098
-
1099
- /**
1100
- * Gets scoped middleware routes, discovering them on first call
1101
- * Routes are cached for performance in production
1102
- */
1103
- const middlewareRef = { value: scopedMiddlewareRoutes };
1104
- const getScopedMiddleware = createScopedMiddlewareGetter(middlewareRef, avalonConfig.srcDir || 'src', isDev);
1105
-
1106
- /**
1107
- * Handles errors with custom error page support
1108
- */
1109
- const handleError = createErrorHandler(enableCustomErrorPages, errorHandlerOptions, isDev);
1110
-
1111
- return async function nitroRendererHandler(event: H3Event): Promise<Response> {
1112
- const url = getRequestURL(event);
1113
- const pathname = url.pathname;
1114
-
1115
- try {
1116
- // Execute route-scoped middleware before page rendering
1117
- // Global middleware has already run (handled by Nitro's middleware/ directory)
1118
- // Requirements: 5.1, 5.3
1119
- const middlewareRoutes = await getScopedMiddleware();
1120
- const middlewareResponse = await executeScopedMiddleware(event, middlewareRoutes, {
1121
- devMode: isDev,
1122
- });
1123
-
1124
- // If middleware returned a response, use it and skip page rendering
1125
- if (middlewareResponse) {
1126
- if (isDev) {
1127
- console.log(`[renderer] Middleware terminated request for ${pathname}`);
1128
- }
1129
- return middlewareResponse;
1130
- }
1131
-
1132
- // Check if Nitro has already resolved route information in the event context
1133
- // This happens when Nitro's file-system routing has matched a route
1134
- const nitroRouteContext = event.context.route as ResolvedPageRoute | undefined;
1135
-
1136
- let route: ResolvedPageRoute | null = null;
1137
-
1138
- if (nitroRouteContext) {
1139
- // Use Nitro's resolved route information
1140
- route = nitroRouteContext;
1141
- } else {
1142
- // Fall back to custom resolution (primarily for development)
1143
- // In production with Nitro, this path is rarely taken as Nitro
1144
- // handles route resolution before reaching the catch-all renderer
1145
- route = options.resolvePageRoute
1146
- ? await options.resolvePageRoute(pathname, avalonConfig.pagesDir)
1147
- : await defaultResolvePageRoute(pathname, avalonConfig.pagesDir);
1148
- }
1149
-
1150
- if (!route) {
1151
- // No page found, return 404 with custom error page support
1152
- const error = createNotFoundError(`Page not found: ${pathname}`);
1153
- return handleError(error, event);
1154
- }
1155
-
1156
- // Load the page module
1157
- const pageModule = options.loadPageModule
1158
- ? await options.loadPageModule(route.filePath)
1159
- : await defaultLoadPageModule(route.filePath);
1160
-
1161
- // Create render context with route params from Nitro or custom resolution
1162
- // Nitro provides params via event.context.params when using its routing
1163
- const routeParams = (event.context.params as Record<string, string>) || route.params;
1164
- const renderContext = createRenderContext(event, routeParams);
1165
-
1166
- // Resolve layouts if available
1167
- if (options.resolveLayouts) {
1168
- const layouts = await options.resolveLayouts(pathname, avalonConfig);
1169
- renderContext.layoutContext = { layouts };
1170
- }
1171
-
1172
- // Render the page
1173
- if (avalonConfig.streaming) {
1174
- // Streaming SSR
1175
- const stream = await renderPageStream(pageModule, renderContext, {
1176
- onShellReady: () => {
1177
- setResponseHeader(event, 'Content-Type', 'text/html; charset=utf-8');
1178
- },
1179
- });
1180
-
1181
- return new Response(stream, {
1182
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
1183
- });
1184
- } else {
1185
- // Non-streaming SSR
1186
- const result = await renderPage(pageModule, renderContext);
1187
-
1188
- // Inject hydration script
1189
- const html = injectHydrationScript(result.html as string, isDev);
1190
-
1191
- return new Response(html, {
1192
- status: result.statusCode,
1193
- headers: result.headers,
1194
- });
1195
- }
1196
- } catch (error) {
1197
- console.error('[Nitro Renderer Error]', error);
1198
-
1199
- const err = error instanceof Error ? error : new Error(String(error));
1200
- return handleError(err, event);
1201
- }
1202
- };
1203
- }
1204
-
1205
- /**
1206
- * Default page route resolver
1207
- *
1208
- * This is a fallback resolver used primarily in development when Nitro's
1209
- * file-system routing hasn't resolved the route. In production with Nitro,
1210
- * route resolution is handled by Nitro's native routing system.
1211
- *
1212
- * The resolver converts URL pathnames to potential file paths in the pages
1213
- * directory. It's intentionally simple as the heavy lifting of route matching
1214
- * is delegated to Nitro's routing system.
1215
- *
1216
- * @param pathname - URL pathname to resolve
1217
- * @param _pagesDir - Pages directory (unused, kept for interface compatibility)
1218
- * @returns Resolved page route or null if not found
1219
- */
1220
- async function defaultResolvePageRoute(pathname: string, _pagesDir: string): Promise<ResolvedPageRoute | null> {
1221
- // Handle root path
1222
- if (pathname === '/' || pathname === '') {
1223
- return {
1224
- filePath: 'src/pages/index.tsx',
1225
- pattern: '/',
1226
- params: {},
1227
- };
1228
- }
1229
-
1230
- // Convert pathname to potential file path
1231
- // This is a simple conversion - Nitro's routing handles complex patterns
1232
- const cleanPath = pathname.replace(/^\//, '').replace(/\/$/, '');
1233
- const filePath = `src/pages/${cleanPath}.tsx`;
1234
-
1235
- return {
1236
- filePath,
1237
- pattern: pathname,
1238
- params: {},
1239
- };
1240
- }
1241
-
1242
- /**
1243
- * Default page module loader
1244
- *
1245
- * This is a placeholder implementation that returns a minimal page module.
1246
- * In actual usage:
1247
- * - Development: Vite's ssrLoadModule is used via the loadPageModule option
1248
- * - Production: Modules are imported from the build output
1249
- *
1250
- * The actual module loading is handled by the integration layer (nitro-integration.ts)
1251
- * which provides the appropriate loader based on the environment.
1252
- *
1253
- * @param _filePath - File path to load (unused in placeholder)
1254
- * @returns Minimal page module
1255
- */
1256
- async function defaultLoadPageModule(_filePath: string): Promise<PageModule> {
1257
- // This is a placeholder - actual loading is done by:
1258
- // - Vite's ssrLoadModule in development
1259
- // - Direct imports from build output in production
1260
-
1261
- return {
1262
- default: () => null,
1263
- metadata: {
1264
- title: 'Avalon Page',
1265
- },
1266
- };
1267
- }
1268
-
1269
- /**
1270
- * Options for the Nitro catch-all renderer
1271
- */
1272
- export interface NitroCatchAllOptions {
1273
- /** Avalon runtime configuration */
1274
- avalonConfig: AvalonRuntimeConfig;
1275
- /** Whether running in development mode */
1276
- isDev?: boolean;
1277
- /**
1278
- * Page module loader function
1279
- * In development, this should use Vite's ssrLoadModule
1280
- * In production, this imports from the build output
1281
- */
1282
- loadPageModule: (filePath: string) => Promise<PageModule>;
1283
- /** Optional layout resolver */
1284
- resolveLayouts?: (routePath: string, config: AvalonRuntimeConfig) => Promise<string[]>;
1285
- /**
1286
- * Enable custom error pages (404.tsx, 500.tsx, _error.tsx)
1287
- * When enabled, the renderer will look for custom error pages in the pages directory
1288
- * Requirements: 10.5
1289
- */
1290
- enableCustomErrorPages?: boolean;
1291
- }
1292
-
1293
- /**
1294
- * Creates a Nitro catch-all renderer handler
1295
- *
1296
- * This is the recommended way to create a renderer for Nitro's catch-all pattern.
1297
- * It's designed to work with Nitro's file-system routing where:
1298
- *
1299
- * 1. API routes are handled by files in the api/ directory
1300
- * 2. Static assets are served from public/
1301
- * 3. This catch-all handles all remaining requests for SSR
1302
- *
1303
- * Middleware execution order:
1304
- * 1. Global middleware (from middleware/ directory) - handled by Nitro
1305
- * 2. Route-scoped middleware (from _middleware.ts files) - handled here
1306
- * 3. Page rendering
1307
- *
1308
- * The handler expects Nitro to provide route information via event.context:
1309
- * - event.context.params: Route parameters from dynamic segments
1310
- * - event.context.route: Optional resolved route information
1311
- *
1312
- * Usage in Nitro routes/[...slug].ts:
1313
- * ```ts
1314
- * import { createNitroCatchAllRenderer } from '@useavalon/nitro/renderer';
1315
- *
1316
- * export default createNitroCatchAllRenderer({
1317
- * avalonConfig: useRuntimeConfig().avalon,
1318
- * isDev: import.meta.dev,
1319
- * loadPageModule: async (filePath) => {
1320
- * return await import(filePath);
1321
- * }
1322
- * });
1323
- * ```
1324
- *
1325
- * Requirements: 2.1, 2.2, 2.6, 5.1, 5.3, 10.5
1326
- *
1327
- * @param options - Catch-all renderer options
1328
- * @returns Nitro event handler function
1329
- */
1330
- export function createNitroCatchAllRenderer(options: NitroCatchAllOptions) {
1331
- const { avalonConfig, isDev = false, loadPageModule, resolveLayouts, enableCustomErrorPages = true } = options;
1332
-
1333
- // Middleware routes cache - discovered once at startup
1334
- let scopedMiddlewareRoutes: MiddlewareRoute[] | null = null;
1335
-
1336
- // Error handler options for custom error pages
1337
- const errorHandlerOptions: ErrorHandlerOptions = {
1338
- isDev,
1339
- avalonConfig,
1340
- loadPageModule,
1341
- pagesDir: avalonConfig.pagesDir,
1342
- };
1343
-
1344
- // Pre-discover error pages if custom error pages are enabled
1345
- if (enableCustomErrorPages) {
1346
- discoverErrorPages(errorHandlerOptions).catch(err => {
1347
- console.warn('[renderer] Failed to discover error pages:', err);
1348
- });
1349
- }
1350
-
1351
- /**
1352
- * Gets scoped middleware routes, discovering them on first call
1353
- * Routes are cached for performance in production
1354
- */
1355
- const middlewareRef = { value: scopedMiddlewareRoutes };
1356
- const getScopedMiddleware = createScopedMiddlewareGetter(middlewareRef, avalonConfig.srcDir || 'src', isDev);
1357
-
1358
- /**
1359
- * Handles errors with custom error page support
1360
- */
1361
- const handleError = createErrorHandler(enableCustomErrorPages, errorHandlerOptions, isDev);
1362
-
1363
- return async function nitroCatchAllHandler(event: H3Event): Promise<Response> {
1364
- const url = getRequestURL(event);
1365
- const pathname = url.pathname;
1366
-
1367
- try {
1368
- // Execute route-scoped middleware before page rendering
1369
- // Global middleware has already run (handled by Nitro's middleware/ directory)
1370
- // Requirements: 5.1, 5.3
1371
- const middlewareRoutes = await getScopedMiddleware();
1372
- const middlewareResponse = await executeScopedMiddleware(event, middlewareRoutes, {
1373
- devMode: isDev,
1374
- });
1375
-
1376
- // If middleware returned a response, use it and skip page rendering
1377
- if (middlewareResponse) {
1378
- if (isDev) {
1379
- console.log(`[renderer] Middleware terminated request for ${pathname}`);
1380
- }
1381
- return middlewareResponse;
1382
- }
1383
-
1384
- // Get route params from Nitro's routing (e.g., from [...slug].ts)
1385
- const params = (event.context.params as Record<string, string>) || {};
1386
-
1387
- // Reconstruct the page file path from the pathname
1388
- // Nitro's catch-all provides the slug, we map it to the pages directory
1389
- const slug = params.slug || pathname.replace(/^\//, '') || 'index';
1390
- const filePath = `${avalonConfig.pagesDir}/${slug}.tsx`;
1391
-
1392
- // Try to load the page module
1393
- let pageModule: PageModule;
1394
- try {
1395
- pageModule = await loadPageModule(filePath);
1396
- } catch (loadError) {
1397
- // Direct path failed — try index file in directory
1398
- try {
1399
- const indexPath = `${avalonConfig.pagesDir}/${slug}/index.tsx`;
1400
- pageModule = await loadPageModule(indexPath);
1401
- } catch (indexLoadError) {
1402
- // Neither direct path nor index path found
1403
- if (isDev) {
1404
- console.debug(`[renderer] Page not found: ${filePath}`, loadError);
1405
- console.debug(
1406
- `[renderer] Index fallback not found: ${avalonConfig.pagesDir}/${slug}/index.tsx`,
1407
- indexLoadError,
1408
- );
1409
- }
1410
- const error = createNotFoundError(`Page not found: ${pathname}`);
1411
- return handleError(error, event);
1412
- }
1413
- }
1414
-
1415
- // Create render context
1416
- const renderContext = createRenderContext(event, params);
1417
-
1418
- // Resolve layouts if available
1419
- if (resolveLayouts) {
1420
- const layouts = await resolveLayouts(pathname, avalonConfig);
1421
- renderContext.layoutContext = { layouts };
1422
- }
1423
-
1424
- // Render the page
1425
- if (avalonConfig.streaming) {
1426
- // Streaming SSR
1427
- const stream = await renderPageStream(pageModule, renderContext, {
1428
- onShellReady: () => {
1429
- setResponseHeader(event, 'Content-Type', 'text/html; charset=utf-8');
1430
- },
1431
- });
1432
-
1433
- return new Response(stream, {
1434
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
1435
- });
1436
- } else {
1437
- // Non-streaming SSR
1438
- const result = await renderPage(pageModule, renderContext);
1439
-
1440
- // Inject hydration script - ensures client-side hydration works
1441
- const html = injectHydrationScript(result.html as string, isDev);
1442
-
1443
- return new Response(html, {
1444
- status: result.statusCode,
1445
- headers: result.headers,
1446
- });
1447
- }
1448
- } catch (error) {
1449
- console.error('[Nitro Catch-All Renderer Error]', error);
1450
-
1451
- const err = error instanceof Error ? error : new Error(String(error));
1452
- return handleError(err, event);
1453
- }
1454
- };
1455
- }
1456
-
1457
- /**
1458
- * Re-export middleware cache clearing for hot reload support
1459
- *
1460
- * Call this function when middleware files change during development
1461
- * to ensure the latest version is loaded on the next request.
1462
- *
1463
- * @example
1464
- * ```ts
1465
- * // In your HMR handler
1466
- * if (file.endsWith('_middleware.ts')) {
1467
- * clearRendererMiddlewareCache();
1468
- * }
1469
- * ```
1470
- */
1471
- export { clearMiddlewareCache as clearRendererMiddlewareCache } from '../middleware/index.ts';