@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.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @typedef {import('@ripple-ts/vite-plugin').Route} Route
3
+ * @typedef {import('@ripple-ts/vite-plugin').RenderRoute} RenderRoute
4
+ * @typedef {import('@ripple-ts/vite-plugin').ServerRoute} ServerRoute
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} RouteMatch
9
+ * @property {Route} route
10
+ * @property {Record<string, string>} params
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} CompiledRoute
15
+ * @property {Route} route
16
+ * @property {RegExp} pattern
17
+ * @property {string[]} paramNames
18
+ * @property {number} specificity - Higher = more specific (static > param > catch-all)
19
+ */
20
+
21
+ /**
22
+ * Convert a route path pattern to a RegExp
23
+ * Supports:
24
+ * - Static segments: /about, /api/hello
25
+ * - Named params: /posts/:id, /users/:userId/posts/:postId
26
+ * - Catch-all: /docs/*slug
27
+ *
28
+ * @param {string} path
29
+ * @returns {{ pattern: RegExp, paramNames: string[], specificity: number }}
30
+ */
31
+ function compilePath(path) {
32
+ /** @type {string[]} */
33
+ const paramNames = [];
34
+ let specificity = 0;
35
+
36
+ // Escape special regex characters except our param syntax
37
+ const regexString = path
38
+ .split('/')
39
+ .map((segment) => {
40
+ if (!segment) return '';
41
+
42
+ // Catch-all param: *slug
43
+ if (segment.startsWith('*')) {
44
+ const paramName = segment.slice(1);
45
+ paramNames.push(paramName);
46
+ specificity += 1; // Lowest specificity
47
+ return '(.+)';
48
+ }
49
+
50
+ // Named param: :id
51
+ if (segment.startsWith(':')) {
52
+ const paramName = segment.slice(1);
53
+ paramNames.push(paramName);
54
+ specificity += 10; // Medium specificity
55
+ return '([^/]+)';
56
+ }
57
+
58
+ // Static segment
59
+ specificity += 100; // Highest specificity
60
+ return escapeRegex(segment);
61
+ })
62
+ .join('/');
63
+
64
+ const pattern = new RegExp(`^${regexString || '/'}$`);
65
+ return { pattern, paramNames, specificity };
66
+ }
67
+
68
+ /**
69
+ * Escape special regex characters
70
+ * @param {string} str
71
+ * @returns {string}
72
+ */
73
+ function escapeRegex(str) {
74
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
75
+ }
76
+
77
+ /**
78
+ * Create a router from a list of routes
79
+ * @param {Route[]} routes
80
+ * @returns {{ match: (method: string, pathname: string) => RouteMatch | null }}
81
+ */
82
+ export function createRouter(routes) {
83
+ /** @type {CompiledRoute[]} */
84
+ const compiledRoutes = routes.map((route) => {
85
+ const { pattern, paramNames, specificity } = compilePath(route.path);
86
+ return { route, pattern, paramNames, specificity };
87
+ });
88
+
89
+ // Sort by specificity (higher first) for correct matching order
90
+ compiledRoutes.sort((a, b) => b.specificity - a.specificity);
91
+
92
+ return {
93
+ /**
94
+ * Match a request to a route
95
+ * @param {string} method
96
+ * @param {string} pathname
97
+ * @returns {RouteMatch | null}
98
+ */
99
+ match(method, pathname) {
100
+ for (const { route, pattern, paramNames } of compiledRoutes) {
101
+ // Check method for ServerRoute
102
+ if (route.type === 'server') {
103
+ const methods = /** @type {ServerRoute} */ (route).methods;
104
+ if (!methods.includes(method.toUpperCase())) {
105
+ continue;
106
+ }
107
+ }
108
+
109
+ const match = pathname.match(pattern);
110
+ if (match) {
111
+ /** @type {Record<string, string>} */
112
+ const params = {};
113
+ for (let i = 0; i < paramNames.length; i++) {
114
+ params[paramNames[i]] = decodeURIComponent(match[i + 1]);
115
+ }
116
+ return { route, params };
117
+ }
118
+ }
119
+ return null;
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @typedef {import('@ripple-ts/vite-plugin').Context} Context
3
+ * @typedef {import('@ripple-ts/vite-plugin').ServerRoute} ServerRoute
4
+ * @typedef {import('@ripple-ts/vite-plugin').Middleware} Middleware
5
+ */
6
+
7
+ import { runMiddlewareChain } from './middleware.js';
8
+
9
+ /**
10
+ * Handle a ServerRoute (API endpoint)
11
+ *
12
+ * @param {ServerRoute} route
13
+ * @param {Context} context
14
+ * @param {Middleware[]} globalMiddlewares
15
+ * @returns {Promise<Response>}
16
+ */
17
+ export async function handleServerRoute(route, context, globalMiddlewares) {
18
+ try {
19
+ // The handler wrapped as a function returning Promise<Response>
20
+ const handler = async () => {
21
+ return route.handler(context);
22
+ };
23
+
24
+ // Run the middleware chain: global → before → handler → after
25
+ const response = await runMiddlewareChain(
26
+ context,
27
+ globalMiddlewares,
28
+ route.before,
29
+ handler,
30
+ route.after,
31
+ );
32
+
33
+ return response;
34
+ } catch (error) {
35
+ console.error('[ripple] API route error:', error);
36
+
37
+ // Return error response
38
+ const message = error instanceof Error ? error.message : 'Internal Server Error';
39
+ return new Response(JSON.stringify({ error: message }), {
40
+ status: 500,
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ },
44
+ });
45
+ }
46
+ }
package/types/index.d.ts CHANGED
@@ -1,9 +1,125 @@
1
1
  import type { Plugin } from 'vite';
2
2
 
3
3
  declare module '@ripple-ts/vite-plugin' {
4
+ // ============================================================================
5
+ // Plugin exports
6
+ // ============================================================================
7
+
4
8
  export function ripple(options?: RipplePluginOptions): Plugin[];
9
+ export function defineConfig(options: RippleConfigOptions): RippleConfigOptions;
10
+
11
+ // ============================================================================
12
+ // Route classes
13
+ // ============================================================================
14
+
15
+ export class RenderRoute {
16
+ readonly type: 'render';
17
+ path: string;
18
+ entry: string;
19
+ layout?: string;
20
+ before: Middleware[];
21
+ constructor(options: RenderRouteOptions);
22
+ }
23
+
24
+ export class ServerRoute {
25
+ readonly type: 'server';
26
+ path: string;
27
+ methods: string[];
28
+ handler: RouteHandler;
29
+ before: Middleware[];
30
+ after: Middleware[];
31
+ constructor(options: ServerRouteOptions);
32
+ }
33
+
34
+ export type Route = RenderRoute | ServerRoute;
35
+
36
+ // ============================================================================
37
+ // Route options
38
+ // ============================================================================
39
+
40
+ export interface RenderRouteOptions {
41
+ /** URL path pattern (e.g., '/', '/posts/:id', '/docs/*slug') */
42
+ path: string;
43
+ /** Path to the Ripple component entry file */
44
+ entry: string;
45
+ /** Path to the layout component (wraps the entry) */
46
+ layout?: string;
47
+ /** Middleware to run before rendering */
48
+ before?: Middleware[];
49
+ }
50
+
51
+ export interface ServerRouteOptions {
52
+ /** URL path pattern (e.g., '/api/hello', '/api/posts/:id') */
53
+ path: string;
54
+ /** HTTP methods to handle (default: ['GET']) */
55
+ methods?: string[];
56
+ /** Request handler that returns a Response */
57
+ handler: RouteHandler;
58
+ /** Middleware to run before the handler */
59
+ before?: Middleware[];
60
+ /** Middleware to run after the handler */
61
+ after?: Middleware[];
62
+ }
63
+
64
+ // ============================================================================
65
+ // Context and middleware
66
+ // ============================================================================
67
+
68
+ export interface Context {
69
+ /** The incoming Request object */
70
+ request: Request;
71
+ /** URL parameters extracted from the route pattern */
72
+ params: Record<string, string>;
73
+ /** Parsed URL object */
74
+ url: URL;
75
+ /** Shared state for passing data between middlewares */
76
+ state: Map<string, unknown>;
77
+ }
78
+
79
+ export type NextFunction = () => Promise<Response>;
80
+ export type Middleware = (context: Context, next: NextFunction) => Response | Promise<Response>;
81
+ export type RouteHandler = (context: Context) => Response | Promise<Response>;
82
+
83
+ // ============================================================================
84
+ // Configuration
85
+ // ============================================================================
5
86
 
6
87
  export interface RipplePluginOptions {
7
88
  excludeRippleExternalModules?: boolean;
8
89
  }
90
+
91
+ export interface RippleConfigOptions {
92
+ build?: {
93
+ minify?: boolean;
94
+ };
95
+ adapter?: {
96
+ serve: AdapterServeFunction;
97
+ };
98
+ router: {
99
+ routes: Route[];
100
+ };
101
+ /** Global middlewares applied to all routes */
102
+ middlewares?: Middleware[];
103
+ platform?: {
104
+ env: Record<string, string>;
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
+ };
119
+ }
120
+
121
+ export type AdapterServeFunction = (
122
+ handler: (request: Request, platform?: unknown) => Response | Promise<Response>,
123
+ options?: Record<string, unknown>,
124
+ ) => { listen: (port?: number) => unknown; close: () => void };
9
125
  }