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.
- package/example.ts +33 -0
- package/index.ts +88 -2
- 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 };
|