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.
Files changed (2) hide show
  1. package/index.ts +95 -11
  2. 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['*'] = decodeURIComponent(remainingPath);
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] = decodeURIComponent(pathSegment);
355
+ params[paramName] = pathSegment;
355
356
  } else if (routeSegment === '*') {
356
357
  // Single segment wildcard
357
- params['*'] = decodeURIComponent(pathSegment);
358
- } else if (routeSegment !== decodeURIComponent(pathSegment)) {
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['*'] = decodeURIComponent(remainingPath);
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] = decodeURIComponent(pathSegment);
419
+ params[paramName] = pathSegment;
419
420
  } else if (routeSegment === '*') {
420
421
  // Single segment wildcard
421
- params['*'] = decodeURIComponent(pathSegment);
422
- } else if (routeSegment !== decodeURIComponent(pathSegment)) {
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 pathname = url.pathname;
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 };
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.31",
4
+ "version": "0.0.5-dev.33",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "devDependencies": {