@rhythmjs/router 0.0.17 → 0.0.20

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/README.md CHANGED
@@ -39,6 +39,7 @@ Bun.serve({ port: 3000, fetch: toFetchHandler(app) });
39
39
  - **`ctx.params`**: captured path params, typed from the path: `/users/:id` gives `params.id: string`, `:id?` is `string | undefined`, and reading a name that is not in the path is a compile error. In `use()` middleware, params are an untyped `Record<string, string>`, since several routes may match.
40
40
  - **Strict context.** Like `Rhythm`, the route context only has what was added: `ctx.nope` is a compile error, and `derive()` (router-level or as a route's first handler) is how you widen it. A route-level `derive` only affects that route.
41
41
  - **Apps and the fetch handler.** Inside an app's own middleware, `ctx.request`/`ctx.response` are typed only if the app declares them as input: `new Rhythm<{}, RhythmHttpContext>()`. `toFetchHandler(app)` rejects, at compile time, an app that requires any other input.
42
+ - **`ctx.server`.** The handler is `(request, server?)`, matching Bun's `fetch(request, server)`. Pass the server through and every handler gets the typed `Bun.Server` as `ctx.server` (for `ctx.server?.requestIP(ctx.request)`, `publish`, `upgrade`, …); without it, `ctx.server` is `undefined`.
42
43
  - **Errors.** A route that throws makes the handler reject; mounted plugins wrap failures as `mounted router "name" failed` with the original as `cause`. `errorToResponse` looks through the `cause` chain for a declared status.
43
44
 
44
45
  ## API
@@ -51,6 +52,22 @@ Bun.serve({ port: 3000, fetch: toFetchHandler(app) });
51
52
  - `createHttpContext(request)` / `toResponse(response)` (`@rhythmjs/router/context`): the request context and its conversion to a `Response`.
52
53
  - `toFetchHandler(app)` (`@rhythmjs/router/fetch`): bridges a `Rhythm` app (or router) to a Web-standard `(Request) => Promise<Response>` handler. Errors reject.
53
54
  - `errorToResponse(error)` (`@rhythmjs/router/fetch`): maps a thrown error to a `Response`.
55
+ - `fromFetch(handler)` (`@rhythmjs/router/fetch`): wraps a fetch-style `(request) => Response` handler (Better Auth, tRPC, a webhook SDK) as a mountable. The returned `Response` becomes the response (status, headers, body stream; every `Set-Cookie` is kept).
56
+ - `pathIs(pattern)` (`@rhythmjs/router/path`): a condition for `mount(plugin, condition)` that is true when the request path matches `pattern`, using the same rou3 conventions as routes; the HTTP method is not part of the match, and a pattern without a leading `/` throws a `TypeError`.
57
+
58
+ ```ts
59
+ import { fromFetch } from "@rhythmjs/router/fetch";
60
+ import { pathIs } from "@rhythmjs/router/path";
61
+
62
+ const app = new Rhythm()
63
+ .use(
64
+ mount(
65
+ fromFetch((request) => auth.handler(request)),
66
+ pathIs("/api/auth/**"),
67
+ ),
68
+ )
69
+ .use(mount(router));
70
+ ```
54
71
 
55
72
  ## Serving: your Bun.serve, no wrapper
56
73
 
@@ -69,14 +86,9 @@ const handler = toFetchHandler(app);
69
86
  const server = Bun.serve({
70
87
  port: 3000,
71
88
  async fetch(request, srv) {
72
- // Optional: expose the client address as request.ip, the field
73
- // @rhythmjs/security's rate limit and @rhythmjs/http's proxy key off.
74
- Object.defineProperty(request, "ip", {
75
- configurable: true,
76
- get: () => srv.requestIP(request)?.address,
77
- });
78
89
  try {
79
- return await handler(request);
90
+ // Passing the server makes it available as ctx.server in every handler.
91
+ return await handler(request, srv);
80
92
  } catch (error) {
81
93
  return errorToResponse(error);
82
94
  }
package/dist/context.d.ts CHANGED
@@ -7,6 +7,7 @@ export declare class RhythmResponse {
7
7
  }
8
8
  export interface RhythmHttpContext {
9
9
  readonly request: Request;
10
+ readonly server: Bun.Server<any> | undefined;
10
11
  readonly response: RhythmResponse;
11
12
  json(data: unknown, status?: number): void;
12
13
  text(body: string, status?: number): void;
@@ -15,5 +16,5 @@ export interface RhythmHttpContext {
15
16
  redirect(url: string, status?: number): void;
16
17
  }
17
18
  export declare const STATUS_TEXT: Record<number, string>;
18
- export declare function createHttpContext(request: Request): RhythmHttpContext;
19
+ export declare function createHttpContext(request: Request, server?: Bun.Server<any>): RhythmHttpContext;
19
20
  export declare function toResponse(response: RhythmResponse): Response;
package/dist/context.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  STATUS_TEXT2,
5
5
  createHttpContext2,
6
6
  toResponse2
7
- } from "./rhythm-router-48zw4cyy.js";
7
+ } from "./rhythm-router-1sak3407.js";
8
8
  export {
9
9
  RhythmResponse2 as RhythmResponse,
10
10
  STATUS_TEXT2 as STATUS_TEXT,
package/dist/fetch.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Mountable } from "@rhythmjs/rhythm";
1
+ import { Rhythm, type Mountable } from "@rhythmjs/rhythm";
2
2
  import { type RhythmHttpContext } from "./context";
3
- export declare function toFetchHandler<I extends object = any>(app: Mountable<I> & (RhythmHttpContext extends I ? unknown : never)): (request: Request) => Promise<Response>;
3
+ export declare function toFetchHandler<I extends object = any>(app: Mountable<I> & (RhythmHttpContext extends I ? unknown : never)): <WebSocketData>(request: Request, server?: Bun.Server<WebSocketData>) => Promise<Response>;
4
+ export declare function fromFetch(handler: (request: Request) => Response | Promise<Response>): Rhythm<{}, RhythmHttpContext, {}>;
4
5
  export declare function errorToResponse(error: unknown): Response;
package/dist/fetch.js CHANGED
@@ -4,16 +4,17 @@ import {
4
4
  STATUS_TEXT2,
5
5
  createHttpContext2,
6
6
  toResponse2
7
- } from "./rhythm-router-48zw4cyy.js";
7
+ } from "./rhythm-router-1sak3407.js";
8
8
 
9
9
  // src/fetch.ts
10
+ import { Rhythm } from "@rhythmjs/rhythm";
10
11
  function isUntouched(response) {
11
12
  return response.status === 200 && response.statusText === undefined && response.body === null && [...response.headers].length === 0;
12
13
  }
13
14
  function toFetchHandler(app) {
14
15
  const run = app.callback();
15
- return async (request) => {
16
- const base = createHttpContext2(request);
16
+ return async (request, server) => {
17
+ const base = createHttpContext2(request, server);
17
18
  const result = await run(base);
18
19
  const response = result?.response ?? base.response;
19
20
  if (isUntouched(response)) {
@@ -26,6 +27,22 @@ function toFetchHandler(app) {
26
27
  return toResponse2(response);
27
28
  };
28
29
  }
30
+ function applyResponse(ctx, response) {
31
+ ctx.response.status = response.status;
32
+ ctx.response.statusText = response.statusText;
33
+ for (const [name, value] of response.headers) {
34
+ if (name === "set-cookie")
35
+ ctx.response.headers.append(name, value);
36
+ else
37
+ ctx.response.headers.set(name, value);
38
+ }
39
+ ctx.response.body = response.body;
40
+ }
41
+ function fromFetch(handler) {
42
+ return new Rhythm({ name: "fetch" }).use(async (ctx) => {
43
+ applyResponse(ctx, await handler(ctx.request));
44
+ });
45
+ }
29
46
  function declaredStatus(error) {
30
47
  const { status, statusCode } = error ?? {};
31
48
  const declared = status ?? statusCode;
@@ -56,5 +73,6 @@ function errorToResponse(error) {
56
73
  }
57
74
  export {
58
75
  errorToResponse,
76
+ fromFetch,
59
77
  toFetchHandler
60
78
  };
package/dist/path.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { RhythmHttpContext } from "@rhythmjs/router/context";
2
+ export declare function pathIs(pattern: string): (ctx: RhythmHttpContext) => boolean;
package/dist/path.js ADDED
@@ -0,0 +1,13 @@
1
+ // @bun
2
+ // src/path.ts
3
+ import { addRoute, createRouter, findRoute } from "rou3";
4
+ function pathIs(pattern) {
5
+ if (!pattern.startsWith("/"))
6
+ throw new TypeError(`path must start with "/", got "${pattern}"`);
7
+ const router = createRouter();
8
+ addRoute(router, "", pattern, true);
9
+ return (ctx) => findRoute(router, ctx.request.method, new URL(ctx.request.url).pathname) !== undefined;
10
+ }
11
+ export {
12
+ pathIs
13
+ };
@@ -25,10 +25,11 @@ var STATUS_TEXT2 = {
25
25
  503: "Service Unavailable",
26
26
  504: "Gateway Timeout"
27
27
  };
28
- function createHttpContext2(request) {
28
+ function createHttpContext2(request, server) {
29
29
  const response = new RhythmResponse2;
30
30
  return {
31
31
  request,
32
+ server,
32
33
  response,
33
34
  json(data, status) {
34
35
  if (status !== undefined)
@@ -12,7 +12,7 @@ export type RouteHandler<T extends object, P extends string> = (ctx: RouteContex
12
12
  export type RouteHandlers<T extends object, P extends string> = [RouteHandler<T, P>, ...RouteHandler<T, P>[]];
13
13
  export declare class RhythmRouter<I extends object = {}, D extends object = {}> extends Pipeline<UseContext<I & D>> {
14
14
  #private;
15
- readonly "~input"?: I;
15
+ readonly "~input"?: I & RhythmHttpContext;
16
16
  constructor(options?: PipelineOptions);
17
17
  use<U extends object>(middleware: ExtensionMiddleware<UseContext<I & D>, U>): RhythmRouter<I, D & U>;
18
18
  use(middleware: Middleware<UseContext<I & D>>): this;
@@ -62,6 +62,8 @@ class RhythmRouter extends Pipeline {
62
62
  return { route: found.data, params };
63
63
  }
64
64
  #route(method, path, handlers) {
65
+ if (!path.startsWith("/"))
66
+ throw new TypeError(`route path must start with "/", got "${path}"`);
65
67
  const chain = compose(handlers);
66
68
  addRoute(this.#routes, method, path, (ctx) => chain(ctx));
67
69
  return this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.17",
3
+ "version": "0.0.20",
4
4
  "description": "Bun-native HTTP router for Rhythm: RhythmRouter plus a fetch handler for Bun.serve, matching with rou3.",
5
5
  "homepage": "https://rhythm.js.org/router",
6
6
  "license": "ISC",
@@ -27,13 +27,17 @@
27
27
  "types": "./dist/fetch.d.ts",
28
28
  "default": "./dist/fetch.js"
29
29
  },
30
+ "./path": {
31
+ "types": "./dist/path.d.ts",
32
+ "default": "./dist/path.js"
33
+ },
30
34
  "./package.json": "./package.json"
31
35
  },
32
36
  "publishConfig": {
33
37
  "access": "public"
34
38
  },
35
39
  "dependencies": {
36
- "@rhythmjs/rhythm": "0.0.17",
40
+ "@rhythmjs/rhythm": "0.0.20",
37
41
  "rou3": "^1.0.0"
38
42
  },
39
43
  "devDependencies": {
@@ -45,7 +49,7 @@
45
49
  "bun": ">=1.2.0"
46
50
  },
47
51
  "scripts": {
48
- "build": "rm -rf dist && bun build src/rhythm-router.ts src/context.ts src/fetch.ts --outdir dist --root src --format esm --target bun --packages external --splitting && tsc -p tsconfig.build.json",
52
+ "build": "rm -rf dist && bun build src/rhythm-router.ts src/context.ts src/fetch.ts src/path.ts --outdir dist --root src --format esm --target bun --packages external --splitting && tsc -p tsconfig.build.json",
49
53
  "typecheck": "tsc --noEmit",
50
54
  "test": "bun test"
51
55
  }