bxo 0.0.5-dev.83 → 0.0.5-dev.85

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.
@@ -5,107 +5,107 @@ const app = new BXO();
5
5
 
6
6
  // Define Zod schema for the form data structure
7
7
  const UserFormSchema = z.object({
8
- name: z.string(),
9
- email: z.string().email(),
10
- password: z.string().min(6),
11
- is_active: z.string().transform(val => val === "1"),
12
- profile: z.object({
13
- name: z.string()
14
- }),
15
- id: z.string().optional(),
16
- created_at: z.string().optional(),
17
- updated_at: z.string().optional(),
18
- idx: z.string().transform(val => parseInt(val, 10))
8
+ name: z.string(),
9
+ email: z.string().email(),
10
+ password: z.string().min(6),
11
+ is_active: z.string().transform(val => val === "1"),
12
+ profile: z.object({
13
+ name: z.string()
14
+ }),
15
+ id: z.string().optional(),
16
+ created_at: z.string().optional(),
17
+ updated_at: z.string().optional(),
18
+ idx: z.string().transform(val => parseInt(val, 10))
19
19
  });
20
20
 
21
21
  // Route to handle multipart/form-data with nested objects
22
22
  app.post("/users", async (ctx) => {
23
- // The form data is automatically parsed into nested objects
24
- // ctx.body will contain the structured data based on the schema
25
- console.log("Parsed form data:", ctx.body);
26
-
27
- return ctx.json({
28
- message: "User created successfully",
29
- data: ctx.body
30
- });
23
+ // The form data is automatically parsed into nested objects
24
+ // ctx.body will contain the structured data based on the schema
25
+ console.log("Parsed form data:", ctx.body);
26
+
27
+ return ctx.json({
28
+ message: "User created successfully",
29
+ data: ctx.body
30
+ });
31
31
  }, {
32
- body: UserFormSchema,
33
- detail: {
34
- summary: "Create user with multipart/form-data",
35
- description: "Handles form data with nested objects and arrays",
36
- tags: ["Users"]
37
- }
32
+ body: UserFormSchema,
33
+ detail: {
34
+ summary: "Create user with multipart/form-data",
35
+ description: "Handles form data with nested objects and arrays",
36
+ tags: ["Users"]
37
+ }
38
38
  });
39
39
 
40
40
  // Example with arrays
41
41
  const ArrayFormSchema = z.object({
42
- items: z.array(z.string()),
43
- tags: z.array(z.string()),
44
- profile: z.object({
45
- name: z.string(),
46
- age: z.string().transform(val => parseInt(val, 10))
47
- })
42
+ items: z.array(z.string()),
43
+ tags: z.array(z.string()),
44
+ profile: z.object({
45
+ name: z.string(),
46
+ age: z.string().transform(val => parseInt(val, 10))
47
+ })
48
48
  });
49
49
 
50
50
  app.post("/items", async (ctx) => {
51
- console.log("Parsed array form data:", ctx.body);
52
-
53
- return ctx.json({
54
- message: "Items processed successfully",
55
- data: ctx.body
56
- });
51
+ console.log("Parsed array form data:", ctx.body);
52
+
53
+ return ctx.json({
54
+ message: "Items processed successfully",
55
+ data: ctx.body
56
+ });
57
57
  }, {
58
- body: ArrayFormSchema,
59
- detail: {
60
- summary: "Process items with arrays",
61
- description: "Handles form data with arrays like items[0], items[1]",
62
- tags: ["Items"]
63
- }
58
+ body: ArrayFormSchema,
59
+ detail: {
60
+ summary: "Process items with arrays",
61
+ description: "Handles form data with arrays like items[0], items[1]",
62
+ tags: ["Items"]
63
+ }
64
64
  });
65
65
 
66
66
  // Example with deep nested array objects (like workspace_items[0][id])
67
67
  const WorkspaceFormSchema = z.object({
68
- id: z.string(),
69
- created_at: z.string(),
70
- updated_at: z.string(),
71
- updated_by: z.string(),
72
- doc_status: z.string().transform(val => parseInt(val, 10)),
73
- idx: z.string().transform(val => parseInt(val, 10)),
74
- workspace_items: z.array(z.object({
75
68
  id: z.string(),
76
- type: z.string(),
77
- value: z.string(),
78
- options: z.string(),
79
- workspaceId: z.string(),
80
- owner: z.string(),
81
69
  created_at: z.string(),
82
70
  updated_at: z.string(),
83
- created_by: z.string(),
84
71
  updated_by: z.string(),
85
72
  doc_status: z.string().transform(val => parseInt(val, 10)),
86
- label: z.string()
87
- }))
73
+ idx: z.string().transform(val => parseInt(val, 10)),
74
+ workspace_items: z.array(z.object({
75
+ id: z.string(),
76
+ type: z.string(),
77
+ value: z.string(),
78
+ options: z.string(),
79
+ workspaceId: z.string(),
80
+ owner: z.string(),
81
+ created_at: z.string(),
82
+ updated_at: z.string(),
83
+ created_by: z.string(),
84
+ updated_by: z.string(),
85
+ doc_status: z.string().transform(val => parseInt(val, 10)),
86
+ label: z.string()
87
+ }))
88
88
  });
89
89
 
90
90
  app.post("/workspace", async (ctx) => {
91
- console.log("Parsed workspace form data:", ctx.body);
92
-
93
- return ctx.json({
94
- message: "Workspace processed successfully",
95
- data: ctx.body
96
- });
91
+ console.log("Parsed workspace form data:", ctx.body);
92
+
93
+ return ctx.json({
94
+ message: "Workspace processed successfully",
95
+ data: ctx.body
96
+ });
97
97
  }, {
98
- body: WorkspaceFormSchema,
99
- detail: {
100
- summary: "Process workspace with deep nested array objects",
101
- description: "Handles form data like workspace_items[0][id], workspace_items[0][type]",
102
- tags: ["Workspace"]
103
- }
98
+ body: WorkspaceFormSchema,
99
+ detail: {
100
+ summary: "Process workspace with deep nested array objects",
101
+ description: "Handles form data like workspace_items[0][id], workspace_items[0][type]",
102
+ tags: ["Workspace"]
103
+ }
104
104
  });
105
105
 
106
106
  // Test route to show how the parsing works
107
107
  app.get("/test-parsing", async (ctx) => {
108
- const html = `
108
+ const html = `
109
109
  <!DOCTYPE html>
110
110
  <html>
111
111
  <head>
@@ -310,10 +310,17 @@ app.get("/test-parsing", async (ctx) => {
310
310
  </body>
311
311
  </html>
312
312
  `;
313
-
314
- return new Response(html, {
315
- headers: { "Content-Type": "text/html" }
316
- });
313
+
314
+ return new Response(html, {
315
+ headers: { "Content-Type": "text/html" }
316
+ });
317
+ });
318
+
319
+ app.get("/test-file", async (ctx) => {
320
+ if (!ctx.query.file) {
321
+ return new Response("File not found", { status: 404 });
322
+ }
323
+ return Bun.file("test.txt");
317
324
  });
318
325
 
319
326
  app.start();
@@ -0,0 +1,298 @@
1
+ # Redirect Functionality in BXO
2
+
3
+ BXO provides multiple ways to handle HTTP redirects in your applications. This guide covers all the available methods and best practices.
4
+
5
+ ## Available Redirect Methods
6
+
7
+ ### 1. Using the `ctx.redirect()` Helper (Recommended)
8
+
9
+ The most convenient way to return redirects is using the built-in `ctx.redirect()` method:
10
+
11
+ ```typescript
12
+ app.get("/old-page", (ctx) => {
13
+ return ctx.redirect("/new-page"); // Default: 302 status
14
+ });
15
+
16
+ app.get("/permanent-redirect", (ctx) => {
17
+ return ctx.redirect("/new-location", 301); // Permanent redirect
18
+ });
19
+ ```
20
+
21
+ ### 2. Using Standard Response with Location Header
22
+
23
+ For more control over headers, use the standard Response API:
24
+
25
+ ```typescript
26
+ app.get("/custom-redirect", (ctx) => {
27
+ return new Response(null, {
28
+ status: 302,
29
+ headers: {
30
+ "Location": "/target",
31
+ "Cache-Control": "no-cache"
32
+ }
33
+ });
34
+ });
35
+ ```
36
+
37
+ ### 3. Using Context Status Method
38
+
39
+ You can also use the existing `ctx.status()` method with manual header setting:
40
+
41
+ ```typescript
42
+ app.get("/manual-redirect", (ctx) => {
43
+ ctx.set.headers["Location"] = "/target";
44
+ return ctx.status(302, null);
45
+ });
46
+ ```
47
+
48
+ ## HTTP Redirect Status Codes
49
+
50
+ BXO supports all standard HTTP redirect status codes:
51
+
52
+ | Status | Code | Name | Description | Method Preservation |
53
+ |--------|------|------|-------------|-------------------|
54
+ | 301 | 301 | Moved Permanently | Resource has permanently moved | No |
55
+ | 302 | 302 | Found | Temporary redirect (default) | No |
56
+ | 303 | 303 | See Other | Forces GET method after POST | No |
57
+ | 307 | 307 | Temporary Redirect | Preserves original method | Yes |
58
+ | 308 | 308 | Permanent Redirect | Preserves original method | Yes |
59
+
60
+ ## Common Use Cases
61
+
62
+ ### 1. Page Migration (301)
63
+ ```typescript
64
+ app.get("/old-url", (ctx) => {
65
+ return ctx.redirect("/new-url", 301);
66
+ });
67
+ ```
68
+
69
+ ### 2. Post-Redirect-Get Pattern (303)
70
+ ```typescript
71
+ app.post("/form-submit", (ctx) => {
72
+ // Process form data
73
+ return ctx.redirect("/success", 303); // Forces GET
74
+ });
75
+ ```
76
+
77
+ ### 3. API Endpoint Migration (307)
78
+ ```typescript
79
+ app.put("/api/old-endpoint", (ctx) => {
80
+ return ctx.redirect("/api/new-endpoint", 307); // Preserves PUT method
81
+ });
82
+ ```
83
+
84
+ ### 4. Conditional Redirects
85
+ ```typescript
86
+ app.get("/dashboard", (ctx) => {
87
+ const userType = ctx.cookies.userType;
88
+
89
+ if (userType === "admin") {
90
+ return ctx.redirect("/admin-dashboard", 302);
91
+ } else {
92
+ return ctx.redirect("/user-dashboard", 302);
93
+ }
94
+ });
95
+ ```
96
+
97
+ ### 5. Query Parameter Preservation
98
+ ```typescript
99
+ app.get("/search", (ctx) => {
100
+ const queryString = new URLSearchParams(ctx.query).toString();
101
+ const redirectUrl = queryString ? `/new-search?${queryString}` : "/new-search";
102
+ return ctx.redirect(redirectUrl, 301);
103
+ });
104
+ ```
105
+
106
+ ### 6. Authentication Redirects
107
+ ```typescript
108
+ app.get("/protected", (ctx) => {
109
+ const session = ctx.cookies.session;
110
+
111
+ if (!session) {
112
+ return ctx.redirect("/login", 302);
113
+ }
114
+
115
+ return ctx.json({ message: "Protected content" });
116
+ });
117
+ ```
118
+
119
+ ## Best Practices
120
+
121
+ ### 1. Choose the Right Status Code
122
+ - **301**: Use for permanent moves (SEO-friendly)
123
+ - **302**: Use for temporary redirects (default)
124
+ - **303**: Use after POST to prevent resubmission
125
+ - **307/308**: Use for API endpoints to preserve HTTP methods
126
+
127
+ ### 2. Handle Relative vs Absolute URLs
128
+ ```typescript
129
+ // Relative URL (stays on same domain)
130
+ ctx.redirect("/new-path", 302);
131
+
132
+ // Absolute URL (can redirect to different domain)
133
+ ctx.redirect("https://example.com/new-path", 301);
134
+ ```
135
+
136
+ ### 3. Preserve Query Parameters When Needed
137
+ ```typescript
138
+ app.get("/old-search", (ctx) => {
139
+ const { q, page, filters } = ctx.query;
140
+ const params = new URLSearchParams(ctx.query);
141
+ const redirectUrl = params.toString() ? `/new-search?${params}` : "/new-search";
142
+ return ctx.redirect(redirectUrl, 301);
143
+ });
144
+ ```
145
+
146
+ ### 4. Add Custom Headers When Necessary
147
+ ```typescript
148
+ app.get("/redirect-with-cache", (ctx) => {
149
+ return new Response(null, {
150
+ status: 301,
151
+ headers: {
152
+ "Location": "/new-location",
153
+ "Cache-Control": "public, max-age=3600"
154
+ }
155
+ });
156
+ });
157
+ ```
158
+
159
+ ## Testing Redirects
160
+
161
+ ### Using curl
162
+ ```bash
163
+ # Check redirect status and location
164
+ curl -I http://localhost:3000/old-page
165
+
166
+ # Follow redirects automatically
167
+ curl -L http://localhost:3000/old-page
168
+
169
+ # Test with different HTTP methods
170
+ curl -X POST http://localhost:3000/form-submit
171
+ curl -X PUT http://localhost:3000/api/update
172
+ ```
173
+
174
+ ### Using JavaScript fetch
175
+ ```javascript
176
+ // Don't follow redirects automatically
177
+ fetch('/old-page', { redirect: 'manual' })
178
+ .then(response => {
179
+ console.log('Status:', response.status);
180
+ console.log('Location:', response.headers.get('Location'));
181
+ });
182
+
183
+ // Follow redirects automatically (default)
184
+ fetch('/old-page')
185
+ .then(response => response.text())
186
+ .then(data => console.log(data));
187
+ ```
188
+
189
+ ## Common Patterns
190
+
191
+ ### 1. URL Shortening
192
+ ```typescript
193
+ app.get("/s/:shortId", (ctx) => {
194
+ const { shortId } = ctx.params;
195
+ const longUrl = getLongUrl(shortId); // Your database lookup
196
+ return ctx.redirect(longUrl, 302);
197
+ });
198
+ ```
199
+
200
+ ### 2. Language/Region Redirects
201
+ ```typescript
202
+ app.get("/", (ctx) => {
203
+ const acceptLanguage = ctx.headers["accept-language"];
204
+ const region = detectRegion(acceptLanguage);
205
+ return ctx.redirect(`/${region}/home`, 302);
206
+ });
207
+ ```
208
+
209
+ ### 3. Maintenance Mode
210
+ ```typescript
211
+ app.get("*", (ctx) => {
212
+ if (isMaintenanceMode()) {
213
+ return ctx.redirect("/maintenance", 503);
214
+ }
215
+ // Continue with normal routing
216
+ });
217
+ ```
218
+
219
+ ### 4. HTTPS Redirect
220
+ ```typescript
221
+ app.get("*", (ctx) => {
222
+ if (ctx.request.url.startsWith("http://") && isProduction()) {
223
+ const httpsUrl = ctx.request.url.replace("http://", "https://");
224
+ return ctx.redirect(httpsUrl, 301);
225
+ }
226
+ // Continue with normal routing
227
+ });
228
+ ```
229
+
230
+ ## Error Handling
231
+
232
+ ### 1. Invalid Redirect URLs
233
+ ```typescript
234
+ app.get("/redirect", (ctx) => {
235
+ const { url } = ctx.query;
236
+
237
+ if (!url || !isValidUrl(url)) {
238
+ return ctx.status(400, { error: "Invalid redirect URL" });
239
+ }
240
+
241
+ return ctx.redirect(url, 302);
242
+ });
243
+ ```
244
+
245
+ ### 2. Redirect Loops Prevention
246
+ ```typescript
247
+ app.get("/redirect", (ctx) => {
248
+ const redirectCount = parseInt(ctx.headers["x-redirect-count"] || "0");
249
+
250
+ if (redirectCount > 5) {
251
+ return ctx.status(400, { error: "Too many redirects" });
252
+ }
253
+
254
+ // Add redirect count header
255
+ ctx.set.headers["x-redirect-count"] = (redirectCount + 1).toString();
256
+ return ctx.redirect("/target", 302);
257
+ });
258
+ ```
259
+
260
+ ## Performance Considerations
261
+
262
+ 1. **Minimize Redirect Chains**: Avoid multiple redirects in sequence
263
+ 2. **Use 301 for Permanent Moves**: Helps with SEO and caching
264
+ 3. **Consider CDN Caching**: Some CDNs cache redirects based on status codes
265
+ 4. **Monitor Redirect Performance**: Track redirect success rates and response times
266
+
267
+ ## Security Considerations
268
+
269
+ 1. **Validate Redirect URLs**: Prevent open redirect vulnerabilities
270
+ 2. **Use HTTPS**: Always redirect to HTTPS in production
271
+ 3. **Sanitize User Input**: Clean any user-provided redirect URLs
272
+ 4. **Implement Rate Limiting**: Prevent redirect abuse
273
+
274
+ ```typescript
275
+ app.get("/redirect", (ctx) => {
276
+ const { url } = ctx.query;
277
+
278
+ // Security: Validate redirect URL
279
+ if (!url || !isAllowedRedirect(url)) {
280
+ return ctx.status(400, { error: "Invalid redirect URL" });
281
+ }
282
+
283
+ return ctx.redirect(url, 302);
284
+ });
285
+
286
+ function isAllowedRedirect(url: string): boolean {
287
+ try {
288
+ const parsedUrl = new URL(url);
289
+ // Only allow redirects to same domain or trusted domains
290
+ return parsedUrl.hostname === "localhost" ||
291
+ parsedUrl.hostname === "example.com";
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+ ```
297
+
298
+ This comprehensive guide should help you implement redirects effectively in your BXO applications!
@@ -0,0 +1,222 @@
1
+ import BXO from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3002 } });
5
+
6
+ // Example 1: Simple temporary redirect (302) - default
7
+ app.get("/old-page", (ctx) => {
8
+ return ctx.redirect("/new-page");
9
+ });
10
+
11
+ // Example 2: Permanent redirect (301)
12
+ app.get("/permanent-redirect", (ctx) => {
13
+ return ctx.redirect("/new-location", 301);
14
+ });
15
+
16
+ // Example 3: Temporary redirect (302) - explicit
17
+ app.get("/temp-redirect", (ctx) => {
18
+ return ctx.redirect("/target", 302);
19
+ });
20
+
21
+ // Example 4: See Other redirect (303) - forces GET method
22
+ app.post("/form-submit", (ctx) => {
23
+ // After POST, redirect to success page with GET
24
+ return ctx.redirect("/success", 303);
25
+ });
26
+
27
+ // Example 5: Temporary redirect preserving method (307)
28
+ app.put("/api/update", (ctx) => {
29
+ // Preserves PUT method in redirect
30
+ return ctx.redirect("/api/updated", 307);
31
+ });
32
+
33
+ // Example 6: Permanent redirect preserving method (308)
34
+ app.patch("/api/patch", (ctx) => {
35
+ // Preserves PATCH method in redirect
36
+ return ctx.redirect("/api/patched", 308);
37
+ });
38
+
39
+ // Example 7: External redirect
40
+ app.get("/external", (ctx) => {
41
+ return ctx.redirect("https://example.com", 301);
42
+ });
43
+
44
+ // Example 8: Conditional redirect based on query parameters
45
+ app.get("/conditional", (ctx) => {
46
+ const { type } = ctx.query;
47
+
48
+ if (type === "admin") {
49
+ return ctx.redirect("/admin-dashboard", 302);
50
+ } else if (type === "user") {
51
+ return ctx.redirect("/user-dashboard", 302);
52
+ } else {
53
+ return ctx.redirect("/default-page", 302);
54
+ }
55
+ });
56
+
57
+ // Example 9: Redirect with path parameters
58
+ app.get("/user/:id", (ctx) => {
59
+ const { id } = ctx.params;
60
+ return ctx.redirect(`/profile/${id}`, 301);
61
+ });
62
+
63
+ // Example 10: Redirect with query string preservation
64
+ app.get("/search", (ctx) => {
65
+ const { q, page } = ctx.query;
66
+ const queryString = new URLSearchParams(ctx.query).toString();
67
+ const redirectUrl = queryString ? `/new-search?${queryString}` : "/new-search";
68
+ return ctx.redirect(redirectUrl, 301);
69
+ });
70
+
71
+ // Example 11: Redirect with custom headers
72
+ app.get("/custom-redirect", (ctx) => {
73
+ // You can still use the standard Response approach for custom headers
74
+ return new Response(null, {
75
+ status: 302,
76
+ headers: {
77
+ "Location": "/target",
78
+ "Cache-Control": "no-cache",
79
+ "X-Custom-Header": "redirect-value"
80
+ }
81
+ });
82
+ });
83
+
84
+ // Example 12: Redirect after authentication
85
+ app.post("/login", (ctx) => {
86
+ const { username, password } = ctx.body;
87
+
88
+ // Simple authentication check
89
+ if (username === "admin" && password === "password") {
90
+ // Set session cookie
91
+ ctx.set.cookie("session", "authenticated", {
92
+ httpOnly: true,
93
+ maxAge: 3600
94
+ });
95
+
96
+ // Redirect to dashboard
97
+ return ctx.redirect("/dashboard", 303);
98
+ } else {
99
+ // Redirect back to login with error
100
+ return ctx.redirect("/login?error=invalid-credentials", 303);
101
+ }
102
+ });
103
+
104
+ // Example 13: Redirect with status code validation
105
+ app.get("/validated-redirect", (ctx) => {
106
+ const { status } = ctx.query;
107
+
108
+ // Validate status code
109
+ const validStatuses = [301, 302, 303, 307, 308];
110
+ const redirectStatus = validStatuses.includes(Number(status)) ? Number(status) : 302;
111
+
112
+ return ctx.redirect("/target", redirectStatus as 301 | 302 | 303 | 307 | 308);
113
+ });
114
+
115
+ // Example 14: Redirect with relative and absolute URLs
116
+ app.get("/relative-redirect", (ctx) => {
117
+ // Relative URL redirect
118
+ return ctx.redirect("./relative-path", 302);
119
+ });
120
+
121
+ app.get("/absolute-redirect", (ctx) => {
122
+ // Absolute URL redirect
123
+ return ctx.redirect("/absolute-path", 302);
124
+ });
125
+
126
+ // Example 15: Redirect with fragment (hash)
127
+ app.get("/fragment-redirect", (ctx) => {
128
+ return ctx.redirect("/page#section", 302);
129
+ });
130
+
131
+ // Target pages for testing redirects
132
+ app.get("/new-page", (ctx) => {
133
+ return ctx.json({ message: "Welcome to the new page!" });
134
+ });
135
+
136
+ app.get("/new-location", (ctx) => {
137
+ return ctx.json({ message: "This is the new permanent location!" });
138
+ });
139
+
140
+ app.get("/target", (ctx) => {
141
+ return ctx.json({ message: "Redirect target reached!" });
142
+ });
143
+
144
+ app.get("/success", (ctx) => {
145
+ return ctx.json({ message: "Form submitted successfully!" });
146
+ });
147
+
148
+ app.get("/dashboard", (ctx) => {
149
+ const session = ctx.cookies.session;
150
+ if (session === "authenticated") {
151
+ return ctx.json({ message: "Welcome to the dashboard!" });
152
+ } else {
153
+ return ctx.redirect("/login", 302);
154
+ }
155
+ });
156
+
157
+ app.get("/login", (ctx) => {
158
+ const { error } = ctx.query;
159
+ return ctx.json({
160
+ message: "Login page",
161
+ error: error || null
162
+ });
163
+ });
164
+
165
+ app.get("/new-search", (ctx) => {
166
+ return ctx.json({
167
+ message: "New search page",
168
+ query: ctx.query
169
+ });
170
+ });
171
+
172
+ app.get("/profile/:id", (ctx) => {
173
+ return ctx.json({
174
+ message: `Profile page for user ${ctx.params.id}`
175
+ });
176
+ });
177
+
178
+ app.get("/admin-dashboard", (ctx) => {
179
+ return ctx.json({ message: "Admin dashboard" });
180
+ });
181
+
182
+ app.get("/user-dashboard", (ctx) => {
183
+ return ctx.json({ message: "User dashboard" });
184
+ });
185
+
186
+ app.get("/default-page", (ctx) => {
187
+ return ctx.json({ message: "Default page" });
188
+ });
189
+
190
+ app.get("/page", (ctx) => {
191
+ return ctx.json({ message: "Page with section" });
192
+ });
193
+
194
+ app.start();
195
+ console.log(`Redirect example server running on http://localhost:${app.server?.port}`);
196
+ console.log("\nTry these redirect endpoints:");
197
+ console.log("GET /old-page -> /new-page (302)");
198
+ console.log("GET /permanent-redirect -> /new-location (301)");
199
+ console.log("GET /temp-redirect -> /target (302)");
200
+ console.log("POST /form-submit -> /success (303)");
201
+ console.log("PUT /api/update -> /api/updated (307)");
202
+ console.log("PATCH /api/patch -> /api/patched (308)");
203
+ console.log("GET /external -> https://example.com (301)");
204
+ console.log("GET /conditional?type=admin -> /admin-dashboard (302)");
205
+ console.log("GET /conditional?type=user -> /user-dashboard (302)");
206
+ console.log("GET /conditional -> /default-page (302)");
207
+ console.log("GET /user/123 -> /profile/123 (301)");
208
+ console.log("GET /search?q=test&page=1 -> /new-search?q=test&page=1 (301)");
209
+ console.log("GET /custom-redirect -> /target (302) with custom headers");
210
+ console.log("POST /login (admin/password) -> /dashboard (303)");
211
+ console.log("GET /validated-redirect?status=301 -> /target (301)");
212
+ console.log("GET /relative-redirect -> ./relative-path (302)");
213
+ console.log("GET /absolute-redirect -> /absolute-path (302)");
214
+ console.log("GET /fragment-redirect -> /page#section (302)");
215
+ console.log("\nTest with curl:");
216
+ console.log("curl -I http://localhost:3002/old-page");
217
+ console.log("curl -I http://localhost:3002/permanent-redirect");
218
+ console.log("curl -X POST http://localhost:3002/form-submit");
219
+ console.log("curl -X PUT http://localhost:3002/api/update");
220
+ }
221
+
222
+ main().catch(console.error);
package/package.json CHANGED
@@ -5,12 +5,13 @@
5
5
  ".": "./src/index.ts",
6
6
  "./plugins": "./plugins/index.ts"
7
7
  },
8
- "version": "0.0.5-dev.83",
8
+ "version": "0.0.5-dev.85",
9
9
  "type": "module",
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
12
12
  },
13
13
  "peerDependencies": {
14
+ "axios": "^1.12.2",
14
15
  "typescript": "^5"
15
16
  },
16
17
  "dependencies": {
package/src/index.ts CHANGED
@@ -83,9 +83,10 @@ export type Context<P extends string = string, S extends RouteSchema | undefined
83
83
  json: <T>(data: T, status?: number) => Response;
84
84
  text: (data: string, status?: number) => Response;
85
85
  status: <T extends number>(status: T, data: InferResponse<S, T>) => Response;
86
+ redirect: (url: string, status?: 301 | 302 | 303 | 307 | 308) => Response;
86
87
  };
87
88
 
88
- type AnyHandler = (ctx: Context<any, any>, app: BXO) => Response | string | Promise<Response | string>;
89
+ type AnyHandler = (ctx: Context<any, any>, app: BXO) => Response | string | BunFile | Promise<Response | string | BunFile>;
89
90
  type Handler<P extends string, S extends RouteSchema | undefined = undefined> = (
90
91
  ctx: Context<P, S>,
91
92
  app: BXO
@@ -757,6 +758,14 @@ export default class BXO {
757
758
  }
758
759
 
759
760
  return toResponse(data, { status });
761
+ },
762
+ redirect: (url, status = 302) => {
763
+ return new Response(null, {
764
+ status,
765
+ headers: {
766
+ "Location": url
767
+ }
768
+ });
760
769
  }
761
770
  };
762
771
 
package/test-bun-file.ts DELETED
File without changes
@@ -1,21 +0,0 @@
1
- import BXO from "./src";
2
-
3
- const app = new BXO({ serve: { port: 3001 } });
4
-
5
- // Test the exact scenario you mentioned
6
- app.get("/api/resources/:resourceType/:id", (ctx) => {
7
- return ctx.json({
8
- message: "Success!",
9
- resourceType: ctx.params.resourceType,
10
- id: ctx.params.id,
11
- allParams: ctx.params,
12
- url: ctx.request.url,
13
- pathname: new URL(ctx.request.url).pathname
14
- });
15
- });
16
-
17
- app.start();
18
- console.log(`Test server running on http://localhost:${app.server?.port}`);
19
- console.log(`Test URL: http://localhost:${app.server?.port}/api/resources/Doctype%20Permission/01992af8-1c69-7000-9219-9b83c2feb2d6`);
20
-
21
-