@octanejs/app-core 0.0.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,319 @@
1
+ import type { RuntimePrimitives } from '@ripple-ts/adapter';
2
+
3
+ // ============================================================================
4
+ // Shared app/config exports
5
+ // ============================================================================
6
+
7
+ export function defineConfig(options: OctaneConfigOptions): OctaneConfigOptions;
8
+ /** Context.state key for a per-request CSP nonce set by middleware. */
9
+ export const OCTANE_NONCE_STATE_KEY: 'octane.nonce';
10
+ export const DEFAULT_OUTDIR: 'dist';
11
+ export const ENTRY_FILENAME: 'entry.js';
12
+ export function resolveOctaneConfig(
13
+ raw: OctaneConfigOptions,
14
+ options?: { requireAdapter?: boolean },
15
+ ): ResolvedOctaneConfig;
16
+
17
+ // ============================================================================
18
+ // Route classes
19
+ // ============================================================================
20
+
21
+ export class RenderRoute {
22
+ readonly type: 'render';
23
+ path: string;
24
+ entry: RenderRouteEntry;
25
+ layout?: string;
26
+ before: Middleware[];
27
+ status?: number;
28
+ constructor(options: RenderRouteOptions);
29
+ }
30
+
31
+ export class ServerRoute {
32
+ readonly type: 'server';
33
+ path: string;
34
+ methods: string[];
35
+ handler: RouteHandler;
36
+ before: Middleware[];
37
+ after: Middleware[];
38
+ constructor(options: ServerRouteOptions);
39
+ }
40
+
41
+ export type Route = RenderRoute | ServerRoute;
42
+
43
+ export interface RouteMatch {
44
+ route: Route;
45
+ params: Record<string, string>;
46
+ }
47
+
48
+ export interface Router {
49
+ match(method: string, pathname: string): RouteMatch | null;
50
+ }
51
+
52
+ export function createRouter(routes: Route[]): Router;
53
+ export function get_route_entry_path(entry?: RenderRouteEntry): string | undefined;
54
+ export function get_route_entry_export_name(entry?: RenderRouteEntry): string | undefined;
55
+ export function get_route_entry_id(entry?: RenderRouteEntry): string | undefined;
56
+ export function get_component_export(
57
+ module: Record<string, unknown>,
58
+ exportName?: string,
59
+ ): Function | null;
60
+
61
+ // ============================================================================
62
+ // Route options
63
+ // ============================================================================
64
+
65
+ export interface RenderRouteOptions {
66
+ /** URL path pattern (e.g., '/', '/posts/:id', '/docs/*slug') */
67
+ path: string;
68
+ /** Path to the component entry file, optionally with a preferred named export */
69
+ entry: RenderRouteEntry;
70
+ /** Path to the layout component (wraps the entry) */
71
+ layout?: string;
72
+ /** Middleware to run before rendering */
73
+ before?: Middleware[];
74
+ /**
75
+ * HTTP status for the rendered response (default 200). Set 404 on a
76
+ * catch-all route so the SSR'd not-found page reports its real status.
77
+ */
78
+ status?: number;
79
+ }
80
+
81
+ export interface ServerRouteOptions {
82
+ /** URL path pattern (e.g., '/api/hello', '/api/posts/:id') */
83
+ path: string;
84
+ /** HTTP methods to handle (default: ['GET']) */
85
+ methods?: string[];
86
+ /** Request handler that returns a Response */
87
+ handler: RouteHandler;
88
+ /** Middleware to run before the handler */
89
+ before?: Middleware[];
90
+ /** Middleware to run after the handler */
91
+ after?: Middleware[];
92
+ }
93
+
94
+ // ============================================================================
95
+ // Context and middleware
96
+ // ============================================================================
97
+
98
+ export interface Context {
99
+ /** The incoming Request object */
100
+ request: Request;
101
+ /** URL parameters extracted from the route pattern */
102
+ params: Record<string, string>;
103
+ /** Parsed URL object */
104
+ url: URL;
105
+ /**
106
+ * Shared state for passing data between middlewares. Set
107
+ * `OCTANE_NONCE_STATE_KEY` (`'octane.nonce'`) to a non-empty string to nonce
108
+ * renderer inline scripts, hydration data, and the hydrate module script.
109
+ */
110
+ state: Map<string, unknown>;
111
+ }
112
+
113
+ export type NextFunction = () => Promise<Response>;
114
+ export type Middleware = (context: Context, next: NextFunction) => Response | Promise<Response>;
115
+ export type RouteHandler = (context: Context) => Response | Promise<Response>;
116
+
117
+ export function compose(
118
+ middlewares: Middleware[],
119
+ ): (context: Context, finalHandler: () => Promise<Response>) => Promise<Response>;
120
+ export function createContext(request: Request, params: Record<string, string>): Context;
121
+ export function runMiddlewareChain(
122
+ context: Context,
123
+ globalMiddlewares: Middleware[],
124
+ beforeMiddlewares: Middleware[],
125
+ handler: () => Promise<Response>,
126
+ afterMiddlewares?: Middleware[],
127
+ ): Promise<Response>;
128
+ export function handleServerRoute(
129
+ route: ServerRoute,
130
+ context: Context,
131
+ globalMiddlewares: Middleware[],
132
+ ): Promise<Response>;
133
+ export function is_rpc_request(pathname: string): boolean;
134
+
135
+ // ============================================================================
136
+ // Configuration
137
+ // ============================================================================
138
+
139
+ export type Component<T = Record<string, any>> = (
140
+ props: T,
141
+ scope: any,
142
+ extra?: any,
143
+ ) => string | void;
144
+
145
+ export type RenderRouteEntry = string | readonly [exportName: string, path: string];
146
+
147
+ /**
148
+ * Props every RenderRoute component (and layout) receives: the route params
149
+ * and the request `url` (pathname + search, origin-free — the client hydrate
150
+ * entry re-renders with the identical string).
151
+ */
152
+ export interface RenderRouteProps {
153
+ params: Record<string, string>;
154
+ url: string;
155
+ /**
156
+ * Request-scoped middleware state on the server. This Map is intentionally
157
+ * absent during browser hydration and is never serialized.
158
+ */
159
+ state?: Map<string, unknown>;
160
+ }
161
+
162
+ /**
163
+ * The app hook run by the client hydrate entry BEFORE `hydrateRoot` (config
164
+ * `router.preHydrate`): commit client-side state the server already resolved —
165
+ * typically a client router loading its match tree — so the first hydration
166
+ * pass adopts the same tree the server rendered.
167
+ */
168
+ export type PreHydrateHook = (info: {
169
+ url: string;
170
+ params: Record<string, string>;
171
+ }) => void | Promise<void>;
172
+
173
+ export interface RootBoundaryOptions {
174
+ /** Component entry rendered while the root route tree is suspended. */
175
+ pending?: RenderRouteEntry;
176
+ /** Component entry rendered when an uncaught root render/effect error reaches the boundary. */
177
+ catch?: RenderRouteEntry;
178
+ }
179
+
180
+ export interface OctaneConfigOptions {
181
+ build?: {
182
+ /** Output directory for the production build. @default 'dist' */
183
+ outDir?: string;
184
+ minify?: boolean;
185
+ target?: BuildTarget;
186
+ };
187
+ adapter?: OctaneAdapter;
188
+ router?: {
189
+ routes: Route[];
190
+ /**
191
+ * Project-root module ID (e.g. '/src/pre-hydrate.ts') whose default
192
+ * export is a {@link PreHydrateHook}. The client hydrate entry imports it
193
+ * and awaits the hook before calling `hydrateRoot`.
194
+ */
195
+ preHydrate?: string;
196
+ };
197
+ /**
198
+ * Global root pending/catch component entries used by client and SSR roots.
199
+ * Paths use project-root module IDs (for example `/src/Pending.tsrx`); a tuple
200
+ * selects a named export.
201
+ */
202
+ rootBoundary?: RootBoundaryOptions;
203
+ /** Global middlewares applied to all routes */
204
+ middlewares?: Middleware[];
205
+ platform?: {
206
+ env: Record<string, string>;
207
+ };
208
+ server?: {
209
+ /**
210
+ * Trust `X-Forwarded-Proto` / `X-Forwarded-Host` when deriving the
211
+ * request origin. Enable only behind a trusted reverse proxy.
212
+ * @default false
213
+ */
214
+ trustProxy?: boolean;
215
+ /**
216
+ * Production SSR mode: 'streaming' (default) flushes the shell at
217
+ * first await and streams suspense segments out-of-order (same engine
218
+ * as dev SSR); 'buffered' awaits everything (`prerender`) and sends
219
+ * one document — for hosts that break streamed responses.
220
+ * @default 'streaming'
221
+ */
222
+ render?: 'streaming' | 'buffered';
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Resolved configuration with all defaults applied.
228
+ */
229
+ export interface ResolvedOctaneConfig {
230
+ build: {
231
+ /** @default 'dist' */
232
+ outDir: string;
233
+ minify?: boolean;
234
+ target?: BuildTarget;
235
+ };
236
+ adapter?: OctaneAdapter;
237
+ router: {
238
+ routes: Route[];
239
+ preHydrate?: string;
240
+ };
241
+ rootBoundary: RootBoundaryOptions;
242
+ /** @default [] */
243
+ middlewares: Middleware[];
244
+ platform: {
245
+ /** @default {} */
246
+ env: Record<string, string>;
247
+ };
248
+ server: {
249
+ /** @default false */
250
+ trustProxy: boolean;
251
+ /** @default 'streaming' */
252
+ render: 'streaming' | 'buffered';
253
+ };
254
+ }
255
+
256
+ /**
257
+ * The build context an Octane app integration passes to an adapter after it
258
+ * produced the client and server bundles.
259
+ */
260
+ export interface AdaptContext {
261
+ /** Absolute project root. */
262
+ root: string;
263
+ /** The config `build.outDir` (relative to root, e.g. 'dist'). */
264
+ outDir: string;
265
+ /** Absolute path of the static client bundle ({outDir}/client). */
266
+ clientDir: string;
267
+ /** Absolute path of the server bundle ({outDir}/server, contains entry.js). */
268
+ serverDir: string;
269
+ /** Prefixed build logger. */
270
+ log: (message: string) => void;
271
+ }
272
+
273
+ /**
274
+ * The octane.config.ts `adapter` contract. All parts are optional and
275
+ * independent:
276
+ *
277
+ * - `adapt(ctx)` — post-build hook: restructure dist/client + dist/server for
278
+ * a deployment target (e.g. @octanejs/adapter-vercel emits `.vercel/output`).
279
+ * - `serve(handler, opts)` — replaces the generated server entry's built-in
280
+ * Node boot when running `node dist/server/entry.js` / `octane-preview`.
281
+ * - `runtime` — platform primitives (hashing, async context) replacing the
282
+ * entry's Node defaults; needed on non-Node runtimes.
283
+ */
284
+ export interface OctaneAdapter {
285
+ name?: string;
286
+ adapt?: (ctx: AdaptContext) => void | Promise<void>;
287
+ serve?: AdapterServeFunction;
288
+ runtime?: RuntimePrimitives;
289
+ }
290
+
291
+ export type AdapterServeFunction = (
292
+ handler: (request: Request, platform?: unknown) => Response | Promise<Response>,
293
+ options?: Record<string, unknown>,
294
+ ) => { listen: (port?: number) => unknown; close: () => void };
295
+
296
+ /** A shared syntax accepted by current app integrations and their transpilers. */
297
+ export type BuildTarget = string | string[] | false;
298
+
299
+ export interface ConfigModuleRunner {
300
+ loadModule(id: string): Promise<Record<string, unknown>>;
301
+ getDependencies?(id: string): string[] | Promise<string[]>;
302
+ getMissingDependencies?(id: string): string[] | Promise<string[]>;
303
+ }
304
+
305
+ export interface LoadConfigOptions {
306
+ /** Config filename relative to the project root, or an absolute path. */
307
+ configFile?: string;
308
+ requireAdapter?: boolean;
309
+ moduleRunner?: ConfigModuleRunner | ConfigModuleRunner['loadModule'];
310
+ /** Directory used for the neutral evaluator's generated ESM module. */
311
+ cacheDir?: string;
312
+ }
313
+
314
+ export interface LoadedOctaneConfig {
315
+ config: ResolvedOctaneConfig;
316
+ configPath: string;
317
+ dependencies: string[];
318
+ missingDependencies: string[];
319
+ }
@@ -0,0 +1,3 @@
1
+ export { compose, createContext, handleServerRoute, runMiddlewareChain } from '@octanejs/app-core';
2
+ export function is_rpc_request(pathname: string): boolean;
3
+ export type { Context, Middleware, NextFunction, RouteHandler } from '@octanejs/app-core';
@@ -0,0 +1,31 @@
1
+ import type { IncomingMessage, ServerResponse, Server } from 'node:http';
2
+
3
+ /** Convert a Node.js IncomingMessage to a Web Request. */
4
+ export function nodeRequestToWebRequest(nodeRequest: IncomingMessage): Request;
5
+
6
+ /**
7
+ * Pipe a Web Response to a Node.js ServerResponse, streaming chunk-by-chunk
8
+ * (a streaming SSR body flushes as it renders).
9
+ */
10
+ export function sendWebResponse(nodeResponse: ServerResponse, webResponse: Response): Promise<void>;
11
+
12
+ /**
13
+ * Serve a static file from `staticDir` when the request path maps to one.
14
+ * Vite's `/assets/*` and Rsbuild's `/static/*` hash-named output get immutable
15
+ * caching; other files revalidate. Returns true when the request was handled.
16
+ */
17
+ export function serveStaticFile(
18
+ req: IncomingMessage,
19
+ res: ServerResponse,
20
+ staticDir: string,
21
+ ): boolean;
22
+
23
+ /**
24
+ * Minimal production HTTP server: static files from `staticDir` first (the
25
+ * built client assets), then the fetch-style SSR handler. The default boot for
26
+ * `node dist/server/entry.js` when octane.config.ts has no adapter.
27
+ */
28
+ export function createNodeServer(
29
+ handler: (request: Request) => Response | Promise<Response>,
30
+ options?: { staticDir?: string },
31
+ ): { listen: (port?: number) => Server; close: () => void };
@@ -0,0 +1,98 @@
1
+ import type { RuntimePrimitives } from '@ripple-ts/adapter';
2
+ import type {
3
+ Route,
4
+ Middleware,
5
+ OctaneConfigOptions,
6
+ ResolvedOctaneConfig,
7
+ Component,
8
+ } from '@octanejs/app-core';
9
+ import type { RenderResult, StreamOptions, RenderOptions } from 'octane/server';
10
+
11
+ export function resolveOctaneConfig(
12
+ raw: OctaneConfigOptions,
13
+ options?: { requireAdapter?: boolean },
14
+ ): ResolvedOctaneConfig;
15
+
16
+ export interface ClientAssetEntry {
17
+ /** Path to the built JS file (relative to the client output dir) */
18
+ js: string;
19
+ /** Paths to the built CSS files (relative to the client output dir) */
20
+ css: string[];
21
+ }
22
+
23
+ export interface ServerManifest {
24
+ routes: Route[];
25
+ /** RenderRoute entry module path → module namespace (export picked per-route) */
26
+ components: Record<string, Record<string, unknown>>;
27
+ /** Layout module path → module namespace */
28
+ layouts: Record<string, Record<string, unknown>>;
29
+ middlewares: Middleware[];
30
+ /** Trust X-Forwarded-* headers when deriving origin for RPC fetch */
31
+ trustProxy?: boolean;
32
+ /** 'streaming' (default) renders via renderToReadableStream; 'buffered' awaits everything via prerender */
33
+ render?: 'streaming' | 'buffered';
34
+ /** Resolved server-compiled global boundary components. */
35
+ rootBoundary?: {
36
+ pending?: Component | null;
37
+ catch?: Component<{ error: unknown; reset: () => void }> | null;
38
+ };
39
+ /** Import descriptors serialized to the client hydration payload. */
40
+ rootBoundaryEntries?: {
41
+ pending: { path: string; exportName: string | null } | null;
42
+ catch: { path: string; exportName: string | null } | null;
43
+ };
44
+ /** config `router.preHydrate`, serialized into #__octane_data for the client entry */
45
+ preHydrate?: string | null;
46
+ /** Map of entry path → `module server` namespace for RPC support */
47
+ rpcModules?: Record<string, Record<string, Function>>;
48
+ /** Platform primitives (adapter's, or the generated entry's Node defaults) */
49
+ runtime?: RuntimePrimitives;
50
+ /** Route entry module path → built client asset paths (preload tags) */
51
+ clientAssets?: Record<string, ClientAssetEntry>;
52
+ }
53
+
54
+ export interface HandlerOptions {
55
+ /** `renderToReadableStream` from 'octane/server' (the streaming engine dev SSR uses) */
56
+ renderToReadableStream: (
57
+ component: Function,
58
+ props?: unknown,
59
+ options?: StreamOptions,
60
+ ) => Promise<ReadableStream<Uint8Array>>;
61
+ /** `prerender` from 'octane/static' (the buffered await-everything fallback) */
62
+ prerender: (
63
+ component: Function,
64
+ props?: unknown,
65
+ options?: RenderOptions,
66
+ ) => Promise<RenderResult>;
67
+ /** The BUILT dist client index.html (moved to dist/server by the build) */
68
+ htmlTemplate: string;
69
+ /** RPC executor from 'octane/server' */
70
+ executeServerFunction: (fn: Function, body: string) => Promise<string>;
71
+ /** Boundary primitives from 'octane/server'. */
72
+ Suspense: Component;
73
+ ErrorBoundary: Component;
74
+ createElement: Function;
75
+ }
76
+
77
+ /**
78
+ * Production fetch-handler factory. Mirrors the dev middleware's render path
79
+ * byte-for-byte in everything hydration can see. Repeated calls in one process
80
+ * refresh the same-origin fetch dispatcher, so a dev server can replace a
81
+ * hot-reloaded manifest without retaining the first handler.
82
+ */
83
+ export function createHandler(
84
+ manifest: ServerManifest,
85
+ options: HandlerOptions,
86
+ ): (request: Request) => Promise<Response>;
87
+
88
+ export function createPropsWrapper(Page: Component, pageProps: Record<string, unknown>): Component;
89
+ export function createLayoutWrapper(
90
+ Layout: Component,
91
+ Page: Component,
92
+ pageProps: Record<string, unknown>,
93
+ ): Component;
94
+ export function createRootBoundaryWrapper(
95
+ Content: Component,
96
+ boundary: { pending?: Component | null; catch?: Component | null },
97
+ runtime: { Suspense: Component; ErrorBoundary: Component; createElement: Function },
98
+ ): Component;
@@ -0,0 +1,17 @@
1
+ export {
2
+ RenderRoute,
3
+ ServerRoute,
4
+ createRouter,
5
+ get_component_export,
6
+ get_route_entry_export_name,
7
+ get_route_entry_id,
8
+ get_route_entry_path,
9
+ } from '@octanejs/app-core';
10
+ export type {
11
+ RenderRouteEntry,
12
+ RenderRouteOptions,
13
+ Route,
14
+ RouteMatch,
15
+ Router,
16
+ ServerRouteOptions,
17
+ } from '@octanejs/app-core';