@bunary/http 0.2.0 → 0.3.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to `@bunary/http` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-02-17
9
+
10
+ ### Added
11
+
12
+ - Built-in CORS middleware via `cors()` factory (#47)
13
+ - `cors()` with no arguments allows all origins (default `Access-Control-Allow-Origin: *`)
14
+ - Configurable `origin` (string, string array, or `"*"`), `methods`, `allowHeaders`, `exposeHeaders`, `credentials`, `maxAge`
15
+ - Handles preflight `OPTIONS` requests automatically — returns 204 with CORS headers
16
+ - Reflects `Access-Control-Request-Headers` by default, or uses explicit `allowHeaders`
17
+ - Adds `Vary: Origin` when origin is not `"*"` for correct cache behavior
18
+ - Works as global middleware (`app.use(cors())`) or per-group (`middleware: [cors()]`)
19
+ - `CorsOptions` type exported for TypeScript consumers
20
+
21
+ - Body parsing helpers on `RequestContext` — `ctx.json()`, `ctx.text()`, `ctx.formData()` (#51)
22
+ - `ctx.json<T>()` — parse JSON body with type inference, throws `BodyParseError` on malformed input
23
+ - `ctx.text()` — get request body as string
24
+ - `ctx.formData()` — parse multipart/URL-encoded form data, throws `BodyParseError` on malformed input
25
+ - `BodyParseError` class exported for catch-based error handling in handlers
26
+ - Thin wrappers around `Request` methods — no parsing framework, no validation, zero new dependencies
27
+
8
28
  ## [0.2.0] - 2026-02-15
9
29
 
10
30
  ### Added
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACX,UAAU,EACV,SAAS,EAWT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAC3B,SAAS,CAAC,OAAO,CAAC,CAyQpB"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACX,UAAU,EACV,SAAS,EAYT,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAC3B,SAAS,CAAC,OAAO,CAAC,CAkSpB"}
@@ -0,0 +1,13 @@
1
+ import type { PathParams } from "./types/pathParams.js";
2
+ import type { RequestContext } from "./types/requestContext.js";
3
+ /**
4
+ * Create a RequestContext for a given request and params.
5
+ *
6
+ * Centralises context construction so that body helpers (`json`, `text`,
7
+ * `formData`) are available everywhere a `RequestContext` is built —
8
+ * including 404/405 handler contexts.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function createRequestContext(request: Request, params: PathParams, query: URLSearchParams): RequestContext;
13
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,eAAe,GACpB,cAAc,CAsBhB"}
package/dist/cors.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ import type { Middleware } from "./types/index.js";
2
+ /**
3
+ * CORS configuration options.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const options: CorsOptions = {
8
+ * origin: "https://myapp.com",
9
+ * methods: ["GET", "POST"],
10
+ * credentials: true,
11
+ * maxAge: 86400,
12
+ * };
13
+ * ```
14
+ */
15
+ export interface CorsOptions {
16
+ /**
17
+ * Allowed origin(s). Use `"*"` (default) for any origin,
18
+ * a single string for one origin, or an array for multiple.
19
+ *
20
+ * @default "*"
21
+ */
22
+ origin?: string | string[];
23
+ /**
24
+ * HTTP methods to advertise in `Access-Control-Allow-Methods`.
25
+ *
26
+ * @default ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
27
+ */
28
+ methods?: string[];
29
+ /**
30
+ * Headers the client is allowed to send.
31
+ * When omitted, the value of `Access-Control-Request-Headers` is reflected.
32
+ */
33
+ allowHeaders?: string[];
34
+ /**
35
+ * Response headers the browser may expose to client-side JavaScript.
36
+ */
37
+ exposeHeaders?: string[];
38
+ /**
39
+ * Whether to include `Access-Control-Allow-Credentials: true`.
40
+ *
41
+ * @default false
42
+ */
43
+ credentials?: boolean;
44
+ /**
45
+ * How long (in seconds) the browser may cache preflight results.
46
+ * Omitted from the response when `undefined`.
47
+ */
48
+ maxAge?: number;
49
+ }
50
+ /**
51
+ * Create a CORS middleware.
52
+ *
53
+ * Handles preflight `OPTIONS` requests (returns 204) and
54
+ * adds CORS headers to actual responses.
55
+ *
56
+ * @param options - CORS configuration (defaults allow all origins)
57
+ * @returns Middleware function
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { createApp, cors } from "@bunary/http";
62
+ *
63
+ * // Allow any origin
64
+ * const app = createApp();
65
+ * app.use(cors());
66
+ *
67
+ * // Restrict to a single origin with credentials
68
+ * app.use(cors({
69
+ * origin: "https://myapp.com",
70
+ * credentials: true,
71
+ * maxAge: 86400,
72
+ * }));
73
+ *
74
+ * // Multiple allowed origins
75
+ * app.use(cors({
76
+ * origin: ["https://app1.com", "https://app2.com"],
77
+ * methods: ["GET", "POST"],
78
+ * }));
79
+ * ```
80
+ */
81
+ export declare function cors(options?: CorsOptions): Middleware;
82
+ //# sourceMappingURL=cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../src/cors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,WAAW;IAC3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE3B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAwF1D"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Error thrown when request body parsing fails.
3
+ *
4
+ * Thrown by `ctx.json()` and `ctx.formData()` when the request body
5
+ * cannot be parsed. Handlers can catch this to return a custom 400 response.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { BodyParseError } from "@bunary/http";
10
+ *
11
+ * app.post("/users", async (ctx) => {
12
+ * try {
13
+ * const body = await ctx.json();
14
+ * return { received: body };
15
+ * } catch (error) {
16
+ * if (error instanceof BodyParseError) {
17
+ * return new Response(JSON.stringify({ error: error.message }), {
18
+ * status: 400,
19
+ * headers: { "Content-Type": "application/json" },
20
+ * });
21
+ * }
22
+ * throw error;
23
+ * }
24
+ * });
25
+ * ```
26
+ */
27
+ export declare class BodyParseError extends Error {
28
+ readonly name = "BodyParseError";
29
+ /** The underlying parse error, if available */
30
+ readonly cause?: unknown;
31
+ constructor(message: string, cause?: unknown);
32
+ }
33
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACxC,SAAkB,IAAI,oBAAoB;IAE1C,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"methodNotAllowed.d.ts","sourceRoot":"","sources":["../../src/handlers/methodNotAllowed.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAkB,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,CAAC,EAAE,UAAU,EACpB,WAAW,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,QAAQ,CAAC,CAgCnB"}
1
+ {"version":3,"file":"methodNotAllowed.d.ts","sourceRoot":"","sources":["../../src/handlers/methodNotAllowed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAkB,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE3E;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC3C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,KAAK,EAAE,EACf,OAAO,CAAC,EAAE,UAAU,EACpB,WAAW,CAAC,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,QAAQ,CAAC,CA2BnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"notFound.d.ts","sourceRoot":"","sources":["../../src/handlers/notFound.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAgBnB"}
1
+ {"version":3,"file":"notFound.d.ts","sourceRoot":"","sources":["../../src/handlers/notFound.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,QAAQ,CAAC,CAWnB"}
package/dist/index.d.ts CHANGED
@@ -19,5 +19,8 @@
19
19
  * @packageDocumentation
20
20
  */
21
21
  export { createApp } from "./app.js";
22
+ export type { CorsOptions } from "./cors.js";
23
+ export { cors } from "./cors.js";
24
+ export { BodyParseError } from "./errors.js";
22
25
  export type { AppOptions, BunaryApp, BunaryServer, GroupCallback, GroupOptions, GroupRouter, HandlerResponse, HttpMethod, ListenOptions, Middleware, PathParams, RequestContext, RouteBuilder, RouteHandler, RouteInfo, } from "./types/index.js";
23
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,YAAY,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,UAAU,EACV,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,GACT,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,YAAY,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACX,eAAe,EACf,UAAU,EACV,aAAa,EACb,UAAU,EACV,UAAU,EACV,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,GACT,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,39 @@
1
1
  // @bun
2
+ // src/errors.ts
3
+ class BodyParseError extends Error {
4
+ name = "BodyParseError";
5
+ cause;
6
+ constructor(message, cause) {
7
+ super(message);
8
+ this.cause = cause;
9
+ }
10
+ }
11
+
12
+ // src/context.ts
13
+ function createRequestContext(request, params, query) {
14
+ return {
15
+ request,
16
+ params,
17
+ query,
18
+ locals: {},
19
+ json: async () => {
20
+ try {
21
+ return await request.json();
22
+ } catch (error) {
23
+ throw new BodyParseError("Failed to parse JSON body", error);
24
+ }
25
+ },
26
+ text: () => request.text(),
27
+ formData: async () => {
28
+ try {
29
+ return await request.formData();
30
+ } catch (error) {
31
+ throw new BodyParseError("Failed to parse form data", error);
32
+ }
33
+ }
34
+ };
35
+ }
36
+
2
37
  // src/response.ts
3
38
  function toResponse(result) {
4
39
  if (result instanceof Response) {
@@ -259,6 +294,17 @@ function resolveRoute(routes, method, path) {
259
294
  allowedMethods: Array.from(allowedMethods).sort()
260
295
  };
261
296
  }
297
+ function findRouteByPath(routes, path) {
298
+ for (const route of routes) {
299
+ if (!route.pattern.test(path))
300
+ continue;
301
+ const params = extractParams(path, route);
302
+ if (!checkConstraints(params, route.constraints))
303
+ continue;
304
+ return { route, params };
305
+ }
306
+ return null;
307
+ }
262
308
  function getAllowedMethods(routes, path) {
263
309
  const methods = new Set;
264
310
  for (const route of routes) {
@@ -345,12 +391,7 @@ function toHeadResponse(response) {
345
391
  async function handleMethodNotAllowed(request, path, routes, options, precomputed) {
346
392
  const url = new URL(request.url);
347
393
  const allowedMethods = precomputed ?? getAllowedMethods(routes, path);
348
- const methodNotAllowedCtx = {
349
- request,
350
- params: {},
351
- query: url.searchParams,
352
- locals: {}
353
- };
394
+ const methodNotAllowedCtx = createRequestContext(request, {}, url.searchParams);
354
395
  if (options?.onMethodNotAllowed) {
355
396
  const result = await options.onMethodNotAllowed(methodNotAllowedCtx, allowedMethods);
356
397
  const response = toResponse(result);
@@ -377,12 +418,7 @@ async function handleMethodNotAllowed(request, path, routes, options, precompute
377
418
  // src/handlers/notFound.ts
378
419
  async function handleNotFound(request, _path, options) {
379
420
  const url = new URL(request.url);
380
- const notFoundCtx = {
381
- request,
382
- params: {},
383
- query: url.searchParams,
384
- locals: {}
385
- };
421
+ const notFoundCtx = createRequestContext(request, {}, url.searchParams);
386
422
  if (options?.onNotFound) {
387
423
  const result = await options.onNotFound(notFoundCtx);
388
424
  return toResponse(result);
@@ -457,6 +493,24 @@ function createApp(options) {
457
493
  const path = url.pathname;
458
494
  const method = request.method;
459
495
  if (method === "OPTIONS") {
496
+ if (request.headers.get("Origin")) {
497
+ const routeMatch = findRouteByPath(routes, path);
498
+ const chain = routeMatch ? getMiddlewareChain(routeMatch.route) : middlewares.length > 0 ? [...middlewares] : [];
499
+ if (chain.length > 0) {
500
+ const params = routeMatch?.params ?? {};
501
+ const ctx2 = createRequestContext(request, params, url.searchParams);
502
+ let index = 0;
503
+ const next = async () => {
504
+ if (index < chain.length) {
505
+ const mw = chain[index++];
506
+ return await mw(ctx2, next);
507
+ }
508
+ return await handleOptions(request, path, routes, internalOpts);
509
+ };
510
+ const result = await next();
511
+ return toResponse(result);
512
+ }
513
+ }
460
514
  return await handleOptions(request, path, routes, internalOpts);
461
515
  }
462
516
  const { match, allowedMethods } = resolveRoute(routes, method, path);
@@ -466,12 +520,7 @@ function createApp(options) {
466
520
  }
467
521
  return await handleNotFound(request, path, internalOpts);
468
522
  }
469
- const ctx = {
470
- request,
471
- params: match.params,
472
- query: url.searchParams,
473
- locals: {}
474
- };
523
+ const ctx = createRequestContext(request, match.params, url.searchParams);
475
524
  try {
476
525
  const response = await executeRoute(match, ctx, getMiddlewareChain);
477
526
  if (method === "HEAD") {
@@ -589,6 +638,81 @@ function createApp(options) {
589
638
  };
590
639
  return app;
591
640
  }
641
+ // src/cors.ts
642
+ var DEFAULT_METHODS = ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"];
643
+ function resolveOrigin(allowed, requestOrigin, credentials) {
644
+ if (allowed === "*") {
645
+ return credentials ? requestOrigin : "*";
646
+ }
647
+ if (typeof allowed === "string") {
648
+ return allowed === requestOrigin ? allowed : null;
649
+ }
650
+ return allowed.includes(requestOrigin) ? requestOrigin : null;
651
+ }
652
+ function cors(options = {}) {
653
+ const {
654
+ origin = "*",
655
+ methods = DEFAULT_METHODS,
656
+ allowHeaders,
657
+ exposeHeaders,
658
+ credentials = false,
659
+ maxAge
660
+ } = options;
661
+ return async (ctx, next) => {
662
+ const requestOrigin = ctx.request.headers.get("Origin");
663
+ if (!requestOrigin) {
664
+ return await next();
665
+ }
666
+ const allowedOrigin = resolveOrigin(origin, requestOrigin, credentials);
667
+ if (!allowedOrigin) {
668
+ return await next();
669
+ }
670
+ const needsVary = origin !== "*" || credentials;
671
+ if (ctx.request.method === "OPTIONS") {
672
+ const headers = new Headers;
673
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
674
+ if (needsVary) {
675
+ headers.append("Vary", "Origin");
676
+ }
677
+ headers.set("Access-Control-Allow-Methods", methods.join(", "));
678
+ if (allowHeaders) {
679
+ headers.set("Access-Control-Allow-Headers", allowHeaders.join(", "));
680
+ } else {
681
+ const requested = ctx.request.headers.get("Access-Control-Request-Headers");
682
+ if (requested) {
683
+ headers.set("Access-Control-Allow-Headers", requested);
684
+ }
685
+ }
686
+ if (credentials) {
687
+ headers.set("Access-Control-Allow-Credentials", "true");
688
+ }
689
+ if (maxAge !== undefined) {
690
+ headers.set("Access-Control-Max-Age", String(maxAge));
691
+ }
692
+ return new Response(null, { status: 204, headers });
693
+ }
694
+ const result = await next();
695
+ const response = toResponse(result);
696
+ const newHeaders = new Headers(response.headers);
697
+ newHeaders.set("Access-Control-Allow-Origin", allowedOrigin);
698
+ if (needsVary) {
699
+ newHeaders.append("Vary", "Origin");
700
+ }
701
+ if (exposeHeaders && exposeHeaders.length > 0) {
702
+ newHeaders.set("Access-Control-Expose-Headers", exposeHeaders.join(", "));
703
+ }
704
+ if (credentials) {
705
+ newHeaders.set("Access-Control-Allow-Credentials", "true");
706
+ }
707
+ return new Response(response.body, {
708
+ status: response.status,
709
+ statusText: response.statusText,
710
+ headers: newHeaders
711
+ });
712
+ };
713
+ }
592
714
  export {
593
- createApp
715
+ createApp,
716
+ cors,
717
+ BodyParseError
594
718
  };
@@ -47,6 +47,13 @@ export declare function findRoute(routes: Route[], method: string, path: string)
47
47
  * Check if any route matches the path (regardless of method).
48
48
  */
49
49
  export declare function hasMatchingPath(routes: Route[], path: string): boolean;
50
+ /**
51
+ * Find the first route whose path matches, regardless of HTTP method.
52
+ *
53
+ * Used by the OPTIONS/CORS preflight handler to locate group middleware
54
+ * attached to a route at this path.
55
+ */
56
+ export declare function findRouteByPath(routes: Route[], path: string): RouteMatch | null;
50
57
  /**
51
58
  * Get all allowed HTTP methods for a given path.
52
59
  * Respects route constraints when determining matches.
@@ -1 +1 @@
1
- {"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/routes/find.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,oGAAoG;IACpG,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAiC3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMtE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYzE"}
1
+ {"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/routes/find.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,kEAAkE;IAClE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,oGAAoG;IACpG,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CAiC3F;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAQhF;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYzE"}
@@ -1,4 +1,4 @@
1
1
  export { compilePattern, createRouteBuilder, wrapBuilderWithNamePrefix } from "./builder.js";
2
- export { findRoute, getAllowedMethods, hasMatchingPath, type RouteMatch, type RouteResolution, resolveRoute, } from "./find.js";
2
+ export { findRoute, findRouteByPath, getAllowedMethods, hasMatchingPath, type RouteMatch, type RouteResolution, resolveRoute, } from "./find.js";
3
3
  export { type AddRouteFn, createGroupRouter } from "./group.js";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EACN,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,YAAY,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EACN,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,YAAY,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,KAAK,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -43,5 +43,59 @@ export interface RequestContext<TLocals extends object = Record<string, unknown>
43
43
  * ```
44
44
  */
45
45
  locals: TLocals;
46
+ /**
47
+ * Parse the request body as JSON.
48
+ *
49
+ * Thin wrapper around `request.json()` with error handling.
50
+ * Throws `BodyParseError` if the body is not valid JSON.
51
+ *
52
+ * @typeParam T — Expected shape of the parsed JSON body
53
+ * @returns The parsed JSON body
54
+ * @throws {BodyParseError} If the body cannot be parsed as JSON
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * app.post("/users", async (ctx) => {
59
+ * const body = await ctx.json<{ name: string }>();
60
+ * return { id: 1, name: body.name };
61
+ * });
62
+ * ```
63
+ */
64
+ json: <T = unknown>() => Promise<T>;
65
+ /**
66
+ * Get the request body as a string.
67
+ *
68
+ * Thin wrapper around `request.text()`.
69
+ *
70
+ * @returns The request body as text
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * app.post("/echo", async (ctx) => {
75
+ * const text = await ctx.text();
76
+ * return { echo: text };
77
+ * });
78
+ * ```
79
+ */
80
+ text: () => Promise<string>;
81
+ /**
82
+ * Parse the request body as FormData.
83
+ *
84
+ * Thin wrapper around `request.formData()` with error handling.
85
+ * Throws `BodyParseError` if the body cannot be parsed as form data.
86
+ *
87
+ * @returns The parsed FormData
88
+ * @throws {BodyParseError} If the body cannot be parsed as form data
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * app.post("/upload", async (ctx) => {
93
+ * const form = await ctx.formData();
94
+ * const name = form.get("name");
95
+ * return { name };
96
+ * });
97
+ * ```
98
+ */
99
+ formData: () => ReturnType<Request["formData"]>;
46
100
  }
47
101
  //# sourceMappingURL=requestContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"requestContext.d.ts","sourceRoot":"","sources":["../../src/types/requestContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,cAAc,CAC9B,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChD,OAAO,SAAS,UAAU,GAAG,UAAU;IAEvC,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,OAAO,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,eAAe,CAAC;IACvB;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,OAAO,CAAC;CAChB"}
1
+ {"version":3,"file":"requestContext.d.ts","sourceRoot":"","sources":["../../src/types/requestContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,cAAc,CAC9B,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChD,OAAO,SAAS,UAAU,GAAG,UAAU;IAEvC,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,MAAM,EAAE,OAAO,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,eAAe,CAAC;IACvB;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IAEpC;;;;;;;;;;;;;;OAcG;IACH,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5B;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,EAAE,MAAM,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;CAChD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunary/http",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "HTTP routing and middleware for Bunary - a Bun-first backend framework inspired by Laravel",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",