@rhythmjs/router 0.0.3 → 0.0.5

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.
@@ -5,6 +5,7 @@ export declare class TreeNode<TMethod extends string = string, TPayload = unknow
5
5
  children: TreeNode<TMethod, TPayload>[];
6
6
  paramChild: TreeNode<TMethod, TPayload> | null;
7
7
  paramName: string;
8
+ wildcardChild: TreeNode<TMethod, TPayload> | null;
8
9
  methods: Map<TMethod, TPayload> | null;
9
10
  }
10
11
  export declare function createNode<TMethod extends string = string, TPayload = unknown>(): TreeNode<TMethod, TPayload>;
@@ -5,6 +5,7 @@ var TreeNode = class {
5
5
  children = [];
6
6
  paramChild = null;
7
7
  paramName = "";
8
+ wildcardChild = null;
8
9
  methods = null;
9
10
  };
10
11
  function createNode() {
@@ -23,12 +24,14 @@ function splitChild(node, at) {
23
24
  tail.children = node.children;
24
25
  tail.paramChild = node.paramChild;
25
26
  tail.paramName = node.paramName;
27
+ tail.wildcardChild = node.wildcardChild;
26
28
  tail.methods = node.methods;
27
29
  node.path = node.path.slice(0, at);
28
30
  node.children = [tail];
29
31
  node.indices = tail.path.charAt(0);
30
32
  node.paramChild = null;
31
33
  node.paramName = "";
34
+ node.wildcardChild = null;
32
35
  node.methods = null;
33
36
  }
34
37
  function insertAt(node, path, method, payload) {
@@ -48,8 +51,19 @@ function insertAt(node, path, method, payload) {
48
51
  insertAt(node.paramChild, rest, method, payload);
49
52
  return;
50
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
+ }
51
61
  const colonIndex = path.indexOf(":");
52
- const staticPart = colonIndex === -1 ? path : path.slice(0, colonIndex);
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);
53
67
  const firstChar = path.charAt(0);
54
68
  for (let i = 0; i < node.children.length; i++) {
55
69
  if (node.indices.charAt(i) !== firstChar) continue;
@@ -66,47 +80,67 @@ function insertAt(node, path, method, payload) {
66
80
  node.indices += firstChar;
67
81
  insertAt(child, path.slice(staticPart.length), method, payload);
68
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
+ }
69
101
  function insertRoute(root, method, path, payload) {
70
- insertAt(root, path, method, payload);
102
+ for (const variant of expandOptionalParams(path)) insertAt(root, variant, method, payload);
71
103
  }
72
- function lookupRoute(root, method, pathname) {
73
- const params = {};
74
- let node = root;
75
- let path = pathname;
76
- while (path.length > 0) {
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 {
77
112
  const firstChar = path.charAt(0);
78
- let matched = null;
79
113
  for (let i = 0; i < node.children.length; i++) {
80
114
  if (node.indices.charAt(i) !== firstChar) continue;
81
115
  const child = node.children[i];
82
- if (path.startsWith(child.path)) {
83
- matched = child;
84
- break;
85
- }
86
- }
87
- if (matched) {
88
- path = path.slice(matched.path.length);
89
- node = matched;
90
- continue;
116
+ if (!path.startsWith(child.path)) continue;
117
+ const result = search(child, method, path.slice(child.path.length));
118
+ if (result) return result;
91
119
  }
92
120
  if (node.paramChild) {
93
121
  const slashIndex = path.indexOf("/");
94
122
  const value = slashIndex === -1 ? path : path.slice(0, slashIndex);
95
- if (value.length === 0) return null;
96
- params[node.paramChild.paramName] = decodeURIComponent(value);
97
- path = slashIndex === -1 ? "" : path.slice(slashIndex);
98
- node = node.paramChild;
99
- continue;
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
+ }
100
131
  }
101
- return null;
102
132
  }
103
- if (!node.methods) return null;
104
- const payload = node.methods.get(method);
105
- if (payload === void 0) return null;
106
- return {
107
- payload,
108
- params
109
- };
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);
110
144
  }
111
145
  function joinPath(prefix, path) {
112
146
  if (!prefix) return path;
@@ -1,5 +1,5 @@
1
1
  import { t as RhythmHttpContext } from "./context-B0711qPV.js";
2
- import { Middleware } from "@rhythmjs/rhythm/types";
2
+ import { DeriveMiddleware, Middleware } from "@rhythmjs/rhythm/types";
3
3
  //#region src/rhythm-router.d.ts
4
4
  export interface RhythmRouterContext {
5
5
  readonly params: Readonly<Record<string, string>>;
@@ -7,16 +7,22 @@ export interface RhythmRouterContext {
7
7
  export interface RhythmRouterOptions {
8
8
  prefix?: string;
9
9
  }
10
+ type RouteHandler<TContext> = Middleware<TContext & RhythmRouterContext>;
10
11
  export declare class RhythmRouter<TContext extends RhythmHttpContext = RhythmHttpContext> {
11
12
  #private;
12
13
  constructor(options?: RhythmRouterOptions);
13
- use(child: RhythmRouter<any>): this;
14
- use<TExtra extends object = {}>(fn: Middleware<TContext>): RhythmRouter<TContext & TExtra>;
15
- get<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
16
- post<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
17
- put<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
18
- patch<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
19
- delete<TExtra extends object = {}>(path: string, ...handlers: Middleware<TContext & RhythmRouterContext & TExtra>[]): this;
20
- routes(): Middleware<TContext>;
14
+ use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): RhythmRouter<TContext & TExtra>;
15
+ use(fn: Middleware<TContext>): this;
16
+ get<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
17
+ get(path: string, ...handlers: RouteHandler<TContext>[]): this;
18
+ post<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
19
+ post(path: string, ...handlers: RouteHandler<TContext>[]): this;
20
+ put<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
21
+ put(path: string, ...handlers: RouteHandler<TContext>[]): this;
22
+ patch<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
23
+ patch(path: string, ...handlers: RouteHandler<TContext>[]): this;
24
+ delete<TExtra extends object>(path: string, middleware: DeriveMiddleware<TContext & RhythmRouterContext, TExtra>, ...handlers: RouteHandler<TContext & TExtra>[]): this;
25
+ delete(path: string, ...handlers: RouteHandler<TContext>[]): this;
26
+ middleware(): Middleware<TContext>;
21
27
  }
22
28
  //#endregion
@@ -1,29 +1,21 @@
1
1
  import { createNode, insertRoute, joinPath, lookupRoute } from "./radix-tree.js";
2
2
  import { compose } from "@rhythmjs/rhythm/compose";
3
3
  //#region src/rhythm-router.ts
4
- var RhythmRouter = class RhythmRouter {
4
+ var RhythmRouter = class {
5
5
  #options;
6
6
  #entries = [];
7
- #composed = null;
8
7
  constructor(options = {}) {
9
8
  this.#options = options;
10
9
  }
11
10
  get #prefix() {
12
11
  return this.#options.prefix ?? "";
13
12
  }
14
- use(arg) {
15
- if (arg instanceof RhythmRouter) for (const entry of arg.#entries) this.#entries.push(entry.kind === "route" ? {
16
- ...entry,
17
- path: joinPath(this.#prefix, entry.path)
18
- } : entry);
19
- else {
20
- if (typeof arg !== "function") throw new TypeError("middleware must be a function!");
21
- this.#entries.push({
22
- kind: "middleware",
23
- fn: arg
24
- });
25
- }
26
- this.#composed = null;
13
+ use(fn) {
14
+ if (typeof fn !== "function") throw new TypeError("middleware must be a function!");
15
+ this.#entries.push({
16
+ kind: "middleware",
17
+ fn
18
+ });
27
19
  return this;
28
20
  }
29
21
  #route(method, path, handlers) {
@@ -33,7 +25,6 @@ var RhythmRouter = class RhythmRouter {
33
25
  path: joinPath(this.#prefix, path),
34
26
  handlers
35
27
  });
36
- this.#composed = null;
37
28
  return this;
38
29
  }
39
30
  get(path, ...handlers) {
@@ -52,7 +43,6 @@ var RhythmRouter = class RhythmRouter {
52
43
  return this.#route("DELETE", path, handlers);
53
44
  }
54
45
  #compile() {
55
- if (this.#composed) return this.#composed;
56
46
  const dispatchFor = (tree) => {
57
47
  return async (ctx, next) => {
58
48
  const match = lookupRoute(tree, ctx.request.method, new URL(ctx.request.url).pathname);
@@ -84,12 +74,12 @@ var RhythmRouter = class RhythmRouter {
84
74
  }
85
75
  stack.push(dispatchFor(tree));
86
76
  }
87
- this.#composed = compose(stack);
88
- return this.#composed;
77
+ return compose(stack);
89
78
  }
90
- routes() {
79
+ middleware() {
80
+ const fn = this.#compile();
91
81
  return async (ctx, next) => {
92
- await this.#compile()(ctx, next);
82
+ await fn(ctx, next);
93
83
  };
94
84
  }
95
85
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhythmjs/router",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Radix-tree HTTP router for the Rhythm middleware kernel, with Bun, Node, and Deno adapters.",
5
5
  "homepage": "https://rhythm.js.org/router",
6
6
  "license": "ISC",
@@ -49,7 +49,7 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@rhythmjs/rhythm": "0.0.3"
52
+ "@rhythmjs/rhythm": "0.0.5"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^26.6.3",