@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.
- package/README.md +54 -54
- package/dist/mod.js +1 -0
- package/dist/src/build/integration-bundler-plugin.js +1 -0
- package/dist/src/build/integration-config.js +1 -0
- package/dist/src/build/integration-detection-plugin.js +1 -0
- package/dist/src/build/integration-resolver-plugin.js +1 -0
- package/dist/src/build/island-manifest.js +1 -0
- package/dist/src/build/island-types-generator.js +5 -0
- package/dist/src/build/mdx-island-transform.js +2 -0
- package/dist/src/build/mdx-plugin.js +1 -0
- package/dist/src/build/page-island-transform.js +3 -0
- package/dist/src/build/prop-extractors/index.js +1 -0
- package/dist/src/build/prop-extractors/lit.js +1 -0
- package/dist/src/build/prop-extractors/qwik.js +1 -0
- package/dist/src/build/prop-extractors/solid.js +1 -0
- package/dist/src/build/prop-extractors/svelte.js +1 -0
- package/dist/src/build/prop-extractors/vue.js +1 -0
- package/dist/src/build/sidecar-file-manager.js +1 -0
- package/dist/src/build/sidecar-renderer.js +6 -0
- package/dist/src/client/adapters/index.js +1 -0
- package/dist/src/client/components.js +1 -0
- package/dist/src/client/css-hmr-handler.js +1 -0
- package/dist/src/client/framework-adapter.js +13 -0
- package/dist/src/client/hmr-coordinator.js +1 -0
- package/dist/src/client/hmr-error-overlay.js +214 -0
- package/dist/src/client/main.js +39 -0
- package/{src → dist/src}/client/types/framework-runtime.d.ts +68 -68
- package/{src → dist/src}/client/types/vite-hmr.d.ts +46 -46
- package/dist/src/client/types/vite-virtual-modules.d.ts +70 -0
- package/dist/src/components/Image.js +1 -0
- package/dist/src/components/IslandErrorBoundary.js +1 -0
- package/dist/src/components/LayoutDataErrorBoundary.js +1 -0
- package/dist/src/components/LayoutErrorBoundary.js +1 -0
- package/dist/src/components/PersistentIsland.js +1 -0
- package/dist/src/components/StreamingErrorBoundary.js +1 -0
- package/dist/src/components/StreamingLayout.js +29 -0
- package/dist/src/core/components/component-analyzer.js +1 -0
- package/dist/src/core/components/component-detection.js +5 -0
- package/dist/src/core/components/enhanced-framework-detector.js +1 -0
- package/dist/src/core/components/framework-registry.js +1 -0
- package/dist/src/core/content/mdx-processor.js +1 -0
- package/dist/src/core/integrations/index.js +1 -0
- package/dist/src/core/integrations/loader.js +1 -0
- package/dist/src/core/integrations/registry.js +1 -0
- package/dist/src/core/islands/island-persistence.js +1 -0
- package/dist/src/core/islands/island-state-serializer.js +1 -0
- package/dist/src/core/islands/persistent-island-context.js +1 -0
- package/dist/src/core/islands/use-persistent-state.js +1 -0
- package/dist/src/core/layout/enhanced-layout-resolver.js +1 -0
- package/dist/src/core/layout/layout-cache-manager.js +1 -0
- package/dist/src/core/layout/layout-composer.js +1 -0
- package/dist/src/core/layout/layout-data-loader.js +1 -0
- package/dist/src/core/layout/layout-discovery.js +1 -0
- package/dist/src/core/layout/layout-matcher.js +1 -0
- package/dist/src/core/layout/layout-types.js +1 -0
- package/dist/src/core/modules/framework-module-resolver.js +1 -0
- package/dist/src/islands/component-analysis.js +1 -0
- package/dist/src/islands/css-utils.js +17 -0
- package/dist/src/islands/discovery/index.js +1 -0
- package/dist/src/islands/discovery/registry.js +1 -0
- package/dist/src/islands/discovery/resolver.js +2 -0
- package/dist/src/islands/discovery/scanner.js +1 -0
- package/dist/src/islands/discovery/types.js +1 -0
- package/dist/src/islands/discovery/validator.js +18 -0
- package/dist/src/islands/discovery/watcher.js +1 -0
- package/dist/src/islands/framework-detection.js +1 -0
- package/dist/src/islands/integration-loader.js +1 -0
- package/dist/src/islands/island.js +1 -0
- package/dist/src/islands/render-cache.js +1 -0
- package/dist/src/islands/types.js +1 -0
- package/dist/src/islands/universal-css-collector.js +5 -0
- package/dist/src/islands/universal-head-collector.js +2 -0
- package/{src → dist/src}/layout-system.d.ts +592 -592
- package/dist/src/layout-system.js +1 -0
- package/dist/src/middleware/discovery.js +1 -0
- package/dist/src/middleware/executor.js +1 -0
- package/dist/src/middleware/index.js +1 -0
- package/dist/src/middleware/types.js +1 -0
- package/dist/src/nitro/build-config.js +1 -0
- package/dist/src/nitro/config.js +1 -0
- package/dist/src/nitro/error-handler.js +198 -0
- package/dist/src/nitro/index.js +1 -0
- package/dist/src/nitro/island-manifest.js +2 -0
- package/dist/src/nitro/middleware-adapter.js +1 -0
- package/dist/src/nitro/renderer.js +183 -0
- package/dist/src/nitro/route-discovery.js +1 -0
- package/dist/src/nitro/types.js +1 -0
- package/dist/src/render/collect-css.js +3 -0
- package/{src/render/error-pages.ts → dist/src/render/error-pages.js} +7 -38
- package/dist/src/render/isolated-ssr-renderer.js +1 -0
- package/dist/src/render/ssr.js +90 -0
- package/dist/src/schemas/api.js +1 -0
- package/dist/src/schemas/core.js +1 -0
- package/dist/src/schemas/index.js +1 -0
- package/dist/src/schemas/layout.js +1 -0
- package/dist/src/schemas/routing/index.js +1 -0
- package/dist/src/schemas/routing.js +1 -0
- package/dist/src/types/as-island.js +1 -0
- package/{src → dist/src}/types/image.d.ts +106 -106
- package/{src → dist/src}/types/index.d.ts +22 -22
- package/{src → dist/src}/types/island-jsx.d.ts +33 -33
- package/{src → dist/src}/types/island-prop.d.ts +20 -20
- package/dist/src/types/layout.js +1 -0
- package/{src → dist/src}/types/mdx.d.ts +6 -6
- package/dist/src/types/routing.js +1 -0
- package/dist/src/types/types.js +1 -0
- package/{src → dist/src}/types/urlpattern.d.ts +49 -49
- package/{src → dist/src}/types/vite-env.d.ts +11 -11
- package/dist/src/utils/dev-logger.js +12 -0
- package/dist/src/utils/fs.js +1 -0
- package/dist/src/vite-plugin/auto-discover.js +1 -0
- package/dist/src/vite-plugin/config.js +1 -0
- package/dist/src/vite-plugin/errors.js +1 -0
- package/dist/src/vite-plugin/image-optimization.js +45 -0
- package/dist/src/vite-plugin/integration-activator.js +1 -0
- package/dist/src/vite-plugin/island-sidecar-plugin.js +1 -0
- package/dist/src/vite-plugin/module-discovery.js +1 -0
- package/dist/src/vite-plugin/nitro-integration.js +42 -0
- package/dist/src/vite-plugin/plugin.js +1 -0
- package/dist/src/vite-plugin/types.js +1 -0
- package/dist/src/vite-plugin/validation.js +2 -0
- package/package.json +57 -26
- package/mod.ts +0 -302
- package/src/build/integration-bundler-plugin.ts +0 -116
- package/src/build/integration-config.ts +0 -168
- package/src/build/integration-detection-plugin.ts +0 -117
- package/src/build/integration-resolver-plugin.ts +0 -90
- package/src/build/island-manifest.ts +0 -269
- package/src/build/island-types-generator.ts +0 -476
- package/src/build/mdx-island-transform.ts +0 -464
- package/src/build/mdx-plugin.ts +0 -98
- package/src/build/page-island-transform.ts +0 -598
- package/src/build/prop-extractors/index.ts +0 -21
- package/src/build/prop-extractors/lit.ts +0 -140
- package/src/build/prop-extractors/qwik.ts +0 -16
- package/src/build/prop-extractors/solid.ts +0 -125
- package/src/build/prop-extractors/svelte.ts +0 -194
- package/src/build/prop-extractors/vue.ts +0 -111
- package/src/build/sidecar-file-manager.ts +0 -104
- package/src/build/sidecar-renderer.ts +0 -30
- package/src/client/adapters/index.js +0 -12
- package/src/client/adapters/index.ts +0 -13
- package/src/client/adapters/lit-adapter.js +0 -467
- package/src/client/adapters/lit-adapter.ts +0 -654
- package/src/client/adapters/preact-adapter.js +0 -223
- package/src/client/adapters/preact-adapter.ts +0 -331
- package/src/client/adapters/qwik-adapter.js +0 -259
- package/src/client/adapters/qwik-adapter.ts +0 -345
- package/src/client/adapters/react-adapter.js +0 -220
- package/src/client/adapters/react-adapter.ts +0 -353
- package/src/client/adapters/solid-adapter.js +0 -295
- package/src/client/adapters/solid-adapter.ts +0 -451
- package/src/client/adapters/svelte-adapter.js +0 -368
- package/src/client/adapters/svelte-adapter.ts +0 -524
- package/src/client/adapters/vue-adapter.js +0 -278
- package/src/client/adapters/vue-adapter.ts +0 -467
- package/src/client/components.js +0 -23
- package/src/client/components.ts +0 -35
- package/src/client/css-hmr-handler.js +0 -263
- package/src/client/css-hmr-handler.ts +0 -344
- package/src/client/framework-adapter.js +0 -283
- package/src/client/framework-adapter.ts +0 -462
- package/src/client/hmr-coordinator.js +0 -274
- package/src/client/hmr-coordinator.ts +0 -396
- package/src/client/hmr-error-overlay.js +0 -533
- package/src/client/main.js +0 -816
- package/src/client/types/vite-virtual-modules.d.ts +0 -60
- package/src/components/Image.tsx +0 -123
- package/src/components/IslandErrorBoundary.tsx +0 -145
- package/src/components/LayoutDataErrorBoundary.tsx +0 -141
- package/src/components/LayoutErrorBoundary.tsx +0 -127
- package/src/components/PersistentIsland.tsx +0 -52
- package/src/components/StreamingErrorBoundary.tsx +0 -233
- package/src/components/StreamingLayout.tsx +0 -538
- package/src/core/components/component-analyzer.ts +0 -192
- package/src/core/components/component-detection.ts +0 -508
- package/src/core/components/enhanced-framework-detector.ts +0 -500
- package/src/core/components/framework-registry.ts +0 -563
- package/src/core/content/mdx-processor.ts +0 -46
- package/src/core/integrations/index.ts +0 -19
- package/src/core/integrations/loader.ts +0 -125
- package/src/core/integrations/registry.ts +0 -175
- package/src/core/islands/island-persistence.ts +0 -325
- package/src/core/islands/island-state-serializer.ts +0 -258
- package/src/core/islands/persistent-island-context.tsx +0 -80
- package/src/core/islands/use-persistent-state.ts +0 -68
- package/src/core/layout/enhanced-layout-resolver.ts +0 -322
- package/src/core/layout/layout-cache-manager.ts +0 -485
- package/src/core/layout/layout-composer.ts +0 -357
- package/src/core/layout/layout-data-loader.ts +0 -516
- package/src/core/layout/layout-discovery.ts +0 -243
- package/src/core/layout/layout-matcher.ts +0 -299
- package/src/core/layout/layout-types.ts +0 -110
- package/src/core/modules/framework-module-resolver.ts +0 -273
- package/src/islands/component-analysis.ts +0 -213
- package/src/islands/css-utils.ts +0 -565
- package/src/islands/discovery/index.ts +0 -80
- package/src/islands/discovery/registry.ts +0 -340
- package/src/islands/discovery/resolver.ts +0 -477
- package/src/islands/discovery/scanner.ts +0 -386
- package/src/islands/discovery/types.ts +0 -117
- package/src/islands/discovery/validator.ts +0 -544
- package/src/islands/discovery/watcher.ts +0 -368
- package/src/islands/framework-detection.ts +0 -428
- package/src/islands/integration-loader.ts +0 -490
- package/src/islands/island.tsx +0 -565
- package/src/islands/render-cache.ts +0 -550
- package/src/islands/types.ts +0 -80
- package/src/islands/universal-css-collector.ts +0 -157
- package/src/islands/universal-head-collector.ts +0 -137
- package/src/layout-system.ts +0 -218
- package/src/middleware/discovery.ts +0 -268
- package/src/middleware/executor.ts +0 -315
- package/src/middleware/index.ts +0 -76
- package/src/middleware/types.ts +0 -99
- package/src/nitro/build-config.ts +0 -576
- package/src/nitro/config.ts +0 -483
- package/src/nitro/error-handler.ts +0 -636
- package/src/nitro/index.ts +0 -173
- package/src/nitro/island-manifest.ts +0 -584
- package/src/nitro/middleware-adapter.ts +0 -260
- package/src/nitro/renderer.ts +0 -1471
- package/src/nitro/route-discovery.ts +0 -439
- package/src/nitro/types.ts +0 -321
- package/src/render/collect-css.ts +0 -198
- package/src/render/isolated-ssr-renderer.ts +0 -654
- package/src/render/ssr.ts +0 -1030
- package/src/schemas/api.ts +0 -30
- package/src/schemas/core.ts +0 -64
- package/src/schemas/index.ts +0 -212
- package/src/schemas/layout.ts +0 -279
- package/src/schemas/routing/index.ts +0 -38
- package/src/schemas/routing.ts +0 -376
- package/src/types/as-island.ts +0 -20
- package/src/types/layout.ts +0 -285
- package/src/types/routing.ts +0 -555
- package/src/types/types.ts +0 -5
- package/src/utils/dev-logger.ts +0 -299
- package/src/utils/fs.ts +0 -151
- package/src/vite-plugin/auto-discover.ts +0 -551
- package/src/vite-plugin/config.ts +0 -266
- package/src/vite-plugin/errors.ts +0 -127
- package/src/vite-plugin/image-optimization.ts +0 -156
- package/src/vite-plugin/integration-activator.ts +0 -126
- package/src/vite-plugin/island-sidecar-plugin.ts +0 -176
- package/src/vite-plugin/module-discovery.ts +0 -189
- package/src/vite-plugin/nitro-integration.ts +0 -1354
- package/src/vite-plugin/plugin.ts +0 -401
- package/src/vite-plugin/types.ts +0 -327
- package/src/vite-plugin/validation.ts +0 -228
package/src/nitro/renderer.ts
DELETED
|
@@ -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('&', '&')
|
|
345
|
-
.replaceAll('<', '<')
|
|
346
|
-
.replaceAll('>', '>')
|
|
347
|
-
.replaceAll('"', '"')
|
|
348
|
-
.replaceAll("'", ''');
|
|
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';
|