@useavalon/avalon 0.1.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 (159) hide show
  1. package/README.md +54 -0
  2. package/mod.ts +301 -0
  3. package/package.json +85 -0
  4. package/src/build/README.md +310 -0
  5. package/src/build/integration-bundler-plugin.ts +116 -0
  6. package/src/build/integration-config.ts +168 -0
  7. package/src/build/integration-detection-plugin.ts +117 -0
  8. package/src/build/integration-resolver-plugin.ts +90 -0
  9. package/src/build/island-manifest.ts +269 -0
  10. package/src/build/island-types-generator.ts +476 -0
  11. package/src/build/mdx-island-transform.ts +464 -0
  12. package/src/build/mdx-plugin.ts +98 -0
  13. package/src/build/page-island-transform.ts +598 -0
  14. package/src/build/prop-extractors/index.ts +21 -0
  15. package/src/build/prop-extractors/lit.ts +140 -0
  16. package/src/build/prop-extractors/qwik.ts +16 -0
  17. package/src/build/prop-extractors/solid.ts +125 -0
  18. package/src/build/prop-extractors/svelte.ts +194 -0
  19. package/src/build/prop-extractors/vue.ts +111 -0
  20. package/src/build/sidecar-file-manager.ts +104 -0
  21. package/src/build/sidecar-renderer.ts +30 -0
  22. package/src/client/adapters/index.ts +13 -0
  23. package/src/client/adapters/lit-adapter.ts +654 -0
  24. package/src/client/adapters/preact-adapter.ts +331 -0
  25. package/src/client/adapters/qwik-adapter.ts +345 -0
  26. package/src/client/adapters/react-adapter.ts +353 -0
  27. package/src/client/adapters/solid-adapter.ts +451 -0
  28. package/src/client/adapters/svelte-adapter.ts +524 -0
  29. package/src/client/adapters/vue-adapter.ts +467 -0
  30. package/src/client/components.ts +35 -0
  31. package/src/client/css-hmr-handler.ts +344 -0
  32. package/src/client/framework-adapter.ts +462 -0
  33. package/src/client/hmr-coordinator.ts +396 -0
  34. package/src/client/hmr-error-overlay.js +533 -0
  35. package/src/client/main.js +816 -0
  36. package/src/client/tests/css-hmr-handler.test.ts +360 -0
  37. package/src/client/tests/framework-adapter.test.ts +519 -0
  38. package/src/client/tests/hmr-coordinator.test.ts +176 -0
  39. package/src/client/tests/hydration-option-parsing.test.ts +107 -0
  40. package/src/client/tests/lit-adapter.test.ts +427 -0
  41. package/src/client/tests/preact-adapter.test.ts +353 -0
  42. package/src/client/tests/qwik-adapter.test.ts +343 -0
  43. package/src/client/tests/react-adapter.test.ts +317 -0
  44. package/src/client/tests/solid-adapter.test.ts +396 -0
  45. package/src/client/tests/svelte-adapter.test.ts +387 -0
  46. package/src/client/tests/vue-adapter.test.ts +407 -0
  47. package/src/client/types/framework-runtime.d.ts +68 -0
  48. package/src/client/types/vite-hmr.d.ts +46 -0
  49. package/src/client/types/vite-virtual-modules.d.ts +60 -0
  50. package/src/components/Image.tsx +123 -0
  51. package/src/components/IslandErrorBoundary.tsx +145 -0
  52. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  53. package/src/components/LayoutErrorBoundary.tsx +127 -0
  54. package/src/components/PersistentIsland.tsx +52 -0
  55. package/src/components/StreamingErrorBoundary.tsx +233 -0
  56. package/src/components/StreamingLayout.tsx +538 -0
  57. package/src/components/tests/component-analyzer.test.ts +96 -0
  58. package/src/components/tests/component-detection.test.ts +347 -0
  59. package/src/components/tests/persistent-islands.test.ts +398 -0
  60. package/src/core/components/component-analyzer.ts +192 -0
  61. package/src/core/components/component-detection.ts +508 -0
  62. package/src/core/components/enhanced-framework-detector.ts +500 -0
  63. package/src/core/components/framework-registry.ts +563 -0
  64. package/src/core/components/tests/enhanced-framework-detector.test.ts +577 -0
  65. package/src/core/components/tests/framework-registry.test.ts +465 -0
  66. package/src/core/content/mdx-processor.ts +46 -0
  67. package/src/core/integrations/README.md +282 -0
  68. package/src/core/integrations/index.ts +19 -0
  69. package/src/core/integrations/loader.ts +125 -0
  70. package/src/core/integrations/registry.ts +195 -0
  71. package/src/core/islands/island-persistence.ts +325 -0
  72. package/src/core/islands/island-state-serializer.ts +258 -0
  73. package/src/core/islands/persistent-island-context.tsx +80 -0
  74. package/src/core/islands/use-persistent-state.ts +68 -0
  75. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  76. package/src/core/layout/layout-cache-manager.ts +485 -0
  77. package/src/core/layout/layout-composer.ts +357 -0
  78. package/src/core/layout/layout-data-loader.ts +516 -0
  79. package/src/core/layout/layout-discovery.ts +243 -0
  80. package/src/core/layout/layout-matcher.ts +299 -0
  81. package/src/core/layout/layout-types.ts +110 -0
  82. package/src/core/layout/tests/enhanced-layout-resolver.test.ts +477 -0
  83. package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
  84. package/src/core/layout/tests/layout-composer.test.ts +486 -0
  85. package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
  86. package/src/core/layout/tests/layout-discovery.test.ts +253 -0
  87. package/src/core/layout/tests/layout-matcher.test.ts +480 -0
  88. package/src/core/modules/framework-module-resolver.ts +273 -0
  89. package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
  90. package/src/core/modules/tests/module-resolution-integration.test.ts +117 -0
  91. package/src/islands/component-analysis.ts +213 -0
  92. package/src/islands/css-utils.ts +565 -0
  93. package/src/islands/discovery/index.ts +80 -0
  94. package/src/islands/discovery/registry.ts +340 -0
  95. package/src/islands/discovery/resolver.ts +477 -0
  96. package/src/islands/discovery/scanner.ts +386 -0
  97. package/src/islands/discovery/tests/island-discovery.test.ts +881 -0
  98. package/src/islands/discovery/types.ts +117 -0
  99. package/src/islands/discovery/validator.ts +544 -0
  100. package/src/islands/discovery/watcher.ts +368 -0
  101. package/src/islands/framework-detection.ts +428 -0
  102. package/src/islands/integration-loader.ts +490 -0
  103. package/src/islands/island.tsx +565 -0
  104. package/src/islands/render-cache.ts +550 -0
  105. package/src/islands/types.ts +80 -0
  106. package/src/islands/universal-css-collector.ts +157 -0
  107. package/src/islands/universal-head-collector.ts +137 -0
  108. package/src/layout-system.d.ts +592 -0
  109. package/src/layout-system.ts +218 -0
  110. package/src/middleware/__tests__/discovery.test.ts +107 -0
  111. package/src/middleware/discovery.ts +268 -0
  112. package/src/middleware/executor.ts +315 -0
  113. package/src/middleware/index.ts +76 -0
  114. package/src/middleware/types.ts +99 -0
  115. package/src/nitro/build-config.ts +576 -0
  116. package/src/nitro/config.ts +483 -0
  117. package/src/nitro/error-handler.ts +636 -0
  118. package/src/nitro/index.ts +173 -0
  119. package/src/nitro/island-manifest.ts +584 -0
  120. package/src/nitro/middleware-adapter.ts +260 -0
  121. package/src/nitro/renderer.ts +1458 -0
  122. package/src/nitro/route-discovery.ts +439 -0
  123. package/src/nitro/types.ts +321 -0
  124. package/src/render/collect-css.ts +198 -0
  125. package/src/render/error-pages.ts +79 -0
  126. package/src/render/isolated-ssr-renderer.ts +654 -0
  127. package/src/render/ssr.ts +1030 -0
  128. package/src/schemas/api.ts +30 -0
  129. package/src/schemas/core.ts +64 -0
  130. package/src/schemas/index.ts +212 -0
  131. package/src/schemas/layout.ts +279 -0
  132. package/src/schemas/routing/index.ts +38 -0
  133. package/src/schemas/routing.ts +376 -0
  134. package/src/types/as-island.ts +20 -0
  135. package/src/types/image.d.ts +106 -0
  136. package/src/types/index.d.ts +22 -0
  137. package/src/types/island-jsx.d.ts +33 -0
  138. package/src/types/island-prop.d.ts +20 -0
  139. package/src/types/layout.ts +285 -0
  140. package/src/types/mdx.d.ts +6 -0
  141. package/src/types/routing.ts +555 -0
  142. package/src/types/tests/layout-types.test.ts +197 -0
  143. package/src/types/types.ts +5 -0
  144. package/src/types/urlpattern.d.ts +49 -0
  145. package/src/types/vite-env.d.ts +11 -0
  146. package/src/utils/dev-logger.ts +299 -0
  147. package/src/utils/fs.ts +151 -0
  148. package/src/vite-plugin/auto-discover.ts +551 -0
  149. package/src/vite-plugin/config.ts +266 -0
  150. package/src/vite-plugin/errors.ts +127 -0
  151. package/src/vite-plugin/image-optimization.ts +151 -0
  152. package/src/vite-plugin/integration-activator.ts +126 -0
  153. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  154. package/src/vite-plugin/module-discovery.ts +189 -0
  155. package/src/vite-plugin/nitro-integration.ts +1334 -0
  156. package/src/vite-plugin/plugin.ts +329 -0
  157. package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
  158. package/src/vite-plugin/types.ts +327 -0
  159. package/src/vite-plugin/validation.ts +228 -0
@@ -0,0 +1,1030 @@
1
+ import { type JSX, h } from 'preact';
2
+ import { render as preactRenderToString } from 'preact-render-to-string';
3
+ import { readFile } from 'node:fs/promises';
4
+ import type { RenderOptions } from '../schemas/core.ts';
5
+ import { getUniversalCSSForHead } from '../islands/universal-css-collector.ts';
6
+ import { getUniversalHeadForInjection } from '../islands/universal-head-collector.ts';
7
+ import { analyzeComponentContent, type AnalyzerOptions } from '../core/components/component-analyzer.ts';
8
+ import type { EnhancedLayoutResolver } from '../core/layout/enhanced-layout-resolver.ts';
9
+ import type { LayoutContext, PageModule } from '../types/layout.ts';
10
+ import { IsolatedSSRRenderer, type SSRIsolationConfig } from './isolated-ssr-renderer.ts';
11
+
12
+ export interface RouteConfig {
13
+ component: () => JSX.Element | Promise<JSX.Element>;
14
+ options?: Partial<RenderOptions>;
15
+ frontmatter?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface RenderStrategy {
19
+ type: 'hydrate' | 'ssr-only';
20
+ reason: string;
21
+ warnings?: string[];
22
+ }
23
+
24
+ /**
25
+ * Automatically injects the client-side hydration script and CSS if not already present
26
+ */
27
+ function injectClientScript(html: string): string {
28
+ let modifiedHtml = html;
29
+
30
+ // Check if there are any islands that need hydration
31
+ const hasIslands = html.includes('data-framework=') || html.includes('data-src=');
32
+
33
+ if (!hasIslands) {
34
+ // No islands found, no need to inject anything
35
+ return html;
36
+ }
37
+
38
+ // Inject universal CSS into the head if not already present
39
+ if (!html.includes('data-universal-ssr="true"')) {
40
+ const universalCSS = getUniversalCSSForHead(true); // Clear after collecting
41
+ if (universalCSS && html.includes('</head>')) {
42
+ modifiedHtml = modifiedHtml.replace('</head>', `${universalCSS}\n</head>`);
43
+ }
44
+ }
45
+
46
+ // Inject universal head content (hydration scripts, etc.) into the head
47
+ const universalHead = getUniversalHeadForInjection(true); // Clear after collecting
48
+ if (universalHead && html.includes('</head>')) {
49
+ modifiedHtml = modifiedHtml.replace('</head>', ` ${universalHead}\n</head>`);
50
+ }
51
+
52
+ // Check if the client script is already included
53
+ if (html.includes('/src/client/main.js') || html.includes('main.js')) {
54
+ return modifiedHtml;
55
+ }
56
+
57
+ // Inject the client script before the closing </body> tag
58
+ const clientScript = '<script type="module" src="/src/client/main.js"></script>';
59
+
60
+ if (modifiedHtml.includes('</body>')) {
61
+ return modifiedHtml.replace('</body>', `${clientScript}\n</body>`);
62
+ }
63
+
64
+ // Fallback: append to the end if no </body> tag found
65
+ return modifiedHtml + clientScript;
66
+ }
67
+
68
+ export interface ComponentRenderOptions {
69
+ forceSSROnly?: boolean;
70
+ detectScripts?: boolean;
71
+ suppressWarnings?: boolean;
72
+ logDecisions?: boolean;
73
+ }
74
+
75
+ interface FrameworkDetection {
76
+ solid: boolean;
77
+ vue: boolean;
78
+ svelte: boolean;
79
+ }
80
+
81
+ // Framework detection patterns
82
+ const FRAMEWORK_PATTERNS = {
83
+ solid: ['solid-js', 'SolidIsland', 'createSignal', '.solid.', 'data-solid-hydrate'],
84
+ vue: ['data-vue-hydrate', '.vue', 'Vue'],
85
+ svelte: ['data-framework="svelte"', '.svelte', 's-'],
86
+ } as const;
87
+
88
+ // Global isolated SSR renderer instance
89
+ let isolatedRenderer: IsolatedSSRRenderer | null = null;
90
+
91
+ /**
92
+ * Gets or creates the isolated SSR renderer
93
+ */
94
+ function getIsolatedRenderer(): IsolatedSSRRenderer {
95
+ if (!isolatedRenderer) {
96
+ const config: Partial<SSRIsolationConfig> = {
97
+ enableStrictIsolation: true,
98
+ allowedCrossFrameworkImports: ['preact', 'preact-render-to-string'],
99
+ errorHandling: 'fallback',
100
+ debugLogging: process.env.NODE_ENV !== 'production',
101
+ };
102
+ isolatedRenderer = new IsolatedSSRRenderer(config);
103
+ }
104
+ return isolatedRenderer;
105
+ }
106
+
107
+ function detectFrameworks(content: string): FrameworkDetection {
108
+ return {
109
+ solid: FRAMEWORK_PATTERNS.solid.some(pattern => content.includes(pattern)),
110
+ vue: FRAMEWORK_PATTERNS.vue.some(pattern => content.includes(pattern)),
111
+ svelte: FRAMEWORK_PATTERNS.svelte.some(pattern => content.includes(pattern)),
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Validates that imports are allowed for the detected framework
117
+ */
118
+ function validateFrameworkImports(componentPath: string, content: string, detectedFramework: string): string[] {
119
+ const warnings: string[] = [];
120
+
121
+ // Extract import statements — match the quoted module specifier at the end of any import line
122
+ const importRegex = /^import\s[^'"]*['"]([^'"]+)['"]/gm;
123
+ const imports: string[] = [];
124
+
125
+ let match;
126
+ while ((match = importRegex.exec(content)) !== null) {
127
+ imports.push(match[1]);
128
+ }
129
+
130
+ // Override framework detection based on naming convention
131
+ let actualFramework = detectedFramework;
132
+ if (componentPath.includes('.solid.')) {
133
+ actualFramework = 'solid';
134
+ } else if (componentPath.includes('.preact.')) {
135
+ actualFramework = 'preact';
136
+ }
137
+
138
+ // Check for problematic cross-framework imports
139
+ const problematicImports = new Map<string, string[]>([
140
+ ['preact', ['solid-js', 'solid-js/web', 'vue', 'svelte']],
141
+ ['solid', ['preact', 'preact-render-to-string', 'vue', 'svelte']],
142
+ ['vue', ['preact', 'solid-js', 'svelte']],
143
+ ['svelte', ['preact', 'solid-js', 'vue']],
144
+ ]);
145
+
146
+ const forbidden = problematicImports.get(actualFramework) || [];
147
+
148
+ for (const importPath of imports) {
149
+ for (const forbiddenPattern of forbidden) {
150
+ if (importPath.startsWith(forbiddenPattern)) {
151
+ warnings.push(
152
+ `Cross-framework import detected: ${actualFramework} component (${componentPath}) importing ${importPath}`,
153
+ );
154
+ }
155
+ }
156
+ }
157
+
158
+ return warnings;
159
+ }
160
+
161
+ function applyStrategyToTag(fullMatch: string, strategy: RenderStrategy): string {
162
+ if (strategy.type === 'ssr-only') {
163
+ return fullMatch
164
+ .replaceAll(/data-hydrate="[^"]*"\s*/g, '')
165
+ .replace('>', ` data-render-strategy="${strategy.type}" data-ssr-reason="${strategy.reason}">`);
166
+ }
167
+ return fullMatch.replace('>', ` data-render-strategy="${strategy.type}" data-hydrate-reason="${strategy.reason}">`);
168
+ }
169
+
170
+ /**
171
+ * Analyzes components in rendered content and adds rendering strategy attributes
172
+ * using the intelligent component detection system with import validation
173
+ */
174
+ async function enhanceContentWithRenderingStrategy(
175
+ content: string,
176
+ renderOptions: ComponentRenderOptions = {},
177
+ ): Promise<string> {
178
+ const hydrateRegex = /(<[^>]*data-hydrate="([^"]*)"[^>]*>)/g;
179
+ let enhancedContent = content;
180
+ const matches = Array.from(content.matchAll(hydrateRegex));
181
+
182
+ for (const match of matches) {
183
+ const [fullMatch, _elementTag, componentPath] = match;
184
+
185
+ try {
186
+ if (fullMatch.includes('data-render-strategy')) {
187
+ continue;
188
+ }
189
+
190
+ const strategy = await determineRenderStrategy(componentPath, renderOptions);
191
+ await validateComponentImports(componentPath, renderOptions);
192
+
193
+ enhancedContent = enhancedContent.replace(fullMatch, applyStrategyToTag(fullMatch, strategy));
194
+
195
+ if (renderOptions.logDecisions === true) {
196
+ console.log(`[SSR Strategy] ${componentPath} -> ${strategy.type.toUpperCase()}: ${strategy.reason}`);
197
+ if (strategy.warnings && strategy.warnings.length > 0 && !renderOptions.suppressWarnings) {
198
+ strategy.warnings.forEach(warning => console.warn(`[SSR Warning] ${componentPath}: ${warning}`));
199
+ }
200
+ }
201
+ } catch (error) {
202
+ console.warn(`Failed to analyze component ${componentPath}:`, error);
203
+ const enhancedTag = fullMatch.replace('>', ` data-render-strategy="hydrate" data-error="analysis-failed">`);
204
+ enhancedContent = enhancedContent.replace(fullMatch, enhancedTag);
205
+ }
206
+ }
207
+
208
+ return enhancedContent;
209
+ }
210
+
211
+ /**
212
+ * Validates component imports to prevent cross-framework contamination
213
+ */
214
+ async function validateComponentImports(
215
+ componentPath: string,
216
+ renderOptions: ComponentRenderOptions = {},
217
+ ): Promise<void> {
218
+ try {
219
+ // Try to read and analyze the component file
220
+ let componentContent: string | undefined;
221
+ let resolvedPath = componentPath;
222
+
223
+ // Handle different path formats
224
+ if (componentPath.startsWith('/')) {
225
+ resolvedPath = componentPath.substring(1);
226
+ }
227
+
228
+ // Try multiple path variations
229
+ const pathVariations = [
230
+ resolvedPath,
231
+ `examples/${resolvedPath.split('/').pop()}`,
232
+ `src/islands/${resolvedPath.split('/').pop()}`,
233
+ `islands/${resolvedPath.split('/').pop()}`,
234
+ ];
235
+
236
+ let foundPath = '';
237
+ for (const pathVariation of pathVariations) {
238
+ try {
239
+ componentContent = await readFile(pathVariation, 'utf-8');
240
+ foundPath = pathVariation;
241
+ break;
242
+ } catch {
243
+ // Continue to next path variation
244
+ continue;
245
+ }
246
+ }
247
+
248
+ if (!foundPath || !componentContent) {
249
+ // Component file not found, skip validation
250
+ return;
251
+ }
252
+
253
+ // Detect framework from content patterns
254
+ const frameworks = detectFrameworks(componentContent);
255
+ let detectedFramework = 'preact'; // default
256
+
257
+ if (frameworks.solid) detectedFramework = 'solid';
258
+ else if (frameworks.vue) detectedFramework = 'vue';
259
+ else if (frameworks.svelte) detectedFramework = 'svelte';
260
+
261
+ // Validate imports for this framework
262
+ const importWarnings = validateFrameworkImports(foundPath, componentContent, detectedFramework);
263
+
264
+ // Log import validation warnings
265
+ if (importWarnings.length > 0 && !renderOptions.suppressWarnings) {
266
+ importWarnings.forEach(warning => console.warn(`[Import Validation] ${warning}`));
267
+ }
268
+ } catch (error) {
269
+ // Validation failed, but don't break the rendering process
270
+ if (renderOptions.logDecisions !== false) {
271
+ console.warn(`Import validation failed for ${componentPath}:`, error);
272
+ }
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Determines the render strategy for a component using intelligent detection
278
+ */
279
+ async function determineRenderStrategy(
280
+ componentPath: string,
281
+ options: ComponentRenderOptions = {},
282
+ ): Promise<RenderStrategy> {
283
+ // Handle explicit SSR-only override
284
+ if (options.forceSSROnly) {
285
+ return {
286
+ type: 'ssr-only',
287
+ reason: 'Explicitly configured for SSR-only rendering',
288
+ };
289
+ }
290
+
291
+ // Quick heuristic checks for known naming patterns that explicitly indicate SSR-only
292
+ if (componentPath.includes('NoHydrate') || componentPath.includes('Static') || componentPath.includes('SSROnly')) {
293
+ return {
294
+ type: 'ssr-only',
295
+ reason: 'Component name explicitly indicates SSR-only rendering',
296
+ };
297
+ }
298
+
299
+ // If script detection is disabled, default to hydration
300
+ if (options.detectScripts === false) {
301
+ return {
302
+ type: 'hydrate',
303
+ reason: 'Script detection disabled, defaulting to hydration',
304
+ };
305
+ }
306
+
307
+ try {
308
+ // Try to read and analyze the component file
309
+ let componentContent: string;
310
+ let resolvedPath = componentPath;
311
+
312
+ // Handle different path formats
313
+ if (componentPath.startsWith('/')) {
314
+ resolvedPath = componentPath.substring(1);
315
+ }
316
+
317
+ // Try multiple path variations
318
+ const pathVariations = [
319
+ resolvedPath,
320
+ `examples/${resolvedPath.split('/').pop()}`,
321
+ `src/islands/${resolvedPath.split('/').pop()}`,
322
+ `islands/${resolvedPath.split('/').pop()}`,
323
+ ];
324
+
325
+ let analysisResult = null;
326
+ for (const pathVariation of pathVariations) {
327
+ try {
328
+ componentContent = await readFile(pathVariation, 'utf-8');
329
+
330
+ // Perform intelligent component analysis
331
+ const analyzerOptions: AnalyzerOptions = {
332
+ forceSSROnly: options.forceSSROnly,
333
+ detectScripts: options.detectScripts,
334
+ suppressWarnings: options.suppressWarnings,
335
+ logDecisions: false, // We'll handle logging at the SSR level
336
+ };
337
+
338
+ analysisResult = analyzeComponentContent(pathVariation, componentContent, analyzerOptions);
339
+ break;
340
+ } catch {
341
+ // Continue to next path variation
342
+ continue;
343
+ }
344
+ }
345
+
346
+ if (analysisResult) {
347
+ return {
348
+ type: analysisResult.decision.shouldHydrate ? 'hydrate' : 'ssr-only',
349
+ reason: analysisResult.decision.reason,
350
+ warnings: analysisResult.decision.warnings,
351
+ };
352
+ }
353
+
354
+ // If we can't read the file, fall back to extension-based heuristics
355
+ return determineStrategyFromPath(componentPath);
356
+ } catch (error) {
357
+ console.warn(`Component analysis failed for ${componentPath}:`, error);
358
+ return {
359
+ type: 'ssr-only',
360
+ reason: 'Analysis failed, defaulting to SSR-only for safety',
361
+ warnings: [`Component analysis error: ${error instanceof Error ? error.message : String(error)}`],
362
+ };
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Fallback strategy determination based on file path and naming conventions
368
+ */
369
+ function determineStrategyFromPath(componentPath: string): RenderStrategy {
370
+ // Check file extension patterns - but default to SSR-only unless we can confirm hydration is needed
371
+ if (
372
+ componentPath.endsWith('.vue') ||
373
+ componentPath.endsWith('.svelte') ||
374
+ componentPath.endsWith('.tsx') ||
375
+ componentPath.endsWith('.jsx')
376
+ ) {
377
+ // Framework components default to SSR-only unless they have explicit hydrate functions
378
+ return {
379
+ type: 'ssr-only',
380
+ reason: 'Framework component detected, defaulting to SSR-only (hydration requires explicit hydrate function)',
381
+ };
382
+ }
383
+
384
+ // Unknown file type, default to SSR-only for safety
385
+ return {
386
+ type: 'ssr-only',
387
+ reason: 'Unknown component type, defaulting to SSR-only for safety',
388
+ };
389
+ }
390
+
391
+ function generateMetaTags(options: Partial<RenderOptions>): string {
392
+ return options.meta?.map(({ name, content }) => `<meta name="${name}" content="${content}">`).join('\n ') || '';
393
+ }
394
+
395
+ function generateStyleTags(options: Partial<RenderOptions>): string {
396
+ const styleTags = options.styles?.map(href => `<link rel="stylesheet" href="${href}">`).join('\n ') || '';
397
+
398
+ // Note: CSS from all frameworks (including Svelte) is now handled by the universal CSS collector
399
+ // which is injected in generateHead() via getUniversalCSSForHead()
400
+
401
+ return styleTags;
402
+ }
403
+
404
+ function generateScriptTags(options: Partial<RenderOptions>): string {
405
+ return (
406
+ options.scripts
407
+ ?.map(script => {
408
+ if (typeof script === 'string') {
409
+ return `<script src="${script}" defer></script>`;
410
+ }
411
+ const attrs = script.src ? `src="${script.src}"` : '';
412
+ const type = script.type ? `type="${script.type}"` : '';
413
+ const content = script.content || '';
414
+ return `<script ${attrs} ${type}>${content}</script>`;
415
+ })
416
+ .join('\n ') || ''
417
+ );
418
+ }
419
+
420
+ function generateClientScripts(isDev: boolean, _frameworks: FrameworkDetection): string {
421
+ const baseScript = isDev ? '/src/client/main.js' : '/dist/client.js';
422
+
423
+ return `
424
+ <script type="module" src="${baseScript}"></script>`;
425
+ }
426
+
427
+ function generateHMRScript(isDev: boolean, viteHmrPort?: number): string {
428
+ return isDev && viteHmrPort
429
+ ? `
430
+ <script type="module">
431
+ if (import.meta.hot) {
432
+ import.meta.hot.accept();
433
+ }
434
+ </script>`
435
+ : '';
436
+ }
437
+
438
+ /** Escape HTML special characters for safe interpolation into HTML */
439
+ function escapeHtml(str: string): string {
440
+ return str
441
+ .replaceAll('&', '&amp;')
442
+ .replaceAll('<', '&lt;')
443
+ .replaceAll('>', '&gt;')
444
+ .replaceAll('"', '&quot;')
445
+ .replaceAll("'", '&#039;');
446
+ }
447
+
448
+ function generateHead(options: Partial<RenderOptions>, frameworks: FrameworkDetection, viteHmrPort?: number): string {
449
+ const isDev = process.env.NODE_ENV !== 'production';
450
+
451
+ const metaTags = generateMetaTags(options);
452
+ const styleTags = generateStyleTags(options);
453
+ const scriptTags = generateScriptTags(options);
454
+ const clientScripts = generateClientScripts(isDev, frameworks);
455
+ const hmrScript = generateHMRScript(isDev, viteHmrPort);
456
+
457
+ // Collect CSS from all framework integrations
458
+ const universalCSS = getUniversalCSSForHead(true); // Clear after collecting
459
+
460
+ // Collect head content (hydration scripts, etc.) from all framework integrations
461
+ const universalHead = getUniversalHeadForInjection(true); // Clear after collecting
462
+
463
+ // Generate importmap for browser to resolve integration packages
464
+ const importMap = `
465
+ <script type="importmap">
466
+ {
467
+ "imports": {
468
+ "@useavalon/preact/client": "/packages/integrations/preact/client/index.ts",
469
+ "@useavalon/vue/client": "/packages/integrations/vue/client/index.ts",
470
+ "@useavalon/solid/client": "/packages/integrations/solid/client/index.ts",
471
+ "@useavalon/svelte/client": "/packages/integrations/svelte/client/index.ts",
472
+ "@useavalon/shared": "/packages/integrations/core/types.ts"
473
+ }
474
+ }
475
+ </script>`;
476
+
477
+ return `
478
+ <head>
479
+ <meta charset="utf-8">
480
+ <meta name="viewport" content="width=device-width, initial-scale=1">
481
+ ${metaTags}
482
+ <title>${escapeHtml(String(options.title || 'Avalon App'))}</title>
483
+ ${importMap}
484
+ ${styleTags}
485
+ ${universalCSS}
486
+ ${universalHead}
487
+ ${scriptTags}${clientScripts}${hmrScript}
488
+ </head>`.trim();
489
+ }
490
+
491
+ export async function renderToHtml(
492
+ routeConfig: RouteConfig,
493
+ defaultOptions: Partial<RenderOptions> = {},
494
+ viteHmrPort?: number,
495
+ renderOptions: ComponentRenderOptions = {},
496
+ ): Promise<string> {
497
+ try {
498
+ let content: string;
499
+ let frameworks: FrameworkDetection;
500
+
501
+ if (renderOptions.forceSSROnly === true) {
502
+ const componentResult = routeConfig.component();
503
+ const resolvedComponent = componentResult instanceof Promise ? await componentResult : componentResult;
504
+ content = preactRenderToString(resolvedComponent);
505
+ frameworks = detectFrameworks(content);
506
+ } else {
507
+ ({ content, frameworks } = await renderWithIsolationOrFallback(routeConfig, renderOptions));
508
+ }
509
+
510
+ content = await enhanceContentWithRenderingStrategy(content, renderOptions);
511
+ const options = { ...defaultOptions, ...routeConfig.options };
512
+ const head = generateHead(options, frameworks, viteHmrPort);
513
+
514
+ return `<!DOCTYPE html>\n<html lang="en">\n${head}\n<body>\n${content}\n</body>\n</html>`;
515
+ } catch (error) {
516
+ console.error('Error rendering component:', error);
517
+ throw new Error('Failed to render component');
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Render to HTML with layout system support
523
+ */
524
+ export async function renderToHtmlWithLayouts(
525
+ routeConfig: RouteConfig,
526
+ layoutResolver: EnhancedLayoutResolver,
527
+ layoutContext: LayoutContext,
528
+ routePath: string,
529
+ defaultOptions: Partial<RenderOptions> = {},
530
+ viteHmrPort?: number,
531
+ renderOptions: ComponentRenderOptions = {},
532
+ ): Promise<string> {
533
+ try {
534
+ const routeConfigExtended = routeConfig as RouteConfig & Partial<PageModule>;
535
+ const pageModule: PageModule = {
536
+ default: routeConfig.component,
537
+ layoutConfig: routeConfigExtended.layoutConfig,
538
+ loader: routeConfigExtended.loader,
539
+ frontmatter: routeConfig.frontmatter,
540
+ };
541
+
542
+ const resolvedLayout = await layoutResolver.resolveAndRender(routePath, pageModule, layoutContext);
543
+
544
+ if (resolvedLayout.handlers.length === 0) {
545
+ return await renderToHtml(routeConfig, defaultOptions, viteHmrPort, renderOptions);
546
+ }
547
+
548
+ const pageContent = await renderPageContent(routeConfig, routePath, renderOptions);
549
+ const wrappedContent = await applyLayoutChain(pageContent, resolvedLayout, pageModule, layoutContext, routePath);
550
+ const enhancedContent = await enhanceContentWithRenderingStrategy(wrappedContent, renderOptions);
551
+
552
+ return assembleLayoutHtml(enhancedContent, routeConfig, defaultOptions, viteHmrPort);
553
+ } catch (error) {
554
+ console.error('Error rendering component with layouts:', error);
555
+ try {
556
+ return await renderToHtml(routeConfig, defaultOptions, viteHmrPort, renderOptions);
557
+ } catch (fallbackError) {
558
+ console.error('Fallback rendering also failed:', fallbackError);
559
+ throw new Error('Failed to render component with layouts and fallback failed');
560
+ }
561
+ }
562
+ }
563
+
564
+ function assembleLayoutHtml(
565
+ enhancedContent: string,
566
+ routeConfig: RouteConfig,
567
+ defaultOptions: Partial<RenderOptions>,
568
+ viteHmrPort: number | undefined,
569
+ ): string {
570
+ const isCompleteDoc =
571
+ enhancedContent.trim().startsWith('<!DOCTYPE html>') || enhancedContent.trim().startsWith('<html');
572
+ if (isCompleteDoc) {
573
+ return injectClientScript(enhancedContent);
574
+ }
575
+ const frameworks = detectFrameworks(enhancedContent);
576
+ const options = { ...defaultOptions, ...routeConfig.options };
577
+ const head = generateHead(options, frameworks, viteHmrPort);
578
+ return injectClientScript(`<!DOCTYPE html>\n<html lang="en">\n${head}\n<body>\n${enhancedContent}\n</body>\n</html>`);
579
+ }
580
+
581
+ /**
582
+ * Streaming render options
583
+ */
584
+ export interface StreamingRenderOptions extends ComponentRenderOptions {
585
+ /**
586
+ * Callback when the shell (initial HTML) is ready to stream
587
+ */
588
+ onShellReady?: () => void;
589
+
590
+ /**
591
+ * Callback when an error occurs before streaming starts
592
+ */
593
+ onShellError?: (error: Error) => void;
594
+
595
+ /**
596
+ * Callback when all content has been rendered
597
+ */
598
+ onAllReady?: () => void;
599
+
600
+ /**
601
+ * Callback for any error during rendering
602
+ */
603
+ onError?: (error: Error) => void;
604
+ }
605
+
606
+ /**
607
+ * Renders route component content to an HTML string, using isolated rendering with fallback.
608
+ */
609
+ async function renderStreamContent(
610
+ routeConfig: RouteConfig,
611
+ defaultOptions: Partial<RenderOptions>,
612
+ viteHmrPort: number | undefined,
613
+ renderOptions: StreamingRenderOptions,
614
+ ): Promise<{ head: string; content: string }> {
615
+ let content: string;
616
+ let frameworks: FrameworkDetection;
617
+
618
+ if (renderOptions.forceSSROnly === true) {
619
+ const componentResult = routeConfig.component();
620
+ const resolvedComponent = componentResult instanceof Promise ? await componentResult : componentResult;
621
+ content = preactRenderToString(resolvedComponent);
622
+ frameworks = detectFrameworks(content);
623
+ } else {
624
+ ({ content, frameworks } = await renderWithIsolationOrFallback(routeConfig, renderOptions));
625
+ }
626
+
627
+ content = await enhanceContentWithRenderingStrategy(content, renderOptions);
628
+ const options = { ...defaultOptions, ...routeConfig.options };
629
+ const head = generateHead(options, frameworks, viteHmrPort);
630
+ return { head, content };
631
+ }
632
+
633
+ async function renderWithIsolationOrFallback(
634
+ routeConfig: RouteConfig,
635
+ renderOptions: StreamingRenderOptions,
636
+ ): Promise<{ content: string; frameworks: FrameworkDetection }> {
637
+ try {
638
+ const renderer = getIsolatedRenderer();
639
+ const isolatedResult = await renderer.renderWithIsolation({
640
+ componentPath: 'route-component',
641
+ component: routeConfig.component,
642
+ });
643
+
644
+ if (!isolatedResult.success) {
645
+ throw new Error(`Isolated rendering failed: ${isolatedResult.errors.join(', ')}`);
646
+ }
647
+
648
+ if (isolatedResult.warnings.length > 0 && !renderOptions.suppressWarnings) {
649
+ isolatedResult.warnings.forEach(w => console.warn(`[SSR Isolation] ${w}`));
650
+ }
651
+
652
+ return { content: isolatedResult.html, frameworks: detectFrameworks(isolatedResult.html) };
653
+ } catch (isolatedError) {
654
+ console.warn('[SSR] Isolated rendering failed, falling back to standard rendering:', isolatedError);
655
+ const componentResult = routeConfig.component();
656
+ const resolvedComponent = componentResult instanceof Promise ? await componentResult : componentResult;
657
+ const content = preactRenderToString(resolvedComponent);
658
+ return { content, frameworks: detectFrameworks(content) };
659
+ }
660
+ }
661
+
662
+ function handleStreamError(
663
+ err: Error,
664
+ shellSent: boolean,
665
+ controller: ReadableStreamDefaultController<Uint8Array>,
666
+ encoder: TextEncoder,
667
+ renderOptions: StreamingRenderOptions,
668
+ label = 'Streaming Error',
669
+ componentId = 'route-component',
670
+ ): void {
671
+ console.error(`[${label}]`, {
672
+ message: err.message,
673
+ stack: err.stack,
674
+ shellSent,
675
+ timestamp: new Date().toISOString(),
676
+ });
677
+ renderOptions.onError?.(err);
678
+
679
+ if (shellSent) {
680
+ console.log(`[${label}] Mid-stream error detected, injecting error boundary`);
681
+ try {
682
+ controller.enqueue(encoder.encode(generateMidStreamErrorBoundary(err, componentId)));
683
+ controller.enqueue(encoder.encode('\n</body>\n</html>'));
684
+ } catch (injectError) {
685
+ console.error(`[${label}] Failed to inject error boundary:`, injectError);
686
+ }
687
+ } else {
688
+ renderOptions.onShellError?.(err);
689
+ controller.enqueue(encoder.encode(generateErrorPage(err)));
690
+ }
691
+ controller.close();
692
+ }
693
+
694
+ /**
695
+ * Renders a route to a streaming HTML response
696
+ * This is the streaming equivalent of renderToHtml()
697
+ */
698
+ export async function renderToHtmlStream(
699
+ routeConfig: RouteConfig,
700
+ defaultOptions: Partial<RenderOptions> = {},
701
+ viteHmrPort?: number,
702
+ renderOptions: StreamingRenderOptions = {},
703
+ ): Promise<ReadableStream<Uint8Array>> {
704
+ const encoder = new TextEncoder();
705
+ let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
706
+ let shellSent = false;
707
+
708
+ const stream = new ReadableStream<Uint8Array>({
709
+ async start(ctrl) {
710
+ controller = ctrl;
711
+ try {
712
+ const { head, content } = await renderStreamContent(routeConfig, defaultOptions, viteHmrPort, renderOptions);
713
+
714
+ controller.enqueue(encoder.encode(`<!DOCTYPE html>\n<html lang="en">\n${head}\n<body>\n`));
715
+ shellSent = true;
716
+ renderOptions.onShellReady?.();
717
+
718
+ controller.enqueue(encoder.encode(content));
719
+ controller.enqueue(encoder.encode('\n</body>\n</html>'));
720
+ renderOptions.onAllReady?.();
721
+ controller.close();
722
+ } catch (error) {
723
+ handleStreamError(
724
+ error instanceof Error ? error : new Error(String(error)),
725
+ shellSent,
726
+ controller,
727
+ encoder,
728
+ renderOptions,
729
+ );
730
+ }
731
+ },
732
+ cancel() {
733
+ try {
734
+ controller?.close();
735
+ } catch {
736
+ /* already closed */
737
+ }
738
+ },
739
+ });
740
+
741
+ return stream;
742
+ }
743
+
744
+ /**
745
+ * Renders a route with layouts to a streaming HTML response
746
+ * This is the streaming equivalent of renderToHtmlWithLayouts()
747
+ */
748
+ export async function renderToHtmlStreamWithLayouts(
749
+ routeConfig: RouteConfig,
750
+ layoutResolver: EnhancedLayoutResolver,
751
+ layoutContext: LayoutContext,
752
+ routePath: string,
753
+ defaultOptions: Partial<RenderOptions> = {},
754
+ viteHmrPort?: number,
755
+ renderOptions: StreamingRenderOptions = {},
756
+ ): Promise<ReadableStream<Uint8Array>> {
757
+ const encoder = new TextEncoder();
758
+ let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
759
+ let shellSent = false;
760
+
761
+ const stream = new ReadableStream<Uint8Array>({
762
+ async start(ctrl) {
763
+ controller = ctrl;
764
+ try {
765
+ const routeConfigExtended = routeConfig as RouteConfig & Partial<PageModule>;
766
+ const pageModule: PageModule = {
767
+ default: routeConfig.component,
768
+ layoutConfig: routeConfigExtended.layoutConfig,
769
+ loader: routeConfigExtended.loader,
770
+ frontmatter: routeConfig.frontmatter,
771
+ };
772
+
773
+ const resolvedLayout = await layoutResolver.resolveAndRender(routePath, pageModule, layoutContext);
774
+
775
+ if (resolvedLayout.handlers.length === 0) {
776
+ const fallbackStream = await renderToHtmlStream(routeConfig, defaultOptions, viteHmrPort, renderOptions);
777
+ const reader = fallbackStream.getReader();
778
+ try {
779
+ while (true) {
780
+ const { done, value } = await reader.read();
781
+ if (done) break;
782
+ controller.enqueue(value);
783
+ }
784
+ } finally {
785
+ reader.releaseLock();
786
+ }
787
+ controller.close();
788
+ return;
789
+ }
790
+
791
+ const pageContent = await renderPageContent(routeConfig, routePath, renderOptions);
792
+ const wrappedContent = await applyLayoutChain(
793
+ pageContent,
794
+ resolvedLayout,
795
+ pageModule,
796
+ layoutContext,
797
+ routePath,
798
+ );
799
+ const enhancedContent = await enhanceContentWithRenderingStrategy(wrappedContent, renderOptions);
800
+
801
+ const isCompleteDoc =
802
+ enhancedContent.trim().startsWith('<!DOCTYPE html>') || enhancedContent.trim().startsWith('<html');
803
+ if (isCompleteDoc) {
804
+ const finalHtml = injectClientScript(enhancedContent);
805
+ controller.enqueue(encoder.encode(finalHtml));
806
+ shellSent = true;
807
+ renderOptions.onShellReady?.();
808
+ renderOptions.onAllReady?.();
809
+ controller.close();
810
+ return;
811
+ }
812
+
813
+ const frameworks = detectFrameworks(enhancedContent);
814
+ const options = { ...defaultOptions, ...routeConfig.options };
815
+ const head = generateHead(options, frameworks, viteHmrPort);
816
+
817
+ controller.enqueue(encoder.encode(`<!DOCTYPE html>\n<html lang="en">\n${head}\n<body>\n`));
818
+ shellSent = true;
819
+ renderOptions.onShellReady?.();
820
+
821
+ controller.enqueue(encoder.encode(enhancedContent));
822
+ controller.enqueue(encoder.encode('\n</body>\n</html>'));
823
+ renderOptions.onAllReady?.();
824
+ controller.close();
825
+ } catch (error) {
826
+ handleStreamError(
827
+ error instanceof Error ? error : new Error(String(error)),
828
+ shellSent,
829
+ controller,
830
+ encoder,
831
+ renderOptions,
832
+ 'Streaming Error with Layouts',
833
+ `layout-${routePath}`,
834
+ );
835
+ }
836
+ },
837
+ cancel() {
838
+ try {
839
+ controller?.close();
840
+ } catch {
841
+ /* already closed */
842
+ }
843
+ },
844
+ });
845
+
846
+ return stream;
847
+ }
848
+
849
+ async function renderPageContent(
850
+ routeConfig: RouteConfig,
851
+ routePath: string,
852
+ renderOptions: StreamingRenderOptions,
853
+ ): Promise<string> {
854
+ if (renderOptions.forceSSROnly === true) {
855
+ const componentResult = routeConfig.component();
856
+ const resolved = componentResult instanceof Promise ? await componentResult : componentResult;
857
+ return preactRenderToString(resolved);
858
+ }
859
+ try {
860
+ const renderer = getIsolatedRenderer();
861
+ const isolatedResult = await renderer.renderWithIsolation({
862
+ componentPath: routePath,
863
+ component: routeConfig.component,
864
+ });
865
+ if (!isolatedResult.success) throw new Error(`Isolated rendering failed: ${isolatedResult.errors.join(', ')}`);
866
+ if (isolatedResult.warnings.length > 0 && !renderOptions.suppressWarnings) {
867
+ isolatedResult.warnings.forEach(w => console.warn(`[SSR Isolation] ${w}`));
868
+ }
869
+ return isolatedResult.html;
870
+ } catch (isolatedError) {
871
+ console.warn('[SSR] Isolated page rendering failed, falling back to standard rendering:', isolatedError);
872
+ const componentResult = routeConfig.component();
873
+ const resolved = componentResult instanceof Promise ? await componentResult : componentResult;
874
+ return preactRenderToString(resolved);
875
+ }
876
+ }
877
+
878
+ async function applyLayoutChain(
879
+ pageContent: string,
880
+ resolvedLayout: Awaited<ReturnType<EnhancedLayoutResolver['resolveAndRender']>>,
881
+ pageModule: PageModule,
882
+ layoutContext: LayoutContext,
883
+ routePath: string,
884
+ ): Promise<string> {
885
+ // Build a composed JSX tree: outermost layout wraps inner layouts wraps page content.
886
+ // The innermost layout receives the page content as actual JSX children,
887
+ // eliminating the need for dangerouslySetInnerHTML in layout components.
888
+ //
889
+ // We start from the innermost layout and work outward, building a nested
890
+ // JSX element tree. The final tree is rendered to HTML in one pass.
891
+
892
+ // Start with the page content as a raw-HTML JSX node.
893
+ // Preact's `dangerouslySetInnerHTML` is used here at the framework level
894
+ // so layout authors never need to use it themselves.
895
+ let tree: JSX.Element = h('avalon-page-content', { dangerouslySetInnerHTML: { __html: pageContent } });
896
+
897
+ // Track whether the final output was already rendered to HTML by an async
898
+ // layout (e.g. the root layout that produces a full <html> document).
899
+ let preRenderedHtml: string | null = null;
900
+
901
+ // Wrap from innermost to outermost layout
902
+ for (let i = resolvedLayout.handlers.length - 1; i >= 0; i--) {
903
+ const handler = resolvedLayout.handlers[i];
904
+ const layoutData = resolvedLayout.dataLoaders[i] ? await resolvedLayout.dataLoaders[i](layoutContext) : {};
905
+ const layoutProps = {
906
+ data: layoutData,
907
+ frontmatter: pageModule.frontmatter || {},
908
+ route: { path: routePath, params: layoutContext.params, query: layoutContext.query },
909
+ } as Record<string, unknown>;
910
+
911
+ // Layout components may be async when they contain island transforms
912
+ // (the page-island-transform plugin injects `await renderIsland(...)` calls).
913
+ // Preact's renderToString doesn't support async components, so we
914
+ // pre-resolve async layouts here before composing the JSX tree.
915
+ const component = handler.component as any;
916
+ const result = component({ ...layoutProps, children: tree });
917
+ if (result instanceof Promise) {
918
+ const resolved = await result;
919
+ const html = preactRenderToString(resolved);
920
+ // If this is the outermost layout (i === 0) or it produced a full
921
+ // HTML document, keep the rendered string directly so downstream
922
+ // code (assembleLayoutHtml) can detect the <html> tag and preserve
923
+ // the layout's own <head> (which may contain stylesheet links like
924
+ // syntax-highlighting.css).
925
+ if (i === 0) {
926
+ preRenderedHtml = html;
927
+ } else {
928
+ tree = h('avalon-layout-fragment', { dangerouslySetInnerHTML: { __html: html } });
929
+ }
930
+ } else {
931
+ // Synchronous layout — use standard Preact composition
932
+ tree = h(component, layoutProps, tree);
933
+ }
934
+ }
935
+
936
+ // If the outermost layout was async and already rendered, return directly.
937
+ if (preRenderedHtml !== null) {
938
+ return preRenderedHtml;
939
+ }
940
+
941
+ // Render the entire composed tree to HTML in one pass
942
+ try {
943
+ const renderer = getIsolatedRenderer();
944
+ const result = await renderer.renderWithIsolation({
945
+ componentPath: `layout-chain-${routePath}`,
946
+ component: () => tree,
947
+ });
948
+ if (result.success) {
949
+ return result.html;
950
+ }
951
+ } catch {
952
+ // Fall through to standard rendering
953
+ }
954
+ return preactRenderToString(tree);
955
+ }
956
+
957
+ /**
958
+ * Generates an error page for streaming errors
959
+ */
960
+ function generateErrorPage(error: Error): string {
961
+ const isDev = process.env.NODE_ENV !== 'production';
962
+
963
+ return `<!DOCTYPE html>
964
+ <html lang="en">
965
+ <head>
966
+ <meta charset="utf-8">
967
+ <meta name="viewport" content="width=device-width, initial-scale=1">
968
+ <title>Error</title>
969
+ <style>
970
+ body {
971
+ font-family: system-ui, -apple-system, sans-serif;
972
+ margin: 0;
973
+ padding: 40px;
974
+ background: #f5f5f5;
975
+ }
976
+ .error-container {
977
+ max-width: 600px;
978
+ margin: 0 auto;
979
+ background: white;
980
+ padding: 40px;
981
+ border-radius: 8px;
982
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
983
+ }
984
+ h1 {
985
+ color: #d32f2f;
986
+ margin-top: 0;
987
+ }
988
+ pre {
989
+ background: #f5f5f5;
990
+ padding: 16px;
991
+ border-radius: 4px;
992
+ overflow-x: auto;
993
+ }
994
+ </style>
995
+ </head>
996
+ <body>
997
+ <div class="error-container">
998
+ <h1>Server Error</h1>
999
+ <p>An error occurred while rendering the page:</p>
1000
+ <pre>${error.message}</pre>
1001
+ ${isDev && error.stack ? `<pre>${error.stack}</pre>` : ''}
1002
+ </div>
1003
+ </body>
1004
+ </html>`;
1005
+ }
1006
+
1007
+ function generateMidStreamErrorBoundary(error: Error, componentId?: string): string {
1008
+ const isDev = process.env.NODE_ENV !== 'production';
1009
+ const componentIdHtml = componentId ? `<p><strong>Component ID:</strong> ${componentId}</p>` : '';
1010
+ const stackHtml = error.stack
1011
+ ? `<pre style="background:#f5f5f5;padding:10px;border-radius:4px;overflow-x:auto;font-size:12px;margin-top:10px">${error.stack}</pre>`
1012
+ : '';
1013
+ const devDetails = isDev
1014
+ ? `<details style="margin-top:15px"><summary style="cursor:pointer;color:#856404;font-weight:bold">Error Details (Development Mode)</summary><div style="margin-top:10px">${componentIdHtml}<p><strong>Error:</strong> ${error.message}</p>${stackHtml}</div></details>`
1015
+ : '';
1016
+ const componentAttr = componentId ? ` data-component-id="${componentId}"` : '';
1017
+
1018
+ return `
1019
+ <div class="streaming-error-boundary" data-error-boundary="true"${componentAttr}>
1020
+ <div class="error-boundary-container" style="background:#fff3cd;border:2px solid #ffc107;border-radius:8px;padding:20px;margin:20px 0;font-family:system-ui,-apple-system,sans-serif">
1021
+ <div class="error-boundary-header" style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
1022
+ <span style="font-size:24px">⚠️</span>
1023
+ <h3 style="margin:0;color:#856404">Component Error</h3>
1024
+ </div>
1025
+ <p style="margin:10px 0;color:#856404">An error occurred while rendering this component. The rest of the page should work normally.</p>
1026
+ ${devDetails}
1027
+ </div>
1028
+ </div>
1029
+ `;
1030
+ }