@rhythmjs/router 0.0.7 → 0.0.8

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
@@ -9,15 +9,15 @@ Web-standard HTTP routing on top of `@rhythmjs/rhythm`. `RhythmRouter` matches r
9
9
  ```ts
10
10
  import { Rhythm } from "@rhythmjs/rhythm";
11
11
  import { RhythmRouter } from "@rhythmjs/router";
12
- import { toFetchHandler } from "@rhythmjs/router/adapters/bun";
13
- import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
12
+ import { serve } from "@rhythmjs/router/serve";
13
+ import type { RhythmHttpContext } from "@rhythmjs/router/context";
14
14
 
15
15
  const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) => {
16
16
  ctx.json({ id: ctx.params.id });
17
17
  });
18
18
 
19
- const app = new Rhythm<RhythmHttpContext>().use(usersRouter.routes());
20
- Bun.serve({ fetch: toFetchHandler(app) });
19
+ const app = new Rhythm<RhythmHttpContext>().use(usersRouter.middleware());
20
+ serve(app, { port: 3000 });
21
21
  ```
22
22
 
23
23
  A fuller runnable version, including nested prefixes and a fallback route, is at [`examples/router`](../../examples/router).
@@ -37,18 +37,116 @@ A fuller runnable version, including nested prefixes and a fallback route, is at
37
37
  - `.get/.post/.put/.patch/.delete(path, ...handlers)` — register a route; `path` may contain `:param` segments.
38
38
  - `.use(fn)` — plain middleware. `.use(child)` — mount a nested `RhythmRouter` (prefixes compose).
39
39
  - `.routes()` — this router as a plain middleware, for mounting into a `Rhythm` app via `.use()`; the router's only way onto a server. Note: mounting a _router_ into a _router_ must use `.use(child)`, not `.use(child.routes())` — an opaque middleware can't have the parent's prefix applied to its routes.
40
- - `ctx.json/.text/.html(body, status?)`, `ctx.error(status, message?)`, `ctx.redirect(url, status?)` — response helpers built into the context by the adapters (`createHttpContext` in `adapters/context`).
40
+ - `ctx.json/.text/.html(body, status?)`, `ctx.error(status, message?)`, `ctx.redirect(url, status?)` — response helpers built into the context by the adapters (`createHttpContext` in `@rhythmjs/router/context`).
41
41
  - `toFetchHandler(app)` — bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
42
42
 
43
- ## Runtime adapters
43
+ ## Serving
44
44
 
45
- Everything above (`RhythmRouter`, `ctx.response`, etc.) is runtime-agnostic; only turning it into an actual server touches a specific runtime.
45
+ Everything above (`RhythmRouter`, `ctx.response`, etc.) is runtime-agnostic. Two primitives turn an app into a server, both built on [srvx](https://srvx.h3.dev):
46
46
 
47
- - **`@rhythmjs/router/adapters/web-std`** — `toFetchHandler(app)`, the runtime-neutral Web-standard `(Request) => Promise<Response>` adapter every fetch-based runtime can use.
48
- - **`@rhythmjs/router/adapters/bun`** — re-exports `toFetchHandler` from `web-std`, for `Bun.serve({ fetch: toFetchHandler(app) })`.
49
- - **`@rhythmjs/router/adapters/deno`** — re-exports the same `toFetchHandler`, for `Deno.serve(toFetchHandler(app))`. `Deno.serve()` accepts the identical handler shape `Bun.serve()` does, so no conversion is needed.
50
- - **`@rhythmjs/router/adapters/node`** — `toNodeHandler(app, options?)`, for `http.createServer(toNodeHandler(app)).listen(port)`. Node's `IncomingMessage`/`ServerResponse` aren't Web-standard, so this one does real conversion:
51
- - The request body is read eagerly into a buffer, up to `options.bodyLimit` (default 1mb). This guarantees the socket is always fully drained before the handler runs, even if the handler never reads `ctx.request`'s body — otherwise, on a keep-alive connection, unconsumed bytes left on the socket would stall the next request on it. A body over the limit gets a `413` and the connection is closed rather than kept alive.
52
- - Pass `{ bodyLimit: false }` to opt out of buffering — `ctx.request`'s body becomes a live stream over the raw connection instead, with no size limit, for uploads or proxying where materializing the whole body in memory isn't acceptable. This reintroduces the keep-alive caveat: if the handler doesn't read the body, unconsumed bytes are left on the socket.
53
- - The resulting `Response` is streamed back to the client via `Readable.fromWeb(...).pipe(res)`.
54
- - Errors thrown anywhere in the middleware chain are caught, logged via `console.error`, and answered with a 500 (or the connection is destroyed if headers were already sent) — without this, an unhandled rejection would crash the whole Node process.
47
+ - **`serve(app, options)`** (`@rhythmjs/router/serve`) — starts a server on Node, Bun, or Deno with one identical call; srvx picks the runtime implementation via conditional exports. On Node, requests are lazy: method, url, headers, and body materialize only when middleware touches them.
48
+ - **`toFetchHandler(app)`** (`@rhythmjs/router/fetch`) — the universal `(Request) => Promise<Response>` handler, for platforms that invoke you per request instead of letting you own a listener.
49
+
50
+ `serve()` accepts every srvx `ServerOptions` field except `fetch`: `port`, `hostname`, `tls` (HTTPS/HTTP2), `maxRequestBodySize` (an over-limit body read is answered with `413`), `reusePort`, `gracefulShutdown`, plus the three extension points below. Errors thrown in the middleware chain are answered with `500` (or the error's own `status`) without crashing the process; override the mapping with `options.error`.
51
+
52
+ ### Extending: CORS, WebSockets, and similar
53
+
54
+ - **`middleware`** — srvx middlewares (`(request, next) => Response`) run around the whole app, the natural place for CORS, logging, or auth gates:
55
+
56
+ ```ts
57
+ serve(app, {
58
+ middleware: [
59
+ async (request, next) => {
60
+ if (request.method === "OPTIONS")
61
+ return new Response(null, { status: 204, headers: { "access-control-allow-origin": "*" } });
62
+ const response = await next();
63
+ response.headers.set("access-control-allow-origin", "*");
64
+ return response;
65
+ },
66
+ ],
67
+ });
68
+ ```
69
+
70
+ - **`plugins`** — a plugin receives the live srvx `Server`, the hook for anything below fetch, WebSockets included ([crossws](https://crossws.h3.dev) attaches here; on Node the raw server is at `server.node.server`):
71
+
72
+ ```ts
73
+ serve(app, { plugins: [(server) => wireWebSockets(server)] });
74
+ ```
75
+
76
+ - **`error`** — replace the default error-to-response mapping.
77
+
78
+ ## Deploying per runtime
79
+
80
+ One app definition; each runtime has its own adapter under `@rhythmjs/router/adapters/*`, hono-style: import `handle` from your target's adapter and export what the platform expects.
81
+
82
+ **Node**
83
+
84
+ ```ts
85
+ import { createServer } from "node:http";
86
+ import { handle } from "@rhythmjs/router/adapters/node";
87
+
88
+ createServer(handle(app, { maxRequestBodySize: 1024 * 1024 })).listen(3000);
89
+ ```
90
+
91
+ (Or skip the adapter entirely and use `serve(app, { port: 3000 })`.)
92
+
93
+ **Bun**
94
+
95
+ ```ts
96
+ import { handle } from "@rhythmjs/router/adapters/bun";
97
+
98
+ Bun.serve({ port: 3000, fetch: handle(app) });
99
+ ```
100
+
101
+ **Deno**
102
+
103
+ ```ts
104
+ import { handle } from "@rhythmjs/router/adapters/deno";
105
+
106
+ Deno.serve({ port: 3000 }, handle(app));
107
+ ```
108
+
109
+ **Vercel** — in a catch-all route file:
110
+
111
+ ```ts
112
+ import { handle } from "@rhythmjs/router/adapters/vercel";
113
+
114
+ const handler = handle(app);
115
+ export const GET = handler;
116
+ export const POST = handler; // …and the other methods you serve
117
+ ```
118
+
119
+ **Cloudflare Workers** — `env` and `ctx` are exposed to middleware as `ctx.request.runtime.cloudflare`:
120
+
121
+ ```ts
122
+ import { handle } from "@rhythmjs/router/adapters/cloudflare";
123
+
124
+ export default { fetch: handle(app) };
125
+ ```
126
+
127
+ **AWS Lambda** — API Gateway v1/v2 events, translated by srvx:
128
+
129
+ ```ts
130
+ import { handle } from "@rhythmjs/router/adapters/aws-lambda";
131
+
132
+ export const lambda = handle(app);
133
+ ```
134
+
135
+ **Netlify Edge Functions** — the Netlify context is exposed as `ctx.request.runtime.netlify`:
136
+
137
+ ```ts
138
+ import { handle } from "@rhythmjs/router/adapters/netlify";
139
+
140
+ export default handle(app);
141
+ export const config = { path: "/*" };
142
+ ```
143
+
144
+ **Service workers**
145
+
146
+ ```ts
147
+ import { handle } from "@rhythmjs/router/adapters/service-worker";
148
+
149
+ addEventListener("fetch", handle(app));
150
+ ```
151
+
152
+ Any other fetch-based runtime works with the raw primitive: `toFetchHandler(app)` from `@rhythmjs/router/fetch`.
@@ -0,0 +1,7 @@
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { AWSLambdaHandler } from "srvx/aws-lambda";
3
+ import { Rhythm } from "@rhythmjs/rhythm";
4
+ //#region src/adapters/aws-lambda.d.ts
5
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): AWSLambdaHandler;
6
+ //#endregion
7
+ export type { AWSLambdaHandler };
@@ -0,0 +1,8 @@
1
+ import { toFetchHandler } from "../fetch.js";
2
+ import { toLambdaHandler } from "srvx/aws-lambda";
3
+ //#region src/adapters/aws-lambda.ts
4
+ function handle(app) {
5
+ return toLambdaHandler({ fetch: toFetchHandler(app) });
6
+ }
7
+ //#endregion
8
+ export { handle };
@@ -1,2 +1,5 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/bun.d.ts
4
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
+ //#endregion
@@ -1,2 +1,7 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { toFetchHandler } from "../fetch.js";
2
+ //#region src/adapters/bun.ts
3
+ function handle(app) {
4
+ return toFetchHandler(app);
5
+ }
6
+ //#endregion
7
+ export { handle };
@@ -0,0 +1,5 @@
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/cloudflare.d.ts
4
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request, env?: unknown, context?: unknown) => Promise<Response>;
5
+ //#endregion
@@ -0,0 +1,20 @@
1
+ import { toFetchHandler } from "../fetch.js";
2
+ //#region src/adapters/cloudflare.ts
3
+ function handle(app) {
4
+ const handler = toFetchHandler(app);
5
+ return (request, env, context) => {
6
+ if (env !== void 0 || context !== void 0) Object.defineProperty(request, "runtime", {
7
+ value: {
8
+ name: "cloudflare",
9
+ cloudflare: {
10
+ env,
11
+ context
12
+ }
13
+ },
14
+ configurable: true
15
+ });
16
+ return handler(request);
17
+ };
18
+ }
19
+ //#endregion
20
+ export { handle };
@@ -1,2 +1,5 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/deno.d.ts
4
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
+ //#endregion
@@ -1,2 +1,7 @@
1
- import { toFetchHandler } from "./web-std.js";
2
- export { toFetchHandler };
1
+ import { toFetchHandler } from "../fetch.js";
2
+ //#region src/adapters/deno.ts
3
+ function handle(app) {
4
+ return toFetchHandler(app);
5
+ }
6
+ //#endregion
7
+ export { handle };
@@ -0,0 +1,5 @@
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/netlify.d.ts
4
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request, context?: unknown) => Promise<Response>;
5
+ //#endregion
@@ -0,0 +1,17 @@
1
+ import { toFetchHandler } from "../fetch.js";
2
+ //#region src/adapters/netlify.ts
3
+ function handle(app) {
4
+ const handler = toFetchHandler(app);
5
+ return (request, context) => {
6
+ if (context !== void 0) Object.defineProperty(request, "runtime", {
7
+ value: {
8
+ name: "netlify",
9
+ netlify: { context }
10
+ },
11
+ configurable: true
12
+ });
13
+ return handler(request);
14
+ };
15
+ }
16
+ //#endregion
17
+ export { handle };
@@ -1,12 +1,9 @@
1
- import { t as RhythmHttpContext } from "../context-B0711qPV.js";
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { NodeHttpHandler } from "srvx";
2
3
  import { Rhythm } from "@rhythmjs/rhythm";
3
- import { IncomingMessage, ServerResponse } from "node:http";
4
4
  //#region src/adapters/node.d.ts
5
- export declare class RhythmBodyTooLargeError extends Error {
6
- constructor(limit: number);
5
+ export interface NodeHandlerOptions {
6
+ maxRequestBodySize?: number;
7
7
  }
8
- export interface RhythmNodeHandlerOptions {
9
- bodyLimit?: number | false;
10
- }
11
- export declare function toNodeHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: RhythmNodeHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
8
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: NodeHandlerOptions): NodeHttpHandler;
12
9
  //#endregion
@@ -1,135 +1,17 @@
1
- import { createHttpContext, toResponse } from "./context.js";
2
- import { Readable } from "node:stream";
3
- import { Buffer } from "node:buffer";
1
+ import { toFetchHandler } from "../fetch.js";
2
+ import { errorToResponse } from "../serve.js";
3
+ import { toNodeHandler } from "srvx/node";
4
4
  //#region src/adapters/node.ts
5
- const DEFAULT_BODY_LIMIT = 1048576;
6
- var RhythmBodyTooLargeError = class extends Error {
7
- constructor(limit) {
8
- super(`request body exceeds the ${limit} byte limit`);
9
- this.name = "RhythmBodyTooLargeError";
10
- }
11
- };
12
- function readRawBody(req, limit) {
13
- return new Promise((resolve, reject) => {
14
- const chunks = [];
15
- let received = 0;
16
- let tooLarge = false;
17
- let settled = false;
18
- req.on("data", (chunk) => {
19
- received += chunk.length;
20
- if (received > limit) {
21
- if (!tooLarge) {
22
- tooLarge = true;
23
- settled = true;
24
- reject(new RhythmBodyTooLargeError(limit));
25
- }
26
- return;
27
- }
28
- chunks.push(chunk);
29
- });
30
- req.on("end", () => {
31
- if (settled) return;
32
- settled = true;
33
- resolve(Buffer.concat(chunks));
34
- });
35
- req.on("error", (err) => {
36
- if (settled) return;
37
- settled = true;
38
- reject(err);
39
- });
40
- req.on("aborted", () => {
41
- if (settled) return;
42
- settled = true;
43
- reject(/* @__PURE__ */ new Error("request aborted"));
44
- });
45
- });
46
- }
47
- async function toWebRequest(req, bodyLimit) {
48
- const method = req.method ?? "GET";
49
- const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
50
- const headers = [];
51
- for (const key in req.headers) {
52
- const value = req.headers[key];
53
- if (value === void 0) continue;
54
- if (Array.isArray(value)) for (const entry of value) headers.push([key, entry]);
55
- else headers.push([key, value]);
56
- }
57
- if (!(method !== "GET" && method !== "HEAD")) return new Request(url, {
58
- method,
59
- headers
60
- });
61
- if (bodyLimit === false) return new Request(url, {
62
- method,
63
- headers,
64
- body: Readable.toWeb(req),
65
- duplex: "half"
66
- });
67
- const body = await readRawBody(req, bodyLimit);
68
- return new Request(url, {
69
- method,
70
- headers,
71
- body
72
- });
73
- }
74
- function toNodeHeaders(headers) {
75
- const result = {};
76
- const setCookie = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [];
77
- if (setCookie.length > 0) result["set-cookie"] = setCookie;
78
- for (const [key, value] of headers) {
79
- if (key === "set-cookie") continue;
80
- result[key] = value;
81
- }
82
- return result;
83
- }
84
- async function writeWebResponse(response, res) {
85
- res.writeHead(response.status, toNodeHeaders(response.headers));
86
- if (!response.body) {
87
- res.end();
88
- return;
89
- }
90
- await new Promise((resolve, reject) => {
91
- const nodeStream = Readable.fromWeb(response.body);
92
- nodeStream.on("error", reject);
93
- res.on("finish", resolve);
94
- nodeStream.pipe(res);
95
- });
96
- }
97
- function writeDirectResponse(response, res) {
98
- const { body, status } = response;
99
- const headers = toNodeHeaders(response.headers);
100
- let payload;
101
- if (typeof body === "string") payload = body;
102
- else if (body instanceof Uint8Array) payload = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
103
- else if (body instanceof ArrayBuffer) payload = Buffer.from(body);
104
- if (!("content-length" in headers) && status !== 204 && status !== 304) headers["content-length"] = payload === void 0 ? "0" : String(typeof payload === "string" ? Buffer.byteLength(payload) : payload.byteLength);
105
- if (response.statusText !== void 0) res.writeHead(status, response.statusText, headers);
106
- else res.writeHead(status, headers);
107
- res.end(payload);
108
- }
109
- function hasDirectBody(body) {
110
- return body === null || typeof body === "string" || body instanceof Uint8Array || body instanceof ArrayBuffer;
111
- }
112
- function toNodeHandler(app, options = {}) {
113
- const run = app.callback();
114
- const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
115
- return async (req, res) => {
5
+ function handle(app, options = {}) {
6
+ const handler = toFetchHandler(app);
7
+ const safeHandler = async (request) => {
116
8
  try {
117
- const request = await toWebRequest(req, bodyLimit);
118
- const ctx = await run(createHttpContext(request));
119
- if (hasDirectBody(ctx.response.body)) writeDirectResponse(ctx.response, res);
120
- else await writeWebResponse(toResponse(ctx.response), res);
121
- } catch (err) {
122
- if (!(err instanceof RhythmBodyTooLargeError)) console.error(err);
123
- if (!res.headersSent) {
124
- const status = err instanceof RhythmBodyTooLargeError ? 413 : 500;
125
- const message = err instanceof RhythmBodyTooLargeError ? err.message : "Internal Server Error";
126
- const headers = { "content-type": "text/plain; charset=utf-8" };
127
- if (err instanceof RhythmBodyTooLargeError) headers.connection = "close";
128
- res.writeHead(status, headers);
129
- res.end(message);
130
- } else res.destroy(err instanceof Error ? err : new Error(String(err)));
9
+ return await handler(request);
10
+ } catch (error) {
11
+ return errorToResponse(error);
131
12
  }
132
13
  };
14
+ return toNodeHandler(safeHandler, options);
133
15
  }
134
16
  //#endregion
135
- export { RhythmBodyTooLargeError, toNodeHandler };
17
+ export { handle };
@@ -0,0 +1,9 @@
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/service-worker.d.ts
4
+ interface FetchEventLike {
5
+ readonly request: Request;
6
+ respondWith(response: Response | Promise<Response>): void;
7
+ }
8
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (event: FetchEventLike) => void;
9
+ //#endregion
@@ -0,0 +1,10 @@
1
+ import { toFetchHandler } from "../fetch.js";
2
+ //#region src/adapters/service-worker.ts
3
+ function handle(app) {
4
+ const handler = toFetchHandler(app);
5
+ return (event) => {
6
+ event.respondWith(handler(event.request));
7
+ };
8
+ }
9
+ //#endregion
10
+ export { handle };
@@ -0,0 +1,5 @@
1
+ import { RhythmHttpContext } from "../context.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/vercel.d.ts
4
+ export declare function handle<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
+ //#endregion
@@ -0,0 +1,7 @@
1
+ import { toFetchHandler } from "../fetch.js";
2
+ //#region src/adapters/vercel.ts
3
+ function handle(app) {
4
+ return toFetchHandler(app);
5
+ }
6
+ //#endregion
7
+ export { handle };
@@ -0,0 +1,20 @@
1
+ //#region src/context.d.ts
2
+ export type RhythmResponseBody = string | ArrayBuffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream<Uint8Array> | null;
3
+ export declare class RhythmResponse {
4
+ status: number;
5
+ statusText: string | undefined;
6
+ headers: Headers;
7
+ body: RhythmResponseBody;
8
+ }
9
+ export interface RhythmHttpContext {
10
+ readonly request: Request;
11
+ readonly response: RhythmResponse;
12
+ json(data: unknown, status?: number): void;
13
+ text(body: string, status?: number): void;
14
+ html(body: string, status?: number): void;
15
+ error(status: number, message?: string): void;
16
+ redirect(url: string, status?: number): void;
17
+ }
18
+ export declare function createHttpContext(request: Request): RhythmHttpContext;
19
+ export declare function toResponse(response: RhythmResponse): Response;
20
+ //#endregion
@@ -1,4 +1,4 @@
1
- //#region src/adapters/context.ts
1
+ //#region src/context.ts
2
2
  var RhythmResponse = class {
3
3
  status = 200;
4
4
  statusText = void 0;
@@ -1,5 +1,5 @@
1
- import { t as RhythmHttpContext } from "../context-B0711qPV.js";
1
+ import { RhythmHttpContext } from "./context.js";
2
2
  import { Rhythm } from "@rhythmjs/rhythm";
3
- //#region src/adapters/web-std.d.ts
3
+ //#region src/fetch.d.ts
4
4
  export declare function toFetchHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
5
  //#endregion
package/dist/fetch.js ADDED
@@ -0,0 +1,16 @@
1
+ import { createHttpContext } from "./context.js";
2
+ import { FastResponse } from "srvx";
3
+ //#region src/fetch.ts
4
+ function toFetchHandler(app) {
5
+ const run = app.callback();
6
+ return async (request) => {
7
+ const response = (await run(createHttpContext(request))).response;
8
+ return new FastResponse(response.body, {
9
+ status: response.status,
10
+ statusText: response.statusText,
11
+ headers: response.headers
12
+ });
13
+ };
14
+ }
15
+ //#endregion
16
+ export { toFetchHandler };
@@ -1,4 +1,4 @@
1
- import { t as RhythmHttpContext } from "./context-B0711qPV.js";
1
+ import { RhythmHttpContext } from "./context.js";
2
2
  import { DeriveMiddleware, Middleware } from "@rhythmjs/rhythm/types";
3
3
  //#region src/rhythm-router.d.ts
4
4
  export interface RhythmRouterContext {
@@ -0,0 +1,9 @@
1
+ import { RhythmHttpContext } from "./context.js";
2
+ import { ErrorHandler, Server, ServerMiddleware, ServerOptions, ServerPlugin } from "srvx";
3
+ import { Rhythm } from "@rhythmjs/rhythm";
4
+ //#region src/serve.d.ts
5
+ export type RhythmServeOptions = Omit<ServerOptions, "fetch">;
6
+ export declare const errorToResponse: ErrorHandler;
7
+ export declare function serve<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: RhythmServeOptions): Server;
8
+ //#endregion
9
+ export type { ErrorHandler, Server, ServerMiddleware, ServerPlugin };
package/dist/serve.js ADDED
@@ -0,0 +1,21 @@
1
+ import { toFetchHandler } from "./fetch.js";
2
+ import { serve as serve$1 } from "srvx";
3
+ //#region src/serve.ts
4
+ const errorToResponse = (error) => {
5
+ const status = error.status ?? error.statusCode ?? 500;
6
+ const message = status >= 500 ? "Internal Server Error" : error instanceof Error ? error.message : String(error);
7
+ if (status >= 500) console.error(error);
8
+ return new Response(message, {
9
+ status,
10
+ headers: { "content-type": "text/plain; charset=utf-8" }
11
+ });
12
+ };
13
+ function serve(app, options = {}) {
14
+ return serve$1({
15
+ error: errorToResponse,
16
+ ...options,
17
+ fetch: toFetchHandler(app)
18
+ });
19
+ }
20
+ //#endregion
21
+ export { errorToResponse, serve };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Radix-tree HTTP router for the Rhythm middleware kernel, with Bun, Node, and Deno adapters.",
5
5
  "homepage": "https://rhythm.js.org/router",
6
6
  "license": "ISC",
@@ -23,9 +23,21 @@
23
23
  "types": "./dist/radix-tree.d.ts",
24
24
  "default": "./dist/radix-tree.js"
25
25
  },
26
- "./adapters/web-std": {
27
- "types": "./dist/adapters/web-std.d.ts",
28
- "default": "./dist/adapters/web-std.js"
26
+ "./context": {
27
+ "types": "./dist/context.d.ts",
28
+ "default": "./dist/context.js"
29
+ },
30
+ "./fetch": {
31
+ "types": "./dist/fetch.d.ts",
32
+ "default": "./dist/fetch.js"
33
+ },
34
+ "./serve": {
35
+ "types": "./dist/serve.d.ts",
36
+ "default": "./dist/serve.js"
37
+ },
38
+ "./adapters/node": {
39
+ "types": "./dist/adapters/node.d.ts",
40
+ "default": "./dist/adapters/node.js"
29
41
  },
30
42
  "./adapters/bun": {
31
43
  "types": "./dist/adapters/bun.d.ts",
@@ -35,13 +47,29 @@
35
47
  "types": "./dist/adapters/deno.d.ts",
36
48
  "default": "./dist/adapters/deno.js"
37
49
  },
38
- "./adapters/node": {
39
- "types": "./dist/adapters/node.d.ts",
40
- "default": "./dist/adapters/node.js"
50
+ "./adapters/vercel": {
51
+ "types": "./dist/adapters/vercel.d.ts",
52
+ "default": "./dist/adapters/vercel.js"
53
+ },
54
+ "./adapters/netlify": {
55
+ "types": "./dist/adapters/netlify.d.ts",
56
+ "default": "./dist/adapters/netlify.js"
57
+ },
58
+ "./adapters/cloudflare": {
59
+ "types": "./dist/adapters/cloudflare.d.ts",
60
+ "default": "./dist/adapters/cloudflare.js"
61
+ },
62
+ "./adapters/aws-lambda": {
63
+ "types": "./dist/adapters/aws-lambda.d.ts",
64
+ "default": "./dist/adapters/aws-lambda.js"
65
+ },
66
+ "./adapters/service-worker": {
67
+ "types": "./dist/adapters/service-worker.d.ts",
68
+ "default": "./dist/adapters/service-worker.js"
41
69
  },
42
70
  "./adapters/context": {
43
- "types": "./dist/adapters/context.d.ts",
44
- "default": "./dist/adapters/context.js"
71
+ "types": "./dist/context.d.ts",
72
+ "default": "./dist/context.js"
45
73
  },
46
74
  "./package.json": "./package.json"
47
75
  },
@@ -49,7 +77,8 @@
49
77
  "access": "public"
50
78
  },
51
79
  "dependencies": {
52
- "@rhythmjs/rhythm": "0.0.7"
80
+ "srvx": "^1.0.5",
81
+ "@rhythmjs/rhythm": "0.0.8"
53
82
  },
54
83
  "devDependencies": {
55
84
  "@types/node": "^26.6.3",
@@ -1,2 +0,0 @@
1
- import { a as toResponse, i as createHttpContext, n as RhythmResponse, r as RhythmResponseBody, t as RhythmHttpContext } from "../context-B0711qPV.js";
2
- export { RhythmHttpContext, RhythmResponse, RhythmResponseBody, createHttpContext, toResponse };
@@ -1,11 +0,0 @@
1
- import { createHttpContext, toResponse } from "./context.js";
2
- //#region src/adapters/web-std.ts
3
- function toFetchHandler(app) {
4
- const run = app.callback();
5
- return async (request) => {
6
- const ctx = await run(createHttpContext(request));
7
- return toResponse(ctx.response);
8
- };
9
- }
10
- //#endregion
11
- export { toFetchHandler };
@@ -1,21 +0,0 @@
1
- //#region src/adapters/context.d.ts
2
- type RhythmResponseBody = string | ArrayBuffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream<Uint8Array> | null;
3
- declare class RhythmResponse {
4
- status: number;
5
- statusText: string | undefined;
6
- headers: Headers;
7
- body: RhythmResponseBody;
8
- }
9
- interface RhythmHttpContext {
10
- readonly request: Request;
11
- readonly response: RhythmResponse;
12
- json(data: unknown, status?: number): void;
13
- text(body: string, status?: number): void;
14
- html(body: string, status?: number): void;
15
- error(status: number, message?: string): void;
16
- redirect(url: string, status?: number): void;
17
- }
18
- declare function createHttpContext(request: Request): RhythmHttpContext;
19
- declare function toResponse(response: RhythmResponse): Response;
20
- //#endregion
21
- export { toResponse as a, createHttpContext as i, RhythmResponse as n, RhythmResponseBody as r, RhythmHttpContext as t };