@octanejs/vite-plugin 0.1.1

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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Production fetch-handler factory + config re-exports.
3
+ *
4
+ * PHASE 1 STUB. `createHandler` is the runtime entry the generated server
5
+ * bundle calls in production; it is wired in Phase 2 (async render, adapter
6
+ * `serve`, asset preload, RPC). Until then it throws a clear error. The
7
+ * `./production` export path stays resolvable, and `resolveOctaneConfig` is
8
+ * re-exported here because the generated server entry imports it from this
9
+ * module (mirrors @ripple-ts/vite-plugin).
10
+ */
11
+
12
+ export { resolveOctaneConfig } from '../load-config.js';
13
+
14
+ /**
15
+ * Create the production fetch handler (Phase 2).
16
+ *
17
+ * @param {unknown} _manifest
18
+ * @param {unknown} _deps
19
+ * @returns {never}
20
+ */
21
+ export function createHandler(_manifest, _deps) {
22
+ throw new Error(
23
+ '[@octanejs/vite-plugin] Production build (createHandler) is Phase 2 — not yet implemented. ' +
24
+ 'Dev SSR works via `vite dev`.',
25
+ );
26
+ }
@@ -0,0 +1,214 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
4
+ import {
5
+ get_component_export,
6
+ get_route_entry_export_name,
7
+ get_route_entry_path,
8
+ } from '../routes.js';
9
+
10
+ /**
11
+ * @typedef {import('@octanejs/vite-plugin').Context} Context
12
+ * @typedef {import('@octanejs/vite-plugin').RenderRoute} RenderRoute
13
+ * @typedef {import('@octanejs/vite-plugin').ResolvedOctaneConfig} ResolvedOctaneConfig
14
+ * @typedef {import('vite').ViteDevServer} ViteDevServer
15
+ */
16
+
17
+ /**
18
+ * octane RenderResult — `render()` is ASYNC and `css` is ALREADY a deduped
19
+ * `<style data-octane="hash">…</style>` string (NOT a Set<string> needing a
20
+ * `get_css_for_hashes` lookup like Ripple). So CSS handling here is identity.
21
+ *
22
+ * @typedef {Object} RenderResult
23
+ * @property {string} head
24
+ * @property {string} body
25
+ * @property {string} css
26
+ */
27
+
28
+ /**
29
+ * Handle SSR rendering for a RenderRoute (dev).
30
+ *
31
+ * @param {RenderRoute} route
32
+ * @param {Context} context
33
+ * @param {ViteDevServer} vite
34
+ * @param {ResolvedOctaneConfig} [octaneConfig]
35
+ * @returns {Promise<Response>}
36
+ */
37
+ export async function handleRenderRoute(route, context, vite, octaneConfig) {
38
+ try {
39
+ // Initialize so the server can register RPC functions from `module server`
40
+ // declarations during SSR module loading (renderer-agnostic; harmless when
41
+ // the app uses no RPC).
42
+ if (!(/** @type {any} */ (globalThis).rpc_modules)) {
43
+ /** @type {any} */ (globalThis).rpc_modules = new Map();
44
+ }
45
+
46
+ // Load the octane server runtime. The wrappers call components directly
47
+ // (no ssrComponent injection — the root must NOT be marker-wrapped), so
48
+ // only `render` is needed here.
49
+ const { render } = await vite.ssrLoadModule('octane/server');
50
+
51
+ // Load the page component (compiled in server mode by octane()).
52
+ const entryPath = get_route_entry_path(route.entry);
53
+ const pageModule = await vite.ssrLoadModule(/** @type {string} */ (entryPath));
54
+ const PageComponent = get_component_export(
55
+ pageModule,
56
+ get_route_entry_export_name(route.entry),
57
+ );
58
+
59
+ if (!PageComponent) {
60
+ throw new Error(`No component found for route ${route.path}`);
61
+ }
62
+
63
+ // Build the component tree (with optional layout).
64
+ let RootComponent;
65
+ const pageProps = { params: context.params };
66
+
67
+ if (route.layout) {
68
+ const layoutModule = await vite.ssrLoadModule(route.layout);
69
+ const LayoutComponent = get_component_export(layoutModule, undefined);
70
+
71
+ if (!LayoutComponent) {
72
+ throw new Error(`No default export found in ${route.layout}`);
73
+ }
74
+
75
+ RootComponent = createLayoutWrapper(
76
+ /** @type {any} */ (LayoutComponent),
77
+ /** @type {any} */ (PageComponent),
78
+ pageProps,
79
+ );
80
+ } else {
81
+ RootComponent = createPropsWrapper(/** @type {any} */ (PageComponent), pageProps);
82
+ }
83
+
84
+ // Render to HTML. octane render() is async; head is '' in Phase 1
85
+ // (no document-head API yet). `body` already contains any inline
86
+ // <script data-octane-suspense> seed.
87
+ /** @type {RenderResult} */
88
+ const { head, body, css } = await render(RootComponent);
89
+
90
+ // CSS is already a ready <style> string (or '') — identity, no re-wrapping.
91
+ const cssContent = css || '';
92
+
93
+ // Build head content with hydration data. The client entry is CONFIG-FREE
94
+ // (importing octane.config.ts into the browser would drag the plugin + the
95
+ // server adapter — and their `node:fs` imports — into the client graph and
96
+ // break at module-eval). So everything the client needs to pick + import
97
+ // the page/layout is serialized HERE: entry path, export name, layout path,
98
+ // and params. routeIndex stays for debugging / Phase-2 static maps.
99
+ const routeData = JSON.stringify({
100
+ entry: entryPath,
101
+ exportName: get_route_entry_export_name(route.entry) ?? null,
102
+ layout: route.layout ?? null,
103
+ routeIndex: getRenderRouteIndex(octaneConfig, route),
104
+ params: context.params,
105
+ });
106
+ const headContent = [
107
+ head,
108
+ cssContent,
109
+ `<script id="__octane_data" type="application/json">${escapeScript(routeData)}</script>`,
110
+ ]
111
+ .filter(Boolean)
112
+ .join('\n');
113
+
114
+ // Load and process index.html template.
115
+ const templatePath = join(vite.config.root, 'index.html');
116
+ let template = await readFile(templatePath, 'utf-8');
117
+
118
+ // Apply Vite's HTML transforms (HMR client, module resolution, etc.).
119
+ template = await vite.transformIndexHtml(context.url.pathname, template);
120
+
121
+ // Replace placeholders.
122
+ let html = template.replace('<!--ssr-head-->', headContent).replace('<!--ssr-body-->', body);
123
+
124
+ // Inject the hydration entry before </body>.
125
+ const hydrationScript = `<script type="module" src="/@id/virtual:octane-hydrate"></script>`;
126
+ html = html.replace('</body>', `${hydrationScript}\n</body>`);
127
+
128
+ return new Response(html, {
129
+ status: 200,
130
+ headers: {
131
+ 'Content-Type': 'text/html; charset=utf-8',
132
+ },
133
+ });
134
+ } catch (error) {
135
+ console.error('[octane] SSR render error:', error);
136
+
137
+ const errorHtml = generateErrorHtml(error, route);
138
+ return new Response(errorHtml, {
139
+ status: 500,
140
+ headers: {
141
+ 'Content-Type': 'text/html; charset=utf-8',
142
+ },
143
+ });
144
+ }
145
+ }
146
+
147
+ /**
148
+ * @param {ResolvedOctaneConfig | undefined} config
149
+ * @param {RenderRoute} route
150
+ * @returns {number | undefined}
151
+ */
152
+ function getRenderRouteIndex(config, route) {
153
+ if (!config) {
154
+ return undefined;
155
+ }
156
+ const renderRoutes = config.router.routes.filter((r) => r.type === 'render');
157
+ const index = renderRoutes.indexOf(route);
158
+ return index === -1 ? undefined : index;
159
+ }
160
+
161
+ /**
162
+ * Escape script content to prevent XSS in the inline JSON data block.
163
+ * @param {string} str
164
+ * @returns {string}
165
+ */
166
+ function escapeScript(str) {
167
+ return str.replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
168
+ }
169
+
170
+ /**
171
+ * Generate an error HTML page for development.
172
+ *
173
+ * @param {unknown} error
174
+ * @param {RenderRoute} route
175
+ * @returns {string}
176
+ */
177
+ function generateErrorHtml(error, route) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ const stack = error instanceof Error ? error.stack : '';
180
+
181
+ return `<!DOCTYPE html>
182
+ <html lang="en">
183
+ <head>
184
+ <meta charset="UTF-8">
185
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
186
+ <title>SSR Error</title>
187
+ <style>
188
+ body { font-family: system-ui, sans-serif; padding: 2rem; background: #1a1a1a; color: #fff; }
189
+ h1 { color: #ff6b6b; }
190
+ pre { background: #2d2d2d; padding: 1rem; border-radius: 4px; overflow-x: auto; }
191
+ .route { color: #888; }
192
+ </style>
193
+ </head>
194
+ <body>
195
+ <h1>SSR Render Error</h1>
196
+ <p class="route">Route: ${route.path} → ${route.entry}</p>
197
+ <pre>${escapeHtml(message)}</pre>
198
+ ${stack ? `<pre>${escapeHtml(stack)}</pre>` : ''}
199
+ </body>
200
+ </html>`;
201
+ }
202
+
203
+ /**
204
+ * Escape HTML entities.
205
+ * @param {string} str
206
+ * @returns {string}
207
+ */
208
+ function escapeHtml(str) {
209
+ return str
210
+ .replace(/&/g, '&amp;')
211
+ .replace(/</g, '&lt;')
212
+ .replace(/>/g, '&gt;')
213
+ .replace(/"/g, '&quot;');
214
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @typedef {import('@octanejs/vite-plugin').Route} Route
3
+ * @typedef {import('@octanejs/vite-plugin').RenderRoute} RenderRoute
4
+ * @typedef {import('@octanejs/vite-plugin').ServerRoute} ServerRoute
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} RouteMatch
9
+ * @property {Route} route
10
+ * @property {Record<string, string>} params
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} CompiledRoute
15
+ * @property {Route} route
16
+ * @property {RegExp} pattern
17
+ * @property {string[]} paramNames
18
+ * @property {number} specificity - Higher = more specific (static > param > catch-all)
19
+ */
20
+
21
+ /**
22
+ * Convert a route path pattern to a RegExp
23
+ * Supports:
24
+ * - Static segments: /about, /api/hello
25
+ * - Named params: /posts/:id, /users/:userId/posts/:postId
26
+ * - Catch-all: /docs/*slug
27
+ *
28
+ * @param {string} path
29
+ * @returns {{ pattern: RegExp, paramNames: string[], specificity: number }}
30
+ */
31
+ function compilePath(path) {
32
+ /** @type {string[]} */
33
+ const paramNames = [];
34
+ let specificity = 0;
35
+
36
+ // Escape special regex characters except our param syntax
37
+ const regexString = path
38
+ .split('/')
39
+ .map((segment) => {
40
+ if (!segment) return '';
41
+
42
+ // Catch-all param: *slug
43
+ if (segment.startsWith('*')) {
44
+ const paramName = segment.slice(1);
45
+ paramNames.push(paramName);
46
+ specificity += 1; // Lowest specificity
47
+ return '(.+)';
48
+ }
49
+
50
+ // Named param: :id
51
+ if (segment.startsWith(':')) {
52
+ const paramName = segment.slice(1);
53
+ paramNames.push(paramName);
54
+ specificity += 10; // Medium specificity
55
+ return '([^/]+)';
56
+ }
57
+
58
+ // Static segment
59
+ specificity += 100; // Highest specificity
60
+ return escapeRegex(segment);
61
+ })
62
+ .join('/');
63
+
64
+ const pattern = new RegExp(`^${regexString || '/'}$`);
65
+ return { pattern, paramNames, specificity };
66
+ }
67
+
68
+ /**
69
+ * Escape special regex characters
70
+ * @param {string} str
71
+ * @returns {string}
72
+ */
73
+ function escapeRegex(str) {
74
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
75
+ }
76
+
77
+ /**
78
+ * Create a router from a list of routes
79
+ * @param {Route[]} routes
80
+ * @returns {{ match: (method: string, pathname: string) => RouteMatch | null }}
81
+ */
82
+ export function createRouter(routes) {
83
+ /** @type {CompiledRoute[]} */
84
+ const compiledRoutes = routes.map((route) => {
85
+ const { pattern, paramNames, specificity } = compilePath(route.path);
86
+ return { route, pattern, paramNames, specificity };
87
+ });
88
+
89
+ // Sort by specificity (higher first) for correct matching order
90
+ compiledRoutes.sort((a, b) => b.specificity - a.specificity);
91
+
92
+ return {
93
+ /**
94
+ * Match a request to a route
95
+ * @param {string} method
96
+ * @param {string} pathname
97
+ * @returns {RouteMatch | null}
98
+ */
99
+ match(method, pathname) {
100
+ for (const { route, pattern, paramNames } of compiledRoutes) {
101
+ // Check method for ServerRoute
102
+ if (route.type === 'server') {
103
+ const methods = /** @type {ServerRoute} */ (route).methods;
104
+ if (!methods.includes(method.toUpperCase())) {
105
+ continue;
106
+ }
107
+ }
108
+
109
+ const match = pathname.match(pattern);
110
+ if (match) {
111
+ /** @type {Record<string, string>} */
112
+ const params = {};
113
+ for (let i = 0; i < paramNames.length; i++) {
114
+ params[paramNames[i]] = decodeURIComponent(match[i + 1]);
115
+ }
116
+ return { route, params };
117
+ }
118
+ }
119
+ return null;
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @typedef {import('@octanejs/vite-plugin').Context} Context
3
+ * @typedef {import('@octanejs/vite-plugin').ServerRoute} ServerRoute
4
+ * @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
5
+ */
6
+
7
+ import { runMiddlewareChain } from './middleware.js';
8
+
9
+ /**
10
+ * Handle a ServerRoute (API endpoint)
11
+ *
12
+ * @param {ServerRoute} route
13
+ * @param {Context} context
14
+ * @param {Middleware[]} globalMiddlewares
15
+ * @returns {Promise<Response>}
16
+ */
17
+ export async function handleServerRoute(route, context, globalMiddlewares) {
18
+ try {
19
+ // The handler wrapped as a function returning Promise<Response>
20
+ const handler = async () => {
21
+ return route.handler(context);
22
+ };
23
+
24
+ // Run the middleware chain: global → before → handler → after
25
+ const response = await runMiddlewareChain(
26
+ context,
27
+ globalMiddlewares,
28
+ route.before,
29
+ handler,
30
+ route.after,
31
+ );
32
+
33
+ return response;
34
+ } catch (error) {
35
+ console.error('[octane] API route error:', error);
36
+
37
+ // Return error response
38
+ const message = error instanceof Error ? error.message : 'Internal Server Error';
39
+ return new Response(JSON.stringify({ error: message }), {
40
+ status: 500,
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ },
44
+ });
45
+ }
46
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Production server-entry generator.
3
+ *
4
+ * PHASE 1 STUB. The generated server entry (imported by the SSR bundle build
5
+ * in `closeBundle`) wires page/layout/rpc modules into `createHandler`. It is
6
+ * unused in dev (dev SSR uses `vite.ssrLoadModule`). Implemented in Phase 2.
7
+ */
8
+
9
+ /**
10
+ * @param {unknown} _options
11
+ * @returns {never}
12
+ */
13
+ export function generateServerEntry(_options) {
14
+ throw new Error('[@octanejs/vite-plugin] Server entry generation is Phase 2.');
15
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "nodenext",
4
+ "moduleResolution": "nodenext",
5
+ "target": "es2022",
6
+ "lib": ["esnext", "dom", "dom.iterable"],
7
+ "noEmit": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "types": ["node"],
13
+ "allowJs": true,
14
+ "checkJs": false
15
+ },
16
+ "include": ["src/**/*", "types/**/*"],
17
+ "exclude": ["node_modules", "dist", "tests"]
18
+ }
@@ -0,0 +1,189 @@
1
+ import type { Plugin, BuildEnvironmentOptions, ViteDevServer } from 'vite';
2
+ import type { RuntimePrimitives } from '@ripple-ts/adapter';
3
+
4
+ // ============================================================================
5
+ // Plugin exports
6
+ // ============================================================================
7
+
8
+ export interface OctanePluginOptions {
9
+ /** Override the client HMR default (on in serve mode, off for SSR). */
10
+ hmr?: boolean;
11
+ }
12
+
13
+ /**
14
+ * The octane metaframework plugin. Returns an array:
15
+ * `[octane(), metaPlugin]` — the first compiles `.tsrx`, the second owns
16
+ * config / routing / dev SSR / hydrate.
17
+ */
18
+ export function octane(options?: OctanePluginOptions): Plugin[];
19
+ export function defineConfig(options: OctaneConfigOptions): OctaneConfigOptions;
20
+ export function resolveOctaneConfig(
21
+ raw: OctaneConfigOptions,
22
+ options?: { requireAdapter?: boolean },
23
+ ): ResolvedOctaneConfig;
24
+ export function getOctaneConfigPath(projectRoot: string): string;
25
+ export function octaneConfigExists(projectRoot: string): boolean;
26
+ export function loadOctaneConfig(
27
+ projectRoot: string,
28
+ options?: { vite?: ViteDevServer; requireAdapter?: boolean },
29
+ ): Promise<ResolvedOctaneConfig>;
30
+
31
+ // ============================================================================
32
+ // Route classes
33
+ // ============================================================================
34
+
35
+ export class RenderRoute {
36
+ readonly type: 'render';
37
+ path: string;
38
+ entry: RenderRouteEntry;
39
+ layout?: string;
40
+ before: Middleware[];
41
+ constructor(options: RenderRouteOptions);
42
+ }
43
+
44
+ export class ServerRoute {
45
+ readonly type: 'server';
46
+ path: string;
47
+ methods: string[];
48
+ handler: RouteHandler;
49
+ before: Middleware[];
50
+ after: Middleware[];
51
+ constructor(options: ServerRouteOptions);
52
+ }
53
+
54
+ export type Route = RenderRoute | ServerRoute;
55
+
56
+ // ============================================================================
57
+ // Route options
58
+ // ============================================================================
59
+
60
+ export interface RenderRouteOptions {
61
+ /** URL path pattern (e.g., '/', '/posts/:id', '/docs/*slug') */
62
+ path: string;
63
+ /** Path to the component entry file, optionally with a preferred named export */
64
+ entry: RenderRouteEntry;
65
+ /** Path to the layout component (wraps the entry) */
66
+ layout?: string;
67
+ /** Middleware to run before rendering */
68
+ before?: Middleware[];
69
+ }
70
+
71
+ export interface ServerRouteOptions {
72
+ /** URL path pattern (e.g., '/api/hello', '/api/posts/:id') */
73
+ path: string;
74
+ /** HTTP methods to handle (default: ['GET']) */
75
+ methods?: string[];
76
+ /** Request handler that returns a Response */
77
+ handler: RouteHandler;
78
+ /** Middleware to run before the handler */
79
+ before?: Middleware[];
80
+ /** Middleware to run after the handler */
81
+ after?: Middleware[];
82
+ }
83
+
84
+ // ============================================================================
85
+ // Context and middleware
86
+ // ============================================================================
87
+
88
+ export interface Context {
89
+ /** The incoming Request object */
90
+ request: Request;
91
+ /** URL parameters extracted from the route pattern */
92
+ params: Record<string, string>;
93
+ /** Parsed URL object */
94
+ url: URL;
95
+ /** Shared state for passing data between middlewares */
96
+ state: Map<string, unknown>;
97
+ }
98
+
99
+ export type NextFunction = () => Promise<Response>;
100
+ export type Middleware = (context: Context, next: NextFunction) => Response | Promise<Response>;
101
+ export type RouteHandler = (context: Context) => Response | Promise<Response>;
102
+
103
+ // ============================================================================
104
+ // Configuration
105
+ // ============================================================================
106
+
107
+ export type Component<T = Record<string, any>> = (
108
+ scope: any,
109
+ props: T,
110
+ extra?: any,
111
+ ) => string | void;
112
+
113
+ export type RenderRouteEntry = string | readonly [exportName: string, path: string];
114
+
115
+ export interface RootBoundaryOptions {
116
+ pending?: Component<Record<string, never>>;
117
+ catch?: Component<{ error: unknown; reset: () => void }>;
118
+ }
119
+
120
+ export interface OctaneConfigOptions {
121
+ build?: {
122
+ /** Output directory for the production build. @default 'dist' */
123
+ outDir?: string;
124
+ minify?: boolean;
125
+ target?: BuildEnvironmentOptions['target'];
126
+ };
127
+ adapter?: {
128
+ serve: AdapterServeFunction;
129
+ /**
130
+ * Platform-specific runtime primitives provided by the adapter.
131
+ * Required for production builds; in development the plugin falls back
132
+ * to Node.js defaults if not provided.
133
+ */
134
+ runtime: RuntimePrimitives;
135
+ };
136
+ router?: {
137
+ routes: Route[];
138
+ };
139
+ /** Global root pending/catch UI used by client and SSR render roots */
140
+ rootBoundary?: RootBoundaryOptions;
141
+ /** Global middlewares applied to all routes */
142
+ middlewares?: Middleware[];
143
+ platform?: {
144
+ env: Record<string, string>;
145
+ };
146
+ server?: {
147
+ /**
148
+ * Trust `X-Forwarded-Proto` / `X-Forwarded-Host` when deriving the
149
+ * request origin. Enable only behind a trusted reverse proxy.
150
+ * @default false
151
+ */
152
+ trustProxy?: boolean;
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Resolved configuration with all defaults applied.
158
+ */
159
+ export interface ResolvedOctaneConfig {
160
+ build: {
161
+ /** @default 'dist' */
162
+ outDir: string;
163
+ minify?: boolean;
164
+ target?: BuildEnvironmentOptions['target'];
165
+ };
166
+ adapter?: {
167
+ serve: AdapterServeFunction;
168
+ runtime: RuntimePrimitives;
169
+ };
170
+ router: {
171
+ routes: Route[];
172
+ };
173
+ rootBoundary: RootBoundaryOptions;
174
+ /** @default [] */
175
+ middlewares: Middleware[];
176
+ platform: {
177
+ /** @default {} */
178
+ env: Record<string, string>;
179
+ };
180
+ server: {
181
+ /** @default false */
182
+ trustProxy: boolean;
183
+ };
184
+ }
185
+
186
+ export type AdapterServeFunction = (
187
+ handler: (request: Request, platform?: unknown) => Response | Promise<Response>,
188
+ options?: Record<string, unknown>,
189
+ ) => { listen: (port?: number) => unknown; close: () => void };
@@ -0,0 +1,59 @@
1
+ import type { RuntimePrimitives } from '@ripple-ts/adapter';
2
+ import type {
3
+ Route,
4
+ Middleware,
5
+ ResolvedOctaneConfig,
6
+ OctaneConfigOptions,
7
+ RootBoundaryOptions,
8
+ } from '@octanejs/vite-plugin';
9
+
10
+ export function resolveOctaneConfig(
11
+ raw: OctaneConfigOptions,
12
+ options?: { requireAdapter?: boolean },
13
+ ): ResolvedOctaneConfig;
14
+
15
+ export interface ClientAssetEntry {
16
+ /** Path to the built JS file (relative to client output dir) */
17
+ js: string;
18
+ /** Paths to the built CSS files (relative to client output dir) */
19
+ css: string[];
20
+ }
21
+
22
+ export interface ServerManifest {
23
+ routes: Route[];
24
+ components: Record<string, Function>;
25
+ layouts: Record<string, Function>;
26
+ middlewares: Middleware[];
27
+ /** Map of entry path → _$_server_$_ object for RPC support */
28
+ rpcModules?: Record<string, Record<string, Function>>;
29
+ /** Trust X-Forwarded-* headers when deriving origin for RPC fetch */
30
+ trustProxy?: boolean;
31
+ rootBoundary?: RootBoundaryOptions;
32
+ /** Platform-specific runtime primitives from the adapter */
33
+ runtime: RuntimePrimitives;
34
+ /** Map of route entry paths to built client asset paths (preload tags). */
35
+ clientAssets?: Record<string, ClientAssetEntry>;
36
+ }
37
+
38
+ /**
39
+ * octane RenderResult — `render()` is async; `css` is ALREADY a ready,
40
+ * deduped `<style data-octane="hash">…</style>` string (NOT a Set<string>
41
+ * needing a `getCss` lookup like Ripple).
42
+ */
43
+ export interface RenderResult {
44
+ head: string;
45
+ body: string;
46
+ css: string;
47
+ }
48
+
49
+ export interface HandlerOptions {
50
+ render: (component: Function, props?: unknown) => Promise<RenderResult>;
51
+ htmlTemplate: string;
52
+ executeServerFunction: (fn: Function, body: string) => Promise<string>;
53
+ }
54
+
55
+ /** Production fetch-handler factory. PHASE 2. */
56
+ export function createHandler(
57
+ manifest: ServerManifest,
58
+ options: HandlerOptions,
59
+ ): (request: Request) => Promise<Response>;