@ripple-ts/vite-plugin 0.2.211 → 0.2.213
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 +108 -6
- package/src/server/render-route.js +8 -1
- package/types/index.d.ts +13 -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.213",
|
|
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.213"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
/** @import {Plugin, ResolvedConfig, ViteDevServer} from 'vite' */
|
|
3
3
|
/** @import {RipplePluginOptions, RippleConfigOptions, Route, Middleware, RenderRoute} from '@ripple-ts/vite-plugin' */
|
|
4
4
|
|
|
5
|
+
/// <reference types="ripple/compiler/internal/rpc" />
|
|
6
|
+
|
|
5
7
|
import { compile } from 'ripple/compiler';
|
|
8
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
9
|
import fs from 'node:fs';
|
|
7
10
|
import path from 'node:path';
|
|
8
11
|
import { createRequire } from 'node:module';
|
|
@@ -19,6 +22,58 @@ export { RenderRoute, ServerRoute } from './routes.js';
|
|
|
19
22
|
const VITE_FS_PREFIX = '/@fs/';
|
|
20
23
|
const IS_WINDOWS = process.platform === 'win32';
|
|
21
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
|
+
|
|
22
77
|
/**
|
|
23
78
|
* @param {string} filename
|
|
24
79
|
* @param {ResolvedConfig['root']} root
|
|
@@ -367,7 +422,13 @@ export function ripple(inlineOptions = {}) {
|
|
|
367
422
|
|
|
368
423
|
// Handle RPC requests for #server blocks
|
|
369
424
|
if (url.pathname.startsWith('/_$_ripple_rpc_$_/')) {
|
|
370
|
-
await handleRpcRequest(
|
|
425
|
+
await handleRpcRequest(
|
|
426
|
+
req,
|
|
427
|
+
res,
|
|
428
|
+
url,
|
|
429
|
+
vite,
|
|
430
|
+
rippleConfig.server?.trustProxy ?? false,
|
|
431
|
+
);
|
|
371
432
|
return;
|
|
372
433
|
}
|
|
373
434
|
|
|
@@ -585,6 +646,39 @@ export function defineConfig(/** @type {RipplePluginOptions} */ options) {
|
|
|
585
646
|
// Helper functions for dev server
|
|
586
647
|
// ============================================================================
|
|
587
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
|
+
|
|
588
682
|
/**
|
|
589
683
|
* Convert a Node.js IncomingMessage to a Web Request
|
|
590
684
|
* @param {import('node:http').IncomingMessage} nodeRequest
|
|
@@ -657,8 +751,10 @@ async function sendWebResponse(nodeResponse, webResponse) {
|
|
|
657
751
|
* @param {import('node:http').ServerResponse} res
|
|
658
752
|
* @param {URL} url
|
|
659
753
|
* @param {import('vite').ViteDevServer} vite
|
|
754
|
+
* @param {boolean} trustProxy
|
|
660
755
|
*/
|
|
661
|
-
async function handleRpcRequest(req, res, url, vite) {
|
|
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
|
|
662
758
|
try {
|
|
663
759
|
const hash = url.pathname.slice('/_$_ripple_rpc_$_/'.length);
|
|
664
760
|
|
|
@@ -695,11 +791,17 @@ async function handleRpcRequest(req, res, url, vite) {
|
|
|
695
791
|
return;
|
|
696
792
|
}
|
|
697
793
|
|
|
698
|
-
const
|
|
794
|
+
const origin = deriveOrigin(req, trustProxy);
|
|
699
795
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
+
});
|
|
703
805
|
} catch (error) {
|
|
704
806
|
console.error('[@ripple-ts/vite-plugin] RPC error:', error);
|
|
705
807
|
res.statusCode = 500;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
/// <reference types="ripple/compiler/internal/rpc" />
|
|
1
2
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {import('@ripple-ts/vite-plugin').Context} Context
|
|
@@ -24,6 +25,12 @@ import { join, sep } from 'node:path';
|
|
|
24
25
|
*/
|
|
25
26
|
export async function handleRenderRoute(route, context, vite) {
|
|
26
27
|
try {
|
|
28
|
+
// Initialize so the server can register
|
|
29
|
+
// RPC functions from #server blocks during SSR module loading
|
|
30
|
+
if (!globalThis.rpc_modules) {
|
|
31
|
+
globalThis.rpc_modules = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
// Load ripple server utilities
|
|
28
35
|
const { render, get_css_for_hashes } = await vite.ssrLoadModule('ripple/server');
|
|
29
36
|
|
package/types/index.d.ts
CHANGED
|
@@ -103,6 +103,19 @@ declare module '@ripple-ts/vite-plugin' {
|
|
|
103
103
|
platform?: {
|
|
104
104
|
env: Record<string, string>;
|
|
105
105
|
};
|
|
106
|
+
server?: {
|
|
107
|
+
/**
|
|
108
|
+
* Whether to trust `X-Forwarded-Proto` and `X-Forwarded-Host` headers
|
|
109
|
+
* when deriving the request origin (protocol + host).
|
|
110
|
+
*
|
|
111
|
+
* Enable this only when the application is behind a trusted reverse proxy
|
|
112
|
+
* (e.g., nginx, Cloudflare, AWS ALB). When `false` (the default), the
|
|
113
|
+
* protocol is inferred from the socket and the host from the `Host` header.
|
|
114
|
+
*
|
|
115
|
+
* @default false
|
|
116
|
+
*/
|
|
117
|
+
trustProxy?: boolean;
|
|
118
|
+
};
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
export type AdapterServeFunction = (
|