@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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # @octanejs/vite-plugin
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1](https://github.com/octanejs/octane/pull/1) [`dcdf237`](https://github.com/octanejs/octane/commit/dcdf2375ce3a8a2e00b1e1de04f65c2529fd287e) Thanks [@trueadm](https://github.com/trueadm)! - Rename the project from `vyre` to `octane`. The runtime now publishes as `octane` and the Vite metaframework plugin as `@octanejs/vite-plugin`. Identifiers inherited from the Ripple fork were also renamed to Octane (e.g. `setIsRippleActEnvironment` → `setIsOctaneActEnvironment`, the metaframework `ripple()` plugin → `octane()`, and the `ripple.config.ts` convention → `octane.config.ts`). References to the upstream Ripple framework and its `@ripple-ts`/`@tsrx` packages are unchanged.
8
+
9
+ - Updated dependencies [[`dcdf237`](https://github.com/octanejs/octane/commit/dcdf2375ce3a8a2e00b1e1de04f65c2529fd287e)]:
10
+ - octane@0.1.1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@octanejs/vite-plugin",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "Vite metaframework plugin for the octane renderer (dev SSR + routing + hydrate)",
7
+ "author": {
8
+ "name": "Dominic Gannaway",
9
+ "email": "dg@domgan.com"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/octanejs/octane.git",
17
+ "directory": "packages/vite-plugin-octane"
18
+ },
19
+ "module": "src/index.js",
20
+ "main": "src/index.js",
21
+ "bin": {
22
+ "octane-preview": "src/bin/preview.js"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./types/index.d.ts",
27
+ "import": "./src/index.js",
28
+ "default": "./src/index.js"
29
+ },
30
+ "./production": {
31
+ "types": "./types/production.d.ts",
32
+ "import": "./src/server/production.js",
33
+ "default": "./src/server/production.js"
34
+ }
35
+ },
36
+ "dependencies": {
37
+ "@ripple-ts/adapter": "^0.3.84",
38
+ "octane": "0.1.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.3.0",
42
+ "type-fest": "^5.6.0",
43
+ "vite": "^8.0.16"
44
+ }
45
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * octane-preview — Start the production SSR server.
5
+ *
6
+ * Loads octane.config.ts, reads `build.outDir`,
7
+ * and spawns `node {outDir}/server/entry.js`.
8
+ *
9
+ * NOTE: the server entry it spawns is produced by the Phase 2 production
10
+ * build. Until then this validates existence and errors clearly.
11
+ */
12
+
13
+ import { spawn } from 'node:child_process';
14
+ import path from 'node:path';
15
+ import fs from 'node:fs';
16
+ import { loadOctaneConfig } from '../load-config.js';
17
+ import { ENTRY_FILENAME } from '../constants.js';
18
+
19
+ const projectRoot = process.cwd();
20
+
21
+ try {
22
+ const config = await loadOctaneConfig(projectRoot);
23
+ const outDir = config.build.outDir;
24
+ const entryPath = path.join(projectRoot, outDir, 'server', ENTRY_FILENAME);
25
+
26
+ if (!fs.existsSync(entryPath)) {
27
+ console.error(`[octane-preview] Server entry not found: ${entryPath}`);
28
+ console.error('[octane-preview] Did you run `pnpm build` first?');
29
+ process.exit(1);
30
+ }
31
+
32
+ console.log(`[octane-preview] Starting server from ${outDir}/server/${ENTRY_FILENAME}`);
33
+
34
+ const child = spawn(process.execPath, [entryPath], {
35
+ stdio: 'inherit',
36
+ cwd: projectRoot,
37
+ });
38
+
39
+ child.on('close', (code) => {
40
+ process.exit(code ?? 0);
41
+ });
42
+ } catch (e) {
43
+ const error = /** @type {Error} */ (e);
44
+ console.error('[octane-preview] Failed to load config:', error.message);
45
+ process.exit(1);
46
+ }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_OUTDIR = 'dist';
2
+ export const ENTRY_FILENAME = 'entry.js';
package/src/index.js ADDED
@@ -0,0 +1,486 @@
1
+ /** @import {Plugin, ResolvedConfig, ViteDevServer, UserConfig} from 'vite' */
2
+ /** @import {OctaneConfigOptions, ResolvedOctaneConfig, RenderRoute} from '@octanejs/vite-plugin' */
3
+
4
+ import fs from 'node:fs';
5
+ import { Readable } from 'node:stream';
6
+ import { AsyncLocalStorage } from 'node:async_hooks';
7
+
8
+ import { octane as octaneCompiler } from 'octane/compiler/vite';
9
+
10
+ import { createRouter } from './server/router.js';
11
+ import { createContext, runMiddlewareChain } from './server/middleware.js';
12
+ import { handleRenderRoute } from './server/render-route.js';
13
+ import { handleServerRoute } from './server/server-route.js';
14
+ import {
15
+ getOctaneConfigPath,
16
+ loadOctaneConfig,
17
+ resolveOctaneConfig,
18
+ octaneConfigExists,
19
+ } from './load-config.js';
20
+ import {
21
+ RESOLVED_ADAPTER_BROWSER_STUB_ID,
22
+ SERVER_ONLY_ADAPTER_IDS,
23
+ create_adapter_browser_stub_source,
24
+ create_client_entry_source,
25
+ to_vite_root_import,
26
+ write_project_generated_file,
27
+ } from './project-codegen.js';
28
+
29
+ import { patch_global_fetch, is_rpc_request, handle_rpc_request } from '@ripple-ts/adapter/rpc';
30
+
31
+ // Re-export route classes + config helpers (public API surface).
32
+ export { RenderRoute, ServerRoute } from './routes.js';
33
+ export {
34
+ getOctaneConfigPath,
35
+ loadOctaneConfig,
36
+ resolveOctaneConfig,
37
+ octaneConfigExists,
38
+ } from './load-config.js';
39
+
40
+ const VIRTUAL_HYDRATE_ID = 'virtual:octane-hydrate';
41
+ const RESOLVED_VIRTUAL_HYDRATE_ID = '\0virtual:octane-hydrate';
42
+ const OCTANE_EXTENSIONS = ['.tsrx'];
43
+
44
+ /**
45
+ * @param {string} file_name
46
+ * @returns {boolean}
47
+ */
48
+ function is_octane_module_path(file_name) {
49
+ return OCTANE_EXTENSIONS.some((extension) => file_name.endsWith(extension));
50
+ }
51
+
52
+ /** @type {import('@ripple-ts/adapter/rpc').AsyncContext | null} */
53
+ let devAsyncContext = null;
54
+
55
+ /**
56
+ * Get (or lazily create) the dev server's async context — Node.js
57
+ * AsyncLocalStorage by default, or the adapter's runtime context if provided.
58
+ * Patches global fetch once so relative-URL fetch + RPC work in dev.
59
+ *
60
+ * @param {OctaneConfigOptions | null} config
61
+ * @returns {import('@ripple-ts/adapter/rpc').AsyncContext}
62
+ */
63
+ function getDevAsyncContext(config) {
64
+ if (devAsyncContext) return devAsyncContext;
65
+
66
+ const adapterRuntime = config?.adapter?.runtime;
67
+ if (adapterRuntime?.createAsyncContext) {
68
+ devAsyncContext = adapterRuntime.createAsyncContext();
69
+ } else {
70
+ const als = new AsyncLocalStorage();
71
+ devAsyncContext = {
72
+ run: (store, fn) => als.run(store, fn),
73
+ getStore: () => als.getStore(),
74
+ };
75
+ }
76
+
77
+ patch_global_fetch(devAsyncContext);
78
+ return devAsyncContext;
79
+ }
80
+
81
+ /**
82
+ * @param {ResolvedOctaneConfig | null} config
83
+ * @returns {boolean}
84
+ */
85
+ function has_route_config(config) {
86
+ return (config?.router.routes.length ?? 0) > 0;
87
+ }
88
+
89
+ /**
90
+ * The octane metaframework Vite plugin.
91
+ *
92
+ * Returns an ARRAY: `[octaneCompiler({ hmr }), metaPlugin]`. The first element is
93
+ * octane/compiler's transform plugin — it owns ALL `.tsrx` compilation, picking
94
+ * client vs server mode per-module from Vite's SSR signal (so the SAME file
95
+ * compiles to a DOM-clone client body for the browser and to an HTML-building
96
+ * server body when pulled via `ssrLoadModule`). The metaPlugin owns config,
97
+ * routing, dev SSR, the client hydrate virtual module, and dev RPC.
98
+ *
99
+ * PHASE 1 = dev SSR + routing + hydrate. Production build (buildStart /
100
+ * closeBundle / transformIndexHtml / server-entry / adapter.serve) is Phase 2.
101
+ *
102
+ * @param {{ hmr?: boolean }} [inlineOptions]
103
+ * @returns {Plugin[]}
104
+ */
105
+ export function octane(inlineOptions = {}) {
106
+ /** @type {ResolvedConfig} */
107
+ let config;
108
+ /** @type {string} */
109
+ let root;
110
+ /** @type {ResolvedOctaneConfig | null} */
111
+ let octaneConfig = null;
112
+ /** @type {ReturnType<typeof createRouter> | null} */
113
+ let router = null;
114
+
115
+ /** @type {Plugin} */
116
+ const metaPlugin = {
117
+ name: '@octanejs/vite-plugin',
118
+
119
+ /**
120
+ * @param {UserConfig} userConfig
121
+ */
122
+ config(userConfig) {
123
+ const exclude = userConfig.optimizeDeps?.exclude || [];
124
+ return {
125
+ // SSR owns routing; do not let Vite SPA-fallback to index.html.
126
+ appType: 'custom',
127
+ optimizeDeps: {
128
+ exclude: [
129
+ // `@octanejs/query` ships a `.tsrx` provider component, so it must NOT
130
+ // be esbuild-prebundled — the octane transform owns `.tsrx` compilation.
131
+ ...new Set([
132
+ ...exclude,
133
+ 'octane',
134
+ 'octane/compiler',
135
+ '@octanejs/query',
136
+ ...SERVER_ONLY_ADAPTER_IDS,
137
+ ]),
138
+ ],
139
+ },
140
+ // Workspace packages with TS source must be transformed by Vite's SSR
141
+ // pipeline (not require()'d raw) so ssrLoadModule gets transpiled code.
142
+ ssr: {
143
+ noExternal: ['octane', 'octane/compiler', '@octanejs/query'],
144
+ },
145
+ };
146
+ },
147
+
148
+ async configResolved(resolvedConfig) {
149
+ root = resolvedConfig.root;
150
+ config = resolvedConfig;
151
+ },
152
+
153
+ async resolveId(id, _importer, options) {
154
+ // Browser stub for server-only adapter packages imported from client code.
155
+ if (!options?.ssr && SERVER_ONLY_ADAPTER_IDS.has(id)) {
156
+ return RESOLVED_ADAPTER_BROWSER_STUB_ID;
157
+ }
158
+ if (id === VIRTUAL_HYDRATE_ID) {
159
+ return RESOLVED_VIRTUAL_HYDRATE_ID;
160
+ }
161
+ return null;
162
+ },
163
+
164
+ async load(id) {
165
+ if (id === RESOLVED_ADAPTER_BROWSER_STUB_ID) {
166
+ return create_adapter_browser_stub_source();
167
+ }
168
+ if (id === RESOLVED_VIRTUAL_HYDRATE_ID) {
169
+ // Dev: dynamic import() of the route entry works through Vite, so the
170
+ // static import map is left empty (the codegen falls back to a dynamic
171
+ // import per entry). The production static map is Phase 2.
172
+ const file = write_project_generated_file(
173
+ config,
174
+ 'client-entry.js',
175
+ create_client_entry_source({
176
+ configPath: to_vite_root_import(getOctaneConfigPath(root), root),
177
+ staticEntries: [],
178
+ }),
179
+ );
180
+ return fs.readFileSync(file, 'utf-8');
181
+ }
182
+ return null;
183
+ },
184
+
185
+ /**
186
+ * Dev SSR middleware. Registered as a pre-hook (no return) so it runs
187
+ * BEFORE Vite's HTML fallback. Config is loaded lazily on first request
188
+ * (ssrLoadModule isn't ready when configureServer runs).
189
+ *
190
+ * @param {ViteDevServer} vite
191
+ */
192
+ configureServer(vite) {
193
+ /** @type {Promise<void> | null} */
194
+ let initPromise = null;
195
+ /** @type {number} */
196
+ let lastConfigErrorMtimeMs = 0;
197
+
198
+ async function ensureConfigLoaded() {
199
+ if (octaneConfig && router) return;
200
+ if (initPromise) {
201
+ await initPromise;
202
+ return;
203
+ }
204
+
205
+ const configPath = getOctaneConfigPath(root);
206
+ if (!octaneConfigExists(root)) return;
207
+
208
+ if (lastConfigErrorMtimeMs) {
209
+ try {
210
+ const stat = fs.statSync(configPath);
211
+ if (stat.mtimeMs <= lastConfigErrorMtimeMs) return;
212
+ } catch {
213
+ return;
214
+ }
215
+ }
216
+
217
+ let preLoadMtimeMs;
218
+ try {
219
+ preLoadMtimeMs = fs.statSync(configPath).mtimeMs;
220
+ } catch {
221
+ preLoadMtimeMs = 0;
222
+ }
223
+
224
+ initPromise = (async () => {
225
+ const nextConfig = await loadOctaneConfig(root, { vite });
226
+ octaneConfig = nextConfig;
227
+ router = has_route_config(nextConfig) ? createRouter(nextConfig.router.routes) : null;
228
+ if (router) {
229
+ console.log(
230
+ `[@octanejs/vite-plugin] Loaded ${nextConfig.router.routes.length} routes from octane.config.ts`,
231
+ );
232
+ }
233
+ })()
234
+ .catch((error) => {
235
+ lastConfigErrorMtimeMs = preLoadMtimeMs;
236
+ throw error;
237
+ })
238
+ .finally(() => {
239
+ initPromise = null;
240
+ });
241
+
242
+ await initPromise;
243
+ }
244
+
245
+ vite.middlewares.use(function octaneDevMiddleware(req, res, next) {
246
+ (async () => {
247
+ try {
248
+ await ensureConfigLoaded();
249
+ } catch (error) {
250
+ vite.ssrFixStacktrace(/** @type {Error} */ (error));
251
+ console.error('[@octanejs/vite-plugin] Failed to load octane.config.ts:', error);
252
+ next();
253
+ return;
254
+ }
255
+
256
+ if (!router || !octaneConfig) {
257
+ next();
258
+ return;
259
+ }
260
+
261
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
262
+ const method = req.method || 'GET';
263
+
264
+ // RPC requests for `module server` declarations.
265
+ if (is_rpc_request(url.pathname)) {
266
+ await handleRpcRequest(req, res, vite, octaneConfig.server.trustProxy, octaneConfig);
267
+ return;
268
+ }
269
+
270
+ const match = router.match(method, url.pathname);
271
+ if (!match) {
272
+ next();
273
+ return;
274
+ }
275
+
276
+ try {
277
+ // Reload config so route edits are picked up (dev HMR for routes).
278
+ const previousRoutes = octaneConfig.router.routes;
279
+ const freshConfig = await loadOctaneConfig(root, { vite });
280
+ if (freshConfig) octaneConfig = freshConfig;
281
+ if (JSON.stringify(previousRoutes) !== JSON.stringify(octaneConfig.router.routes)) {
282
+ console.log(
283
+ `[@octanejs/vite-plugin] Detected route changes. Reloaded ${octaneConfig.router.routes.length} routes`,
284
+ );
285
+ }
286
+ router = createRouter(octaneConfig.router.routes);
287
+
288
+ const freshMatch = router.match(method, url.pathname);
289
+ if (!freshMatch) {
290
+ next();
291
+ return;
292
+ }
293
+
294
+ const request = nodeRequestToWebRequest(req);
295
+ const context = createContext(request, freshMatch.params);
296
+ const globalMiddlewares = octaneConfig.middlewares;
297
+
298
+ let response;
299
+ if (freshMatch.route.type === 'render') {
300
+ response = await runMiddlewareChain(
301
+ context,
302
+ globalMiddlewares,
303
+ freshMatch.route.before || [],
304
+ async () =>
305
+ handleRenderRoute(
306
+ /** @type {RenderRoute} */ (freshMatch.route),
307
+ context,
308
+ vite,
309
+ octaneConfig ?? undefined,
310
+ ),
311
+ [],
312
+ );
313
+ } else {
314
+ response = await handleServerRoute(freshMatch.route, context, globalMiddlewares);
315
+ }
316
+
317
+ await sendWebResponse(res, response);
318
+ } catch (error) {
319
+ console.error('[@octanejs/vite-plugin] Request error:', error);
320
+ vite.ssrFixStacktrace(/** @type {Error} */ (error));
321
+ res.statusCode = 500;
322
+ res.setHeader('Content-Type', 'text/html');
323
+ res.end(
324
+ `<pre style="color:red;background:#1a1a1a;padding:2rem;margin:0;">${escapeHtml(
325
+ error instanceof Error ? error.stack || error.message : String(error),
326
+ )}</pre>`,
327
+ );
328
+ }
329
+ })().catch((err) => {
330
+ console.error('[@octanejs/vite-plugin] Unhandled middleware error:', err);
331
+ if (!res.headersSent) {
332
+ res.statusCode = 500;
333
+ res.end('Internal Server Error');
334
+ }
335
+ });
336
+ });
337
+ },
338
+
339
+ /**
340
+ * HMR: let self-accepting client modules update normally; otherwise
341
+ * invalidate the matching SSR modules so the next SSR request recompiles,
342
+ * and full-reload. (octane has no compile-time CSS cache — styles ride
343
+ * `injectStyle` calls in the compiled body — so there is no CSS to diff.)
344
+ */
345
+ hotUpdate: {
346
+ order: 'pre',
347
+ async handler({ file, modules, server }) {
348
+ if (this.environment.name !== 'client') return;
349
+ if (modules.length > 0 && modules.every((m) => m.isSelfAccepting)) return;
350
+ if (!is_octane_module_path(file)) return;
351
+
352
+ const ssr = server.environments.ssr;
353
+ if (ssr) {
354
+ const ssrModules = ssr.moduleGraph.getModulesByFile(file);
355
+ if (ssrModules) {
356
+ for (const mod of ssrModules) ssr.moduleGraph.invalidateModule(mod);
357
+ }
358
+ }
359
+ this.environment.hot.send({ type: 'full-reload' });
360
+ return [];
361
+ },
362
+ },
363
+ };
364
+
365
+ const hmr = inlineOptions.hmr;
366
+ return [octaneCompiler(hmr === undefined ? {} : { hmr }), metaPlugin];
367
+ }
368
+
369
+ // Mainly to enforce types / DX.
370
+ export function defineConfig(/** @type {OctaneConfigOptions} */ options) {
371
+ return options;
372
+ }
373
+
374
+ // ============================================================================
375
+ // Dev-server HTTP helpers
376
+ // ============================================================================
377
+
378
+ /**
379
+ * Convert a Node.js IncomingMessage to a Web Request.
380
+ * @param {import('node:http').IncomingMessage} nodeRequest
381
+ * @returns {Request}
382
+ */
383
+ function nodeRequestToWebRequest(nodeRequest) {
384
+ const host = nodeRequest.headers.host || 'localhost';
385
+ const url = new URL(nodeRequest.url || '/', `http://${host}`);
386
+
387
+ const headers = new Headers();
388
+ for (const [key, value] of Object.entries(nodeRequest.headers)) {
389
+ if (value == null) continue;
390
+ if (Array.isArray(value)) {
391
+ for (const v of value) headers.append(key, v);
392
+ } else {
393
+ headers.set(key, value);
394
+ }
395
+ }
396
+
397
+ const method = (nodeRequest.method || 'GET').toUpperCase();
398
+ /** @type {RequestInit & { duplex?: 'half' }} */
399
+ const init = { method, headers };
400
+ if (method !== 'GET' && method !== 'HEAD') {
401
+ init.body = Readable.toWeb(nodeRequest);
402
+ init.duplex = 'half';
403
+ }
404
+ return new Request(url, init);
405
+ }
406
+
407
+ /**
408
+ * Pipe a Web Response to a Node.js ServerResponse.
409
+ * @param {import('node:http').ServerResponse} nodeResponse
410
+ * @param {Response} webResponse
411
+ */
412
+ async function sendWebResponse(nodeResponse, webResponse) {
413
+ nodeResponse.statusCode = webResponse.status;
414
+ if (webResponse.statusText) nodeResponse.statusMessage = webResponse.statusText;
415
+ webResponse.headers.forEach((value, key) => {
416
+ nodeResponse.setHeader(key, value);
417
+ });
418
+ if (webResponse.body) {
419
+ const reader = webResponse.body.getReader();
420
+ try {
421
+ while (true) {
422
+ const { done, value } = await reader.read();
423
+ if (done) break;
424
+ nodeResponse.write(value);
425
+ }
426
+ } finally {
427
+ reader.releaseLock();
428
+ }
429
+ }
430
+ nodeResponse.end();
431
+ }
432
+
433
+ /**
434
+ * Handle a dev RPC request for `module server` declarations.
435
+ *
436
+ * @param {import('node:http').IncomingMessage} req
437
+ * @param {import('node:http').ServerResponse} res
438
+ * @param {import('vite').ViteDevServer} vite
439
+ * @param {boolean} trustProxy
440
+ * @param {OctaneConfigOptions | null} config
441
+ */
442
+ async function handleRpcRequest(req, res, vite, trustProxy, config) {
443
+ try {
444
+ const webRequest = nodeRequestToWebRequest(req);
445
+ const asyncContext = getDevAsyncContext(config);
446
+
447
+ const response = await handle_rpc_request(webRequest, {
448
+ async resolveFunction(hash) {
449
+ const rpcModules = globalThis.rpc_modules;
450
+ if (!rpcModules) return null;
451
+ const moduleInfo = rpcModules.get(hash);
452
+ if (!moduleInfo) return null;
453
+ const [filePath, funcName] = moduleInfo;
454
+ const module = await vite.ssrLoadModule(filePath);
455
+ const server = module._$_server_$_;
456
+ if (!server || !server[funcName]) return null;
457
+ return server[funcName];
458
+ },
459
+ async executeServerFunction(fn, body) {
460
+ const { executeServerFunction } = await vite.ssrLoadModule('octane/server');
461
+ return executeServerFunction(fn, body);
462
+ },
463
+ asyncContext,
464
+ trustProxy,
465
+ });
466
+
467
+ await sendWebResponse(res, response);
468
+ } catch (error) {
469
+ console.error('[@octanejs/vite-plugin] RPC error:', error);
470
+ res.statusCode = 500;
471
+ res.setHeader('Content-Type', 'application/json');
472
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'RPC failed' }));
473
+ }
474
+ }
475
+
476
+ /**
477
+ * @param {string} str
478
+ * @returns {string}
479
+ */
480
+ function escapeHtml(str) {
481
+ return str
482
+ .replace(/&/g, '&amp;')
483
+ .replace(/</g, '&lt;')
484
+ .replace(/>/g, '&gt;')
485
+ .replace(/"/g, '&quot;');
486
+ }