bxo 0.0.5-dev.31 → 0.0.5-dev.33
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/index.ts +95 -11
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -86,6 +86,7 @@ export type Context<TConfig extends RouteConfig = {}> = {
|
|
|
86
86
|
: TConfig['response'] extends ResponseSchema
|
|
87
87
|
? InferZodType<TConfig['response']>
|
|
88
88
|
: any;
|
|
89
|
+
redirect: (location: string, status?: number) => Response;
|
|
89
90
|
[key: string]: any;
|
|
90
91
|
};
|
|
91
92
|
|
|
@@ -340,7 +341,7 @@ export default class BXO {
|
|
|
340
341
|
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
|
341
342
|
// Wildcard at end matches remaining path segments
|
|
342
343
|
const remainingPath = pathSegments.slice(i).join('/');
|
|
343
|
-
params['*'] =
|
|
344
|
+
params['*'] = remainingPath;
|
|
344
345
|
break;
|
|
345
346
|
}
|
|
346
347
|
|
|
@@ -351,11 +352,11 @@ export default class BXO {
|
|
|
351
352
|
|
|
352
353
|
if (routeSegment.startsWith(':')) {
|
|
353
354
|
const paramName = routeSegment.slice(1);
|
|
354
|
-
params[paramName] =
|
|
355
|
+
params[paramName] = pathSegment;
|
|
355
356
|
} else if (routeSegment === '*') {
|
|
356
357
|
// Single segment wildcard
|
|
357
|
-
params['*'] =
|
|
358
|
-
} else if (routeSegment !==
|
|
358
|
+
params['*'] = pathSegment;
|
|
359
|
+
} else if (routeSegment !== pathSegment) {
|
|
359
360
|
isMatch = false;
|
|
360
361
|
break;
|
|
361
362
|
}
|
|
@@ -404,7 +405,7 @@ export default class BXO {
|
|
|
404
405
|
if (routeSegment === '*' && i === routeSegments.length - 1) {
|
|
405
406
|
// Wildcard at end matches remaining path segments
|
|
406
407
|
const remainingPath = pathSegments.slice(i).join('/');
|
|
407
|
-
params['*'] =
|
|
408
|
+
params['*'] = remainingPath;
|
|
408
409
|
break;
|
|
409
410
|
}
|
|
410
411
|
|
|
@@ -415,11 +416,11 @@ export default class BXO {
|
|
|
415
416
|
|
|
416
417
|
if (routeSegment.startsWith(':')) {
|
|
417
418
|
const paramName = routeSegment.slice(1);
|
|
418
|
-
params[paramName] =
|
|
419
|
+
params[paramName] = pathSegment;
|
|
419
420
|
} else if (routeSegment === '*') {
|
|
420
421
|
// Single segment wildcard
|
|
421
|
-
params['*'] =
|
|
422
|
-
} else if (routeSegment !==
|
|
422
|
+
params['*'] = pathSegment;
|
|
423
|
+
} else if (routeSegment !== pathSegment) {
|
|
423
424
|
isMatch = false;
|
|
424
425
|
break;
|
|
425
426
|
}
|
|
@@ -510,7 +511,13 @@ export default class BXO {
|
|
|
510
511
|
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
|
511
512
|
const url = new URL(request.url);
|
|
512
513
|
const method = request.method;
|
|
513
|
-
const
|
|
514
|
+
const rawPathname = url.pathname;
|
|
515
|
+
let pathname: string;
|
|
516
|
+
try {
|
|
517
|
+
pathname = decodeURI(rawPathname);
|
|
518
|
+
} catch {
|
|
519
|
+
pathname = rawPathname;
|
|
520
|
+
}
|
|
514
521
|
|
|
515
522
|
// Check for WebSocket upgrade
|
|
516
523
|
if (request.headers.get('upgrade') === 'websocket') {
|
|
@@ -591,7 +598,41 @@ export default class BXO {
|
|
|
591
598
|
status: ((code: number, data?: any) => {
|
|
592
599
|
ctx.set.status = code;
|
|
593
600
|
return data;
|
|
594
|
-
}) as any
|
|
601
|
+
}) as any,
|
|
602
|
+
redirect: ((location: string, status: number = 302) => {
|
|
603
|
+
// Persist redirect intent on context so it also works without returning
|
|
604
|
+
ctx.set.status = status;
|
|
605
|
+
ctx.set.headers = {
|
|
606
|
+
...(ctx.set.headers || {}),
|
|
607
|
+
Location: location
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// Prepare headers for immediate Response return (merging any existing headers)
|
|
611
|
+
const responseHeaders: Record<string, string> = { ...ctx.set.headers };
|
|
612
|
+
|
|
613
|
+
// Handle cookies if any are set on context
|
|
614
|
+
if (ctx.set.cookies && ctx.set.cookies.length > 0) {
|
|
615
|
+
const cookieHeaders = ctx.set.cookies.map(cookie => {
|
|
616
|
+
let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
|
|
617
|
+
if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
|
|
618
|
+
if (cookie.path) cookieString += `; Path=${cookie.path}`;
|
|
619
|
+
if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
|
|
620
|
+
if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
|
|
621
|
+
if (cookie.secure) cookieString += `; Secure`;
|
|
622
|
+
if (cookie.httpOnly) cookieString += `; HttpOnly`;
|
|
623
|
+
if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
|
|
624
|
+
return cookieString;
|
|
625
|
+
});
|
|
626
|
+
cookieHeaders.forEach((cookieHeader, index) => {
|
|
627
|
+
responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return new Response(null, {
|
|
632
|
+
status,
|
|
633
|
+
headers: responseHeaders
|
|
634
|
+
});
|
|
635
|
+
}) as any
|
|
595
636
|
};
|
|
596
637
|
} catch (validationError) {
|
|
597
638
|
// Validation failed - return error response
|
|
@@ -648,6 +689,41 @@ export default class BXO {
|
|
|
648
689
|
}
|
|
649
690
|
}
|
|
650
691
|
|
|
692
|
+
// If the handler did not return a response, but a redirect was configured via ctx.set,
|
|
693
|
+
// automatically create a redirect Response so users can call ctx.redirect(...) or set headers without returning.
|
|
694
|
+
if ((response === undefined || response === null)
|
|
695
|
+
&& typeof ctx.set.status === 'number'
|
|
696
|
+
&& ctx.set.status >= 300
|
|
697
|
+
&& ctx.set.status < 400) {
|
|
698
|
+
const hasLocationHeader = !!(ctx.set.headers && Object.keys(ctx.set.headers).some(k => k.toLowerCase() === 'location'));
|
|
699
|
+
if (hasLocationHeader) {
|
|
700
|
+
let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
|
|
701
|
+
|
|
702
|
+
// Handle cookies if any are set
|
|
703
|
+
if (ctx.set.cookies && ctx.set.cookies.length > 0) {
|
|
704
|
+
const cookieHeaders = ctx.set.cookies.map(cookie => {
|
|
705
|
+
let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
|
|
706
|
+
if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
|
|
707
|
+
if (cookie.path) cookieString += `; Path=${cookie.path}`;
|
|
708
|
+
if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
|
|
709
|
+
if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
|
|
710
|
+
if (cookie.secure) cookieString += `; Secure`;
|
|
711
|
+
if (cookie.httpOnly) cookieString += `; HttpOnly`;
|
|
712
|
+
if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
|
|
713
|
+
return cookieString;
|
|
714
|
+
});
|
|
715
|
+
cookieHeaders.forEach((cookieHeader, index) => {
|
|
716
|
+
responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return new Response(null, {
|
|
721
|
+
status: ctx.set.status,
|
|
722
|
+
headers: responseHeaders
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
651
727
|
// Validate response against schema if provided
|
|
652
728
|
if (this.enableValidation && route.config?.response && !(response instanceof Response)) {
|
|
653
729
|
try {
|
|
@@ -1039,8 +1115,16 @@ const file = (path: string, options?: { type?: string; headers?: Record<string,
|
|
|
1039
1115
|
return bunFile;
|
|
1040
1116
|
}
|
|
1041
1117
|
|
|
1118
|
+
// Redirect helper function (like Elysia)
|
|
1119
|
+
const redirect = (location: string, status: number = 302) => {
|
|
1120
|
+
return new Response(null, {
|
|
1121
|
+
status,
|
|
1122
|
+
headers: { Location: location }
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1042
1126
|
// Export Zod for convenience
|
|
1043
|
-
export { z, error, file };
|
|
1127
|
+
export { z, error, file, redirect };
|
|
1044
1128
|
|
|
1045
1129
|
// Export types for external use
|
|
1046
1130
|
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie, BXOOptions };
|