@ripple-ts/vite-plugin 0.2.210 → 0.2.212
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 +4 -0
- package/package.json +2 -2
- package/src/index.js +470 -4
- package/src/routes.js +70 -0
- package/src/server/middleware.js +126 -0
- package/src/server/production.js +266 -0
- package/src/server/render-route.js +239 -0
- package/src/server/router.js +122 -0
- package/src/server/server-route.js +46 -0
- package/types/index.d.ts +116 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Vite plugin for Ripple",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.212",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/index.js",
|
|
9
9
|
"main": "src/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"type-fest": "^5.1.0",
|
|
28
28
|
"vite": "^7.1.9",
|
|
29
|
-
"ripple": "0.2.
|
|
29
|
+
"ripple": "0.2.212"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,79 @@
|
|
|
1
1
|
/** @import {PackageJson} from 'type-fest' */
|
|
2
|
-
/** @import {Plugin, ResolvedConfig} from 'vite' */
|
|
3
|
-
/** @import {RipplePluginOptions} from '@ripple-ts/vite-plugin' */
|
|
2
|
+
/** @import {Plugin, ResolvedConfig, ViteDevServer} from 'vite' */
|
|
3
|
+
/** @import {RipplePluginOptions, RippleConfigOptions, Route, Middleware, RenderRoute} from '@ripple-ts/vite-plugin' */
|
|
4
|
+
|
|
5
|
+
/// <reference types="ripple/compiler/internal/rpc" />
|
|
4
6
|
|
|
5
7
|
import { compile } from 'ripple/compiler';
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
9
|
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
7
11
|
import { createRequire } from 'node:module';
|
|
12
|
+
import { Readable } from 'node:stream';
|
|
13
|
+
|
|
14
|
+
import { createRouter } from './server/router.js';
|
|
15
|
+
import { createContext, runMiddlewareChain } from './server/middleware.js';
|
|
16
|
+
import { handleRenderRoute } from './server/render-route.js';
|
|
17
|
+
import { handleServerRoute } from './server/server-route.js';
|
|
18
|
+
|
|
19
|
+
// Re-export route classes
|
|
20
|
+
export { RenderRoute, ServerRoute } from './routes.js';
|
|
8
21
|
|
|
9
22
|
const VITE_FS_PREFIX = '/@fs/';
|
|
10
23
|
const IS_WINDOWS = process.platform === 'win32';
|
|
11
24
|
|
|
25
|
+
// AsyncLocalStorage for request-scoped fetch patching
|
|
26
|
+
const rpcContext = new AsyncLocalStorage();
|
|
27
|
+
|
|
28
|
+
// Patch fetch once at module level to support relative URLs in #server blocks
|
|
29
|
+
const originalFetch = globalThis.fetch;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Quick check whether a string looks like it already has a URL scheme (e.g. "http://", "https://", "data:").
|
|
33
|
+
* @param {string} url
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function hasScheme(url) {
|
|
37
|
+
return /^[a-z][a-z0-9+\-.]*:/i.test(url);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Patch global fetch to resolve relative URLs based on the current request context.
|
|
42
|
+
* This allows server functions in #server blocks to use relative URLs
|
|
43
|
+
* (root-relative like "/api/foo" or path-relative like "api/foo", "./api/foo", "../api/foo")
|
|
44
|
+
* that are resolved against the incoming request's origin.
|
|
45
|
+
* // TODO: a similar logic needs to be ported to the adapters
|
|
46
|
+
* @param {string | Request | URL} input
|
|
47
|
+
* @param {RequestInit} [init]
|
|
48
|
+
* @returns {Promise<Response>}
|
|
49
|
+
*/
|
|
50
|
+
globalThis.fetch = function (input, init) {
|
|
51
|
+
const context = rpcContext.getStore();
|
|
52
|
+
|
|
53
|
+
if (context?.origin) {
|
|
54
|
+
// Handle string URLs — resolve any non-absolute URL against the origin
|
|
55
|
+
if (typeof input === 'string' && !hasScheme(input)) {
|
|
56
|
+
input = new URL(input, context.origin).href;
|
|
57
|
+
}
|
|
58
|
+
// Handle Request objects
|
|
59
|
+
else if (input instanceof Request) {
|
|
60
|
+
const url = input.url;
|
|
61
|
+
if (!hasScheme(url)) {
|
|
62
|
+
input = new Request(new URL(url, context.origin).href, input);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Handle URL objects constructed with relative paths
|
|
66
|
+
else if (input instanceof URL) {
|
|
67
|
+
if (!input.protocol || input.protocol === '' || input.origin === 'null') {
|
|
68
|
+
const relative = input.pathname + (input.search || '') + (input.hash || '');
|
|
69
|
+
input = new URL(relative, context.origin);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return originalFetch(input, init);
|
|
75
|
+
};
|
|
76
|
+
|
|
12
77
|
/**
|
|
13
78
|
* @param {string} filename
|
|
14
79
|
* @param {ResolvedConfig['root']} root
|
|
@@ -252,10 +317,15 @@ export function ripple(inlineOptions = {}) {
|
|
|
252
317
|
const ripplePackages = new Set();
|
|
253
318
|
const cssCache = new Map();
|
|
254
319
|
|
|
320
|
+
/** @type {RippleConfigOptions | null} */
|
|
321
|
+
let rippleConfig = null;
|
|
322
|
+
/** @type {ReturnType<typeof createRouter> | null} */
|
|
323
|
+
let router = null;
|
|
324
|
+
|
|
255
325
|
/** @type {Plugin[]} */
|
|
256
326
|
const plugins = [
|
|
257
327
|
{
|
|
258
|
-
name: 'vite-plugin',
|
|
328
|
+
name: 'vite-plugin-ripple',
|
|
259
329
|
// make sure our resolver runs before vite internal resolver to resolve ripple field correctly
|
|
260
330
|
enforce: 'pre',
|
|
261
331
|
api,
|
|
@@ -304,7 +374,161 @@ export function ripple(inlineOptions = {}) {
|
|
|
304
374
|
config = resolvedConfig;
|
|
305
375
|
},
|
|
306
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Configure the dev server with SSR middleware
|
|
379
|
+
* @param {ViteDevServer} vite
|
|
380
|
+
*/
|
|
381
|
+
configureServer(vite) {
|
|
382
|
+
// Return a function to be called after Vite's internal middlewares
|
|
383
|
+
return async () => {
|
|
384
|
+
// Load ripple.config.ts
|
|
385
|
+
const configPath = path.join(root, 'ripple.config.ts');
|
|
386
|
+
if (!fs.existsSync(configPath)) {
|
|
387
|
+
console.log('[@ripple-ts/vite-plugin] No ripple.config.ts found, skipping SSR setup');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const configModule = await vite.ssrLoadModule(configPath);
|
|
393
|
+
rippleConfig = configModule.default;
|
|
394
|
+
|
|
395
|
+
if (!rippleConfig?.router?.routes) {
|
|
396
|
+
console.log('[@ripple-ts/vite-plugin] No routes defined in ripple.config.ts');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Create router from config
|
|
401
|
+
router = createRouter(rippleConfig.router.routes);
|
|
402
|
+
console.log(
|
|
403
|
+
`[@ripple-ts/vite-plugin] Loaded ${rippleConfig.router.routes.length} routes from ripple.config.ts`,
|
|
404
|
+
);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error('[@ripple-ts/vite-plugin] Failed to load ripple.config.ts:', error);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Add SSR middleware
|
|
411
|
+
vite.middlewares.use((req, res, next) => {
|
|
412
|
+
// Handle async logic in an IIFE
|
|
413
|
+
(async () => {
|
|
414
|
+
// Skip if no router
|
|
415
|
+
if (!router || !rippleConfig) {
|
|
416
|
+
next();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
421
|
+
const method = req.method || 'GET';
|
|
422
|
+
|
|
423
|
+
// Handle RPC requests for #server blocks
|
|
424
|
+
if (url.pathname.startsWith('/_$_ripple_rpc_$_/')) {
|
|
425
|
+
await handleRpcRequest(
|
|
426
|
+
req,
|
|
427
|
+
res,
|
|
428
|
+
url,
|
|
429
|
+
vite,
|
|
430
|
+
rippleConfig.server?.trustProxy ?? false,
|
|
431
|
+
);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Match route
|
|
436
|
+
const match = router.match(method, url.pathname);
|
|
437
|
+
|
|
438
|
+
if (!match) {
|
|
439
|
+
next();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
// Reload config to get fresh routes (for HMR)
|
|
445
|
+
const freshConfig = await vite.ssrLoadModule(configPath);
|
|
446
|
+
rippleConfig = freshConfig.default;
|
|
447
|
+
|
|
448
|
+
if (!rippleConfig || !rippleConfig.router || !rippleConfig.router.routes) {
|
|
449
|
+
console.log('[@ripple-ts/vite-plugin] No routes defined in ripple.config.ts');
|
|
450
|
+
next();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check if routes have changed
|
|
455
|
+
if (
|
|
456
|
+
JSON.stringify(freshConfig.default.router.routes) !==
|
|
457
|
+
JSON.stringify(rippleConfig.router.routes)
|
|
458
|
+
) {
|
|
459
|
+
console.log(
|
|
460
|
+
`[@ripple-ts/vite-plugin] Detected route changes. Re-loading ${rippleConfig.router.routes.length} routes from ripple.config.ts`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
router = createRouter(rippleConfig.router.routes);
|
|
465
|
+
|
|
466
|
+
// Re-match with fresh router
|
|
467
|
+
const freshMatch = router.match(method, url.pathname);
|
|
468
|
+
if (!freshMatch) {
|
|
469
|
+
next();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Create context
|
|
474
|
+
const request = nodeRequestToWebRequest(req);
|
|
475
|
+
const context = createContext(request, freshMatch.params);
|
|
476
|
+
|
|
477
|
+
// Get global middlewares
|
|
478
|
+
const globalMiddlewares = rippleConfig.middlewares || [];
|
|
479
|
+
|
|
480
|
+
let response;
|
|
481
|
+
|
|
482
|
+
if (freshMatch.route.type === 'render') {
|
|
483
|
+
// Handle RenderRoute with global middlewares
|
|
484
|
+
response = await runMiddlewareChain(
|
|
485
|
+
context,
|
|
486
|
+
globalMiddlewares,
|
|
487
|
+
freshMatch.route.before || [],
|
|
488
|
+
async () =>
|
|
489
|
+
handleRenderRoute(
|
|
490
|
+
/** @type {RenderRoute} */ (freshMatch.route),
|
|
491
|
+
context,
|
|
492
|
+
vite,
|
|
493
|
+
),
|
|
494
|
+
[],
|
|
495
|
+
);
|
|
496
|
+
} else {
|
|
497
|
+
// Handle ServerRoute
|
|
498
|
+
response = await handleServerRoute(freshMatch.route, context, globalMiddlewares);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Send response
|
|
502
|
+
await sendWebResponse(res, response);
|
|
503
|
+
} catch (/** @type {any} */ error) {
|
|
504
|
+
console.error('[@ripple-ts/vite-plugin] Request error:', error);
|
|
505
|
+
vite.ssrFixStacktrace(error);
|
|
506
|
+
|
|
507
|
+
res.statusCode = 500;
|
|
508
|
+
res.setHeader('Content-Type', 'text/html');
|
|
509
|
+
res.end(
|
|
510
|
+
`<pre style="color: red; background: #1a1a1a; padding: 2rem; margin: 0;">${escapeHtml(
|
|
511
|
+
error instanceof Error ? error.stack || error.message : String(error),
|
|
512
|
+
)}</pre>`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
})().catch((err) => {
|
|
516
|
+
console.error('[@ripple-ts/vite-plugin] Unhandled middleware error:', err);
|
|
517
|
+
if (!res.headersSent) {
|
|
518
|
+
res.statusCode = 500;
|
|
519
|
+
res.end('Internal Server Error');
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
|
|
307
526
|
async resolveId(id, importer, options) {
|
|
527
|
+
// Handle virtual hydrate module
|
|
528
|
+
if (id === 'virtual:ripple-hydrate') {
|
|
529
|
+
return '\0virtual:ripple-hydrate';
|
|
530
|
+
}
|
|
531
|
+
|
|
308
532
|
// Skip non-package imports (relative/absolute paths)
|
|
309
533
|
if (id.startsWith('.') || id.startsWith('/') || id.includes(':')) {
|
|
310
534
|
return null;
|
|
@@ -345,6 +569,42 @@ export function ripple(inlineOptions = {}) {
|
|
|
345
569
|
},
|
|
346
570
|
|
|
347
571
|
async load(id, opts) {
|
|
572
|
+
// Handle virtual hydrate module
|
|
573
|
+
if (id === '\0virtual:ripple-hydrate') {
|
|
574
|
+
return `
|
|
575
|
+
import { hydrate, mount } from 'ripple';
|
|
576
|
+
|
|
577
|
+
const data = JSON.parse(document.getElementById('__ripple_data').textContent);
|
|
578
|
+
const target = document.getElementById('root');
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const module = await import(/* @vite-ignore */ data.entry);
|
|
582
|
+
const Component =
|
|
583
|
+
module.default ||
|
|
584
|
+
Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
585
|
+
|
|
586
|
+
if (!Component || !target) {
|
|
587
|
+
console.error('[ripple] Unable to hydrate route: missing component export or #root target.');
|
|
588
|
+
} else {
|
|
589
|
+
try {
|
|
590
|
+
hydrate(Component, {
|
|
591
|
+
target,
|
|
592
|
+
props: { params: data.params }
|
|
593
|
+
});
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.warn('[ripple] Hydration failed, falling back to mount.', error);
|
|
596
|
+
mount(Component, {
|
|
597
|
+
target,
|
|
598
|
+
props: { params: data.params }
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error('[ripple] Failed to bootstrap client hydration.', error);
|
|
604
|
+
}
|
|
605
|
+
`;
|
|
606
|
+
}
|
|
607
|
+
|
|
348
608
|
if (cssCache.has(id)) {
|
|
349
609
|
return cssCache.get(id);
|
|
350
610
|
}
|
|
@@ -355,7 +615,7 @@ export function ripple(inlineOptions = {}) {
|
|
|
355
615
|
|
|
356
616
|
async handler(code, id, opts) {
|
|
357
617
|
const filename = id.replace(root, '');
|
|
358
|
-
const ssr = this.environment.config.consumer === 'server';
|
|
618
|
+
const ssr = opts?.ssr === true || this.environment.config.consumer === 'server';
|
|
359
619
|
|
|
360
620
|
const { js, css } = await compile(code, filename, {
|
|
361
621
|
mode: ssr ? 'server' : 'client',
|
|
@@ -376,3 +636,209 @@ export function ripple(inlineOptions = {}) {
|
|
|
376
636
|
|
|
377
637
|
return plugins;
|
|
378
638
|
}
|
|
639
|
+
|
|
640
|
+
// This is mainly to enforce types and provide a better DX with types than anything else
|
|
641
|
+
export function defineConfig(/** @type {RipplePluginOptions} */ options) {
|
|
642
|
+
return options;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ============================================================================
|
|
646
|
+
// Helper functions for dev server
|
|
647
|
+
// ============================================================================
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Derive the request origin (protocol + host) from a Node.js request.
|
|
651
|
+
* Only honors `X-Forwarded-Proto` and `X-Forwarded-Host` headers when
|
|
652
|
+
* `trustProxy` is explicitly enabled; otherwise the protocol comes from the
|
|
653
|
+
* socket and the host from the `Host` header.
|
|
654
|
+
*
|
|
655
|
+
* @param {import('node:http').IncomingMessage} req
|
|
656
|
+
* @param {boolean} trustProxy
|
|
657
|
+
* @returns {string}
|
|
658
|
+
*/
|
|
659
|
+
function deriveOrigin(req, trustProxy) {
|
|
660
|
+
let protocol = /** @type {import('node:tls').TLSSocket} */ (req.socket).encrypted
|
|
661
|
+
? 'https'
|
|
662
|
+
: 'http';
|
|
663
|
+
let host = req.headers.host || 'localhost';
|
|
664
|
+
|
|
665
|
+
if (trustProxy) {
|
|
666
|
+
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
667
|
+
const proto = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
|
|
668
|
+
if (proto) {
|
|
669
|
+
protocol = proto.split(',')[0].trim();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const forwardedHost = req.headers['x-forwarded-host'];
|
|
673
|
+
const fwdHost = Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost;
|
|
674
|
+
if (fwdHost) {
|
|
675
|
+
host = fwdHost.split(',')[0].trim();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return `${protocol}://${host}`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Convert a Node.js IncomingMessage to a Web Request
|
|
684
|
+
* @param {import('node:http').IncomingMessage} nodeRequest
|
|
685
|
+
* @returns {Request}
|
|
686
|
+
*/
|
|
687
|
+
function nodeRequestToWebRequest(nodeRequest) {
|
|
688
|
+
const protocol = 'http';
|
|
689
|
+
const host = nodeRequest.headers.host || 'localhost';
|
|
690
|
+
const url = new URL(nodeRequest.url || '/', `${protocol}://${host}`);
|
|
691
|
+
|
|
692
|
+
const headers = new Headers();
|
|
693
|
+
for (const [key, value] of Object.entries(nodeRequest.headers)) {
|
|
694
|
+
if (value == null) continue;
|
|
695
|
+
if (Array.isArray(value)) {
|
|
696
|
+
for (const v of value) headers.append(key, v);
|
|
697
|
+
} else {
|
|
698
|
+
headers.set(key, value);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const method = (nodeRequest.method || 'GET').toUpperCase();
|
|
703
|
+
/** @type {RequestInit & { duplex?: 'half' }} */
|
|
704
|
+
const init = { method, headers };
|
|
705
|
+
|
|
706
|
+
// Add body for non-GET/HEAD requests
|
|
707
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
708
|
+
init.body = /** @type {any} */ (Readable.toWeb(nodeRequest));
|
|
709
|
+
init.duplex = 'half';
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return new Request(url, init);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Send a Web Response to a Node.js ServerResponse
|
|
717
|
+
* @param {import('node:http').ServerResponse} nodeResponse
|
|
718
|
+
* @param {Response} webResponse
|
|
719
|
+
*/
|
|
720
|
+
async function sendWebResponse(nodeResponse, webResponse) {
|
|
721
|
+
nodeResponse.statusCode = webResponse.status;
|
|
722
|
+
if (webResponse.statusText) {
|
|
723
|
+
nodeResponse.statusMessage = webResponse.statusText;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Copy headers
|
|
727
|
+
webResponse.headers.forEach((value, key) => {
|
|
728
|
+
nodeResponse.setHeader(key, value);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Send body
|
|
732
|
+
if (webResponse.body) {
|
|
733
|
+
const reader = webResponse.body.getReader();
|
|
734
|
+
try {
|
|
735
|
+
while (true) {
|
|
736
|
+
const { done, value } = await reader.read();
|
|
737
|
+
if (done) break;
|
|
738
|
+
nodeResponse.write(value);
|
|
739
|
+
}
|
|
740
|
+
} finally {
|
|
741
|
+
reader.releaseLock();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
nodeResponse.end();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Handle RPC requests for #server blocks
|
|
750
|
+
* @param {import('node:http').IncomingMessage} req
|
|
751
|
+
* @param {import('node:http').ServerResponse} res
|
|
752
|
+
* @param {URL} url
|
|
753
|
+
* @param {import('vite').ViteDevServer} vite
|
|
754
|
+
* @param {boolean} trustProxy
|
|
755
|
+
*/
|
|
756
|
+
async function handleRpcRequest(req, res, url, vite, trustProxy) {
|
|
757
|
+
// we don't really need trustProxy in vite but leaving it as a model for production adapters
|
|
758
|
+
try {
|
|
759
|
+
const hash = url.pathname.slice('/_$_ripple_rpc_$_/'.length);
|
|
760
|
+
|
|
761
|
+
// Get request body
|
|
762
|
+
const body = await getRequestBody(req);
|
|
763
|
+
|
|
764
|
+
// Load the RPC module info from globalThis (set during SSR)
|
|
765
|
+
const rpcModules = /** @type {Map<string, [string, string]>} */ (
|
|
766
|
+
/** @type {any} */ (globalThis).rpc_modules
|
|
767
|
+
);
|
|
768
|
+
if (!rpcModules) {
|
|
769
|
+
res.statusCode = 500;
|
|
770
|
+
res.end('RPC modules not initialized');
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const moduleInfo = rpcModules.get(hash);
|
|
775
|
+
if (!moduleInfo) {
|
|
776
|
+
res.statusCode = 404;
|
|
777
|
+
res.end(`RPC function not found: ${hash}`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const [filePath, funcName] = moduleInfo;
|
|
782
|
+
|
|
783
|
+
// Load the module and execute the function
|
|
784
|
+
const { executeServerFunction } = await vite.ssrLoadModule('ripple/server');
|
|
785
|
+
const module = await vite.ssrLoadModule(filePath);
|
|
786
|
+
const server = module._$_server_$_;
|
|
787
|
+
|
|
788
|
+
if (!server || !server[funcName]) {
|
|
789
|
+
res.statusCode = 404;
|
|
790
|
+
res.end(`Server function not found: ${funcName}`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const origin = deriveOrigin(req, trustProxy);
|
|
795
|
+
|
|
796
|
+
// Execute server function within async context
|
|
797
|
+
// This allows the patched fetch to access the origin without global state
|
|
798
|
+
await rpcContext.run({ origin }, async () => {
|
|
799
|
+
const result = await executeServerFunction(server[funcName], body);
|
|
800
|
+
|
|
801
|
+
res.statusCode = 200;
|
|
802
|
+
res.setHeader('Content-Type', 'application/json');
|
|
803
|
+
res.end(result);
|
|
804
|
+
});
|
|
805
|
+
} catch (error) {
|
|
806
|
+
console.error('[@ripple-ts/vite-plugin] RPC error:', error);
|
|
807
|
+
res.statusCode = 500;
|
|
808
|
+
res.setHeader('Content-Type', 'application/json');
|
|
809
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'RPC failed' }));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Get the body of a request as a string
|
|
815
|
+
* @param {import('node:http').IncomingMessage} req
|
|
816
|
+
* @returns {Promise<string>}
|
|
817
|
+
*/
|
|
818
|
+
function getRequestBody(req) {
|
|
819
|
+
return new Promise((resolve, reject) => {
|
|
820
|
+
let data = '';
|
|
821
|
+
req.on('data', (chunk) => {
|
|
822
|
+
data += chunk;
|
|
823
|
+
if (data.length > 1e6) {
|
|
824
|
+
req.destroy();
|
|
825
|
+
reject(new Error('Request body too large'));
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
req.on('end', () => resolve(data));
|
|
829
|
+
req.on('error', reject);
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Escape HTML entities
|
|
835
|
+
* @param {string} str
|
|
836
|
+
* @returns {string}
|
|
837
|
+
*/
|
|
838
|
+
function escapeHtml(str) {
|
|
839
|
+
return str
|
|
840
|
+
.replace(/&/g, '&')
|
|
841
|
+
.replace(/</g, '<')
|
|
842
|
+
.replace(/>/g, '>')
|
|
843
|
+
.replace(/"/g, '"');
|
|
844
|
+
}
|
package/src/routes.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('@ripple-ts/vite-plugin').Context} Context
|
|
3
|
+
* @typedef {import('@ripple-ts/vite-plugin').Middleware} Middleware
|
|
4
|
+
* @typedef {import('@ripple-ts/vite-plugin').RenderRouteOptions} RenderRouteOptions
|
|
5
|
+
* @typedef {import('@ripple-ts/vite-plugin').ServerRouteOptions} ServerRouteOptions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Route for rendering Ripple components with SSR
|
|
10
|
+
*/
|
|
11
|
+
export class RenderRoute {
|
|
12
|
+
/** @type {'render'} */
|
|
13
|
+
type = 'render';
|
|
14
|
+
|
|
15
|
+
/** @type {string} */
|
|
16
|
+
path;
|
|
17
|
+
|
|
18
|
+
/** @type {string} */
|
|
19
|
+
entry;
|
|
20
|
+
|
|
21
|
+
/** @type {string | undefined} */
|
|
22
|
+
layout;
|
|
23
|
+
|
|
24
|
+
/** @type {Middleware[]} */
|
|
25
|
+
before;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {RenderRouteOptions} options
|
|
29
|
+
*/
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.path = options.path;
|
|
32
|
+
this.entry = options.entry;
|
|
33
|
+
this.layout = options.layout;
|
|
34
|
+
this.before = options.before ?? [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Route for API endpoints (returns Response directly)
|
|
40
|
+
*/
|
|
41
|
+
export class ServerRoute {
|
|
42
|
+
/** @type {'server'} */
|
|
43
|
+
type = 'server';
|
|
44
|
+
|
|
45
|
+
/** @type {string} */
|
|
46
|
+
path;
|
|
47
|
+
|
|
48
|
+
/** @type {string[]} */
|
|
49
|
+
methods;
|
|
50
|
+
|
|
51
|
+
/** @type {(context: Context) => Response | Promise<Response>} */
|
|
52
|
+
handler;
|
|
53
|
+
|
|
54
|
+
/** @type {Middleware[]} */
|
|
55
|
+
before;
|
|
56
|
+
|
|
57
|
+
/** @type {Middleware[]} */
|
|
58
|
+
after;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {ServerRouteOptions} options
|
|
62
|
+
*/
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.path = options.path;
|
|
65
|
+
this.methods = options.methods ?? ['GET'];
|
|
66
|
+
this.handler = options.handler;
|
|
67
|
+
this.before = options.before ?? [];
|
|
68
|
+
this.after = options.after ?? [];
|
|
69
|
+
}
|
|
70
|
+
}
|