@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
|
@@ -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
|
}
|