@octanejs/vite-plugin 0.1.5 → 0.1.9

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.
@@ -1,286 +1 @@
1
- // @ts-check
2
- /**
3
- * Production fetch-handler factory + config re-exports.
4
- *
5
- * `createHandler(manifest, deps)` is the runtime entry the generated server
6
- * bundle (dist/server/entry.js) calls in production. It is designed to be
7
- * BUNDLED: platform-agnostic (no Node imports — platform capabilities come via
8
- * `manifest.runtime`), and free of vite / octane-compiler imports (which is why
9
- * `resolveOctaneConfig` is re-exported from resolve-config.js, not
10
- * load-config.js).
11
- *
12
- * The render path mirrors the DEV middleware's `handleRenderRoute`
13
- * (server/render-route.js) byte-for-byte in everything hydration can see —
14
- * the same `renderToReadableStream` engine, the same `#__octane_data` payload
15
- * (same keys, same order), and the same template-prefix → render-stream →
16
- * template-suffix assembly — so `hydrateRoot()` adopts a production response
17
- * exactly as it adopts a dev one. Deliberate differences: the template is the
18
- * BUILT dist/client/index.html (hashed hydrate script already in place, so
19
- * nothing is injected per-request), per-route `<link rel=stylesheet/modulepreload>`
20
- * tags from the client manifest join the head, and render errors produce a
21
- * plain 500 (no dev stack page). Keep the two files in sync when the shape
22
- * changes.
23
- */
24
-
25
- import { createRouter } from './router.js';
26
- import { createContext, runMiddlewareChain } from './middleware.js';
27
- import { handleServerRoute } from './server-route.js';
28
- import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
29
- import {
30
- get_component_export,
31
- get_route_entry_export_name,
32
- get_route_entry_path,
33
- } from '../routes.js';
34
- import {
35
- patch_global_fetch,
36
- build_rpc_lookup,
37
- is_rpc_request,
38
- handle_rpc_request,
39
- } from '@ripple-ts/adapter/rpc';
40
-
41
- export { resolveOctaneConfig } from '../resolve-config.js';
42
-
43
- /**
44
- * @typedef {import('@octanejs/vite-plugin').RenderRoute} RenderRoute
45
- * @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
46
- * @typedef {import('@octanejs/vite-plugin').Context} Context
47
- */
48
- /**
49
- @import { ServerManifest, HandlerOptions, ClientAssetEntry } from '../../types/production.d.ts'
50
- */
51
-
52
- /**
53
- * Create the production request handler from a manifest.
54
- *
55
- * The returned function is a standard Web `fetch`-style handler:
56
- * `(request: Request) => Promise<Response>` — the generated server entry boots
57
- * it behind the adapter's `serve()` (or the built-in Node server), and
58
- * serverless wrappers import it directly.
59
- *
60
- * @param {ServerManifest} manifest
61
- * @param {HandlerOptions} deps
62
- * @returns {(request: Request) => Promise<Response>}
63
- */
64
- export function createHandler(manifest, deps) {
65
- const { renderToReadableStream, prerender, htmlTemplate, executeServerFunction } = deps;
66
- const router = createRouter(manifest.routes);
67
- const globalMiddlewares = manifest.middlewares ?? [];
68
- const trustProxy = manifest.trustProxy ?? false;
69
- const runtime = manifest.runtime;
70
-
71
- // RPC lookup for `module server` functions (hash → server function). Empty
72
- // today — the octane compiler does not emit `module server` modules yet —
73
- // but the wiring matches the dev middleware so it lights up when it does.
74
- const rpcLookup =
75
- manifest.rpcModules && runtime ? build_rpc_lookup(manifest.rpcModules, runtime.hash) : null;
76
-
77
- // Request-scoped async context + same-origin fetch short-circuit: fetch()
78
- // during SSR that resolves to this origin routes through the handler
79
- // in-process instead of a network round-trip.
80
- const asyncContext = runtime?.createAsyncContext();
81
- const fetchHandle = asyncContext ? patch_global_fetch(asyncContext) : null;
82
-
83
- const handler = async function handler(/** @type {Request} */ request) {
84
- const url = new URL(request.url);
85
- const method = request.method;
86
-
87
- if (is_rpc_request(url.pathname)) {
88
- if (!rpcLookup || !asyncContext) {
89
- return new Response(JSON.stringify({ error: 'RPC is not configured' }), {
90
- status: 404,
91
- headers: { 'Content-Type': 'application/json' },
92
- });
93
- }
94
- return handle_rpc_request(request, {
95
- resolveFunction(/** @type {string} */ hash) {
96
- const entry = rpcLookup.get(hash);
97
- if (!entry) return null;
98
- const fn = entry.serverObj[entry.funcName];
99
- return typeof fn === 'function' ? fn : null;
100
- },
101
- executeServerFunction,
102
- asyncContext,
103
- trustProxy,
104
- });
105
- }
106
-
107
- const match = router.match(method, url.pathname);
108
- if (!match) {
109
- // Static assets never reach here (the static layer — the built-in Node
110
- // server, or the platform's file serving — runs first); an app with a
111
- // catch-all RenderRoute matches everything else, so this is only hit
112
- // when no catch-all exists.
113
- return new Response('Not Found', { status: 404 });
114
- }
115
-
116
- const context = createContext(request, match.params);
117
-
118
- try {
119
- if (match.route.type === 'render') {
120
- return await runMiddlewareChain(
121
- context,
122
- globalMiddlewares,
123
- match.route.before || [],
124
- async () => renderRoute(/** @type {RenderRoute} */ (match.route), context),
125
- [],
126
- );
127
- }
128
- return await handleServerRoute(match.route, context, globalMiddlewares);
129
- } catch (error) {
130
- console.error('[@octanejs/vite-plugin] Request error:', error);
131
- return new Response('Internal Server Error', { status: 500 });
132
- }
133
- };
134
-
135
- fetchHandle?.set_handler(handler);
136
-
137
- /**
138
- * Render a RenderRoute — the production twin of dev's `handleRenderRoute`.
139
- *
140
- * @param {RenderRoute} route
141
- * @param {Context} context
142
- * @returns {Promise<Response>}
143
- */
144
- async function renderRoute(route, context) {
145
- const entryPath = get_route_entry_path(route.entry);
146
- const exportName = get_route_entry_export_name(route.entry);
147
- const PageComponent = entryPath
148
- ? get_component_export(manifest.components[entryPath] ?? {}, exportName)
149
- : null;
150
- if (!PageComponent) {
151
- throw new Error(`Component not found for route ${route.path}`);
152
- }
153
-
154
- // Identical props to dev: `{ params, url }`, url origin-free so the client
155
- // re-renders the exact string.
156
- const requestUrl = context.url.pathname + context.url.search;
157
- const pageProps = { params: context.params, url: requestUrl };
158
-
159
- let RootComponent;
160
- if (route.layout) {
161
- const LayoutComponent = get_component_export(manifest.layouts[route.layout] ?? {}, undefined);
162
- if (!LayoutComponent) {
163
- throw new Error(`No layout component found for ${route.layout}`);
164
- }
165
- RootComponent = createLayoutWrapper(
166
- /** @type {any} */ (LayoutComponent),
167
- /** @type {any} */ (PageComponent),
168
- pageProps,
169
- );
170
- } else {
171
- RootComponent = createPropsWrapper(/** @type {any} */ (PageComponent), pageProps);
172
- }
173
-
174
- // The hydration payload — SAME keys, SAME order as dev render-route.js, so
175
- // the data script is byte-identical between dev and production.
176
- const routeData = JSON.stringify({
177
- entry: entryPath,
178
- exportName: exportName ?? null,
179
- layout: route.layout ?? null,
180
- routeIndex: getRenderRouteIndex(manifest.routes, route),
181
- params: context.params,
182
- url: requestUrl,
183
- preHydrate: manifest.preHydrate ?? null,
184
- });
185
- const dataScript = `<script id="__octane_data" type="application/json">${escapeScript(routeData)}</script>`;
186
-
187
- // Per-route asset hints from the client manifest: stylesheet links so
188
- // page CSS applies before hydration, modulepreload so the page chunk
189
- // downloads in parallel with the hydrate entry (which the template's own
190
- // script tag already references).
191
- /** @type {string[]} */
192
- const preloadTags = [];
193
- const entryAssets = entryPath ? manifest.clientAssets?.[entryPath] : undefined;
194
- if (entryAssets) {
195
- for (const cssFile of entryAssets.css) {
196
- preloadTags.push(`<link rel="stylesheet" href="/${cssFile}">`);
197
- }
198
- if (entryAssets.js) {
199
- preloadTags.push(`<link rel="modulepreload" href="/${entryAssets.js}">`);
200
- }
201
- }
202
-
203
- const headContent = [...preloadTags, dataScript].join('\n');
204
- const html = htmlTemplate.replace('<!--ssr-head-->', headContent);
205
-
206
- const status = route.status ?? 200;
207
- const headers = { 'Content-Type': 'text/html; charset=utf-8' };
208
-
209
- const splitAt = html.indexOf('<!--ssr-body-->');
210
- if (splitAt === -1) {
211
- return new Response(html, { status, headers });
212
- }
213
- const prefix = html.slice(0, splitAt);
214
- const suffix = html.slice(splitAt + '<!--ssr-body-->'.length);
215
-
216
- if (manifest.render === 'buffered') {
217
- // Await-everything fallback (`prerender` from octane/static): no
218
- // streaming, one document. The deduped scoped-style tags lead the body
219
- // markup inside #root — the same position they hold in the streamed
220
- // shell — so hydrateRoot's leading-style skip applies unchanged.
221
- const { html: body, css } = await prerender(RootComponent, undefined, {
222
- onError(/** @type {unknown} */ error) {
223
- console.error('[octane] SSR render error:', error);
224
- },
225
- });
226
- return new Response(prefix + css + body + suffix, { status, headers });
227
- }
228
-
229
- // Streaming (default): shell flushes at first await, suspense segments
230
- // stream out-of-order behind it — identical to dev.
231
- /** @type {ReadableStream<Uint8Array>} */
232
- const renderStream = await renderToReadableStream(RootComponent, undefined, {
233
- onError(/** @type {unknown} */ error) {
234
- console.error('[octane] SSR render error:', error);
235
- },
236
- });
237
-
238
- const encoder = new TextEncoder();
239
- const body = new ReadableStream({
240
- async start(controller) {
241
- controller.enqueue(encoder.encode(prefix));
242
- const reader = renderStream.getReader();
243
- try {
244
- while (true) {
245
- const { done, value } = await reader.read();
246
- if (done) break;
247
- controller.enqueue(value);
248
- }
249
- controller.enqueue(encoder.encode(suffix));
250
- controller.close();
251
- } catch (error) {
252
- controller.error(error);
253
- } finally {
254
- reader.releaseLock();
255
- }
256
- },
257
- cancel(reason) {
258
- return renderStream.cancel(reason);
259
- },
260
- });
261
-
262
- return new Response(body, { status, headers });
263
- }
264
-
265
- return handler;
266
- }
267
-
268
- /**
269
- * @param {import('@octanejs/vite-plugin').Route[]} routes
270
- * @param {RenderRoute} route
271
- * @returns {number | undefined}
272
- */
273
- function getRenderRouteIndex(routes, route) {
274
- const renderRoutes = routes.filter((r) => r.type === 'render');
275
- const index = renderRoutes.indexOf(route);
276
- return index === -1 ? undefined : index;
277
- }
278
-
279
- /**
280
- * Escape script content to prevent XSS in the inline JSON data block.
281
- * @param {string} str
282
- * @returns {string}
283
- */
284
- function escapeScript(str) {
285
- return str.replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
286
- }
1
+ export * from '@octanejs/app-core/production';
@@ -1,7 +1,18 @@
1
1
  // @ts-check
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { createLayoutWrapper, createPropsWrapper } from './component-wrappers.js';
4
+ import { composeHtmlStream } from './html-stream.js';
5
+ import {
6
+ getContextNonce,
7
+ injectHydrationEntry,
8
+ nonceAttribute,
9
+ splitSsrTemplate,
10
+ } from './html-template.js';
11
+ import {
12
+ createLayoutWrapper,
13
+ createPropsWrapper,
14
+ createRootBoundaryWrapper,
15
+ } from './component-wrappers.js';
5
16
  import {
6
17
  get_component_export,
7
18
  get_route_entry_export_name,
@@ -55,7 +66,8 @@ export async function handleRenderRoute(route, context, vite, octaneConfig) {
55
66
  // Load the octane streaming renderer. The wrappers call components
56
67
  // directly (no ssrComponent injection — the root must NOT be
57
68
  // marker-wrapped).
58
- const { renderToReadableStream } = await vite.ssrLoadModule('octane/server');
69
+ const serverRuntime = await vite.ssrLoadModule('octane/server');
70
+ const { renderToReadableStream } = serverRuntime;
59
71
 
60
72
  // Load the page component (compiled in server mode by octane()).
61
73
  const entryPath = get_route_entry_path(route.entry);
@@ -76,7 +88,8 @@ export async function handleRenderRoute(route, context, vite, octaneConfig) {
76
88
  // entry exports.
77
89
  let RootComponent;
78
90
  const requestUrl = context.url.pathname + context.url.search;
79
- const pageProps = { params: context.params, url: requestUrl };
91
+ const pageProps = { params: context.params, url: requestUrl, state: context.state };
92
+ const nonce = getContextNonce(context);
80
93
 
81
94
  if (route.layout) {
82
95
  const layoutModule = await vite.ssrLoadModule(route.layout);
@@ -95,6 +108,16 @@ export async function handleRenderRoute(route, context, vite, octaneConfig) {
95
108
  RootComponent = createPropsWrapper(/** @type {any} */ (PageComponent), pageProps);
96
109
  }
97
110
 
111
+ const pendingEntry = octaneConfig?.rootBoundary.pending;
112
+ const catchEntry = octaneConfig?.rootBoundary.catch;
113
+ const PendingComponent = await loadBoundaryComponent(vite, pendingEntry, 'pending');
114
+ const CatchComponent = await loadBoundaryComponent(vite, catchEntry, 'catch');
115
+ RootComponent = createRootBoundaryWrapper(
116
+ RootComponent,
117
+ { pending: PendingComponent, catch: CatchComponent },
118
+ /** @type {any} */ (serverRuntime),
119
+ );
120
+
98
121
  // Build head content with hydration data. The client entry is CONFIG-FREE
99
122
  // (importing octane.config.ts into the browser would drag the plugin + the
100
123
  // server adapter — and their `node:fs` imports — into the client graph and
@@ -111,8 +134,12 @@ export async function handleRenderRoute(route, context, vite, octaneConfig) {
111
134
  params: context.params,
112
135
  url: requestUrl,
113
136
  preHydrate: octaneConfig?.router.preHydrate ?? null,
137
+ rootBoundary: {
138
+ pending: serializeComponentEntry(pendingEntry),
139
+ catch: serializeComponentEntry(catchEntry),
140
+ },
114
141
  });
115
- const headContent = `<script id="__octane_data" type="application/json">${escapeScript(routeData)}</script>`;
142
+ const headContent = `<script id="__octane_data" type="application/json"${nonceAttribute(nonce)}>${escapeScript(routeData)}</script>`;
116
143
 
117
144
  // Load and process index.html template.
118
145
  const templatePath = join(vite.config.root, 'index.html');
@@ -121,17 +148,18 @@ export async function handleRenderRoute(route, context, vite, octaneConfig) {
121
148
  // Apply Vite's HTML transforms (HMR client, module resolution, etc.).
122
149
  template = await vite.transformIndexHtml(context.url.pathname, template);
123
150
 
124
- let html = template.replace('<!--ssr-head-->', headContent);
125
-
126
- // Inject the hydration entry before </body>.
127
- const hydrationScript = `<script type="module" src="/@id/virtual:octane-hydrate"></script>`;
128
- html = html.replace('</body>', `${hydrationScript}\n</body>`);
151
+ // Validate the raw SSR template and inject the request-nonced hydrate entry
152
+ // before consuming the one required head marker with request data.
153
+ let html = injectHydrationEntry(template, '/@id/virtual:octane-hydrate', nonce);
154
+ html = html.replace('<!--ssr-head-->', headContent);
129
155
 
130
156
  // Start the render. This await resolves at SHELL-ready (so a synchronous
131
157
  // render error still falls into the catch below and produces the dev 500
132
158
  // page); segments keep flushing through the returned stream afterwards.
133
159
  /** @type {ReadableStream<Uint8Array>} */
134
160
  const renderStream = await renderToReadableStream(RootComponent, undefined, {
161
+ nonce: nonce ?? undefined,
162
+ signal: context.request.signal,
135
163
  onError(/** @type {unknown} */ error) {
136
164
  if (error instanceof Error) vite.ssrFixStacktrace(error);
137
165
  console.error('[octane] SSR render error:', error);
@@ -141,42 +169,12 @@ export async function handleRenderRoute(route, context, vite, octaneConfig) {
141
169
  const status = route.status ?? 200;
142
170
  const headers = { 'Content-Type': 'text/html; charset=utf-8' };
143
171
 
144
- // No body placeholder → nothing to stream into; cancel the render and
145
- // serve the transformed template (matches the old buffered behavior).
146
- const splitAt = html.indexOf('<!--ssr-body-->');
147
- if (splitAt === -1) {
148
- await renderStream.cancel();
149
- return new Response(html, { status, headers });
150
- }
151
- const prefix = html.slice(0, splitAt);
152
- const suffix = html.slice(splitAt + '<!--ssr-body-->'.length);
172
+ const [prefix, suffix] = splitSsrTemplate(html);
153
173
 
154
174
  // Template prefix → render stream (shell, then out-of-order segments) →
155
175
  // template suffix. The hydration <script> is in the SUFFIX, so by the time
156
176
  // the browser requests the entry every segment is already in the DOM.
157
- const encoder = new TextEncoder();
158
- const body = new ReadableStream({
159
- async start(controller) {
160
- controller.enqueue(encoder.encode(prefix));
161
- const reader = renderStream.getReader();
162
- try {
163
- while (true) {
164
- const { done, value } = await reader.read();
165
- if (done) break;
166
- controller.enqueue(value);
167
- }
168
- controller.enqueue(encoder.encode(suffix));
169
- controller.close();
170
- } catch (error) {
171
- controller.error(error);
172
- } finally {
173
- reader.releaseLock();
174
- }
175
- },
176
- cancel(reason) {
177
- return renderStream.cancel(reason);
178
- },
179
- });
177
+ const body = composeHtmlStream(prefix, renderStream, suffix);
180
178
 
181
179
  return new Response(body, { status, headers });
182
180
  } catch (error) {
@@ -192,6 +190,33 @@ export async function handleRenderRoute(route, context, vite, octaneConfig) {
192
190
  }
193
191
  }
194
192
 
193
+ /**
194
+ * @param {ViteDevServer} vite
195
+ * @param {import('@octanejs/vite-plugin').RenderRouteEntry | undefined} entry
196
+ * @param {'pending' | 'catch'} kind
197
+ * @returns {Promise<((props?: any, scope?: any, extra?: any) => string | void) | null>}
198
+ */
199
+ async function loadBoundaryComponent(vite, entry, kind) {
200
+ if (!entry) return null;
201
+ const modulePath = get_route_entry_path(entry);
202
+ const module = await vite.ssrLoadModule(/** @type {string} */ (modulePath));
203
+ const component = get_component_export(module, get_route_entry_export_name(entry));
204
+ if (!component) {
205
+ throw new Error(`No ${kind} rootBoundary component found in ${modulePath}`);
206
+ }
207
+ return /** @type {(props?: any, scope?: any, extra?: any) => string | void} */ (component);
208
+ }
209
+
210
+ /**
211
+ * @param {import('@octanejs/vite-plugin').RenderRouteEntry | undefined} entry
212
+ * @returns {{ path: string, exportName: string | null } | null}
213
+ */
214
+ function serializeComponentEntry(entry) {
215
+ const path = get_route_entry_path(entry);
216
+ if (!path) return null;
217
+ return { path, exportName: get_route_entry_export_name(entry) ?? null };
218
+ }
219
+
195
220
  /**
196
221
  * @param {ResolvedOctaneConfig | undefined} config
197
222
  * @param {RenderRoute} route
@@ -1,123 +1 @@
1
- // @ts-check
2
- /**
3
- * @typedef {import('@octanejs/vite-plugin').Route} Route
4
- * @typedef {import('@octanejs/vite-plugin').RenderRoute} RenderRoute
5
- * @typedef {import('@octanejs/vite-plugin').ServerRoute} ServerRoute
6
- */
7
-
8
- /**
9
- * @typedef {Object} RouteMatch
10
- * @property {Route} route
11
- * @property {Record<string, string>} params
12
- */
13
-
14
- /**
15
- * @typedef {Object} CompiledRoute
16
- * @property {Route} route
17
- * @property {RegExp} pattern
18
- * @property {string[]} paramNames
19
- * @property {number} specificity - Higher = more specific (static > param > catch-all)
20
- */
21
-
22
- /**
23
- * Convert a route path pattern to a RegExp
24
- * Supports:
25
- * - Static segments: /about, /api/hello
26
- * - Named params: /posts/:id, /users/:userId/posts/:postId
27
- * - Catch-all: /docs/*slug
28
- *
29
- * @param {string} path
30
- * @returns {{ pattern: RegExp, paramNames: string[], specificity: number }}
31
- */
32
- function compilePath(path) {
33
- /** @type {string[]} */
34
- const paramNames = [];
35
- let specificity = 0;
36
-
37
- // Escape special regex characters except our param syntax
38
- const regexString = path
39
- .split('/')
40
- .map((segment) => {
41
- if (!segment) return '';
42
-
43
- // Catch-all param: *slug
44
- if (segment.startsWith('*')) {
45
- const paramName = segment.slice(1);
46
- paramNames.push(paramName);
47
- specificity += 1; // Lowest specificity
48
- return '(.+)';
49
- }
50
-
51
- // Named param: :id
52
- if (segment.startsWith(':')) {
53
- const paramName = segment.slice(1);
54
- paramNames.push(paramName);
55
- specificity += 10; // Medium specificity
56
- return '([^/]+)';
57
- }
58
-
59
- // Static segment
60
- specificity += 100; // Highest specificity
61
- return escapeRegex(segment);
62
- })
63
- .join('/');
64
-
65
- const pattern = new RegExp(`^${regexString || '/'}$`);
66
- return { pattern, paramNames, specificity };
67
- }
68
-
69
- /**
70
- * Escape special regex characters
71
- * @param {string} str
72
- * @returns {string}
73
- */
74
- function escapeRegex(str) {
75
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
76
- }
77
-
78
- /**
79
- * Create a router from a list of routes
80
- * @param {Route[]} routes
81
- * @returns {{ match: (method: string, pathname: string) => RouteMatch | null }}
82
- */
83
- export function createRouter(routes) {
84
- /** @type {CompiledRoute[]} */
85
- const compiledRoutes = routes.map((route) => {
86
- const { pattern, paramNames, specificity } = compilePath(route.path);
87
- return { route, pattern, paramNames, specificity };
88
- });
89
-
90
- // Sort by specificity (higher first) for correct matching order
91
- compiledRoutes.sort((a, b) => b.specificity - a.specificity);
92
-
93
- return {
94
- /**
95
- * Match a request to a route
96
- * @param {string} method
97
- * @param {string} pathname
98
- * @returns {RouteMatch | null}
99
- */
100
- match(method, pathname) {
101
- for (const { route, pattern, paramNames } of compiledRoutes) {
102
- // Check method for ServerRoute
103
- if (route.type === 'server') {
104
- const methods = /** @type {ServerRoute} */ (route).methods;
105
- if (!methods.includes(method.toUpperCase())) {
106
- continue;
107
- }
108
- }
109
-
110
- const match = pathname.match(pattern);
111
- if (match) {
112
- /** @type {Record<string, string>} */
113
- const params = {};
114
- for (let i = 0; i < paramNames.length; i++) {
115
- params[paramNames[i]] = decodeURIComponent(match[i + 1]);
116
- }
117
- return { route, params };
118
- }
119
- }
120
- return null;
121
- },
122
- };
123
- }
1
+ export { createRouter } from '@octanejs/app-core/routes';
@@ -1,47 +1 @@
1
- // @ts-check
2
- /**
3
- * @typedef {import('@octanejs/vite-plugin').Context} Context
4
- * @typedef {import('@octanejs/vite-plugin').ServerRoute} ServerRoute
5
- * @typedef {import('@octanejs/vite-plugin').Middleware} Middleware
6
- */
7
-
8
- import { runMiddlewareChain } from './middleware.js';
9
-
10
- /**
11
- * Handle a ServerRoute (API endpoint)
12
- *
13
- * @param {ServerRoute} route
14
- * @param {Context} context
15
- * @param {Middleware[]} globalMiddlewares
16
- * @returns {Promise<Response>}
17
- */
18
- export async function handleServerRoute(route, context, globalMiddlewares) {
19
- try {
20
- // The handler wrapped as a function returning Promise<Response>
21
- const handler = async () => {
22
- return route.handler(context);
23
- };
24
-
25
- // Run the middleware chain: global → before → handler → after
26
- const response = await runMiddlewareChain(
27
- context,
28
- globalMiddlewares,
29
- route.before,
30
- handler,
31
- route.after,
32
- );
33
-
34
- return response;
35
- } catch (error) {
36
- console.error('[octane] API route error:', error);
37
-
38
- // Return error response
39
- const message = error instanceof Error ? error.message : 'Internal Server Error';
40
- return new Response(JSON.stringify({ error: message }), {
41
- status: 500,
42
- headers: {
43
- 'Content-Type': 'application/json',
44
- },
45
- });
46
- }
47
- }
1
+ export { handleServerRoute } from '@octanejs/app-core/middleware';