bxo 0.0.5-dev.51 → 0.0.5-dev.53

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,216 @@
1
+ import type { Context, InternalCookie } from '../types';
2
+ import { validateResponse, mergeHeadersWithCookies } from './index';
3
+
4
+ // Process and format the response from a route handler
5
+ export function processResponse(
6
+ response: any,
7
+ ctx: Context,
8
+ internalCookies: InternalCookie[],
9
+ enableValidation: boolean,
10
+ routeConfig?: any
11
+ ): Response {
12
+ // If the handler did not return a response, but a redirect was configured via ctx.set,
13
+ // automatically create a redirect Response so users can call ctx.redirect(...) or set headers without returning.
14
+ const hasImplicitRedirectIntent = !!ctx.set.redirect
15
+ || (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400);
16
+
17
+ if ((response === undefined || response === null) && hasImplicitRedirectIntent) {
18
+ const locationFromHeaders = ctx.set.headers && Object.entries(ctx.set.headers).find(([k]) => k.toLowerCase() === 'location')?.[1];
19
+ const location = ctx.set.redirect?.location || locationFromHeaders;
20
+ if (location) {
21
+ // Build headers, ensuring Location is present
22
+ let responseHeaders: Record<string, string> = ctx.set.headers ? { ...ctx.set.headers } : {};
23
+ if (!Object.keys(responseHeaders).some(k => k.toLowerCase() === 'location')) {
24
+ responseHeaders['Location'] = location;
25
+ }
26
+ // Determine status precedence: redirect.status > set.status > 302
27
+ const status = ctx.set.redirect?.status ?? ctx.set.status ?? 302;
28
+
29
+ // Handle cookies if any are set
30
+ if (internalCookies.length > 0) {
31
+ const headers = mergeHeadersWithCookies(responseHeaders, internalCookies);
32
+ const finalHeaders: Record<string, string> = {};
33
+ headers.forEach((value, key) => {
34
+ finalHeaders[key] = value;
35
+ });
36
+ responseHeaders = finalHeaders;
37
+ }
38
+
39
+ return new Response(null, {
40
+ status,
41
+ headers: responseHeaders
42
+ });
43
+ }
44
+ }
45
+
46
+ // Validate response against schema if provided
47
+ if (enableValidation && routeConfig?.response && !(response instanceof Response)) {
48
+ try {
49
+ const status = ctx.set.status || 200;
50
+ response = validateResponse(routeConfig.response, response, status);
51
+ } catch (validationError) {
52
+ // Response validation failed
53
+ let validationDetails = undefined;
54
+ if (validationError instanceof Error) {
55
+ if ('errors' in validationError && Array.isArray(validationError.errors)) {
56
+ validationDetails = validationError.errors;
57
+ } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
58
+ validationDetails = validationError.issues;
59
+ }
60
+ }
61
+
62
+ const errorMessage = validationDetails && validationDetails.length > 0
63
+ ? `Validation failed for ${validationDetails.length} field(s)`
64
+ : 'Validation failed';
65
+
66
+ return new Response(JSON.stringify({
67
+ error: errorMessage,
68
+ details: validationDetails
69
+ }), {
70
+ status: 500,
71
+ headers: { 'Content-Type': 'application/json' }
72
+ });
73
+ }
74
+ }
75
+
76
+ // Convert response to Response object
77
+ if (response instanceof Response) {
78
+ // If there are headers set via ctx.set.headers, merge them with the Response headers
79
+ if (ctx.set.headers && Object.keys(ctx.set.headers).length > 0) {
80
+ const newHeaders = mergeHeadersWithCookies(ctx.set.headers, internalCookies);
81
+
82
+ // Create new Response with merged headers
83
+ return new Response(response.body, {
84
+ status: ctx.set.status || response.status,
85
+ statusText: response.statusText,
86
+ headers: newHeaders
87
+ });
88
+ }
89
+
90
+ return response;
91
+ }
92
+
93
+ // Handle File response (like Elysia)
94
+ if (response instanceof File || (typeof Bun !== 'undefined' && response instanceof Bun.file('').constructor)) {
95
+ const file = response as File;
96
+ const responseInit: ResponseInit = {
97
+ status: ctx.set.status || 200,
98
+ headers: {
99
+ 'Content-Type': file.type || 'application/octet-stream',
100
+ 'Content-Length': file.size.toString(),
101
+ ...ctx.set.headers
102
+ }
103
+ };
104
+ return new Response(file, responseInit);
105
+ }
106
+
107
+ // Handle Bun.file() response
108
+ if (typeof response === 'object' && response && 'stream' in response && 'size' in response) {
109
+ const bunFile = response as any;
110
+ const responseInit: ResponseInit = {
111
+ status: ctx.set.status || 200,
112
+ headers: {
113
+ 'Content-Type': bunFile.type || 'application/octet-stream',
114
+ 'Content-Length': bunFile.size?.toString() || '',
115
+ ...ctx.set.headers,
116
+ ...(bunFile.headers || {}) // Support custom headers from file helper
117
+ }
118
+ };
119
+ return new Response(bunFile, responseInit);
120
+ }
121
+
122
+ // Prepare headers with cookies
123
+ let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
124
+
125
+ // Handle cookies if any are set
126
+ if (internalCookies.length > 0) {
127
+ const headers = mergeHeadersWithCookies(responseHeaders, internalCookies);
128
+
129
+ if (typeof response === 'string') {
130
+ headers.set('Content-Type', 'text/plain');
131
+ return new Response(response, {
132
+ status: ctx.set.status || 200,
133
+ headers: headers
134
+ });
135
+ }
136
+
137
+ headers.set('Content-Type', 'application/json');
138
+ return new Response(JSON.stringify(response), {
139
+ status: ctx.set.status || 200,
140
+ headers: headers
141
+ });
142
+ }
143
+
144
+ // If no cookies, use the original responseHeaders
145
+ const responseInit: ResponseInit = {
146
+ status: ctx.set.status || 200,
147
+ headers: responseHeaders
148
+ };
149
+
150
+ if (typeof response === 'string') {
151
+ return new Response(response, {
152
+ ...responseInit,
153
+ headers: {
154
+ 'Content-Type': 'text/plain',
155
+ ...responseInit.headers
156
+ }
157
+ });
158
+ }
159
+
160
+ return new Response(JSON.stringify(response), {
161
+ ...responseInit,
162
+ headers: {
163
+ 'Content-Type': 'application/json',
164
+ ...responseInit.headers
165
+ }
166
+ });
167
+ }
168
+
169
+ // Create error response
170
+ export function createErrorResponse(
171
+ error: Error | string,
172
+ status: number = 500,
173
+ details?: any
174
+ ): Response {
175
+ const errorMessage = error instanceof Error ? error.message : error;
176
+ const response = {
177
+ error: errorMessage,
178
+ ...(details && { details })
179
+ };
180
+
181
+ return new Response(JSON.stringify(response), {
182
+ status,
183
+ headers: { 'Content-Type': 'application/json' }
184
+ });
185
+ }
186
+
187
+ // Create validation error response
188
+ export function createValidationErrorResponse(
189
+ validationError: any,
190
+ status: number = 400
191
+ ): Response {
192
+ let validationDetails = undefined;
193
+
194
+ // Handle Error objects
195
+ if (validationError instanceof Error) {
196
+ if ('errors' in validationError && Array.isArray(validationError.errors)) {
197
+ validationDetails = validationError.errors;
198
+ } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
199
+ validationDetails = validationError.issues;
200
+ }
201
+ }
202
+ // Handle plain objects with errors or issues properties
203
+ else if (validationError && typeof validationError === 'object') {
204
+ if ('errors' in validationError && Array.isArray(validationError.errors)) {
205
+ validationDetails = validationError.errors;
206
+ } else if ('issues' in validationError && Array.isArray(validationError.issues)) {
207
+ validationDetails = validationError.issues;
208
+ }
209
+ }
210
+
211
+ const errorMessage = validationDetails && validationDetails.length > 0
212
+ ? `Validation failed for ${validationDetails.length} field(s)`
213
+ : 'Validation failed';
214
+
215
+ return createErrorResponse(errorMessage, status, validationDetails);
216
+ }
@@ -0,0 +1,191 @@
1
+ import type { Route, WSRoute } from '../types';
2
+
3
+ // Route matching utility for HTTP routes
4
+ export function matchRoute(
5
+ method: string,
6
+ pathname: string,
7
+ routes: Route[]
8
+ ): { route: Route; params: Record<string, string> } | null {
9
+ for (const route of routes) {
10
+ if (route.method !== method) continue;
11
+
12
+ const routeSegments = route.path.split('/').filter(Boolean);
13
+ const pathSegments = pathname.split('/').filter(Boolean);
14
+
15
+ const params: Record<string, string> = {};
16
+ let isMatch = true;
17
+
18
+ // Check for wildcards in the route
19
+ const hasWildcards = routeSegments.some(segment => segment === '*' || segment === '**');
20
+
21
+ // Handle routes with wildcards
22
+ if (hasWildcards) {
23
+ isMatch = matchRouteWithWildcards(routeSegments, pathSegments, params);
24
+ } else {
25
+ // Exact matching for routes without wildcards
26
+ if (routeSegments.length !== pathSegments.length) {
27
+ continue;
28
+ }
29
+
30
+ for (let i = 0; i < routeSegments.length; i++) {
31
+ const routeSegment = routeSegments[i];
32
+ const pathSegment = pathSegments[i];
33
+
34
+ if (routeSegment.startsWith(':')) {
35
+ const paramName = routeSegment.slice(1);
36
+ params[paramName] = pathSegment;
37
+ } else if (routeSegment !== pathSegment) {
38
+ isMatch = false;
39
+ break;
40
+ }
41
+ }
42
+ }
43
+
44
+ if (isMatch) {
45
+ return { route, params };
46
+ }
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ // Helper function to match routes with wildcards
53
+ function matchRouteWithWildcards(
54
+ routeSegments: string[],
55
+ pathSegments: string[],
56
+ params: Record<string, string>
57
+ ): boolean {
58
+ let routeIndex = 0;
59
+ let pathIndex = 0;
60
+ let wildcardCount = 0;
61
+
62
+ while (routeIndex < routeSegments.length && pathIndex < pathSegments.length) {
63
+ const routeSegment = routeSegments[routeIndex];
64
+ const pathSegment = pathSegments[pathIndex];
65
+
66
+ if (routeSegment === '**') {
67
+ // Double wildcard - find the next non-wildcard segment
68
+ let nextNonWildcardIndex = routeIndex + 1;
69
+ while (nextNonWildcardIndex < routeSegments.length &&
70
+ (routeSegments[nextNonWildcardIndex] === '*' || routeSegments[nextNonWildcardIndex] === '**')) {
71
+ nextNonWildcardIndex++;
72
+ }
73
+
74
+ if (nextNonWildcardIndex >= routeSegments.length) {
75
+ // Double wildcard at the end - capture everything remaining
76
+ const remainingPath = pathSegments.slice(pathIndex).join('/');
77
+ params[`**${wildcardCount}`] = remainingPath;
78
+ return true;
79
+ }
80
+
81
+ // Find the next matching segment in the path
82
+ const nextRouteSegment = routeSegments[nextNonWildcardIndex];
83
+ let foundMatch = false;
84
+
85
+ for (let j = pathIndex; j < pathSegments.length; j++) {
86
+ const currentPathSegment = pathSegments[j];
87
+
88
+ if (nextRouteSegment.startsWith(':')) {
89
+ // Param segment - always matches
90
+ const matchedPath = pathSegments.slice(pathIndex, j).join('/');
91
+ params[`**${wildcardCount}`] = matchedPath;
92
+ pathIndex = j;
93
+ routeIndex = nextNonWildcardIndex;
94
+ foundMatch = true;
95
+ break;
96
+ } else if (nextRouteSegment === '*') {
97
+ // Single wildcard - always matches
98
+ const matchedPath = pathSegments.slice(pathIndex, j).join('/');
99
+ params[`**${wildcardCount}`] = matchedPath;
100
+ pathIndex = j;
101
+ routeIndex = nextNonWildcardIndex;
102
+ foundMatch = true;
103
+ break;
104
+ } else if (nextRouteSegment === currentPathSegment) {
105
+ // Exact match
106
+ const matchedPath = pathSegments.slice(pathIndex, j).join('/');
107
+ params[`**${wildcardCount}`] = matchedPath;
108
+ pathIndex = j;
109
+ routeIndex = nextNonWildcardIndex;
110
+ foundMatch = true;
111
+ break;
112
+ }
113
+ }
114
+
115
+ if (!foundMatch) {
116
+ return false;
117
+ }
118
+
119
+ wildcardCount++;
120
+ continue;
121
+ } else if (routeSegment === '*') {
122
+ // Single wildcard - capture one segment
123
+ params[`*${wildcardCount}`] = pathSegment;
124
+ wildcardCount++;
125
+ routeIndex++;
126
+ pathIndex++;
127
+ } else if (routeSegment.startsWith(':')) {
128
+ // Parameter segment
129
+ const paramName = routeSegment.slice(1);
130
+ params[paramName] = pathSegment;
131
+ routeIndex++;
132
+ pathIndex++;
133
+ } else if (routeSegment === pathSegment) {
134
+ // Exact match
135
+ routeIndex++;
136
+ pathIndex++;
137
+ } else {
138
+ // No match
139
+ return false;
140
+ }
141
+ }
142
+
143
+ // Check if we've processed all route segments
144
+ return routeIndex >= routeSegments.length && pathIndex >= pathSegments.length;
145
+ }
146
+
147
+ // WebSocket route matching utility
148
+ export function matchWSRoute(
149
+ pathname: string,
150
+ routes: WSRoute[]
151
+ ): { route: WSRoute; params: Record<string, string> } | null {
152
+ for (const route of routes) {
153
+ const routeSegments = route.path.split('/').filter(Boolean);
154
+ const pathSegments = pathname.split('/').filter(Boolean);
155
+
156
+ const params: Record<string, string> = {};
157
+ let isMatch = true;
158
+
159
+ // Check for wildcards in the route
160
+ const hasWildcards = routeSegments.some(segment => segment === '*' || segment === '**');
161
+
162
+ // Handle routes with wildcards
163
+ if (hasWildcards) {
164
+ isMatch = matchRouteWithWildcards(routeSegments, pathSegments, params);
165
+ } else {
166
+ // Exact matching for routes without wildcards
167
+ if (routeSegments.length !== pathSegments.length) {
168
+ continue;
169
+ }
170
+
171
+ for (let i = 0; i < routeSegments.length; i++) {
172
+ const routeSegment = routeSegments[i];
173
+ const pathSegment = pathSegments[i];
174
+
175
+ if (routeSegment.startsWith(':')) {
176
+ const paramName = routeSegment.slice(1);
177
+ params[paramName] = pathSegment;
178
+ } else if (routeSegment !== pathSegment) {
179
+ isMatch = false;
180
+ break;
181
+ }
182
+ }
183
+ }
184
+
185
+ if (isMatch) {
186
+ return { route, params };
187
+ }
188
+ }
189
+
190
+ return null;
191
+ }