@rhythmjs/router 0.0.16 → 0.0.17

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,48 +1,56 @@
1
1
  # @rhythmjs/router
2
2
 
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.
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).
4
4
 
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.
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 URI-decoded; a malformed escape does not match.
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`, `derive`; `next()` takes no arguments, extend the context with `derive()`), but has no startup `context` 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 a `Pipeline`, like `Rhythm`: it has request-time `use()` middleware and routes, but no startup phase. Startup work (`register`, `decorate`, `include`) belongs to the `Rhythm` app that mounts it with `mount(router)`.
8
8
 
9
9
  ## Example
10
10
 
11
11
  ```ts
12
- import { Rhythm } from "@rhythmjs/rhythm";
12
+ import { Rhythm, decorate, derive, mount } from "@rhythmjs/rhythm";
13
13
  import { RhythmRouter } from "@rhythmjs/router";
14
14
  import { toFetchHandler } from "@rhythmjs/router/fetch";
15
- import type { RhythmHttpContext } from "@rhythmjs/router/context";
16
15
 
17
- const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) => {
18
- ctx.json({ id: ctx.params.id });
19
- });
16
+ const users = new RhythmRouter({ name: "users" })
17
+ .get("/users/:id", (ctx) => ctx.json({ id: ctx.params.id }))
18
+ .post(
19
+ "/users",
20
+ derive(async (ctx) => ({ body: await ctx.request.json() })),
21
+ (ctx) => ctx.json(ctx.body, 201),
22
+ );
23
+
24
+ const app = new Rhythm()
25
+ .register(decorate(() => ({ startedAt: Date.now() })))
26
+ .use(derive(() => ({ requestId: crypto.randomUUID() })))
27
+ .use(mount(users));
20
28
 
21
- const app = new Rhythm<RhythmHttpContext>().use(usersRouter.middleware());
22
29
  Bun.serve({ port: 3000, fetch: toFetchHandler(app) });
23
30
  ```
24
31
 
25
- A fuller runnable version, including nested prefixes and a fallback route, is at [`examples/router`](../../examples/router).
26
-
27
32
  ## Concepts
28
33
 
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)` (status must be 301, 302, 303, 307 or 308; the URL is used as given, so never pass a user-supplied `next`/`returnTo` value without checking it against an allowlist or requiring a same-origin path) 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, and runs only when one of them matches the request's method and path (so a guard in a `/projects` router never answers `/docs`); a mounted router (`.use(child.middleware())`) always runs; 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
- - **Conditional middleware**: `.use(fn, condition)` also needs `condition(ctx)` to return true (sync or async); the second callback only narrows, so a middleware registered after every route still never runs. `@rhythmjs/router/match` provides ready-made predicates: `matches("/api/**")` (patterns, `exclude`, `methods`) and `routed(module)` (true only for requests the module's own routers serve), e.g. `app.use(audit(), routed(usersModule))`.
35
- - **A router is a controller, not a module**: it has no startup `context` 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())`.
34
+ - **Mounting.** A router mounts into a `Rhythm` app (or into another router) with `mount(router)`. After the router handles a request, the parent continues with `next()`, so later middleware still runs; check `ctx.params` or the response state if it should only act on unmatched requests. Give each router its full paths (there is no prefix option); to mount conditionally pass a second argument: `mount(router, (ctx) => ...)`.
35
+ - **Request-time only.** `router.use(middleware)` wraps the router's routes and runs only when one of them matches the method and path, so a guard in one router never answers another's requests. Startup values live on the parent app's context and are on every request.
36
+ - **`derive` as a route handler.** `derive(fn)` can be the first handler of a route (or `use()`), and widens the context type for the handlers after it.
37
+ - **`ctx.response`** is a plain mutable object (`status`, `statusText`, `headers`, `body`): set it directly rather than constructing a `Response` yourself. `toFetchHandler` converts it to a real `Response` at the end, and answers `404` if nothing touched it.
38
+ - **Response helpers**: `ctx.json(data, status?)`, `ctx.text(body, status?)`, `ctx.html(body, status?)`, `ctx.error(status, message?)`, and `ctx.redirect(url, status = 302)` (status must be 301, 302, 303, 307 or 308; the URL is used as given, so never pass a user-supplied `next`/`returnTo` value without checking it against an allowlist or requiring a same-origin path) set the content type, body, and status on `ctx.response` in one call. `error()` defaults the message from the status code (`ctx.error(404)` is `"Not Found"`). They're sugar over `ctx.response`, so mixing both styles is fine, and later writes win.
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
+ - **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
+ - **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
+ - **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.
36
43
 
37
44
  ## API
38
45
 
39
- - `new RhythmRouter(options?)`: `options.prefix`.
40
- - `.get/.post/.put/.patch/.delete(path, ...handlers)`: register a route; `path` may contain `:param` segments.
41
- - `.use(fn)`: plain middleware; it takes only functions, so a nested router mounts as `.use(child.middleware())`.
42
- - `.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.
43
- - `.entries`: a read-only snapshot of registered middlewares and routes, in order. `.middleware()` is tagged with the router as its source, so a parent module lists it in `sources`.
44
- - `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`).
45
- - `toFetchHandler(app)`: bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
46
+ - `new RhythmRouter<I, D>(options?)`: `I` is context the router needs from the parent (checked by `mount()`), `D` is what its own `derive()` calls add; both are usually inferred. `options.name` labels failures (`type` defaults to `"router"`).
47
+ - `.get/.post/.put/.patch/.delete(path, ...handlers)`: register a route; handlers are `(ctx, next)` middleware, and the first may be `derive()`.
48
+ - `.use(middleware)`: router-level middleware; `derive()` extends the context type.
49
+ - `.callback()`: the router as a `(ctx) => Promise<ctx>` function, used by `mount()` and `toFetchHandler()`.
50
+ - `.sources` / `.parent` / `.options`: inherited from `Pipeline`.
51
+ - `createHttpContext(request)` / `toResponse(response)` (`@rhythmjs/router/context`): the request context and its conversion to a `Response`.
52
+ - `toFetchHandler(app)` (`@rhythmjs/router/fetch`): bridges a `Rhythm` app (or router) to a Web-standard `(Request) => Promise<Response>` handler. Errors reject.
53
+ - `errorToResponse(error)` (`@rhythmjs/router/fetch`): maps a thrown error to a `Response`.
46
54
 
47
55
  ## Serving: your Bun.serve, no wrapper
48
56
 
package/dist/fetch.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Rhythm } from "@rhythmjs/rhythm";
1
+ import type { Mountable } from "@rhythmjs/rhythm";
2
2
  import { type RhythmHttpContext } from "./context";
3
- export declare function toFetchHandler<TContext extends RhythmHttpContext>(app: Rhythm<RhythmHttpContext, any, TContext>): (request: Request) => Promise<Response>;
3
+ export declare function toFetchHandler<I extends object = any>(app: Mountable<I> & (RhythmHttpContext extends I ? unknown : never)): (request: Request) => Promise<Response>;
4
4
  export declare function errorToResponse(error: unknown): Response;
package/dist/fetch.js CHANGED
@@ -1,29 +1,54 @@
1
1
  // @bun
2
2
  import {
3
+ RhythmResponse2,
3
4
  STATUS_TEXT2,
4
- createHttpContext2
5
+ createHttpContext2,
6
+ toResponse2
5
7
  } from "./rhythm-router-48zw4cyy.js";
6
8
 
7
9
  // src/fetch.ts
10
+ function isUntouched(response) {
11
+ return response.status === 200 && response.statusText === undefined && response.body === null && [...response.headers].length === 0;
12
+ }
8
13
  function toFetchHandler(app) {
9
14
  const run = app.callback();
10
15
  return async (request) => {
11
- const ctx = await run(createHttpContext2(request));
12
- const response = ctx.response;
13
- return new Response(response.body, {
14
- status: response.status,
15
- statusText: response.statusText,
16
- headers: response.headers
17
- });
16
+ const base = createHttpContext2(request);
17
+ const result = await run(base);
18
+ const response = result?.response ?? base.response;
19
+ if (isUntouched(response)) {
20
+ const fallback = new RhythmResponse2;
21
+ fallback.status = 404;
22
+ fallback.headers.set("content-type", "text/plain; charset=utf-8");
23
+ fallback.body = STATUS_TEXT2[404];
24
+ return toResponse2(fallback);
25
+ }
26
+ return toResponse2(response);
18
27
  };
19
28
  }
29
+ function declaredStatus(error) {
30
+ const { status, statusCode } = error ?? {};
31
+ const declared = status ?? statusCode;
32
+ return Number.isInteger(declared) && declared >= 400 && declared <= 599 ? declared : undefined;
33
+ }
34
+ function rootCause(error) {
35
+ for (let current = error, depth = 0;depth < 16; depth++) {
36
+ if (declaredStatus(current) !== undefined)
37
+ return current;
38
+ const cause = current?.cause;
39
+ if (cause === undefined)
40
+ return current;
41
+ current = cause;
42
+ }
43
+ return error;
44
+ }
20
45
  function errorToResponse(error) {
21
- const declared = error.status ?? error.statusCode;
22
- const status = Number.isInteger(declared) && declared >= 400 && declared <= 599 ? declared : 500;
46
+ const source = rootCause(error);
47
+ const status = declaredStatus(source) ?? 500;
23
48
  if (status >= 500)
24
49
  console.error(error);
25
- const expose = status < 500 && error.expose !== false;
26
- const message = expose ? error instanceof Error ? error.message : String(error) : status >= 500 ? "Internal Server Error" : STATUS_TEXT2[status] ?? `Error ${status}`;
50
+ const expose = status < 500 && source.expose !== false;
51
+ const message = expose ? source instanceof Error ? source.message : String(source) : status >= 500 ? "Internal Server Error" : STATUS_TEXT2[status] ?? `Error ${status}`;
27
52
  return new Response(message, {
28
53
  status,
29
54
  headers: { "content-type": "text/plain; charset=utf-8" }
@@ -1,41 +1,30 @@
1
- import type { Condition, DeriveMiddleware, Middleware } from "@rhythmjs/rhythm/types";
2
- import { type RouterContext } from "rou3";
1
+ import { type InferRouteParams } from "rou3";
2
+ import { Pipeline, type ExtensionMiddleware, type Middleware, type Next, type PipelineOptions } from "@rhythmjs/rhythm";
3
3
  import type { RhythmHttpContext } from "./context";
4
- export interface RhythmRouterContext {
5
- readonly params: Readonly<Record<string, string>>;
6
- }
7
- export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
8
- export interface RhythmRouterOptions {
9
- prefix?: string;
10
- }
11
- export type RouterEntry = {
12
- readonly kind: "middleware";
13
- readonly fn: Middleware<any>;
14
- } | {
15
- readonly kind: "route";
16
- readonly method: HttpMethod;
17
- readonly path: string;
18
- readonly handlers: readonly Middleware<any>[];
4
+ export type RouterContext<T extends object> = T & RhythmHttpContext;
5
+ export type UseContext<T extends object> = RouterContext<T> & {
6
+ readonly params: Record<string, string>;
7
+ };
8
+ export type RouteContext<T extends object, P extends string> = RouterContext<T> & {
9
+ params: InferRouteParams<P>;
19
10
  };
20
- type RouteHandler<TContext> = Middleware<TContext & RhythmRouterContext>;
21
- export declare function joinPath(prefix: string, path: string): string;
22
- export declare function mountedRoutes(router: RhythmRouter<any, any>, tree: RouterContext<any>, seen?: Set<object>): void;
23
- export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext, TInput extends RhythmHttpContext = TContext> {
11
+ export type RouteHandler<T extends object, P extends string> = (ctx: RouteContext<T, P>, next: Next) => unknown | Promise<unknown>;
12
+ export type RouteHandlers<T extends object, P extends string> = [RouteHandler<T, P>, ...RouteHandler<T, P>[]];
13
+ export declare class RhythmRouter<I extends object = {}, D extends object = {}> extends Pipeline<UseContext<I & D>> {
24
14
  #private;
25
- constructor(options?: RhythmRouterOptions);
26
- get entries(): readonly RouterEntry[];
27
- use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): RhythmRouter<TContext & TExtra, TInput>;
28
- use(fn: Middleware<TContext>, condition?: Condition<TContext>): this;
29
- get<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
30
- get(path: string, ...handlers: RouteHandler<TContext>[]): this;
31
- post<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
32
- post(path: string, ...handlers: RouteHandler<TContext>[]): this;
33
- put<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
34
- put(path: string, ...handlers: RouteHandler<TContext>[]): this;
35
- patch<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
36
- patch(path: string, ...handlers: RouteHandler<TContext>[]): this;
37
- delete<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
38
- delete(path: string, ...handlers: RouteHandler<TContext>[]): this;
39
- middleware(): Middleware<TInput>;
15
+ readonly "~input"?: I;
16
+ constructor(options?: PipelineOptions);
17
+ use<U extends object>(middleware: ExtensionMiddleware<UseContext<I & D>, U>): RhythmRouter<I, D & U>;
18
+ use(middleware: Middleware<UseContext<I & D>>): this;
19
+ get<P extends string, U extends object>(path: P, middleware: ExtensionMiddleware<RouteContext<I & D, P>, U>, ...handlers: RouteHandler<I & D & U, P>[]): this;
20
+ get<P extends string>(path: P, ...handlers: RouteHandlers<I & D, P>): this;
21
+ post<P extends string, U extends object>(path: P, middleware: ExtensionMiddleware<RouteContext<I & D, P>, U>, ...handlers: RouteHandler<I & D & U, P>[]): this;
22
+ post<P extends string>(path: P, ...handlers: RouteHandlers<I & D, P>): this;
23
+ put<P extends string, U extends object>(path: P, middleware: ExtensionMiddleware<RouteContext<I & D, P>, U>, ...handlers: RouteHandler<I & D & U, P>[]): this;
24
+ put<P extends string>(path: P, ...handlers: RouteHandlers<I & D, P>): this;
25
+ patch<P extends string, U extends object>(path: P, middleware: ExtensionMiddleware<RouteContext<I & D, P>, U>, ...handlers: RouteHandler<I & D & U, P>[]): this;
26
+ patch<P extends string>(path: P, ...handlers: RouteHandlers<I & D, P>): this;
27
+ delete<P extends string, U extends object>(path: P, middleware: ExtensionMiddleware<RouteContext<I & D, P>, U>, ...handlers: RouteHandler<I & D & U, P>[]): this;
28
+ delete<P extends string>(path: P, ...handlers: RouteHandlers<I & D, P>): this;
29
+ callback(): (input: RouterContext<I>) => Promise<RouterContext<I & D>>;
40
30
  }
41
- export {};
@@ -1,11 +1,72 @@
1
1
  // @bun
2
+ // src/rhythm-router.ts
3
+ import { addRoute, createRouter, findRoute } from "rou3";
2
4
  import {
3
- joinPath2,
4
- mountedRoutes2,
5
- RhythmRouter2
6
- } from "./rhythm-router-wmzh62d5.js";
5
+ Pipeline,
6
+ compose
7
+ } from "@rhythmjs/rhythm";
8
+ function decodeParams(params) {
9
+ const decoded = {};
10
+ try {
11
+ for (const [key, value] of Object.entries(params ?? {})) {
12
+ decoded[key] = decodeURIComponent(value);
13
+ }
14
+ } catch {
15
+ return;
16
+ }
17
+ return decoded;
18
+ }
19
+
20
+ class RhythmRouter extends Pipeline {
21
+ #routes = createRouter();
22
+ constructor(options) {
23
+ super({ type: "router", ...options });
24
+ }
25
+ use(middleware) {
26
+ return super.use(middleware);
27
+ }
28
+ get(path, ...handlers) {
29
+ return this.#route("GET", path, handlers);
30
+ }
31
+ post(path, ...handlers) {
32
+ return this.#route("POST", path, handlers);
33
+ }
34
+ put(path, ...handlers) {
35
+ return this.#route("PUT", path, handlers);
36
+ }
37
+ patch(path, ...handlers) {
38
+ return this.#route("PATCH", path, handlers);
39
+ }
40
+ delete(path, ...handlers) {
41
+ return this.#route("DELETE", path, handlers);
42
+ }
43
+ callback() {
44
+ const run = this.chain();
45
+ return async (input) => {
46
+ const ctx = input;
47
+ const match = this.#match(ctx);
48
+ if (!match)
49
+ return ctx;
50
+ Object.assign(ctx, { params: match.params });
51
+ await run(ctx, () => match.route(ctx));
52
+ return ctx;
53
+ };
54
+ }
55
+ #match(ctx) {
56
+ const found = findRoute(this.#routes, ctx.request.method, new URL(ctx.request.url).pathname);
57
+ if (!found)
58
+ return;
59
+ const params = decodeParams(found.params);
60
+ if (!params)
61
+ return;
62
+ return { route: found.data, params };
63
+ }
64
+ #route(method, path, handlers) {
65
+ const chain = compose(handlers);
66
+ addRoute(this.#routes, method, path, (ctx) => chain(ctx));
67
+ return this;
68
+ }
69
+ }
7
70
  export {
8
- RhythmRouter2 as RhythmRouter,
9
- joinPath2 as joinPath,
10
- mountedRoutes2 as mountedRoutes
71
+ RhythmRouter
11
72
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.16",
4
- "description": "Bun-native HTTP router for the Rhythm middleware kernel, matching with rou3 and serving with Bun.serve.",
3
+ "version": "0.0.17",
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",
7
7
  "repository": {
@@ -27,22 +27,14 @@
27
27
  "types": "./dist/fetch.d.ts",
28
28
  "default": "./dist/fetch.js"
29
29
  },
30
- "./match": {
31
- "types": "./dist/match.d.ts",
32
- "default": "./dist/match.js"
33
- },
34
- "./adapters/context": {
35
- "types": "./dist/context.d.ts",
36
- "default": "./dist/context.js"
37
- },
38
30
  "./package.json": "./package.json"
39
31
  },
40
32
  "publishConfig": {
41
33
  "access": "public"
42
34
  },
43
35
  "dependencies": {
44
- "rou3": "^0.11.0",
45
- "@rhythmjs/rhythm": "0.0.16"
36
+ "@rhythmjs/rhythm": "0.0.17",
37
+ "rou3": "^1.0.0"
46
38
  },
47
39
  "devDependencies": {
48
40
  "@types/bun": "^1.2.0",
@@ -53,7 +45,7 @@
53
45
  "bun": ">=1.2.0"
54
46
  },
55
47
  "scripts": {
56
- "build": "bun build src/rhythm-router.ts src/context.ts src/fetch.ts src/match.ts --outdir dist --root src --format esm --target bun --packages external --splitting && tsc -p tsconfig.build.json",
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",
57
49
  "typecheck": "tsc --noEmit",
58
50
  "test": "bun test"
59
51
  }
package/dist/match.d.ts DELETED
@@ -1,14 +0,0 @@
1
- import type { HttpMethod } from "./rhythm-router";
2
- export type RoutePattern = string | readonly string[];
3
- export interface RouteMatchOptions {
4
- include?: RoutePattern;
5
- exclude?: RoutePattern;
6
- methods?: readonly HttpMethod[];
7
- }
8
- export type RequestPredicate = (ctx: {
9
- readonly request: Request;
10
- }) => boolean;
11
- export declare function matches(spec: RoutePattern | RouteMatchOptions): RequestPredicate;
12
- export declare function routed(module: {
13
- readonly sources: readonly object[];
14
- }): RequestPredicate;
package/dist/match.js DELETED
@@ -1,53 +0,0 @@
1
- // @bun
2
- import {
3
- mountedRoutes2,
4
- RhythmRouter2
5
- } from "./rhythm-router-wmzh62d5.js";
6
-
7
- // src/match.ts
8
- import { addRoute, createRouter, findRoute } from "rou3";
9
- var toList = (pattern) => pattern === undefined ? [] : typeof pattern === "string" ? [pattern] : pattern;
10
- var pathnameOf = (request) => new URL(request.url).pathname;
11
- function treeOf(patterns, methods) {
12
- const tree = createRouter();
13
- for (const pattern of patterns) {
14
- if (methods && methods.length > 0)
15
- for (const method of methods)
16
- addRoute(tree, method, pattern, true);
17
- else
18
- addRoute(tree, undefined, pattern, true);
19
- }
20
- return tree;
21
- }
22
- function matches(spec) {
23
- const options = typeof spec === "string" || Array.isArray(spec) ? { include: spec } : spec;
24
- const include = toList(options.include);
25
- const includeTree = include.length > 0 ? treeOf(include, options.methods) : undefined;
26
- const excludeTree = toList(options.exclude).length > 0 ? treeOf(toList(options.exclude), options.methods) : undefined;
27
- const methodsOnly = !includeTree && options.methods && options.methods.length > 0 ? new Set(options.methods) : undefined;
28
- return ({ request }) => {
29
- const path = pathnameOf(request);
30
- if (excludeTree && findRoute(excludeTree, request.method, path, { params: false }))
31
- return false;
32
- if (includeTree)
33
- return !!findRoute(includeTree, request.method, path, { params: false });
34
- return methodsOnly ? methodsOnly.has(request.method) : true;
35
- };
36
- }
37
- function routed(module) {
38
- let tree;
39
- return ({ request }) => {
40
- if (!tree) {
41
- tree = createRouter();
42
- const seen = new Set;
43
- for (const source of module.sources)
44
- if (source instanceof RhythmRouter2)
45
- mountedRoutes2(source, tree, seen);
46
- }
47
- return !!findRoute(tree, request.method, pathnameOf(request), { params: false });
48
- };
49
- }
50
- export {
51
- matches,
52
- routed
53
- };
@@ -1,133 +0,0 @@
1
- // @bun
2
- // src/rhythm-router.ts
3
- import { compose, gate } from "@rhythmjs/rhythm/compose";
4
- import { sourceOf, withSource } from "@rhythmjs/rhythm/source";
5
- import { addRoute, createRouter, findRoute } from "rou3";
6
- function joinPath2(prefix, path) {
7
- if (!prefix)
8
- return path;
9
- if (path === "/" || path === "")
10
- return prefix;
11
- const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
12
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
13
- return `${trimmedPrefix}${normalizedPath}`;
14
- }
15
- function mountedRoutes2(router, tree, seen = new Set) {
16
- if (seen.has(router))
17
- return;
18
- seen.add(router);
19
- for (const entry of router.entries) {
20
- if (entry.kind === "route")
21
- addRoute(tree, entry.method, entry.path, true);
22
- else {
23
- const child = sourceOf(entry.fn);
24
- if (child instanceof RhythmRouter2)
25
- mountedRoutes2(child, tree, seen);
26
- }
27
- }
28
- }
29
-
30
- class RhythmRouter2 {
31
- #options;
32
- #entries = [];
33
- constructor(options = {}) {
34
- this.#options = options;
35
- }
36
- get #prefixPath() {
37
- return this.#options.prefix ?? "";
38
- }
39
- get entries() {
40
- return [...this.#entries];
41
- }
42
- use(fn, when) {
43
- if (typeof fn !== "function")
44
- throw new TypeError("middleware must be a function!");
45
- if (when !== undefined && typeof when !== "function")
46
- throw new TypeError("when must be a function!");
47
- const source = sourceOf(fn);
48
- const wrapped = when ? gate(fn, when) : fn;
49
- this.#entries.push({ kind: "middleware", fn: when && source ? withSource(wrapped, source) : wrapped });
50
- return this;
51
- }
52
- #route(method, path, handlers) {
53
- this.#entries.push({ kind: "route", method, path: joinPath2(this.#prefixPath, path), handlers });
54
- return this;
55
- }
56
- get(path, ...handlers) {
57
- return this.#route("GET", path, handlers);
58
- }
59
- post(path, ...handlers) {
60
- return this.#route("POST", path, handlers);
61
- }
62
- put(path, ...handlers) {
63
- return this.#route("PUT", path, handlers);
64
- }
65
- patch(path, ...handlers) {
66
- return this.#route("PATCH", path, handlers);
67
- }
68
- delete(path, ...handlers) {
69
- return this.#route("DELETE", path, handlers);
70
- }
71
- #compile() {
72
- const dispatchFor = (tree) => {
73
- return async (ctx, next) => {
74
- const match = findRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
75
- if (!match) {
76
- await next();
77
- return;
78
- }
79
- await match.data({ ...ctx, params: match.params ?? {} }, next);
80
- };
81
- };
82
- const trees = [];
83
- const reaches = (ctx, from) => {
84
- const method = ctx.request.method;
85
- const pathname = new URL(ctx.request.url).pathname;
86
- for (let t = from;t < trees.length; t++)
87
- if (findRoute(trees[t], method, pathname))
88
- return true;
89
- return false;
90
- };
91
- const stack = [];
92
- let i = 0;
93
- while (i < this.#entries.length) {
94
- const entry = this.#entries[i];
95
- if (entry.kind === "middleware") {
96
- const { fn } = entry;
97
- const source = sourceOf(fn);
98
- if (source) {
99
- if (source instanceof RhythmRouter2) {
100
- const tree = createRouter();
101
- mountedRoutes2(source, tree);
102
- trees.push(tree);
103
- }
104
- stack.push(fn);
105
- } else {
106
- const from = trees.length;
107
- stack.push((ctx, next) => reaches(ctx, from) ? fn(ctx, next) : next());
108
- }
109
- i++;
110
- continue;
111
- }
112
- const tree = createRouter();
113
- while (i < this.#entries.length) {
114
- const route = this.#entries[i];
115
- if (route.kind !== "route")
116
- break;
117
- addRoute(tree, route.method, route.path, compose(route.handlers));
118
- i++;
119
- }
120
- trees.push(tree);
121
- stack.push(dispatchFor(tree));
122
- }
123
- return compose(stack);
124
- }
125
- middleware() {
126
- const fn = this.#compile();
127
- return withSource(async (ctx, next) => {
128
- await fn(ctx, next);
129
- }, this);
130
- }
131
- }
132
-
133
- export { joinPath2, mountedRoutes2, RhythmRouter2 };
@@ -1,20 +0,0 @@
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 };
@@ -1,53 +0,0 @@
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 };
@@ -1,133 +0,0 @@
1
- // @bun
2
- // src/rhythm-router.ts
3
- import { compose, gate } from "@rhythmjs/rhythm/compose";
4
- import { sourceOf, withSource } from "@rhythmjs/rhythm/source";
5
- import { addRoute, createRouter, findRoute } from "rou3";
6
- function joinPath2(prefix, path) {
7
- if (!prefix)
8
- return path;
9
- if (path === "/" || path === "")
10
- return prefix;
11
- const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
12
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
13
- return `${trimmedPrefix}${normalizedPath}`;
14
- }
15
- function mountedRoutes2(router, tree, seen = new Set) {
16
- if (seen.has(router))
17
- return;
18
- seen.add(router);
19
- for (const entry of router.entries) {
20
- if (entry.kind === "route")
21
- addRoute(tree, entry.method, entry.path, true);
22
- else {
23
- const child = sourceOf(entry.fn);
24
- if (child instanceof RhythmRouter2)
25
- mountedRoutes2(child, tree, seen);
26
- }
27
- }
28
- }
29
-
30
- class RhythmRouter2 {
31
- #options;
32
- #entries = [];
33
- constructor(options = {}) {
34
- this.#options = options;
35
- }
36
- get #prefixPath() {
37
- return this.#options.prefix ?? "";
38
- }
39
- get entries() {
40
- return [...this.#entries];
41
- }
42
- use(fn, condition) {
43
- if (typeof fn !== "function")
44
- throw new TypeError("middleware must be a function!");
45
- if (condition !== undefined && typeof condition !== "function")
46
- throw new TypeError("condition must be a function!");
47
- const source = sourceOf(fn);
48
- const wrapped = condition ? gate(fn, condition) : fn;
49
- this.#entries.push({ kind: "middleware", fn: condition && source ? withSource(wrapped, source) : wrapped });
50
- return this;
51
- }
52
- #route(method, path, handlers) {
53
- this.#entries.push({ kind: "route", method, path: joinPath2(this.#prefixPath, path), handlers });
54
- return this;
55
- }
56
- get(path, ...handlers) {
57
- return this.#route("GET", path, handlers);
58
- }
59
- post(path, ...handlers) {
60
- return this.#route("POST", path, handlers);
61
- }
62
- put(path, ...handlers) {
63
- return this.#route("PUT", path, handlers);
64
- }
65
- patch(path, ...handlers) {
66
- return this.#route("PATCH", path, handlers);
67
- }
68
- delete(path, ...handlers) {
69
- return this.#route("DELETE", path, handlers);
70
- }
71
- #compile() {
72
- const dispatchFor = (tree) => {
73
- return async (ctx, next) => {
74
- const match = findRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
75
- if (!match) {
76
- await next();
77
- return;
78
- }
79
- await match.data({ ...ctx, params: match.params ?? {} }, next);
80
- };
81
- };
82
- const trees = [];
83
- const reaches = (ctx, from) => {
84
- const method = ctx.request.method;
85
- const pathname = new URL(ctx.request.url).pathname;
86
- for (let t = from;t < trees.length; t++)
87
- if (findRoute(trees[t], method, pathname))
88
- return true;
89
- return false;
90
- };
91
- const stack = [];
92
- let i = 0;
93
- while (i < this.#entries.length) {
94
- const entry = this.#entries[i];
95
- if (entry.kind === "middleware") {
96
- const { fn } = entry;
97
- const source = sourceOf(fn);
98
- if (source) {
99
- if (source instanceof RhythmRouter2) {
100
- const tree = createRouter();
101
- mountedRoutes2(source, tree);
102
- trees.push(tree);
103
- }
104
- stack.push(fn);
105
- } else {
106
- const from = trees.length;
107
- stack.push((ctx, next) => reaches(ctx, from) ? fn(ctx, next) : next());
108
- }
109
- i++;
110
- continue;
111
- }
112
- const tree = createRouter();
113
- while (i < this.#entries.length) {
114
- const route = this.#entries[i];
115
- if (route.kind !== "route")
116
- break;
117
- addRoute(tree, route.method, route.path, compose(route.handlers));
118
- i++;
119
- }
120
- trees.push(tree);
121
- stack.push(dispatchFor(tree));
122
- }
123
- return compose(stack);
124
- }
125
- middleware() {
126
- const fn = this.#compile();
127
- return withSource(async (ctx, next) => {
128
- await fn(ctx, next);
129
- }, this);
130
- }
131
- }
132
-
133
- export { joinPath2, mountedRoutes2, RhythmRouter2 };
@@ -1,132 +0,0 @@
1
- // @bun
2
- // src/rhythm-router.ts
3
- import { compose, gate } from "@rhythmjs/rhythm/compose";
4
- import { sourceOf, withSource } from "@rhythmjs/rhythm/source";
5
- import { addRoute, createRouter, findRoute } from "rou3";
6
- function joinPath2(prefix, path) {
7
- if (!prefix)
8
- return path;
9
- if (path === "/" || path === "")
10
- return prefix;
11
- const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
12
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
13
- return `${trimmedPrefix}${normalizedPath}`;
14
- }
15
- function mountedRoutes2(router, tree, seen = new Set) {
16
- if (seen.has(router))
17
- return;
18
- seen.add(router);
19
- for (const entry of router.entries) {
20
- if (entry.kind === "route")
21
- addRoute(tree, entry.method, entry.path, true);
22
- else {
23
- const child = sourceOf(entry.fn);
24
- if (child instanceof RhythmRouter2)
25
- mountedRoutes2(child, tree, seen);
26
- }
27
- }
28
- }
29
-
30
- class RhythmRouter2 {
31
- #options;
32
- #entries = [];
33
- constructor(options = {}) {
34
- this.#options = options;
35
- }
36
- get #prefixPath() {
37
- return this.#options.prefix ?? "";
38
- }
39
- get entries() {
40
- return [...this.#entries];
41
- }
42
- use(fn, when) {
43
- if (typeof fn !== "function")
44
- throw new TypeError("middleware must be a function!");
45
- if (when !== undefined && typeof when !== "function")
46
- throw new TypeError("when must be a function!");
47
- this.#entries.push(when ? { kind: "middleware", fn, when } : { kind: "middleware", fn });
48
- return this;
49
- }
50
- #route(method, path, handlers) {
51
- this.#entries.push({ kind: "route", method, path: joinPath2(this.#prefixPath, path), handlers });
52
- return this;
53
- }
54
- get(path, ...handlers) {
55
- return this.#route("GET", path, handlers);
56
- }
57
- post(path, ...handlers) {
58
- return this.#route("POST", path, handlers);
59
- }
60
- put(path, ...handlers) {
61
- return this.#route("PUT", path, handlers);
62
- }
63
- patch(path, ...handlers) {
64
- return this.#route("PATCH", path, handlers);
65
- }
66
- delete(path, ...handlers) {
67
- return this.#route("DELETE", path, handlers);
68
- }
69
- #compile() {
70
- const dispatchFor = (tree) => {
71
- return async (ctx, next) => {
72
- const match = findRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
73
- if (!match) {
74
- await next();
75
- return;
76
- }
77
- await match.data({ ...ctx, params: match.params ?? {} }, next);
78
- };
79
- };
80
- const trees = [];
81
- const reaches = (ctx, from) => {
82
- const method = ctx.request.method;
83
- const pathname = new URL(ctx.request.url).pathname;
84
- for (let t = from;t < trees.length; t++)
85
- if (findRoute(trees[t], method, pathname))
86
- return true;
87
- return false;
88
- };
89
- const stack = [];
90
- let i = 0;
91
- while (i < this.#entries.length) {
92
- const entry = this.#entries[i];
93
- if (entry.kind === "middleware") {
94
- const { fn, when } = entry;
95
- const guarded = when ? gate(fn, when) : fn;
96
- const source = sourceOf(fn);
97
- if (source) {
98
- if (source instanceof RhythmRouter2) {
99
- const tree = createRouter();
100
- mountedRoutes2(source, tree);
101
- trees.push(tree);
102
- }
103
- stack.push(guarded);
104
- } else {
105
- const from = trees.length;
106
- stack.push((ctx, next) => reaches(ctx, from) ? guarded(ctx, next) : next());
107
- }
108
- i++;
109
- continue;
110
- }
111
- const tree = createRouter();
112
- while (i < this.#entries.length) {
113
- const route = this.#entries[i];
114
- if (route.kind !== "route")
115
- break;
116
- addRoute(tree, route.method, route.path, compose(route.handlers));
117
- i++;
118
- }
119
- trees.push(tree);
120
- stack.push(dispatchFor(tree));
121
- }
122
- return compose(stack);
123
- }
124
- middleware() {
125
- const fn = this.#compile();
126
- return withSource(async (ctx, next) => {
127
- await fn(ctx, next);
128
- }, this);
129
- }
130
- }
131
-
132
- export { joinPath2, mountedRoutes2, RhythmRouter2 };
@@ -1,70 +0,0 @@
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 };
package/dist/serve.d.ts DELETED
@@ -1,31 +0,0 @@
1
- import type { Rhythm } from "@rhythmjs/rhythm";
2
- import type { RhythmHttpContext } from "./context";
3
- import { type StaticMiddlewareOptions } from "./static";
4
- export type { StaticMiddlewareOptions };
5
- export type Server = Bun.Server<unknown>;
6
- export type ErrorHandler = (error: unknown) => Response | Promise<Response>;
7
- export type ServeMiddleware = (request: Request, next: () => Promise<Response>) => Response | Promise<Response>;
8
- export type UpgradeHandler = (request: Request, server: Server) => Response | undefined | null | Promise<Response | undefined> | null;
9
- export interface RhythmServeOptions {
10
- port?: number;
11
- hostname?: string;
12
- unix?: string;
13
- reusePort?: boolean;
14
- idleTimeout?: number;
15
- development?: boolean;
16
- maxRequestBodySize?: number;
17
- tls?: Bun.TLSOptions;
18
- error?: ErrorHandler;
19
- middleware?: ServeMiddleware[];
20
- static?: StaticMiddlewareOptions | StaticMiddlewareOptions[];
21
- /** Bun websocket behavior; pair with `upgrade` to accept connections. */
22
- websocket?: Bun.WebSocketHandler<never>;
23
- /**
24
- * Called for websocket upgrade requests — `RhythmWs.upgrade` fits directly.
25
- * A Response rejects, undefined means `server.upgrade()` succeeded, and
26
- * null falls through to the HTTP app.
27
- */
28
- upgrade?: UpgradeHandler;
29
- }
30
- export declare const errorToResponse: ErrorHandler;
31
- export declare function serve<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: RhythmServeOptions): Server;
package/dist/serve.js DELETED
@@ -1,67 +0,0 @@
1
- // @bun
2
- import"./rhythm-router-yj4ffyyg.js";
3
- import {
4
- toFetchHandler2
5
- } from "./rhythm-router-88dtq5mx.js";
6
- import {
7
- staticMiddleware2
8
- } from "./rhythm-router-n08cmxbj.js";
9
-
10
- // src/serve.ts
11
- var errorToResponse = (error) => {
12
- const status = error.status ?? error.statusCode ?? 500;
13
- const message = status >= 500 ? "Internal Server Error" : error instanceof Error ? error.message : String(error);
14
- if (status >= 500)
15
- console.error(error);
16
- return new Response(message, {
17
- status,
18
- headers: { "content-type": "text/plain; charset=utf-8" }
19
- });
20
- };
21
- function serve(app, options = {}) {
22
- const {
23
- static: staticOptions,
24
- middleware = [],
25
- error = errorToResponse,
26
- websocket,
27
- upgrade,
28
- ...bunOptions
29
- } = options;
30
- const assets = staticOptions === undefined ? [] : [staticOptions].flat().map(staticMiddleware2);
31
- const chain = [...middleware, ...assets];
32
- const handler = toFetchHandler2(app);
33
- const dispatch = (request) => {
34
- let index = -1;
35
- const next = async (step) => {
36
- if (step <= index)
37
- throw new Error("next() called multiple times");
38
- index = step;
39
- return step < chain.length ? chain[step](request, () => next(step + 1)) : handler(request);
40
- };
41
- return next(0);
42
- };
43
- return Bun.serve({
44
- ...bunOptions,
45
- ...websocket === undefined ? {} : { websocket },
46
- async fetch(request, server) {
47
- try {
48
- Object.defineProperty(request, "ip", {
49
- configurable: true,
50
- get: () => server.requestIP(request)?.address
51
- });
52
- if (upgrade !== undefined && request.headers.get("upgrade")?.toLowerCase() === "websocket") {
53
- const result = await upgrade(request, server);
54
- if (result !== null)
55
- return result;
56
- }
57
- return await dispatch(request);
58
- } catch (thrown) {
59
- return error(thrown);
60
- }
61
- }
62
- });
63
- }
64
- export {
65
- errorToResponse,
66
- serve
67
- };
package/dist/static.d.ts DELETED
@@ -1,13 +0,0 @@
1
- export interface ServeStaticOptions {
2
- dir: string;
3
- /** URL mount point (default the site root). */
4
- prefix?: string;
5
- /** Directory index filename served for trailing-slash requests (default "index.html"). */
6
- index?: string;
7
- /** Emit `Cache-Control: max-age=<n>`, with ", immutable" appended when `immutable` is set. */
8
- maxAge?: number;
9
- immutable?: boolean;
10
- }
11
- /** A Response for a hit, null for a miss — chain with `??` into your app handler. */
12
- export type StaticHandler = (request: Request) => Promise<Response | null>;
13
- export declare function serveStatic(options: ServeStaticOptions): StaticHandler;
package/dist/static.js DELETED
@@ -1,67 +0,0 @@
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 serveStatic(options) {
9
- const prefix = normalizePrefix(options.prefix ?? "/");
10
- const index = options.index ?? "index.html";
11
- const cacheControl = options.maxAge === undefined ? undefined : `max-age=${options.maxAge}${options.immutable ? ", immutable" : ""}`;
12
- return async (request) => {
13
- if (request.method !== "GET" && request.method !== "HEAD")
14
- return null;
15
- let pathname;
16
- try {
17
- pathname = decodeURIComponent(new URL(request.url).pathname);
18
- } catch {
19
- return null;
20
- }
21
- if (prefix !== "" && pathname !== prefix && !pathname.startsWith(`${prefix}/`))
22
- return null;
23
- const relative = normalize(pathname.slice(prefix.length));
24
- if (relative.includes("\x00") || relative === ".." || relative.startsWith(`..${sep}`))
25
- return null;
26
- const path = pathname.endsWith("/") ? join(options.dir, relative, index) : join(options.dir, relative);
27
- const file = Bun.file(path);
28
- if (!await file.exists())
29
- return null;
30
- const size = file.size;
31
- const mtime = Math.floor(file.lastModified);
32
- const etag = `W/"${size.toString(16)}-${mtime.toString(16)}"`;
33
- const headers = new Headers({
34
- etag,
35
- "last-modified": new Date(mtime).toUTCString(),
36
- "accept-ranges": "bytes"
37
- });
38
- if (cacheControl !== undefined)
39
- headers.set("cache-control", cacheControl);
40
- const ifNoneMatch = request.headers.get("if-none-match");
41
- if (ifNoneMatch !== null) {
42
- if (ifNoneMatch.split(/\s*,\s*/).includes(etag))
43
- return new Response(null, { status: 304, headers });
44
- } else {
45
- const ifModifiedSince = request.headers.get("if-modified-since");
46
- if (ifModifiedSince !== null && Date.parse(ifModifiedSince) >= mtime - mtime % 1000) {
47
- return new Response(null, { status: 304, headers });
48
- }
49
- }
50
- const range = request.method === "GET" ? request.headers.get("range") : null;
51
- const match = range === null ? null : /^bytes=(\d*)-(\d*)$/.exec(range);
52
- if (match !== null && (match[1] !== "" || match[2] !== "")) {
53
- const start = match[1] === "" ? Math.max(size - Number(match[2]), 0) : Number(match[1]);
54
- const end = match[1] !== "" && match[2] !== "" ? Math.min(Number(match[2]), size - 1) : size - 1;
55
- if (start >= size || start > end) {
56
- headers.set("content-range", `bytes */${size}`);
57
- return new Response(null, { status: 416, headers });
58
- }
59
- headers.set("content-range", `bytes ${start}-${end}/${size}`);
60
- return new Response(file.slice(start, end + 1), { status: 206, headers });
61
- }
62
- return new Response(file, { headers });
63
- };
64
- }
65
- export {
66
- serveStatic
67
- };