@rhythmjs/router 0.0.9 → 0.0.10

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,8 +1,10 @@
1
1
  # @rhythmjs/router
2
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 `Rhythm` app via `.use(router.routes())`, so an unmatched request correctly falls through to whatever's registered after it.
3
+ Web-standard HTTP routing on top of `@rhythmjs/rhythm`. `RhythmRouter` matches routes with [rou3](https://github.com/h3js/rou3), the router that powers h3 (a static segment always wins over a `:param` segment, regardless of registration order), supports prefixes and nested routers, and mounts flat into a parent `Rhythm` app via `.use(router.middleware())`, so an unmatched request correctly falls through to whatever's registered after it.
4
4
 
5
- `RhythmRouter` is not an app and does not extend `Rhythm` — it is a controller that compiles routes and middleware down to a single middleware (`.routes()`). It shares the core middleware contract (`compose`, `Middleware`, `next(extra)`), but has no `provide()` or `register()`, and it can't be served on its own: a `Rhythm` app is always the host that owns the lifecycle and the adapters.
5
+ Route patterns follow rou3's conventions: `:name` params (`:name?` optional, `:id(\\d+)` regex-constrained), `*` for one unnamed segment (captured as `params["0"]`), and `**` for the rest of the path (`params._`, or `params.name` with `**:name`). Param values are the raw path segments, undecoded.
6
+
7
+ `RhythmRouter` is not an app and does not extend `Rhythm` — it is a controller that compiles routes and middleware down to a single middleware (`.middleware()`). It shares the core middleware contract (`compose`, `Middleware`, `next(extra)`), but has no `provide()` or `register()`, and it can't be served on its own: a `Rhythm` app is always the host that owns the lifecycle and the adapters.
6
8
 
7
9
  ## Example
8
10
 
@@ -27,16 +29,16 @@ A fuller runnable version, including nested prefixes and a fallback route, is at
27
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.
28
30
  - **Response helpers** — `ctx.json(data, status?)`, `ctx.text(body, status?)`, `ctx.html(body, status?)`, `ctx.error(status, message?)`, and `ctx.redirect(url, status = 302)` set the content type, body, and status on `ctx.response` in one call. `error()` defaults the message from the status code (`ctx.error(404)` → `"Not Found"`). They're sugar over `ctx.response`, so mixing both styles is fine, and later writes win.
29
31
  - **`ctx.params`** — captured `:name` path segments, added once a route matches.
30
- - **Prefixes compose across nesting** — a child router mounted into a prefixed parent via `.use(child)` gets the parent's prefix joined onto every one of its routes, at any nesting depth. Mounting copies the child's routes and middleware at that moment; routes added to the child afterwards don't appear in the parent, and the child keeps working standalone.
31
- - **Registration order is execution order** — a `.use()` middleware wraps only the routes registered after it; routes registered before it are untouched, and a matched route that doesn't call `next()` returns without reaching anything registered later. Consecutive routes share one radix tree lookup; an unmatched request falls through, entry by entry, to the outer `next()`.
32
- - **A router is a controller, not a module** — it has no `provide()` or `register()`, and it cannot be `register()`ed into a `Rhythm` app either; `register()` composes `Rhythm` modules only. A router mounts into an app exactly one way: koa-style, via `.use(router.routes())`.
32
+ - **Nesting is `.use(child.middleware())`** — a router mounts into another router (or into the app) as a compiled middleware. The mount is opaque, so the parent's prefix is **not** applied to the child's routes: the child carries its own absolute prefix (`new RhythmRouter({ prefix: "/api/users" })`). On a miss the child falls through to `next()`, so the parent's later middleware and routes still run, and the child keeps working standalone.
33
+ - **Registration order is execution order** — a `.use()` middleware wraps only the routes registered after it; routes registered before it are untouched, and a matched route that doesn't call `next()` returns without reaching anything registered later. Consecutive routes share one rou3 lookup; an unmatched request falls through, entry by entry, to the outer `next()`.
34
+ - **A router is a controller, not a module** — it has no `provide()` or `register()`, and it cannot be `register()`ed into a `Rhythm` app either; `register()` composes `Rhythm` modules only. A router mounts into an app exactly one way: koa-style, via `.use(router.middleware())`.
33
35
 
34
36
  ## API
35
37
 
36
38
  - `new RhythmRouter(options?)` — `options.prefix`.
37
39
  - `.get/.post/.put/.patch/.delete(path, ...handlers)` — register a route; `path` may contain `:param` segments.
38
- - `.use(fn)` — plain middleware. `.use(child)` — mount a nested `RhythmRouter` (prefixes compose).
39
- - `.routes()` — this router as a plain middleware, for mounting into a `Rhythm` app via `.use()`; the router's only way onto a server. Note: mounting a _router_ into a _router_ must use `.use(child)`, not `.use(child.routes())` — an opaque middleware can't have the parent's prefix applied to its routes.
40
+ - `.use(fn)` — plain middleware; it takes only functions, so a nested router mounts as `.use(child.middleware())`.
41
+ - `.middleware()` — this router compiled to a plain middleware: the one form that mounts anywhere, into a `Rhythm` app or into another router. Because the compiled form is opaque, the mounting router's prefix is not applied to it — give the child its full prefix.
40
42
  - `ctx.json/.text/.html(body, status?)`, `ctx.error(status, message?)`, `ctx.redirect(url, status?)` — response helpers built into the context by the adapters (`createHttpContext` in `@rhythmjs/router/context`).
41
43
  - `toFetchHandler(app)` — bridges a `Rhythm` app to a Web-standard `(Request) => Promise<Response>` handler.
42
44
 
@@ -81,6 +83,8 @@ One app definition; each runtime has its own adapter under `@rhythmjs/router/ada
81
83
 
82
84
  Every adapter maps errors thrown in the middleware chain to a response (`500`, or the error's own `status`) instead of crashing, and accepts `{ onError }` to replace that mapping: `handle(app, { onError: (error) => new Response("down", { status: 503 }) })`. Platform metadata (Cloudflare `env`/`ctx`, the Netlify context) is readable in middleware via `getRuntime(ctx.request)` from `@rhythmjs/router/context`.
83
85
 
86
+ Fetch-shaped adapters also accept `{ websocket }` — anything with a `handleUpgrade(request, ...extra)` method, such as a [crossws](https://crossws.h3.dev) instance or an `@rhythmjs/ws` adapter. Requests carrying an `upgrade: websocket` header divert to it (with the runtime's extra arguments — Bun's `server`, Deno's `info`, Cloudflare's `env`/`ctx` — forwarded) before the app runs; every other request is untouched.
87
+
84
88
  **Node** — `getRequestListener` returns a listener for your own `node:http` server:
85
89
 
86
90
  ```ts
@@ -1,5 +1,5 @@
1
1
  import { t as RhythmHttpContext } from "../context-ChHlG-q0.js";
2
- import { t as AdapterOptions } from "../base--TyruAfY.js";
2
+ import { t as AdapterOptions } from "../base-DwJhCkwZ.js";
3
3
  import { AWSLambdaHandler } from "srvx/aws-lambda";
4
4
  import { Rhythm } from "@rhythmjs/rhythm";
5
5
  //#region src/adapters/aws-lambda.d.ts
@@ -1,4 +1,4 @@
1
- import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  import { toLambdaHandler } from "srvx/aws-lambda";
3
3
  //#region src/adapters/aws-lambda.ts
4
4
  const fetchHandle = createFetchAdapter();
@@ -1,5 +1,5 @@
1
- import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base-DwJhCkwZ.js";
2
2
  //#region src/adapters/bun.d.ts
3
- export declare const handle: FetchAdapter;
3
+ export declare const handle: FetchAdapter<[server?: unknown]>;
4
4
  //#endregion
5
5
  export type { AdapterOptions };
@@ -1,4 +1,4 @@
1
- import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  //#region src/adapters/bun.ts
3
3
  const handle = createFetchAdapter();
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base-DwJhCkwZ.js";
2
2
  //#region src/adapters/cloudflare.d.ts
3
3
  export declare const handle: FetchAdapter<[env?: unknown, context?: unknown]>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as setRuntime, t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { n as setRuntime, t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  //#region src/adapters/cloudflare.ts
3
3
  const handle = createFetchAdapter((request, env, context) => {
4
4
  if (env !== void 0 || context !== void 0) setRuntime(request, {
@@ -1,5 +1,5 @@
1
- import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base-DwJhCkwZ.js";
2
2
  //#region src/adapters/deno.d.ts
3
- export declare const handle: FetchAdapter;
3
+ export declare const handle: FetchAdapter<[info?: unknown]>;
4
4
  //#endregion
5
5
  export type { AdapterOptions };
@@ -1,4 +1,4 @@
1
- import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  //#region src/adapters/deno.ts
3
3
  const handle = createFetchAdapter();
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base-DwJhCkwZ.js";
2
2
  //#region src/adapters/netlify.d.ts
3
3
  export declare const handle: FetchAdapter<[context?: unknown]>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { n as setRuntime, t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { n as setRuntime, t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  //#region src/adapters/netlify.ts
3
3
  const handle = createFetchAdapter((request, context) => {
4
4
  if (context !== void 0) setRuntime(request, {
@@ -1,5 +1,5 @@
1
1
  import { t as RhythmHttpContext } from "../context-ChHlG-q0.js";
2
- import { t as AdapterOptions } from "../base--TyruAfY.js";
2
+ import { t as AdapterOptions } from "../base-DwJhCkwZ.js";
3
3
  import { NodeHttp1Handler, NodeHttp2Handler } from "srvx";
4
4
  import { Rhythm } from "@rhythmjs/rhythm";
5
5
  //#region src/adapters/node.d.ts
@@ -1,4 +1,4 @@
1
- import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  import { toNodeHandler } from "srvx/node";
3
3
  //#region src/adapters/node.ts
4
4
  const fetchHandle = createFetchAdapter();
@@ -1,5 +1,5 @@
1
1
  import { t as RhythmHttpContext } from "../context-ChHlG-q0.js";
2
- import { t as AdapterOptions } from "../base--TyruAfY.js";
2
+ import { t as AdapterOptions } from "../base-DwJhCkwZ.js";
3
3
  import { Rhythm } from "@rhythmjs/rhythm";
4
4
  //#region src/adapters/service-worker.d.ts
5
5
  interface FetchEventLike {
@@ -1,4 +1,4 @@
1
- import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  //#region src/adapters/service-worker.ts
3
3
  const fetchHandle = createFetchAdapter();
4
4
  function handle(app, options = {}) {
@@ -1,4 +1,4 @@
1
- import { n as FetchAdapter, t as AdapterOptions } from "../base--TyruAfY.js";
1
+ import { n as FetchAdapter, t as AdapterOptions } from "../base-DwJhCkwZ.js";
2
2
  //#region src/adapters/vercel.d.ts
3
3
  export declare const handle: FetchAdapter;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { t as createFetchAdapter } from "../base-B8GEu3z4.js";
1
+ import { t as createFetchAdapter } from "../base-BMFEzT_g.js";
2
2
  //#region src/adapters/vercel.ts
3
3
  const handle = createFetchAdapter();
4
4
  //#endregion
@@ -12,6 +12,7 @@ function createFetchAdapter(enrich) {
12
12
  const handler = toFetchHandler(app);
13
13
  const onError = options.onError ?? errorToResponse;
14
14
  return async (request, ...extra) => {
15
+ if (options.websocket && request.headers.get("upgrade")?.toLowerCase() === "websocket") return options.websocket.handleUpgrade(request, ...extra);
15
16
  try {
16
17
  enrich?.(request, ...extra);
17
18
  return await handler(request);
@@ -2,8 +2,12 @@ import { t as RhythmHttpContext } from "./context-ChHlG-q0.js";
2
2
  import { ErrorHandler, ServerRuntimeContext } from "srvx";
3
3
  import { Rhythm } from "@rhythmjs/rhythm";
4
4
  //#region src/adapters/base.d.ts
5
+ interface WebSocketUpgrader {
6
+ handleUpgrade(request: Request, ...extra: unknown[]): Response | undefined | Promise<Response | undefined>;
7
+ }
5
8
  interface AdapterOptions {
6
9
  onError?: ErrorHandler;
10
+ websocket?: WebSocketUpgrader;
7
11
  }
8
12
  type FetchAdapter<TExtra extends unknown[] = []> = <TContext extends RhythmHttpContext, TProviders extends object = {}>(app: Rhythm<RhythmHttpContext, TContext, TProviders>, options?: AdapterOptions) => (request: Request, ...extra: TExtra) => Promise<Response>;
9
13
  //#endregion
@@ -18,6 +18,7 @@ export type RouterEntry = {
18
18
  readonly handlers: readonly Middleware<any>[];
19
19
  };
20
20
  type RouteHandler<TContext> = Middleware<TContext & RhythmRouterContext>;
21
+ export declare function joinPath(prefix: string, path: string): string;
21
22
  export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext> {
22
23
  #private;
23
24
  constructor(options?: RhythmRouterOptions);
@@ -1,6 +1,10 @@
1
- import { createNode, insertRoute, joinPath, lookupRoute } from "./radix-tree.js";
2
1
  import { compose } from "@rhythmjs/rhythm/compose";
2
+ import { addRoute, createRouter, findRoute } from "rou3";
3
3
  //#region src/rhythm-router.ts
4
+ function joinPath(prefix, path) {
5
+ if (!prefix) return path;
6
+ return `${prefix.endsWith("/") ? prefix.slice(0, -1) : prefix}${path.startsWith("/") ? path : `/${path}`}`;
7
+ }
4
8
  var RhythmRouter = class {
5
9
  #options;
6
10
  #entries = [];
@@ -48,14 +52,14 @@ var RhythmRouter = class {
48
52
  #compile() {
49
53
  const dispatchFor = (tree) => {
50
54
  return async (ctx, next) => {
51
- const match = lookupRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
55
+ const match = findRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
52
56
  if (!match) {
53
57
  await next();
54
58
  return;
55
59
  }
56
- await match.payload({
60
+ await match.data({
57
61
  ...ctx,
58
- params: match.params
62
+ params: match.params ?? {}
59
63
  }, next);
60
64
  };
61
65
  };
@@ -68,11 +72,11 @@ var RhythmRouter = class {
68
72
  i++;
69
73
  continue;
70
74
  }
71
- const tree = createNode();
75
+ const tree = createRouter();
72
76
  while (i < this.#entries.length) {
73
77
  const route = this.#entries[i];
74
78
  if (route.kind !== "route") break;
75
- insertRoute(tree, route.method, route.path, compose(route.handlers));
79
+ addRoute(tree, route.method, route.path, compose(route.handlers));
76
80
  i++;
77
81
  }
78
82
  stack.push(dispatchFor(tree));
@@ -87,4 +91,4 @@ var RhythmRouter = class {
87
91
  }
88
92
  };
89
93
  //#endregion
90
- export { RhythmRouter };
94
+ export { RhythmRouter, joinPath };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.9",
4
- "description": "Radix-tree HTTP router for the Rhythm middleware kernel, with Bun, Node, and Deno adapters.",
3
+ "version": "0.0.10",
4
+ "description": "HTTP router for the Rhythm middleware kernel, matching with rou3, with Bun, Node, and Deno adapters.",
5
5
  "homepage": "https://rhythm.js.org/router",
6
6
  "license": "ISC",
7
7
  "repository": {
@@ -19,10 +19,6 @@
19
19
  "types": "./dist/rhythm-router.d.ts",
20
20
  "default": "./dist/rhythm-router.js"
21
21
  },
22
- "./radix-tree": {
23
- "types": "./dist/radix-tree.d.ts",
24
- "default": "./dist/radix-tree.js"
25
- },
26
22
  "./context": {
27
23
  "types": "./dist/context.d.ts",
28
24
  "default": "./dist/context.js"
@@ -77,8 +73,9 @@
77
73
  "access": "public"
78
74
  },
79
75
  "dependencies": {
76
+ "rou3": "^0.11.0",
80
77
  "srvx": "^1.0.5",
81
- "@rhythmjs/rhythm": "0.0.9"
78
+ "@rhythmjs/rhythm": "0.0.10"
82
79
  },
83
80
  "devDependencies": {
84
81
  "@types/node": "^26.6.3",
@@ -1,18 +0,0 @@
1
- //#region src/radix-tree.d.ts
2
- export declare class TreeNode<TMethod extends string = string, TPayload = unknown> {
3
- path: string;
4
- indices: string;
5
- children: TreeNode<TMethod, TPayload>[];
6
- paramChild: TreeNode<TMethod, TPayload> | null;
7
- paramName: string;
8
- wildcardChild: TreeNode<TMethod, TPayload> | null;
9
- methods: Map<TMethod, TPayload> | null;
10
- }
11
- export declare function createNode<TMethod extends string = string, TPayload = unknown>(): TreeNode<TMethod, TPayload>;
12
- export declare function insertRoute<TMethod extends string, TPayload>(root: TreeNode<TMethod, TPayload>, method: TMethod, path: string, payload: TPayload): void;
13
- export declare function lookupRoute<TMethod extends string, TPayload>(root: TreeNode<TMethod, TPayload>, method: string, pathname: string): {
14
- payload: TPayload;
15
- params: Record<string, string>;
16
- } | null;
17
- export declare function joinPath(prefix: string, path: string): string;
18
- //#endregion
@@ -1,150 +0,0 @@
1
- //#region src/radix-tree.ts
2
- var TreeNode = class {
3
- path = "";
4
- indices = "";
5
- children = [];
6
- paramChild = null;
7
- paramName = "";
8
- wildcardChild = null;
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.wildcardChild = node.wildcardChild;
28
- tail.methods = node.methods;
29
- node.path = node.path.slice(0, at);
30
- node.children = [tail];
31
- node.indices = tail.path.charAt(0);
32
- node.paramChild = null;
33
- node.paramName = "";
34
- node.wildcardChild = null;
35
- node.methods = null;
36
- }
37
- function insertAt(node, path, method, payload) {
38
- if (path.length === 0) {
39
- if (!node.methods) node.methods = /* @__PURE__ */ new Map();
40
- node.methods.set(method, payload);
41
- return;
42
- }
43
- if (path.charCodeAt(0) === 58) {
44
- const slashIndex = path.indexOf("/");
45
- const name = slashIndex === -1 ? path.slice(1) : path.slice(1, slashIndex);
46
- const rest = slashIndex === -1 ? "" : path.slice(slashIndex);
47
- if (!node.paramChild) {
48
- node.paramChild = new TreeNode();
49
- node.paramChild.paramName = name;
50
- }
51
- insertAt(node.paramChild, rest, method, payload);
52
- return;
53
- }
54
- if (path.charCodeAt(0) === 42) {
55
- if (path.length !== 1) throw new Error("wildcard \"*\" is only allowed at the end of a path");
56
- if (!node.wildcardChild) node.wildcardChild = new TreeNode();
57
- if (!node.wildcardChild.methods) node.wildcardChild.methods = /* @__PURE__ */ new Map();
58
- node.wildcardChild.methods.set(method, payload);
59
- return;
60
- }
61
- const colonIndex = path.indexOf(":");
62
- const starIndex = path.indexOf("*");
63
- let staticEnd = path.length;
64
- if (colonIndex !== -1) staticEnd = colonIndex;
65
- if (starIndex !== -1 && starIndex < staticEnd) staticEnd = starIndex;
66
- const staticPart = path.slice(0, staticEnd);
67
- const firstChar = path.charAt(0);
68
- for (let i = 0; i < node.children.length; i++) {
69
- if (node.indices.charAt(i) !== firstChar) continue;
70
- const child = node.children[i];
71
- const cpl = commonPrefixLength(staticPart, child.path);
72
- if (cpl === 0) continue;
73
- if (cpl < child.path.length) splitChild(child, cpl);
74
- insertAt(child, path.slice(cpl), method, payload);
75
- return;
76
- }
77
- const child = new TreeNode();
78
- child.path = staticPart;
79
- node.children.push(child);
80
- node.indices += firstChar;
81
- insertAt(child, path.slice(staticPart.length), method, payload);
82
- }
83
- function expandOptionalParams(path) {
84
- if (!path.includes("?")) return [path];
85
- const segments = path.split("/");
86
- let firstOptional = -1;
87
- for (let i = 0; i < segments.length; i++) {
88
- const segment = segments[i];
89
- const isOptional = segment.length > 2 && segment.charCodeAt(0) === 58 && segment.endsWith("?");
90
- if (!isOptional && segment.includes("?")) throw new Error(`"?" is only allowed to mark an optional param like ":name?" (in "${path}")`);
91
- if (firstOptional !== -1 && !isOptional) throw new Error(`only optional params may follow an optional param (in "${path}")`);
92
- if (isOptional && firstOptional === -1) firstOptional = i;
93
- }
94
- const variants = [];
95
- for (let end = firstOptional; end <= segments.length; end++) {
96
- const parts = segments.slice(0, end).map((segment) => segment.endsWith("?") ? segment.slice(0, -1) : segment);
97
- variants.push(parts.join("/") || "/");
98
- }
99
- return variants;
100
- }
101
- function insertRoute(root, method, path, payload) {
102
- for (const variant of expandOptionalParams(path)) insertAt(root, variant, method, payload);
103
- }
104
- function search(node, method, path) {
105
- if (path.length === 0) {
106
- const payload = node.methods?.get(method);
107
- if (payload !== void 0) return {
108
- payload,
109
- params: {}
110
- };
111
- } else {
112
- const firstChar = path.charAt(0);
113
- for (let i = 0; i < node.children.length; i++) {
114
- if (node.indices.charAt(i) !== firstChar) continue;
115
- const child = node.children[i];
116
- if (!path.startsWith(child.path)) continue;
117
- const result = search(child, method, path.slice(child.path.length));
118
- if (result) return result;
119
- }
120
- if (node.paramChild) {
121
- const slashIndex = path.indexOf("/");
122
- const value = slashIndex === -1 ? path : path.slice(0, slashIndex);
123
- if (value.length > 0) {
124
- const rest = slashIndex === -1 ? "" : path.slice(slashIndex);
125
- const result = search(node.paramChild, method, rest);
126
- if (result) {
127
- result.params[node.paramChild.paramName] = decodeURIComponent(value);
128
- return result;
129
- }
130
- }
131
- }
132
- }
133
- if (node.wildcardChild) {
134
- const payload = node.wildcardChild.methods?.get(method);
135
- if (payload !== void 0) return {
136
- payload,
137
- params: { "*": decodeURIComponent(path) }
138
- };
139
- }
140
- return null;
141
- }
142
- function lookupRoute(root, method, pathname) {
143
- return search(root, method, pathname);
144
- }
145
- function joinPath(prefix, path) {
146
- if (!prefix) return path;
147
- return `${prefix.endsWith("/") ? prefix.slice(0, -1) : prefix}${path.startsWith("/") ? path : `/${path}`}`;
148
- }
149
- //#endregion
150
- export { TreeNode, createNode, insertRoute, joinPath, lookupRoute };