bxo 0.0.5-dev.32 → 0.0.5-dev.34

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.
Files changed (3) hide show
  1. package/example.ts +33 -0
  2. package/index.ts +88 -2
  3. package/package.json +1 -1
package/example.ts ADDED
@@ -0,0 +1,33 @@
1
+ import BXO, { createCookie, redirect } from 'bxo';
2
+
3
+ const app = new BXO();
4
+
5
+ // 1) Explicit return redirect (302 default)
6
+ app.get('/old', ({ redirect }) => {
7
+ return redirect('/new');
8
+ });
9
+
10
+ // 2) Implicit redirect (no return needed)
11
+ app.get('/implicit', (ctx) => {
12
+ ctx.redirect('/new');
13
+ });
14
+
15
+ // 3) Custom status (303 after POST)
16
+ app.post('/submit', (ctx) => {
17
+ // ...handle form...
18
+ return ctx.redirect('/thanks', 303);
19
+ });
20
+
21
+ // 4) Redirect with cookie
22
+ app.get('/login', (ctx) => {
23
+ ctx.set.cookies = [
24
+ createCookie('sid', 'abc123', { httpOnly: true, path: '/' })
25
+ ];
26
+ ctx.redirect('/dashboard'); // no return required
27
+ });
28
+
29
+ // 5) Using top-level helper (outside ctx)
30
+ app.get('/go-external', () => redirect('https://example.com', 307));
31
+
32
+ await app.start(3000);
33
+ console.log('Server running at http://localhost:3000');
package/index.ts CHANGED
@@ -67,6 +67,7 @@ export type Context<TConfig extends RouteConfig = {}> = {
67
67
  status?: number;
68
68
  headers?: Record<string, string>;
69
69
  cookies?: Cookie[];
70
+ redirect?: { location: string; status?: number };
70
71
  };
71
72
  status: <T extends number>(
72
73
  code: TConfig['response'] extends StatusResponseSchema
@@ -86,6 +87,7 @@ export type Context<TConfig extends RouteConfig = {}> = {
86
87
  : TConfig['response'] extends ResponseSchema
87
88
  ? InferZodType<TConfig['response']>
88
89
  : any;
90
+ redirect: (location: string, status?: number) => Response;
89
91
  [key: string]: any;
90
92
  };
91
93
 
@@ -597,7 +599,42 @@ export default class BXO {
597
599
  status: ((code: number, data?: any) => {
598
600
  ctx.set.status = code;
599
601
  return data;
600
- }) as any
602
+ }) as any,
603
+ redirect: ((location: string, status: number = 302) => {
604
+ // Persist redirect intent on context so it also works without returning
605
+ ctx.set.status = status;
606
+ ctx.set.headers = {
607
+ ...(ctx.set.headers || {}),
608
+ Location: location
609
+ };
610
+ ctx.set.redirect = { location, status };
611
+
612
+ // Prepare headers for immediate Response return (merging any existing headers)
613
+ const responseHeaders: Record<string, string> = { ...ctx.set.headers };
614
+
615
+ // Handle cookies if any are set on context
616
+ if (ctx.set.cookies && ctx.set.cookies.length > 0) {
617
+ const cookieHeaders = ctx.set.cookies.map(cookie => {
618
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
619
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
620
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
621
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
622
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
623
+ if (cookie.secure) cookieString += `; Secure`;
624
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
625
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
626
+ return cookieString;
627
+ });
628
+ cookieHeaders.forEach((cookieHeader, index) => {
629
+ responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
630
+ });
631
+ }
632
+
633
+ return new Response(null, {
634
+ status,
635
+ headers: responseHeaders
636
+ });
637
+ }) as any
601
638
  };
602
639
  } catch (validationError) {
603
640
  // Validation failed - return error response
@@ -654,6 +691,47 @@ export default class BXO {
654
691
  }
655
692
  }
656
693
 
694
+ // If the handler did not return a response, but a redirect was configured via ctx.set,
695
+ // automatically create a redirect Response so users can call ctx.redirect(...) or set headers without returning.
696
+ const hasImplicitRedirectIntent = !!ctx.set.redirect
697
+ || (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400);
698
+ if ((response === undefined || response === null) && hasImplicitRedirectIntent) {
699
+ const locationFromHeaders = ctx.set.headers && Object.entries(ctx.set.headers).find(([k]) => k.toLowerCase() === 'location')?.[1];
700
+ const location = ctx.set.redirect?.location || locationFromHeaders;
701
+ if (location) {
702
+ // Build headers, ensuring Location is present
703
+ let responseHeaders: Record<string, string> = ctx.set.headers ? { ...ctx.set.headers } : {};
704
+ if (!Object.keys(responseHeaders).some(k => k.toLowerCase() === 'location')) {
705
+ responseHeaders['Location'] = location;
706
+ }
707
+ // Determine status precedence: redirect.status > set.status > 302
708
+ const status = ctx.set.redirect?.status ?? ctx.set.status ?? 302;
709
+
710
+ // Handle cookies if any are set
711
+ if (ctx.set.cookies && ctx.set.cookies.length > 0) {
712
+ const cookieHeaders = ctx.set.cookies.map(cookie => {
713
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
714
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
715
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
716
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
717
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
718
+ if (cookie.secure) cookieString += `; Secure`;
719
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
720
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
721
+ return cookieString;
722
+ });
723
+ cookieHeaders.forEach((cookieHeader, index) => {
724
+ responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
725
+ });
726
+ }
727
+
728
+ return new Response(null, {
729
+ status,
730
+ headers: responseHeaders
731
+ });
732
+ }
733
+ }
734
+
657
735
  // Validate response against schema if provided
658
736
  if (this.enableValidation && route.config?.response && !(response instanceof Response)) {
659
737
  try {
@@ -1045,8 +1123,16 @@ const file = (path: string, options?: { type?: string; headers?: Record<string,
1045
1123
  return bunFile;
1046
1124
  }
1047
1125
 
1126
+ // Redirect helper function (like Elysia)
1127
+ const redirect = (location: string, status: number = 302) => {
1128
+ return new Response(null, {
1129
+ status,
1130
+ headers: { Location: location }
1131
+ });
1132
+ }
1133
+
1048
1134
  // Export Zod for convenience
1049
- export { z, error, file };
1135
+ export { z, error, file, redirect };
1050
1136
 
1051
1137
  // Export types for external use
1052
1138
  export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie, BXOOptions };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.5-dev.32",
4
+ "version": "0.0.5-dev.34",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {