@rhythmjs/router 0.0.1

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 ADDED
@@ -0,0 +1,48 @@
1
+ # @rhythmjs/router
2
+
3
+ Web-standard HTTP routing on top of `@rhythmjs/rhythm`. `RhythmRouter` matches routes with a compressed radix tree (a static segment always wins over a `:param` segment, regardless of registration order), supports prefixes and nested routers, and mounts flat into a parent via `.use(router.routes())`, so an unmatched request correctly falls through to whatever's registered after it.
4
+
5
+ ## Example
6
+
7
+ ```ts
8
+ import { Rhythm } from "@rhythmjs/rhythm";
9
+ import { RhythmRouter } from "@rhythmjs/router";
10
+ import { toFetchHandler } from "@rhythmjs/router/adapters/bun";
11
+ import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
12
+
13
+ const usersRouter = new RhythmRouter({ prefix: "/users" }).get("/:id", (ctx) => {
14
+ ctx.response.body = JSON.stringify({ id: ctx.params.id });
15
+ });
16
+
17
+ const app = new Rhythm<RhythmHttpContext>().use(usersRouter.routes());
18
+ Bun.serve({ fetch: toFetchHandler(app) });
19
+ ```
20
+
21
+ A fuller runnable version, including nested prefixes and a fallback route, is at [`examples/router`](../../examples/router).
22
+
23
+ ## Concepts
24
+
25
+ - **`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.
26
+ - **`ctx.params`** — captured `:name` path segments, added once a route matches.
27
+ - **Prefixes compose across nesting** — a router mounted into a prefixed parent via `.use(child.routes())` gets the parent's prefix joined onto every one of its routes, at any nesting depth.
28
+ - **`register()` is disabled** on `RhythmRouter` — a router is a "controller"; it composes routes and middleware, not other modules. Mount it into a module with `.use(router.routes())`, not `.register()`.
29
+
30
+ ## API
31
+
32
+ - `new RhythmRouter(options?)` — `options.name`, `options.prefix`.
33
+ - `.get/.post/.put/.patch/.delete(path, ...handlers)` — register a route; `path` may contain `:param` segments.
34
+ - `.use(fn)` — plain middleware, or mount a nested `RhythmRouter` via `.use(child.routes())`.
35
+ - `.routes()` — returns this router as a plain middleware, for mounting into a parent via `.use()`.
36
+ - `toFetchHandler(app)` — bridges a `Rhythm`/`RhythmRouter` app to a Web-standard `(Request) => Promise<Response>` handler.
37
+
38
+ ## Runtime adapters
39
+
40
+ Everything above (`RhythmRouter`, `ctx.response`, etc.) is runtime-agnostic; only turning it into an actual server touches a specific runtime.
41
+
42
+ - **`@rhythmjs/router/adapters/bun`** — `toFetchHandler(app)`, for `Bun.serve({ fetch: toFetchHandler(app) })`.
43
+ - **`@rhythmjs/router/adapters/deno`** — re-exports the same `toFetchHandler`, for `Deno.serve(toFetchHandler(app))`. `Deno.serve()` accepts the identical `(Request) => Promise<Response>` shape `Bun.serve()` does, so no conversion is needed.
44
+ - **`@rhythmjs/router/adapters/node`** — `toNodeHandler(app, options?)`, for `http.createServer(toNodeHandler(app)).listen(port)`. Node's `IncomingMessage`/`ServerResponse` aren't Web-standard, so this one does real conversion:
45
+ - The request body is read eagerly into a buffer, up to `options.bodyLimit` (default 1mb). This guarantees the socket is always fully drained before the handler runs, even if the handler never reads `ctx.request`'s body — otherwise, on a keep-alive connection, unconsumed bytes left on the socket would stall the next request on it. A body over the limit gets a `413` and the connection is closed rather than kept alive.
46
+ - Pass `{ bodyLimit: false }` to opt out of buffering — `ctx.request`'s body becomes a live stream over the raw connection instead, with no size limit, for uploads or proxying where materializing the whole body in memory isn't acceptable. This reintroduces the keep-alive caveat: if the handler doesn't read the body, unconsumed bytes are left on the socket.
47
+ - The resulting `Response` is streamed back to the client via `Readable.fromWeb(...).pipe(res)`.
48
+ - Errors thrown anywhere in the middleware chain are caught, logged via `console.error`, and answered with a 500 (or the connection is destroyed if headers were already sent) — without this, an unhandled rejection would crash the whole Node process.
@@ -0,0 +1,5 @@
1
+ import { t as RhythmHttpContext } from "../context-CjxK7bH9.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/adapters/bun.d.ts
4
+ export declare function toFetchHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>): (request: Request) => Promise<Response>;
5
+ //#endregion
@@ -0,0 +1,14 @@
1
+ import { RhythmResponse, toResponse } from "./context.js";
2
+ //#region src/adapters/bun.ts
3
+ function toFetchHandler(app) {
4
+ const run = app.callback();
5
+ return async (request) => {
6
+ const ctx = await run({
7
+ request,
8
+ response: new RhythmResponse()
9
+ });
10
+ return toResponse(ctx.response);
11
+ };
12
+ }
13
+ //#endregion
14
+ export { toFetchHandler };
@@ -0,0 +1,2 @@
1
+ import { i as toResponse, n as RhythmResponse, r as RhythmResponseBody, t as RhythmHttpContext } from "../context-CjxK7bH9.js";
2
+ export { RhythmHttpContext, RhythmResponse, RhythmResponseBody, toResponse };
@@ -0,0 +1,18 @@
1
+ import { RhythmMutable } from "@rhythmjs/rhythm";
2
+ //#region src/adapters/context.ts
3
+ var RhythmResponse = class {
4
+ [RhythmMutable] = true;
5
+ status = 200;
6
+ statusText = void 0;
7
+ headers = new Headers();
8
+ body = null;
9
+ };
10
+ function toResponse(response) {
11
+ return new Response(response.body, {
12
+ status: response.status,
13
+ statusText: response.statusText,
14
+ headers: response.headers
15
+ });
16
+ }
17
+ //#endregion
18
+ export { RhythmResponse, toResponse };
@@ -0,0 +1,2 @@
1
+ import { toFetchHandler } from "./bun.js";
2
+ export { toFetchHandler };
@@ -0,0 +1,2 @@
1
+ import { toFetchHandler } from "./bun.js";
2
+ export { toFetchHandler };
@@ -0,0 +1,22 @@
1
+ import { t as RhythmHttpContext } from "../context-CjxK7bH9.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ import { IncomingMessage, ServerResponse } from "node:http";
4
+ //#region src/adapters/node.d.ts
5
+ export declare class RhythmBodyTooLargeError extends Error {
6
+ constructor(limit: number);
7
+ }
8
+ export interface RhythmNodeHandlerOptions {
9
+ /**
10
+ * Maximum request body size in bytes, enforced by eagerly buffering the body
11
+ * before the handler runs. Defaults to 1mb, same as raw-body/koa-bodyparser.
12
+ *
13
+ * Pass `false` to opt out of buffering entirely: ctx.request's body becomes a
14
+ * live stream over the raw connection, with no size limit and no automatic
15
+ * drain if the handler doesn't read it (see the adapter's README for the
16
+ * keep-alive caveat that reintroduces). Use this for uploads or proxying,
17
+ * where materializing the whole body in memory isn't acceptable.
18
+ */
19
+ bodyLimit?: number | false;
20
+ }
21
+ export declare function toNodeHandler<TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: RhythmNodeHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
22
+ //#endregion
@@ -0,0 +1,120 @@
1
+ import { RhythmResponse, toResponse } from "./context.js";
2
+ import { Readable } from "node:stream";
3
+ //#region src/adapters/node.ts
4
+ const DEFAULT_BODY_LIMIT = 1048576;
5
+ var RhythmBodyTooLargeError = class extends Error {
6
+ constructor(limit) {
7
+ super(`request body exceeds the ${limit} byte limit`);
8
+ this.name = "RhythmBodyTooLargeError";
9
+ }
10
+ };
11
+ function readRawBody(req, limit) {
12
+ return new Promise((resolve, reject) => {
13
+ const chunks = [];
14
+ let received = 0;
15
+ let tooLarge = false;
16
+ let settled = false;
17
+ req.on("data", (chunk) => {
18
+ received += chunk.length;
19
+ if (received > limit) {
20
+ if (!tooLarge) {
21
+ tooLarge = true;
22
+ settled = true;
23
+ reject(new RhythmBodyTooLargeError(limit));
24
+ }
25
+ return;
26
+ }
27
+ chunks.push(chunk);
28
+ });
29
+ req.on("end", () => {
30
+ if (settled) return;
31
+ settled = true;
32
+ resolve(Buffer.concat(chunks));
33
+ });
34
+ req.on("error", (err) => {
35
+ if (settled) return;
36
+ settled = true;
37
+ reject(err);
38
+ });
39
+ req.on("aborted", () => {
40
+ if (settled) return;
41
+ settled = true;
42
+ reject(/* @__PURE__ */ new Error("request aborted"));
43
+ });
44
+ });
45
+ }
46
+ async function toWebRequest(req, bodyLimit) {
47
+ const method = req.method ?? "GET";
48
+ const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
49
+ const headers = new Headers();
50
+ for (const [key, value] of Object.entries(req.headers)) {
51
+ if (value === void 0) continue;
52
+ if (Array.isArray(value)) for (const entry of value) headers.append(key, entry);
53
+ else headers.set(key, value);
54
+ }
55
+ if (!(method !== "GET" && method !== "HEAD")) return new Request(url, {
56
+ method,
57
+ headers
58
+ });
59
+ if (bodyLimit === false) return new Request(url, {
60
+ method,
61
+ headers,
62
+ body: Readable.toWeb(req),
63
+ duplex: "half"
64
+ });
65
+ const body = await readRawBody(req, bodyLimit);
66
+ return new Request(url, {
67
+ method,
68
+ headers,
69
+ body
70
+ });
71
+ }
72
+ function toNodeHeaders(headers) {
73
+ const result = {};
74
+ const setCookie = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [];
75
+ if (setCookie.length > 0) result["set-cookie"] = setCookie;
76
+ for (const [key, value] of headers) {
77
+ if (key === "set-cookie") continue;
78
+ result[key] = value;
79
+ }
80
+ return result;
81
+ }
82
+ async function writeWebResponse(response, res) {
83
+ res.writeHead(response.status, toNodeHeaders(response.headers));
84
+ if (!response.body) {
85
+ res.end();
86
+ return;
87
+ }
88
+ await new Promise((resolve, reject) => {
89
+ const nodeStream = Readable.fromWeb(response.body);
90
+ nodeStream.on("error", reject);
91
+ res.on("finish", resolve);
92
+ nodeStream.pipe(res);
93
+ });
94
+ }
95
+ function toNodeHandler(app, options = {}) {
96
+ const run = app.callback();
97
+ const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
98
+ return async (req, res) => {
99
+ try {
100
+ const request = await toWebRequest(req, bodyLimit);
101
+ const ctx = await run({
102
+ request,
103
+ response: new RhythmResponse()
104
+ });
105
+ await writeWebResponse(toResponse(ctx.response), res);
106
+ } catch (err) {
107
+ if (!(err instanceof RhythmBodyTooLargeError)) console.error(err);
108
+ if (!res.headersSent) {
109
+ const status = err instanceof RhythmBodyTooLargeError ? 413 : 500;
110
+ const message = err instanceof RhythmBodyTooLargeError ? err.message : "Internal Server Error";
111
+ const headers = { "content-type": "text/plain; charset=utf-8" };
112
+ if (err instanceof RhythmBodyTooLargeError) headers.connection = "close";
113
+ res.writeHead(status, headers);
114
+ res.end(message);
115
+ } else res.destroy(err instanceof Error ? err : new Error(String(err)));
116
+ }
117
+ };
118
+ }
119
+ //#endregion
120
+ export { RhythmBodyTooLargeError, toNodeHandler };
@@ -0,0 +1,17 @@
1
+ import { RhythmMutable } from "@rhythmjs/rhythm";
2
+ //#region src/adapters/context.d.ts
3
+ type RhythmResponseBody = string | ArrayBuffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream<Uint8Array> | null;
4
+ declare class RhythmResponse {
5
+ readonly [RhythmMutable] = true;
6
+ status: number;
7
+ statusText: string | undefined;
8
+ headers: Headers;
9
+ body: RhythmResponseBody;
10
+ }
11
+ interface RhythmHttpContext {
12
+ request: Request;
13
+ response: RhythmResponse;
14
+ }
15
+ declare function toResponse(response: RhythmResponse): Response;
16
+ //#endregion
17
+ export { toResponse as i, RhythmResponse as n, RhythmResponseBody as r, RhythmHttpContext as t };
@@ -0,0 +1,23 @@
1
+ import { Middleware, NextFn } from "@rhythmjs/rhythm";
2
+ //#region src/radix-tree.d.ts
3
+ type RouteDispatch = (context: any, next?: NextFn<any>) => Promise<any>;
4
+ export type MethodEntry = {
5
+ handlers: Middleware<any>[];
6
+ dispatch: RouteDispatch;
7
+ };
8
+ export declare class TreeNode<TMethod extends string = string> {
9
+ path: string;
10
+ indices: string;
11
+ children: TreeNode<TMethod>[];
12
+ paramChild: TreeNode<TMethod> | null;
13
+ paramName: string;
14
+ methods: Map<TMethod, MethodEntry> | null;
15
+ }
16
+ export declare function createNode<TMethod extends string = string>(): TreeNode<TMethod>;
17
+ export declare function insertRoute<TMethod extends string>(root: TreeNode<TMethod>, method: TMethod, path: string, handlers: Middleware<any>[]): void;
18
+ export declare function lookupRoute<TMethod extends string>(root: TreeNode<TMethod>, method: string, pathname: string): {
19
+ entry: MethodEntry;
20
+ params: Record<string, string>;
21
+ } | null;
22
+ export declare function joinPath(prefix: string, path: string): string;
23
+ //#endregion
@@ -0,0 +1,120 @@
1
+ import { compose } from "@rhythmjs/rhythm";
2
+ //#region src/radix-tree.ts
3
+ var TreeNode = class {
4
+ path = "";
5
+ indices = "";
6
+ children = [];
7
+ paramChild = null;
8
+ paramName = "";
9
+ methods = null;
10
+ };
11
+ function createNode() {
12
+ return new TreeNode();
13
+ }
14
+ function commonPrefixLength(a, b) {
15
+ const max = a.length < b.length ? a.length : b.length;
16
+ let i = 0;
17
+ while (i < max && a.charCodeAt(i) === b.charCodeAt(i)) i++;
18
+ return i;
19
+ }
20
+ function splitChild(node, at) {
21
+ const tail = new TreeNode();
22
+ tail.path = node.path.slice(at);
23
+ tail.indices = node.indices;
24
+ tail.children = node.children;
25
+ tail.paramChild = node.paramChild;
26
+ tail.paramName = node.paramName;
27
+ tail.methods = node.methods;
28
+ node.path = node.path.slice(0, at);
29
+ node.children = [tail];
30
+ node.indices = tail.path.charAt(0);
31
+ node.paramChild = null;
32
+ node.paramName = "";
33
+ node.methods = null;
34
+ }
35
+ function insertAt(node, path, method, handlers) {
36
+ if (path.length === 0) {
37
+ if (!node.methods) node.methods = /* @__PURE__ */ new Map();
38
+ node.methods.set(method, {
39
+ handlers,
40
+ dispatch: compose(handlers)
41
+ });
42
+ return;
43
+ }
44
+ if (path.charCodeAt(0) === 58) {
45
+ const slashIndex = path.indexOf("/");
46
+ const name = slashIndex === -1 ? path.slice(1) : path.slice(1, slashIndex);
47
+ const rest = slashIndex === -1 ? "" : path.slice(slashIndex);
48
+ if (!node.paramChild) {
49
+ node.paramChild = new TreeNode();
50
+ node.paramChild.paramName = name;
51
+ }
52
+ insertAt(node.paramChild, rest, method, handlers);
53
+ return;
54
+ }
55
+ const colonIndex = path.indexOf(":");
56
+ const staticPart = colonIndex === -1 ? path : path.slice(0, colonIndex);
57
+ const firstChar = path.charAt(0);
58
+ for (let i = 0; i < node.children.length; i++) {
59
+ if (node.indices.charAt(i) !== firstChar) continue;
60
+ const child = node.children[i];
61
+ const cpl = commonPrefixLength(staticPart, child.path);
62
+ if (cpl === 0) continue;
63
+ if (cpl < child.path.length) splitChild(child, cpl);
64
+ insertAt(child, path.slice(cpl), method, handlers);
65
+ return;
66
+ }
67
+ const child = new TreeNode();
68
+ child.path = staticPart;
69
+ node.children.push(child);
70
+ node.indices += firstChar;
71
+ insertAt(child, path.slice(staticPart.length), method, handlers);
72
+ }
73
+ function insertRoute(root, method, path, handlers) {
74
+ insertAt(root, path, method, handlers);
75
+ }
76
+ function lookupRoute(root, method, pathname) {
77
+ const params = {};
78
+ let node = root;
79
+ let path = pathname;
80
+ while (path.length > 0) {
81
+ const firstChar = path.charAt(0);
82
+ let matched = null;
83
+ for (let i = 0; i < node.children.length; i++) {
84
+ if (node.indices.charAt(i) !== firstChar) continue;
85
+ const child = node.children[i];
86
+ if (path.startsWith(child.path)) {
87
+ matched = child;
88
+ break;
89
+ }
90
+ }
91
+ if (matched) {
92
+ path = path.slice(matched.path.length);
93
+ node = matched;
94
+ continue;
95
+ }
96
+ if (node.paramChild) {
97
+ const slashIndex = path.indexOf("/");
98
+ const value = slashIndex === -1 ? path : path.slice(0, slashIndex);
99
+ if (value.length === 0) return null;
100
+ params[node.paramChild.paramName] = decodeURIComponent(value);
101
+ path = slashIndex === -1 ? "" : path.slice(slashIndex);
102
+ node = node.paramChild;
103
+ continue;
104
+ }
105
+ return null;
106
+ }
107
+ if (!node.methods) return null;
108
+ const entry = node.methods.get(method);
109
+ if (!entry) return null;
110
+ return {
111
+ entry,
112
+ params
113
+ };
114
+ }
115
+ function joinPath(prefix, path) {
116
+ if (!prefix) return path;
117
+ return `${prefix.endsWith("/") ? prefix.slice(0, -1) : prefix}${path.startsWith("/") ? path : `/${path}`}`;
118
+ }
119
+ //#endregion
120
+ export { TreeNode, createNode, insertRoute, joinPath, lookupRoute };
@@ -0,0 +1,24 @@
1
+ import { t as RhythmHttpContext } from "./context-CjxK7bH9.js";
2
+ import { DeepReadonly, Middleware, OmitHashKeys, Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/rhythm-router.d.ts
4
+ export interface RhythmRouterContext {
5
+ params: Record<string, string>;
6
+ }
7
+ export interface RhythmRouterOptions {
8
+ name?: string;
9
+ prefix?: string;
10
+ }
11
+ export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext, TProviders extends object = {}> extends Rhythm<RhythmHttpContext, TContext, TProviders> {
12
+ #private;
13
+ constructor(options?: RhythmRouterOptions);
14
+ use<TExtra extends object = {}>(fn: Middleware<TContext>): RhythmRouter<TContext & TExtra, TProviders>;
15
+ provide<TValue extends object>(factory: (deps: DeepReadonly<TProviders>) => TValue | Promise<TValue>, dispose?: (value: TValue) => void | Promise<void>): RhythmRouter<TContext & OmitHashKeys<TValue>, TProviders & OmitHashKeys<TValue>>;
16
+ register(): never;
17
+ get<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
18
+ post<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
19
+ put<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
20
+ patch<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
21
+ delete<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
22
+ routes(): Middleware<TContext>;
23
+ }
24
+ //#endregion
@@ -0,0 +1,93 @@
1
+ import { createNode, insertRoute, joinPath, lookupRoute } from "./radix-tree.js";
2
+ import { Rhythm } from "@rhythmjs/rhythm";
3
+ //#region src/rhythm-router.ts
4
+ const RhythmRouterTag = Symbol("RhythmRouterTag");
5
+ var RhythmRouter = class extends Rhythm {
6
+ #prefix;
7
+ #entries = [];
8
+ #tree = createNode();
9
+ #dispatchInstalled = false;
10
+ constructor(options = {}) {
11
+ super({
12
+ name: options.name ?? "router",
13
+ type: "controller"
14
+ });
15
+ this.#prefix = options.prefix ?? "";
16
+ }
17
+ use(fn) {
18
+ const nested = fn[RhythmRouterTag];
19
+ if (nested) for (const entry of nested.#entries) this.#mount(entry);
20
+ else {
21
+ this.#entries.push({
22
+ kind: "middleware",
23
+ fn
24
+ });
25
+ super.use(fn);
26
+ }
27
+ return this;
28
+ }
29
+ provide(factory, dispose) {
30
+ super.provide(factory, dispose);
31
+ return this;
32
+ }
33
+ register() {
34
+ throw new Error("RhythmRouter is a controller and cannot register() other modules or controllers");
35
+ }
36
+ #mount(entry) {
37
+ if (entry.kind === "middleware") {
38
+ this.#entries.push(entry);
39
+ super.use(entry.fn);
40
+ return;
41
+ }
42
+ this.#registerRoute(entry.method, joinPath(this.#prefix, entry.path), entry.handlers);
43
+ }
44
+ #registerRoute(method, fullPath, handlers) {
45
+ this.#entries.push({
46
+ kind: "route",
47
+ method,
48
+ path: fullPath,
49
+ handlers
50
+ });
51
+ insertRoute(this.#tree, method, fullPath, handlers);
52
+ if (this.#dispatchInstalled) return;
53
+ this.#dispatchInstalled = true;
54
+ const tree = this.#tree;
55
+ super.use(async (ctx, next) => {
56
+ const match = lookupRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
57
+ if (!match) {
58
+ await next();
59
+ return;
60
+ }
61
+ await match.entry.dispatch({
62
+ ...ctx,
63
+ params: match.params
64
+ }, next);
65
+ });
66
+ }
67
+ #route(method, path, handlers) {
68
+ this.#registerRoute(method, joinPath(this.#prefix, path), handlers);
69
+ return this;
70
+ }
71
+ get(path, ...handlers) {
72
+ return this.#route("GET", path, handlers);
73
+ }
74
+ post(path, ...handlers) {
75
+ return this.#route("POST", path, handlers);
76
+ }
77
+ put(path, ...handlers) {
78
+ return this.#route("PUT", path, handlers);
79
+ }
80
+ patch(path, ...handlers) {
81
+ return this.#route("PATCH", path, handlers);
82
+ }
83
+ delete(path, ...handlers) {
84
+ return this.#route("DELETE", path, handlers);
85
+ }
86
+ routes() {
87
+ const mw = this.middleware();
88
+ mw[RhythmRouterTag] = this;
89
+ return mw;
90
+ }
91
+ };
92
+ //#endregion
93
+ export { RhythmRouter };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@rhythmjs/router",
3
+ "version": "0.0.1",
4
+ "description": "Radix-tree HTTP router for the Rhythm middleware kernel, with Bun, Node, and Deno adapters.",
5
+ "license": "ISC",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/rhythmjs/rhythm.git",
9
+ "directory": "packages/router"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/rhythm-router.d.ts",
19
+ "default": "./dist/rhythm-router.js"
20
+ },
21
+ "./radix-tree": {
22
+ "types": "./dist/radix-tree.d.ts",
23
+ "default": "./dist/radix-tree.js"
24
+ },
25
+ "./adapters/bun": {
26
+ "types": "./dist/adapters/bun.d.ts",
27
+ "default": "./dist/adapters/bun.js"
28
+ },
29
+ "./adapters/deno": {
30
+ "types": "./dist/adapters/deno.d.ts",
31
+ "default": "./dist/adapters/deno.js"
32
+ },
33
+ "./adapters/node": {
34
+ "types": "./dist/adapters/node.d.ts",
35
+ "default": "./dist/adapters/node.js"
36
+ },
37
+ "./adapters/context": {
38
+ "types": "./dist/adapters/context.d.ts",
39
+ "default": "./dist/adapters/context.js"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@rhythmjs/rhythm": "0.0.1"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^26.6.3",
51
+ "typescript": "^7.0.2",
52
+ "vite-plus": "^1.0.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=20.19.0"
56
+ },
57
+ "scripts": {
58
+ "build": "vp pack",
59
+ "typecheck": "tsc --noEmit",
60
+ "test": "vp test"
61
+ }
62
+ }