@rhythmjs/router 0.0.10 → 0.0.12

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
@@ -1,17 +1,17 @@
1
1
  # @rhythmjs/router
2
2
 
3
- Web-standard HTTP routing on top of `@rhythmjs/rhythm`. `RhythmRouter` matches routes with [rou3](https://github.com/h3js/rou3), the router that powers h3 (a static segment always wins over a `:param` segment, regardless of registration order), supports prefixes and nested routers, and mounts flat into a parent `Rhythm` app via `.use(router.middleware())`, so an unmatched request correctly falls through to whatever's registered after it.
3
+ The HTTP layer of Rhythm, the Bun-native backend framework: web-standard (`Request`/`Response`) routing on top of the `@rhythmjs/rhythm` kernel, served on `Bun.serve`. `RhythmRouter` matches routes with [rou3](https://github.com/h3js/rou3), the router that powers h3 (a static segment always wins over a `:param` segment, regardless of registration order), supports prefixes and nested routers, and mounts flat into a parent `Rhythm` app via `.use(router.middleware())`, so an unmatched request correctly falls through to whatever's registered after it.
4
4
 
5
5
  Route patterns follow rou3's conventions: `:name` params (`:name?` optional, `:id(\\d+)` regex-constrained), `*` for one unnamed segment (captured as `params["0"]`), and `**` for the rest of the path (`params._`, or `params.name` with `**:name`). Param values are the raw path segments, undecoded.
6
6
 
7
- `RhythmRouter` is not an app and does not extend `Rhythm` — it is a controller that compiles routes and middleware down to a single middleware (`.middleware()`). It shares the core middleware contract (`compose`, `Middleware`, `next(extra)`), but has no `provide()` or `register()`, and it can't be served on its own: a `Rhythm` app is always the host that owns the lifecycle and the adapters.
7
+ `RhythmRouter` is not an app and does not extend `Rhythm`; it is a controller that compiles routes and middleware down to a single middleware (`.middleware()`). It shares the core middleware contract (`compose`, `Middleware`, `next(extra)`), but has no `provide()` or `register()`, and it can't be served on its own: a `Rhythm` app is always the host that owns the lifecycle and the adapters.
8
8
 
9
9
  ## Example
10
10
 
11
11
  ```ts
12
12
  import { Rhythm } from "@rhythmjs/rhythm";
13
13
  import { RhythmRouter } from "@rhythmjs/router";
14
- import { serve } from "@rhythmjs/router/serve";
14
+ import { toFetchHandler } from "@rhythmjs/router/fetch";
15
15
  import type { RhythmHttpContext } from "@rhythmjs/router/context";
16
16
 
17
17
  const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) => {
@@ -19,140 +19,80 @@ const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) =>
19
19
  });
20
20
 
21
21
  const app = new Rhythm<RhythmHttpContext>().use(usersRouter.middleware());
22
- serve(app, { port: 3000 });
22
+ Bun.serve({ port: 3000, fetch: toFetchHandler(app) });
23
23
  ```
24
24
 
25
25
  A fuller runnable version, including nested prefixes and a fallback route, is at [`examples/router`](../../examples/router).
26
26
 
27
27
  ## Concepts
28
28
 
29
- - **`ctx.response`** is a plain mutable object (`status`, `statusText`, `headers`, `body`) — set it directly rather than constructing a `Response` yourself. The adapter converts it to a real `Response` at the end.
30
- - **Response helpers** — `ctx.json(data, status?)`, `ctx.text(body, status?)`, `ctx.html(body, status?)`, `ctx.error(status, message?)`, and `ctx.redirect(url, status = 302)` set the content type, body, and status on `ctx.response` in one call. `error()` defaults the message from the status code (`ctx.error(404)` → `"Not Found"`). They're sugar over `ctx.response`, so mixing both styles is fine, and later writes win.
31
- - **`ctx.params`** — captured `:name` path segments, added once a route matches.
32
- - **Nesting is `.use(child.middleware())`** — a router mounts into another router (or into the app) as a compiled middleware. The mount is opaque, so the parent's prefix is **not** applied to the child's routes: the child carries its own absolute prefix (`new RhythmRouter({ prefix: "/api/users" })`). On a miss the child falls through to `next()`, so the parent's later middleware and routes still run, and the child keeps working standalone.
33
- - **Registration order is execution order** — a `.use()` middleware wraps only the routes registered after it; routes registered before it are untouched, and a matched route that doesn't call `next()` returns without reaching anything registered later. Consecutive routes share one rou3 lookup; an unmatched request falls through, entry by entry, to the outer `next()`.
34
- - **A router is a controller, not a module** — it has no `provide()` or `register()`, and it cannot be `register()`ed into a `Rhythm` app either; `register()` composes `Rhythm` modules only. A router mounts into an app exactly one way: koa-style, via `.use(router.middleware())`.
29
+ - **`ctx.response`** is a plain mutable object (`status`, `statusText`, `headers`, `body`): set it directly rather than constructing a `Response` yourself. The adapter converts it to a real `Response` at the end.
30
+ - **Response helpers**: `ctx.json(data, status?)`, `ctx.text(body, status?)`, `ctx.html(body, status?)`, `ctx.error(status, message?)`, and `ctx.redirect(url, status = 302)` set the content type, body, and status on `ctx.response` in one call. `error()` defaults the message from the status code (`ctx.error(404)` → `"Not Found"`). They're sugar over `ctx.response`, so mixing both styles is fine, and later writes win.
31
+ - **`ctx.params`**: captured `:name` path segments, added once a route matches.
32
+ - **Nesting is `.use(child.middleware())`**: a router mounts into another router (or into the app) as a compiled middleware. The mount is opaque, so the parent's prefix is **not** applied to the child's routes: the child carries its own absolute prefix (`new RhythmRouter({ prefix: "/api/users" })`). On a miss the child falls through to `next()`, so the parent's later middleware and routes still run, and the child keeps working standalone.
33
+ - **Registration order is execution order**: a `.use()` middleware wraps only the routes registered after it; routes registered before it are untouched, and a matched route that doesn't call `next()` returns without reaching anything registered later. Consecutive routes share one rou3 lookup; an unmatched request falls through, entry by entry, to the outer `next()`.
34
+ - **A router is a controller, not a module**: it has no `provide()` or `register()`, and it cannot be `register()`ed into a `Rhythm` app either; `register()` composes `Rhythm` modules only. A router mounts into an app exactly one way: koa-style, via `.use(router.middleware())`.
35
35
 
36
36
  ## API
37
37
 
38
- - `new RhythmRouter(options?)` — `options.prefix`.
39
- - `.get/.post/.put/.patch/.delete(path, ...handlers)` — register a route; `path` may contain `:param` segments.
40
- - `.use(fn)` — plain middleware; it takes only functions, so a nested router mounts as `.use(child.middleware())`.
41
- - `.middleware()` — this router compiled to a plain middleware: the one form that mounts anywhere, into a `Rhythm` app or into another router. Because the compiled form is opaque, the mounting router's prefix is not applied to it — give the child its full prefix.
42
- - `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`).
43
- - `toFetchHandler(app)` — bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
38
+ - `new RhythmRouter(options?)`: `options.prefix`.
39
+ - `.get/.post/.put/.patch/.delete(path, ...handlers)`: register a route; `path` may contain `:param` segments.
40
+ - `.use(fn)`: plain middleware; it takes only functions, so a nested router mounts as `.use(child.middleware())`.
41
+ - `.middleware()`: this router compiled to a plain middleware: the one form that mounts anywhere, into a `Rhythm` app or into another router. Because the compiled form is opaque, the mounting router's prefix is not applied to it, so give the child its full prefix.
42
+ - `ctx.json/.text/.html(body, status?)`, `ctx.error(status, message?)`, `ctx.redirect(url, status?)`: response helpers built into the context (`createHttpContext` in `@rhythmjs/router/context`).
43
+ - `toFetchHandler(app)`: bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
44
44
 
45
- ## Serving
45
+ ## Serving: your Bun.serve, no wrapper
46
46
 
47
- 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):
47
+ There is no `serve()` helper and no static-file helper. You write `Bun.serve` in your own `main.ts`, and the package gives you exactly two plain pieces for its `fetch`:
48
48
 
49
- - **`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.
50
- - **`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
+ - **`toFetchHandler(app)`** (`@rhythmjs/router/fetch`): the app as a `(Request) => Promise<Response>` handler.
50
+ - **`errorToResponse(error)`** (`@rhythmjs/router/fetch`): maps a thrown error to a Response: the error's own `status`/`statusCode` when set, else a logged `500`.
51
51
 
52
- `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`.
53
-
54
- ### Extending: CORS, WebSockets, and similar
55
-
56
- - **`middleware`** — srvx middlewares (`(request, next) => Response`) run around the whole app, the natural place for CORS, logging, or auth gates:
57
-
58
- ```ts
59
- serve(app, {
60
- middleware: [
61
- async (request, next) => {
62
- if (request.method === "OPTIONS")
63
- return new Response(null, { status: 204, headers: { "access-control-allow-origin": "*" } });
64
- const response = await next();
65
- response.headers.set("access-control-allow-origin", "*");
66
- return response;
67
- },
68
- ],
69
- });
70
- ```
71
-
72
- - **`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`):
73
-
74
- ```ts
75
- serve(app, { plugins: [(server) => wireWebSockets(server)] });
76
- ```
77
-
78
- - **`error`** — replace the default error-to-response mapping.
79
-
80
- ## Deploying per runtime
81
-
82
- One app definition; each runtime has its own adapter under `@rhythmjs/router/adapters/*`, hono-style: import from your target's adapter and export what the platform expects. Runtimes that speak web-standard `Request`/`Response` natively (Bun, Deno, Vercel) can equally use `toFetchHandler(app)` from `@rhythmjs/router/fetch` — their adapters are aliases for it.
83
-
84
- Every adapter maps errors thrown in the middleware chain to a response (`500`, or the error's own `status`) instead of crashing, and accepts `{ onError }` to replace that mapping: `handle(app, { onError: (error) => new Response("down", { status: 503 }) })`. Platform metadata (Cloudflare `env`/`ctx`, the Netlify context) is readable in middleware via `getRuntime(ctx.request)` from `@rhythmjs/router/context`.
85
-
86
- Fetch-shaped adapters also accept `{ websocket }` — anything with a `handleUpgrade(request, ...extra)` method, such as a [crossws](https://crossws.h3.dev) instance or an `@rhythmjs/ws` adapter. Requests carrying an `upgrade: websocket` header divert to it (with the runtime's extra arguments — Bun's `server`, Deno's `info`, Cloudflare's `env`/`ctx` — forwarded) before the app runs; every other request is untouched.
87
-
88
- **Node** — `getRequestListener` returns a listener for your own `node:http` server:
89
-
90
- ```ts
91
- import { createServer } from "node:http";
92
- import { getRequestListener } from "@rhythmjs/router/adapters/node";
93
-
94
- createServer(getRequestListener(app, { maxRequestBodySize: 1024 * 1024 })).listen(3000);
95
- ```
96
-
97
- (Or skip the adapter entirely and use `serve(app, { port: 3000 })`.)
98
-
99
- **Bun**
100
-
101
- ```ts
102
- import { handle } from "@rhythmjs/router/adapters/bun";
103
-
104
- Bun.serve({ port: 3000, fetch: handle(app) });
105
- ```
106
-
107
- **Deno**
108
-
109
- ```ts
110
- import { handle } from "@rhythmjs/router/adapters/deno";
111
-
112
- Deno.serve({ port: 3000 }, handle(app));
113
- ```
114
-
115
- **Vercel** — in a catch-all route file:
116
-
117
- ```ts
118
- import { handle } from "@rhythmjs/router/adapters/vercel";
119
-
120
- const handler = handle(app);
121
- export const GET = handler;
122
- export const POST = handler; // …and the other methods you serve
123
- ```
124
-
125
- **Cloudflare Workers** — `env` and `ctx` are exposed to middleware as `ctx.request.runtime.cloudflare`:
52
+ Everything wired, explicitly:
126
53
 
127
54
  ```ts
128
- import { handle } from "@rhythmjs/router/adapters/cloudflare";
129
-
130
- export default { fetch: handle(app) };
55
+ import { toFetchHandler, errorToResponse } from "@rhythmjs/router/fetch";
56
+
57
+ const handler = toFetchHandler(app);
58
+
59
+ const server = Bun.serve({
60
+ port: 3000,
61
+ async fetch(request, srv) {
62
+ // Optional: expose the client address as request.ip, the field
63
+ // @rhythmjs/security's rate limit and @rhythmjs/http's proxy key off.
64
+ Object.defineProperty(request, "ip", {
65
+ configurable: true,
66
+ get: () => srv.requestIP(request)?.address,
67
+ });
68
+ try {
69
+ return await handler(request);
70
+ } catch (error) {
71
+ return errorToResponse(error);
72
+ }
73
+ },
74
+ });
131
75
  ```
132
76
 
133
- **AWS Lambda** — API Gateway v1/v2 events, translated by srvx:
77
+ Since `Bun.serve` is yours, all of Bun's server options (`port`, `hostname`, `unix`, `tls`, `idleTimeout`, `maxRequestBodySize`, `reusePort`, `development`, …) and the `Server` itself (`server.url`, `server.publish`, `server.stop()`) are used directly; nothing is proxied or renamed.
134
78
 
135
- ```ts
136
- import { handle } from "@rhythmjs/router/adapters/aws-lambda";
137
-
138
- export const lambda = handle(app);
139
- ```
79
+ ### Static files
140
80
 
141
- **Netlify Edge Functions** — the Netlify context is exposed as `ctx.request.runtime.netlify`:
81
+ Use Bun's built-in `routes`; there is nothing to import:
142
82
 
143
83
  ```ts
144
- import { handle } from "@rhythmjs/router/adapters/netlify";
145
-
146
- export default handle(app);
147
- export const config = { path: "/*" };
84
+ const server = Bun.serve({
85
+ routes: {
86
+ "/": new Response(Bun.file("public/index.html")), // one known file
87
+ "/static/*": { dir: "./public" }, // a whole folder
88
+ },
89
+ fetch: toFetchHandler(app), // everything else is the app
90
+ });
148
91
  ```
149
92
 
150
- **Service workers**
93
+ Directory routes (`{ dir }`, path must end in `/*`) come with content types, `Last-Modified` + weak `ETag` with `304` revalidation, `Range` requests, `index.html` for trailing-slash requests (and a `301` to add the slash), and `404` for missing or non-canonical (traversal) paths.
151
94
 
152
- ```ts
153
- import { handle } from "@rhythmjs/router/adapters/service-worker";
154
-
155
- addEventListener("fetch", handle(app));
156
- ```
95
+ > **Warning: never mount a directory at `"/*"`.** A directory route answers its own `404`s: with `"/*": { dir }`, every URL that isn't a file dies there and your app's `fetch` never runs. Keep folders on dedicated prefixes (`/static/*`, `/assets/*`) and let `fetch` stay the app's. For root-level files (favicon, robots.txt), map each one explicitly: `"/favicon.svg": new Response(Bun.file("public/favicon.svg"))`.
96
+ ### WebSockets
157
97
 
158
- Any other fetch-based runtime works with the raw primitive: `toFetchHandler(app)` from `@rhythmjs/router/fetch`.
98
+ [`@rhythmjs/ws`](https://github.com/rhythmjs/ws) plugs into the same hand-wired `fetch`: its `upgrade()` returns `null` synchronously for non-websocket requests, so it composes as `ws.upgrade(request, srv) ?? handler(request)`, with `websocket: ws.websocket` on the same `Bun.serve` call.
package/dist/context.d.ts CHANGED
@@ -1,2 +1,18 @@
1
- import { a as createHttpContext, i as RhythmRuntime, n as RhythmResponse, o as getRuntime, r as RhythmResponseBody, s as toResponse, t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
- export { RhythmHttpContext, RhythmResponse, RhythmResponseBody, RhythmRuntime, createHttpContext, getRuntime, toResponse };
1
+ export type RhythmResponseBody = string | ArrayBuffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream<Uint8Array> | null;
2
+ export declare class RhythmResponse {
3
+ status: number;
4
+ statusText: string | undefined;
5
+ headers: Headers;
6
+ body: RhythmResponseBody;
7
+ }
8
+ export interface RhythmHttpContext {
9
+ readonly request: Request;
10
+ readonly response: RhythmResponse;
11
+ json(data: unknown, status?: number): void;
12
+ text(body: string, status?: number): void;
13
+ html(body: string, status?: number): void;
14
+ error(status: number, message?: string): void;
15
+ redirect(url: string, status?: number): void;
16
+ }
17
+ export declare function createHttpContext(request: Request): RhythmHttpContext;
18
+ export declare function toResponse(response: RhythmResponse): Response;
package/dist/context.js CHANGED
@@ -1,69 +1,11 @@
1
- //#region src/context.ts
2
- function getRuntime(request) {
3
- return request.runtime;
4
- }
5
- var RhythmResponse = class {
6
- status = 200;
7
- statusText = void 0;
8
- headers = new Headers();
9
- body = null;
1
+ // @bun
2
+ import {
3
+ RhythmResponse2,
4
+ createHttpContext2,
5
+ toResponse2
6
+ } from "./rhythm-router-yj4ffyyg.js";
7
+ export {
8
+ RhythmResponse2 as RhythmResponse,
9
+ createHttpContext2 as createHttpContext,
10
+ toResponse2 as toResponse
10
11
  };
11
- const STATUS_TEXT = {
12
- 400: "Bad Request",
13
- 401: "Unauthorized",
14
- 403: "Forbidden",
15
- 404: "Not Found",
16
- 405: "Method Not Allowed",
17
- 409: "Conflict",
18
- 410: "Gone",
19
- 413: "Payload Too Large",
20
- 415: "Unsupported Media Type",
21
- 422: "Unprocessable Entity",
22
- 429: "Too Many Requests",
23
- 500: "Internal Server Error",
24
- 501: "Not Implemented",
25
- 502: "Bad Gateway",
26
- 503: "Service Unavailable",
27
- 504: "Gateway Timeout"
28
- };
29
- function createHttpContext(request) {
30
- const response = new RhythmResponse();
31
- return {
32
- request,
33
- response,
34
- json(data, status) {
35
- if (status !== void 0) response.status = status;
36
- response.headers.set("content-type", "application/json; charset=utf-8");
37
- response.body = JSON.stringify(data);
38
- },
39
- text(body, status) {
40
- if (status !== void 0) response.status = status;
41
- response.headers.set("content-type", "text/plain; charset=utf-8");
42
- response.body = body;
43
- },
44
- html(body, status) {
45
- if (status !== void 0) response.status = status;
46
- response.headers.set("content-type", "text/html; charset=utf-8");
47
- response.body = body;
48
- },
49
- error(status, message) {
50
- response.status = status;
51
- response.headers.set("content-type", "text/plain; charset=utf-8");
52
- response.body = message ?? STATUS_TEXT[status] ?? `Error ${status}`;
53
- },
54
- redirect(url, status = 302) {
55
- response.status = status;
56
- response.headers.set("location", url);
57
- response.body = null;
58
- }
59
- };
60
- }
61
- function toResponse(response) {
62
- return new Response(response.body, {
63
- status: response.status,
64
- statusText: response.statusText,
65
- headers: response.headers
66
- });
67
- }
68
- //#endregion
69
- export { RhythmResponse, createHttpContext, getRuntime, toResponse };
package/dist/fetch.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
- import { Rhythm } from "@rhythmjs/rhythm";
3
- //#region src/fetch.d.ts
1
+ import type { Rhythm } from "@rhythmjs/rhythm";
2
+ import { type RhythmHttpContext } from "./context";
4
3
  export declare function toFetchHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
- //#endregion
4
+ export declare function errorToResponse(error: unknown): Response;
package/dist/fetch.js CHANGED
@@ -1,16 +1,32 @@
1
- import { createHttpContext } from "./context.js";
2
- import { FastResponse } from "srvx";
3
- //#region src/fetch.ts
1
+ // @bun
2
+ import {
3
+ createHttpContext2
4
+ } from "./rhythm-router-yj4ffyyg.js";
5
+
6
+ // src/fetch.ts
4
7
  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
- };
8
+ const run = app.callback();
9
+ return async (request) => {
10
+ const ctx = await run(createHttpContext2(request));
11
+ const response = ctx.response;
12
+ return new Response(response.body, {
13
+ status: response.status,
14
+ statusText: response.statusText,
15
+ headers: response.headers
16
+ });
17
+ };
14
18
  }
15
- //#endregion
16
- export { toFetchHandler };
19
+ function errorToResponse(error) {
20
+ const status = error.status ?? error.statusCode ?? 500;
21
+ const message = status >= 500 ? "Internal Server Error" : error instanceof Error ? error.message : String(error);
22
+ if (status >= 500)
23
+ console.error(error);
24
+ return new Response(message, {
25
+ status,
26
+ headers: { "content-type": "text/plain; charset=utf-8" }
27
+ });
28
+ }
29
+ export {
30
+ errorToResponse,
31
+ toFetchHandler
32
+ };
@@ -0,0 +1,20 @@
1
+ // @bun
2
+ import {
3
+ createHttpContext2
4
+ } from "./rhythm-router-yj4ffyyg.js";
5
+
6
+ // src/fetch.ts
7
+ function toFetchHandler2(app) {
8
+ const run = app.callback();
9
+ return async (request) => {
10
+ const ctx = await run(createHttpContext2(request));
11
+ const response = ctx.response;
12
+ return new Response(response.body, {
13
+ status: response.status,
14
+ statusText: response.statusText,
15
+ headers: response.headers
16
+ });
17
+ };
18
+ }
19
+
20
+ export { toFetchHandler2 };
@@ -0,0 +1,53 @@
1
+ // @bun
2
+ // src/static.ts
3
+ import { join, normalize, sep } from "path";
4
+ function normalizePrefix(prefix) {
5
+ const withLeading = prefix.startsWith("/") ? prefix : `/${prefix}`;
6
+ return withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
7
+ }
8
+ function staticMiddleware2(options) {
9
+ const prefix = normalizePrefix(options.prefix ?? "/");
10
+ const index = options.index ?? "index.html";
11
+ const useEtag = options.etag ?? true;
12
+ const cacheControl = options.maxAge === undefined ? undefined : `max-age=${options.maxAge}${options.immutable ? ", immutable" : ""}`;
13
+ return async (request, next) => {
14
+ if (request.method !== "GET" && request.method !== "HEAD")
15
+ return next();
16
+ let pathname;
17
+ try {
18
+ pathname = decodeURIComponent(new URL(request.url).pathname);
19
+ } catch {
20
+ return next();
21
+ }
22
+ if (prefix !== "" && pathname !== prefix && !pathname.startsWith(`${prefix}/`))
23
+ return next();
24
+ const relative = normalize(pathname.slice(prefix.length));
25
+ if (relative.includes("\x00") || relative === ".." || relative.startsWith(`..${sep}`))
26
+ return next();
27
+ const base = join(options.dir, relative);
28
+ const candidates = pathname.endsWith("/") ? [join(base, index)] : [base, join(base, index)];
29
+ let file;
30
+ for (const candidate of candidates) {
31
+ const found = Bun.file(candidate);
32
+ if (await found.exists()) {
33
+ file = found;
34
+ break;
35
+ }
36
+ }
37
+ if (file === undefined)
38
+ return next();
39
+ const headers = new Headers;
40
+ if (cacheControl !== undefined)
41
+ headers.set("cache-control", cacheControl);
42
+ if (useEtag) {
43
+ const etag = `"${file.size.toString(16)}-${Math.floor(file.lastModified).toString(16)}"`;
44
+ headers.set("etag", etag);
45
+ if (request.headers.get("if-none-match") === etag) {
46
+ return new Response(null, { status: 304, headers });
47
+ }
48
+ }
49
+ return new Response(file, { headers });
50
+ };
51
+ }
52
+
53
+ export { staticMiddleware2 };
@@ -0,0 +1,70 @@
1
+ // @bun
2
+ // src/context.ts
3
+ class RhythmResponse2 {
4
+ status = 200;
5
+ statusText = undefined;
6
+ headers = new Headers;
7
+ body = null;
8
+ }
9
+ var STATUS_TEXT = {
10
+ 400: "Bad Request",
11
+ 401: "Unauthorized",
12
+ 403: "Forbidden",
13
+ 404: "Not Found",
14
+ 405: "Method Not Allowed",
15
+ 409: "Conflict",
16
+ 410: "Gone",
17
+ 413: "Payload Too Large",
18
+ 415: "Unsupported Media Type",
19
+ 422: "Unprocessable Entity",
20
+ 429: "Too Many Requests",
21
+ 500: "Internal Server Error",
22
+ 501: "Not Implemented",
23
+ 502: "Bad Gateway",
24
+ 503: "Service Unavailable",
25
+ 504: "Gateway Timeout"
26
+ };
27
+ function createHttpContext2(request) {
28
+ const response = new RhythmResponse2;
29
+ return {
30
+ request,
31
+ response,
32
+ json(data, status) {
33
+ if (status !== undefined)
34
+ response.status = status;
35
+ response.headers.set("content-type", "application/json; charset=utf-8");
36
+ response.body = JSON.stringify(data);
37
+ },
38
+ text(body, status) {
39
+ if (status !== undefined)
40
+ response.status = status;
41
+ response.headers.set("content-type", "text/plain; charset=utf-8");
42
+ response.body = body;
43
+ },
44
+ html(body, status) {
45
+ if (status !== undefined)
46
+ response.status = status;
47
+ response.headers.set("content-type", "text/html; charset=utf-8");
48
+ response.body = body;
49
+ },
50
+ error(status, message) {
51
+ response.status = status;
52
+ response.headers.set("content-type", "text/plain; charset=utf-8");
53
+ response.body = message ?? STATUS_TEXT[status] ?? `Error ${status}`;
54
+ },
55
+ redirect(url, status = 302) {
56
+ response.status = status;
57
+ response.headers.set("location", url);
58
+ response.body = null;
59
+ }
60
+ };
61
+ }
62
+ function toResponse2(response) {
63
+ return new Response(response.body, {
64
+ status: response.status,
65
+ statusText: response.statusText,
66
+ headers: response.headers
67
+ });
68
+ }
69
+
70
+ export { RhythmResponse2, createHttpContext2, toResponse2 };
@@ -1,40 +1,39 @@
1
- import { t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
- import { DeriveMiddleware, Middleware } from "@rhythmjs/rhythm/types";
3
- //#region src/rhythm-router.d.ts
1
+ import type { DeriveMiddleware, Middleware } from "@rhythmjs/rhythm/types";
2
+ import type { RhythmHttpContext } from "./context";
4
3
  export interface RhythmRouterContext {
5
- readonly params: Readonly<Record<string, string>>;
4
+ readonly params: Readonly<Record<string, string>>;
6
5
  }
7
6
  export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
8
7
  export interface RhythmRouterOptions {
9
- prefix?: string;
8
+ prefix?: string;
10
9
  }
11
10
  export type RouterEntry = {
12
- readonly kind: "middleware";
13
- readonly fn: Middleware<any>;
11
+ readonly kind: "middleware";
12
+ readonly fn: Middleware<any>;
14
13
  } | {
15
- readonly kind: "route";
16
- readonly method: HttpMethod;
17
- readonly path: string;
18
- readonly handlers: readonly Middleware<any>[];
14
+ readonly kind: "route";
15
+ readonly method: HttpMethod;
16
+ readonly path: string;
17
+ readonly handlers: readonly Middleware<any>[];
19
18
  };
20
19
  type RouteHandler<TContext> = Middleware<TContext & RhythmRouterContext>;
21
20
  export declare function joinPath(prefix: string, path: string): string;
22
21
  export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext> {
23
- #private;
24
- constructor(options?: RhythmRouterOptions);
25
- get entries(): readonly RouterEntry[];
26
- use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): RhythmRouter<TContext & TExtra>;
27
- use(fn: Middleware<TContext>): this;
28
- get<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
29
- get(path: string, ...handlers: RouteHandler<TContext>[]): this;
30
- post<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
31
- post(path: string, ...handlers: RouteHandler<TContext>[]): this;
32
- put<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
33
- put(path: string, ...handlers: RouteHandler<TContext>[]): this;
34
- patch<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
35
- patch(path: string, ...handlers: RouteHandler<TContext>[]): this;
36
- delete<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
37
- delete(path: string, ...handlers: RouteHandler<TContext>[]): this;
38
- middleware(): Middleware<TContext>;
22
+ #private;
23
+ constructor(options?: RhythmRouterOptions);
24
+ get entries(): readonly RouterEntry[];
25
+ use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): RhythmRouter<TContext & TExtra>;
26
+ use(fn: Middleware<TContext>): this;
27
+ get<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
28
+ get(path: string, ...handlers: RouteHandler<TContext>[]): this;
29
+ post<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
30
+ post(path: string, ...handlers: RouteHandler<TContext>[]): this;
31
+ put<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
32
+ put(path: string, ...handlers: RouteHandler<TContext>[]): this;
33
+ patch<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
34
+ patch(path: string, ...handlers: RouteHandler<TContext>[]): this;
35
+ delete<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
36
+ delete(path: string, ...handlers: RouteHandler<TContext>[]): this;
37
+ middleware(): Middleware<TContext>;
39
38
  }
40
- //#endregion
39
+ export {};