@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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @ripple-ts/vite-plugin
2
2
 
3
+ ## 0.2.213
4
+
5
+ ## 0.2.212
6
+
3
7
  ## 0.2.211
4
8
 
5
9
  ## 0.2.210
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.211",
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.211"
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(req, res, url, vite);
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 result = await executeServerFunction(server[funcName], body);
794
+ const origin = deriveOrigin(req, trustProxy);
699
795
 
700
- res.statusCode = 200;
701
- res.setHeader('Content-Type', 'application/json');
702
- res.end(result);
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, sep } from 'node:path';
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 = (