@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 +33 -25
- package/dist/fetch.d.ts +2 -2
- package/dist/fetch.js +37 -12
- package/dist/rhythm-router.d.ts +26 -37
- package/dist/rhythm-router.js +68 -7
- package/package.json +5 -13
- package/dist/match.d.ts +0 -14
- package/dist/match.js +0 -53
- package/dist/rhythm-router-1w0gr0r3.js +0 -133
- package/dist/rhythm-router-88dtq5mx.js +0 -20
- package/dist/rhythm-router-n08cmxbj.js +0 -53
- package/dist/rhythm-router-wmzh62d5.js +0 -133
- package/dist/rhythm-router-wpdgx942.js +0 -132
- package/dist/rhythm-router-yj4ffyyg.js +0 -70
- package/dist/serve.d.ts +0 -31
- package/dist/serve.js +0 -67
- package/dist/static.d.ts +0 -13
- package/dist/static.js +0 -67
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
30
|
-
- **
|
|
31
|
-
- **`
|
|
32
|
-
-
|
|
33
|
-
- **
|
|
34
|
-
-
|
|
35
|
-
- **
|
|
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.
|
|
40
|
-
- `.get/.post/.put/.patch/.delete(path, ...handlers)`: register a route; `
|
|
41
|
-
- `.use(
|
|
42
|
-
- `.
|
|
43
|
-
- `.
|
|
44
|
-
- `
|
|
45
|
-
- `toFetchHandler(app)
|
|
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 {
|
|
1
|
+
import type { Mountable } from "@rhythmjs/rhythm";
|
|
2
2
|
import { type RhythmHttpContext } from "./context";
|
|
3
|
-
export declare function toFetchHandler<
|
|
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
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
22
|
-
const status =
|
|
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 &&
|
|
26
|
-
const message = expose ?
|
|
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" }
|
package/dist/rhythm-router.d.ts
CHANGED
|
@@ -1,41 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { type
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
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<
|
|
21
|
-
export
|
|
22
|
-
export declare
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
use<
|
|
28
|
-
use(
|
|
29
|
-
get<
|
|
30
|
-
get(path:
|
|
31
|
-
post<
|
|
32
|
-
post(path:
|
|
33
|
-
put<
|
|
34
|
-
put(path:
|
|
35
|
-
patch<
|
|
36
|
-
patch(path:
|
|
37
|
-
delete<
|
|
38
|
-
delete(path:
|
|
39
|
-
|
|
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 {};
|
package/dist/rhythm-router.js
CHANGED
|
@@ -1,11 +1,72 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// src/rhythm-router.ts
|
|
3
|
+
import { addRoute, createRouter, findRoute } from "rou3";
|
|
2
4
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Bun-native HTTP router for
|
|
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
|
-
"
|
|
45
|
-
"
|
|
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
|
|
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
|
-
};
|