@nextrush/router 3.0.0
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/LICENSE +21 -0
- package/README.md +618 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +853 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/src/__tests__/router-edge-cases.test.ts +486 -0
- package/src/__tests__/router.test.ts +621 -0
- package/src/index.ts +31 -0
- package/src/radix-tree.ts +184 -0
- package/src/router.ts +1029 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/router.ts
|
|
5
|
+
import { HTTP_METHODS } from "@nextrush/types";
|
|
6
|
+
|
|
7
|
+
// src/radix-tree.ts
|
|
8
|
+
var NodeType = /* @__PURE__ */ (function(NodeType2) {
|
|
9
|
+
NodeType2[NodeType2["STATIC"] = 0] = "STATIC";
|
|
10
|
+
NodeType2[NodeType2["PARAM"] = 1] = "PARAM";
|
|
11
|
+
NodeType2[NodeType2["WILDCARD"] = 2] = "WILDCARD";
|
|
12
|
+
return NodeType2;
|
|
13
|
+
})({});
|
|
14
|
+
var RESOLVED_PROMISE = Promise.resolve();
|
|
15
|
+
var NOOP_NEXT = /* @__PURE__ */ __name(() => RESOLVED_PROMISE, "NOOP_NEXT");
|
|
16
|
+
function compileExecutor(handler, middleware) {
|
|
17
|
+
const len = middleware.length;
|
|
18
|
+
if (len === 0) {
|
|
19
|
+
return async (ctx) => {
|
|
20
|
+
await handler(ctx, NOOP_NEXT);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (len === 1) {
|
|
24
|
+
const mw = middleware[0];
|
|
25
|
+
if (mw === void 0) throw new Error("middleware length mismatch");
|
|
26
|
+
return async (ctx) => {
|
|
27
|
+
await mw(ctx, async () => {
|
|
28
|
+
await handler(ctx, NOOP_NEXT);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (len === 2) {
|
|
33
|
+
const mw0 = middleware[0];
|
|
34
|
+
const mw1 = middleware[1];
|
|
35
|
+
if (mw0 === void 0 || mw1 === void 0) throw new Error("middleware length mismatch");
|
|
36
|
+
return async (ctx) => {
|
|
37
|
+
await mw0(ctx, async () => {
|
|
38
|
+
await mw1(ctx, async () => {
|
|
39
|
+
await handler(ctx, NOOP_NEXT);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return async (ctx) => {
|
|
45
|
+
let index = 0;
|
|
46
|
+
const dispatch = /* @__PURE__ */ __name(async () => {
|
|
47
|
+
if (index < len) {
|
|
48
|
+
const mw = middleware[index++];
|
|
49
|
+
if (mw === void 0) throw new Error("middleware length mismatch");
|
|
50
|
+
await mw(ctx, dispatch);
|
|
51
|
+
} else {
|
|
52
|
+
await handler(ctx, NOOP_NEXT);
|
|
53
|
+
}
|
|
54
|
+
}, "dispatch");
|
|
55
|
+
await dispatch();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
__name(compileExecutor, "compileExecutor");
|
|
59
|
+
function createNode(segment, type = 0) {
|
|
60
|
+
return {
|
|
61
|
+
segment,
|
|
62
|
+
type,
|
|
63
|
+
children: /* @__PURE__ */ new Map(),
|
|
64
|
+
handlers: /* @__PURE__ */ new Map()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
__name(createNode, "createNode");
|
|
68
|
+
function parseSegments(path, caseSensitive = true) {
|
|
69
|
+
const normalized = path.startsWith("/") ? path.slice(1) : path;
|
|
70
|
+
if (normalized === "") return [];
|
|
71
|
+
const parts = normalized.split("/");
|
|
72
|
+
const segments = [];
|
|
73
|
+
for (const part of parts) {
|
|
74
|
+
if (part.startsWith(":")) {
|
|
75
|
+
const paramName = part.slice(1);
|
|
76
|
+
segments.push({
|
|
77
|
+
segment: part,
|
|
78
|
+
type: 1,
|
|
79
|
+
paramName
|
|
80
|
+
});
|
|
81
|
+
} else if (part === "*") {
|
|
82
|
+
segments.push({
|
|
83
|
+
segment: "*",
|
|
84
|
+
type: 2
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
} else {
|
|
88
|
+
segments.push({
|
|
89
|
+
segment: caseSensitive ? part : part.toLowerCase(),
|
|
90
|
+
type: 0
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return segments;
|
|
95
|
+
}
|
|
96
|
+
__name(parseSegments, "parseSegments");
|
|
97
|
+
|
|
98
|
+
// src/router.ts
|
|
99
|
+
var EMPTY_PARAMS = Object.freeze(/* @__PURE__ */ Object.create(null));
|
|
100
|
+
var Router = class _Router {
|
|
101
|
+
static {
|
|
102
|
+
__name(this, "Router");
|
|
103
|
+
}
|
|
104
|
+
root;
|
|
105
|
+
opts;
|
|
106
|
+
routerMiddleware = [];
|
|
107
|
+
/**
|
|
108
|
+
* Static route hash map for O(1) lookup.
|
|
109
|
+
* Key: "METHOD path" (e.g. "GET /users"), Value: HandlerEntry
|
|
110
|
+
*/
|
|
111
|
+
staticRoutes = /* @__PURE__ */ new Map();
|
|
112
|
+
/** Whether any routes have params or wildcards (disables static-only fast path) */
|
|
113
|
+
hasParamRoutes = false;
|
|
114
|
+
constructor(options = {}) {
|
|
115
|
+
this.root = createNode("");
|
|
116
|
+
this.opts = {
|
|
117
|
+
prefix: options.prefix ?? "",
|
|
118
|
+
caseSensitive: options.caseSensitive ?? false,
|
|
119
|
+
strict: options.strict ?? false
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Normalize path based on router options
|
|
124
|
+
*/
|
|
125
|
+
normalizePath(path) {
|
|
126
|
+
let prefix = this.opts.prefix;
|
|
127
|
+
if (prefix.endsWith("/") && path.startsWith("/")) {
|
|
128
|
+
prefix = prefix.slice(0, -1);
|
|
129
|
+
}
|
|
130
|
+
let normalized = prefix + path;
|
|
131
|
+
if (normalized.includes("//")) {
|
|
132
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
133
|
+
}
|
|
134
|
+
if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
|
|
135
|
+
normalized = normalized.slice(0, -1);
|
|
136
|
+
}
|
|
137
|
+
return normalized.startsWith("/") ? normalized : "/" + normalized;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Add a route to the radix tree
|
|
141
|
+
*/
|
|
142
|
+
addRoute(method, path, handlers, middleware = []) {
|
|
143
|
+
const normalized = this.normalizePath(path);
|
|
144
|
+
const segments = parseSegments(normalized, this.opts.caseSensitive);
|
|
145
|
+
let node = this.root;
|
|
146
|
+
for (const seg of segments) {
|
|
147
|
+
if (seg.type === NodeType.PARAM) {
|
|
148
|
+
if (!node.paramChild) {
|
|
149
|
+
node.paramChild = createNode(seg.segment, NodeType.PARAM);
|
|
150
|
+
node.paramChild.paramName = seg.paramName;
|
|
151
|
+
} else if (node.paramChild.paramName !== seg.paramName) {
|
|
152
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
153
|
+
const existing = node.paramChild.paramName;
|
|
154
|
+
console.warn(`[nextrush:router] Route param name conflict at "${normalized}": ":${String(seg.paramName)}" conflicts with existing ":${String(existing)}". The existing name ":${String(existing)}" will be used.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
node = node.paramChild;
|
|
158
|
+
} else if (seg.type === NodeType.WILDCARD) {
|
|
159
|
+
node.wildcardChild ??= createNode("*", NodeType.WILDCARD);
|
|
160
|
+
node = node.wildcardChild;
|
|
161
|
+
break;
|
|
162
|
+
} else {
|
|
163
|
+
const key = seg.segment;
|
|
164
|
+
let child = node.children.get(key);
|
|
165
|
+
if (!child) {
|
|
166
|
+
child = createNode(seg.segment, NodeType.STATIC);
|
|
167
|
+
node.children.set(key, child);
|
|
168
|
+
}
|
|
169
|
+
node = child;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const combinedMiddleware = [
|
|
173
|
+
...middleware
|
|
174
|
+
];
|
|
175
|
+
const finalHandler = handlers[handlers.length - 1];
|
|
176
|
+
if (!finalHandler) {
|
|
177
|
+
throw new Error("At least one handler is required");
|
|
178
|
+
}
|
|
179
|
+
const inlineMiddleware = handlers.slice(0, -1);
|
|
180
|
+
for (const mw of inlineMiddleware) {
|
|
181
|
+
combinedMiddleware.push(mw);
|
|
182
|
+
}
|
|
183
|
+
const executor = compileExecutor(finalHandler, combinedMiddleware);
|
|
184
|
+
const entry = {
|
|
185
|
+
handler: finalHandler,
|
|
186
|
+
middleware: combinedMiddleware,
|
|
187
|
+
executor
|
|
188
|
+
};
|
|
189
|
+
if (node.handlers.has(method)) {
|
|
190
|
+
throw new Error(`Route conflict: ${method} ${normalized} is already registered. Remove the duplicate or use a different path.`);
|
|
191
|
+
}
|
|
192
|
+
node.handlers.set(method, entry);
|
|
193
|
+
const hasParams = segments.some((s) => s.type === NodeType.PARAM || s.type === NodeType.WILDCARD);
|
|
194
|
+
if (hasParams) {
|
|
195
|
+
this.hasParamRoutes = true;
|
|
196
|
+
} else {
|
|
197
|
+
const normalizedKey = this.opts.caseSensitive ? normalized : normalized.toLowerCase();
|
|
198
|
+
this.staticRoutes.set(`${method} ${normalizedKey}`, entry);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ===========================================================================
|
|
202
|
+
// HTTP Method Shortcuts
|
|
203
|
+
// ===========================================================================
|
|
204
|
+
get(path, ...handlers) {
|
|
205
|
+
this.addRoute("GET", path, handlers);
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
post(path, ...handlers) {
|
|
209
|
+
this.addRoute("POST", path, handlers);
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
put(path, ...handlers) {
|
|
213
|
+
this.addRoute("PUT", path, handlers);
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
delete(path, ...handlers) {
|
|
217
|
+
this.addRoute("DELETE", path, handlers);
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
patch(path, ...handlers) {
|
|
221
|
+
this.addRoute("PATCH", path, handlers);
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
head(path, ...handlers) {
|
|
225
|
+
this.addRoute("HEAD", path, handlers);
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
options(path, ...handlers) {
|
|
229
|
+
this.addRoute("OPTIONS", path, handlers);
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
all(path, ...handlers) {
|
|
233
|
+
for (const method of HTTP_METHODS) {
|
|
234
|
+
this.addRoute(method, path, handlers);
|
|
235
|
+
}
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
route(method, path, ...handlers) {
|
|
239
|
+
this.addRoute(method, path, handlers);
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Register a redirect route from one path to another
|
|
244
|
+
*
|
|
245
|
+
* @param from - Source path to redirect from
|
|
246
|
+
* @param to - Target path or URL to redirect to
|
|
247
|
+
* @param status - HTTP status code (default: 301 permanent redirect)
|
|
248
|
+
* @returns this for chaining
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* // Permanent redirect (301)
|
|
253
|
+
* router.redirect('/old-page', '/new-page');
|
|
254
|
+
*
|
|
255
|
+
* // Temporary redirect (302)
|
|
256
|
+
* router.redirect('/temp', '/destination', 302);
|
|
257
|
+
*
|
|
258
|
+
* // Redirect to external URL
|
|
259
|
+
* router.redirect('/docs', 'https://docs.example.com');
|
|
260
|
+
*
|
|
261
|
+
* // With parameters - redirects /users/:id to /profiles/:id
|
|
262
|
+
* router.redirect('/users/:id', '/profiles/:id');
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
redirect(from, to, status = 301) {
|
|
266
|
+
let compiledParts;
|
|
267
|
+
{
|
|
268
|
+
const parts = [];
|
|
269
|
+
let pos = 0;
|
|
270
|
+
let found = false;
|
|
271
|
+
while (pos < to.length) {
|
|
272
|
+
let idx = -1;
|
|
273
|
+
for (let i = pos; i < to.length; i++) {
|
|
274
|
+
if (to[i] === ":" && (i === 0 || to[i - 1] === "/") && i + 1 < to.length && to[i + 1] !== "/") {
|
|
275
|
+
idx = i;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (idx === -1) break;
|
|
280
|
+
found = true;
|
|
281
|
+
parts.push(to.slice(pos, idx));
|
|
282
|
+
const end = to.indexOf("/", idx + 1);
|
|
283
|
+
if (end === -1) {
|
|
284
|
+
parts.push(to.slice(idx + 1));
|
|
285
|
+
pos = to.length;
|
|
286
|
+
} else {
|
|
287
|
+
parts.push(to.slice(idx + 1, end));
|
|
288
|
+
pos = end;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (found) {
|
|
292
|
+
parts.push(to.slice(pos));
|
|
293
|
+
compiledParts = parts;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const redirectHandler = /* @__PURE__ */ __name((ctx) => {
|
|
297
|
+
let targetPath;
|
|
298
|
+
if (compiledParts) {
|
|
299
|
+
const params = ctx.params;
|
|
300
|
+
const parts = compiledParts;
|
|
301
|
+
const head = parts[0];
|
|
302
|
+
if (head === void 0) {
|
|
303
|
+
targetPath = to;
|
|
304
|
+
} else {
|
|
305
|
+
let result = head;
|
|
306
|
+
for (let i = 1; i < parts.length - 1; i += 2) {
|
|
307
|
+
const paramKey = parts[i];
|
|
308
|
+
const tail = parts[i + 1];
|
|
309
|
+
if (paramKey === void 0 || tail === void 0) break;
|
|
310
|
+
result += (params[paramKey] ?? "") + tail;
|
|
311
|
+
}
|
|
312
|
+
targetPath = result;
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
targetPath = to;
|
|
316
|
+
}
|
|
317
|
+
ctx.status = status;
|
|
318
|
+
ctx.set("Location", targetPath);
|
|
319
|
+
ctx.body = "";
|
|
320
|
+
}, "redirectHandler");
|
|
321
|
+
this.addRoute("GET", from, [
|
|
322
|
+
redirectHandler
|
|
323
|
+
]);
|
|
324
|
+
this.addRoute("HEAD", from, [
|
|
325
|
+
redirectHandler
|
|
326
|
+
]);
|
|
327
|
+
if (status === 307 || status === 308) {
|
|
328
|
+
this.addRoute("POST", from, [
|
|
329
|
+
redirectHandler
|
|
330
|
+
]);
|
|
331
|
+
this.addRoute("PUT", from, [
|
|
332
|
+
redirectHandler
|
|
333
|
+
]);
|
|
334
|
+
this.addRoute("PATCH", from, [
|
|
335
|
+
redirectHandler
|
|
336
|
+
]);
|
|
337
|
+
this.addRoute("DELETE", from, [
|
|
338
|
+
redirectHandler
|
|
339
|
+
]);
|
|
340
|
+
}
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
// ===========================================================================
|
|
344
|
+
// Router Composition
|
|
345
|
+
// ===========================================================================
|
|
346
|
+
use(pathOrMiddleware, routerOrUndefined) {
|
|
347
|
+
if (typeof pathOrMiddleware === "function") {
|
|
348
|
+
this.routerMiddleware.push(pathOrMiddleware);
|
|
349
|
+
} else if (typeof pathOrMiddleware === "string" && routerOrUndefined instanceof _Router) {
|
|
350
|
+
this.mountRouter(pathOrMiddleware, routerOrUndefined);
|
|
351
|
+
} else if (typeof pathOrMiddleware === "string") {
|
|
352
|
+
throw new Error(`router.use('${pathOrMiddleware}', ...) requires a Router instance as the second argument. Use router.group(prefix, callback) for prefix-scoped middleware, or router.use(middlewareFn) to register middleware without a prefix.`);
|
|
353
|
+
} else if (pathOrMiddleware instanceof _Router) {
|
|
354
|
+
this.mountRouter("", pathOrMiddleware);
|
|
355
|
+
}
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Mount a sub-router at a path prefix (Hono-style)
|
|
360
|
+
*
|
|
361
|
+
* This is the explicit API for mounting sub-routers.
|
|
362
|
+
* Equivalent to `router.use(path, subRouter)` but more semantic.
|
|
363
|
+
*
|
|
364
|
+
* @param path - Path prefix for the sub-router
|
|
365
|
+
* @param router - Router instance to mount
|
|
366
|
+
* @returns this for chaining
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* // Create modular routers
|
|
371
|
+
* const users = createRouter();
|
|
372
|
+
* users.get('/', listUsers);
|
|
373
|
+
* users.get('/:id', getUser);
|
|
374
|
+
*
|
|
375
|
+
* const posts = createRouter();
|
|
376
|
+
* posts.get('/', listPosts);
|
|
377
|
+
*
|
|
378
|
+
* // Mount sub-routers
|
|
379
|
+
* const api = createRouter();
|
|
380
|
+
* api.mount('/users', users);
|
|
381
|
+
* api.mount('/posts', posts);
|
|
382
|
+
*
|
|
383
|
+
* // Or use on main router
|
|
384
|
+
* const router = createRouter();
|
|
385
|
+
* router.mount('/api', api);
|
|
386
|
+
*
|
|
387
|
+
* app.use(router.routes());
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
mount(path, router) {
|
|
391
|
+
this.mountRouter(path, router);
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Mount a sub-router (internal)
|
|
396
|
+
*
|
|
397
|
+
* Carries the sub-router's own `routerMiddleware` forward so that
|
|
398
|
+
* `subrouter.use(mw)` middleware applies to every copied route.
|
|
399
|
+
*/
|
|
400
|
+
mountRouter(prefix, router) {
|
|
401
|
+
this.copyRoutes(router.root, prefix, [], router.routerMiddleware);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Recursively copy routes from another router
|
|
405
|
+
*/
|
|
406
|
+
copyRoutes(node, prefix, segments, subRouterMiddleware) {
|
|
407
|
+
for (const [method, entry] of node.handlers) {
|
|
408
|
+
const path = prefix + "/" + segments.join("/");
|
|
409
|
+
const combined = subRouterMiddleware.length > 0 ? [
|
|
410
|
+
...subRouterMiddleware,
|
|
411
|
+
...entry.middleware
|
|
412
|
+
] : entry.middleware;
|
|
413
|
+
this.addRoute(method, path || "/", [
|
|
414
|
+
entry.handler
|
|
415
|
+
], combined);
|
|
416
|
+
}
|
|
417
|
+
for (const [, child] of node.children) {
|
|
418
|
+
this.copyRoutes(child, prefix, [
|
|
419
|
+
...segments,
|
|
420
|
+
child.segment
|
|
421
|
+
], subRouterMiddleware);
|
|
422
|
+
}
|
|
423
|
+
if (node.paramChild) {
|
|
424
|
+
this.copyRoutes(node.paramChild, prefix, [
|
|
425
|
+
...segments,
|
|
426
|
+
node.paramChild.segment
|
|
427
|
+
], subRouterMiddleware);
|
|
428
|
+
}
|
|
429
|
+
if (node.wildcardChild) {
|
|
430
|
+
this.copyRoutes(node.wildcardChild, prefix, [
|
|
431
|
+
...segments,
|
|
432
|
+
"*"
|
|
433
|
+
], subRouterMiddleware);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// ===========================================================================
|
|
437
|
+
// Route Matching
|
|
438
|
+
// ===========================================================================
|
|
439
|
+
/**
|
|
440
|
+
* Match a route and return handler + params
|
|
441
|
+
*/
|
|
442
|
+
match(method, path) {
|
|
443
|
+
const isCaseInsensitive = !this.opts.caseSensitive;
|
|
444
|
+
let normalized = isCaseInsensitive ? path.toLowerCase() : path;
|
|
445
|
+
if (normalized.includes("//")) {
|
|
446
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
447
|
+
}
|
|
448
|
+
if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
|
|
449
|
+
normalized = normalized.slice(0, -1);
|
|
450
|
+
}
|
|
451
|
+
const staticKey = normalized.length > 1 && normalized.endsWith("/") ? `${method} ${normalized.slice(0, -1)}` : `${method} ${normalized}`;
|
|
452
|
+
const staticEntry = this.staticRoutes.get(staticKey);
|
|
453
|
+
if (staticEntry) {
|
|
454
|
+
return {
|
|
455
|
+
handler: staticEntry.handler,
|
|
456
|
+
params: EMPTY_PARAMS,
|
|
457
|
+
middleware: this.routerMiddleware,
|
|
458
|
+
executor: staticEntry.executor
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (!this.hasParamRoutes) return null;
|
|
462
|
+
const params = {};
|
|
463
|
+
let originalPath;
|
|
464
|
+
if (isCaseInsensitive) {
|
|
465
|
+
originalPath = path;
|
|
466
|
+
if (originalPath.includes("//")) {
|
|
467
|
+
originalPath = originalPath.replace(/\/+/g, "/");
|
|
468
|
+
}
|
|
469
|
+
if (!this.opts.strict && originalPath.length > 1 && originalPath.endsWith("/")) {
|
|
470
|
+
originalPath = originalPath.slice(0, -1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const result = this.matchNodeIndexed(this.root, normalized, 1, params, method, originalPath);
|
|
474
|
+
if (!result) return null;
|
|
475
|
+
let hasParams = false;
|
|
476
|
+
for (const key of Object.keys(params)) {
|
|
477
|
+
if (params[key] === void 0) {
|
|
478
|
+
Reflect.deleteProperty(params, key);
|
|
479
|
+
} else {
|
|
480
|
+
hasParams = true;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
handler: result.handler,
|
|
485
|
+
params: hasParams ? params : EMPTY_PARAMS,
|
|
486
|
+
middleware: this.routerMiddleware,
|
|
487
|
+
executor: result.executor
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Extract the next segment from path at given position without allocating arrays.
|
|
492
|
+
* Returns [segment, nextIndex] where nextIndex is position after the trailing '/'.
|
|
493
|
+
*/
|
|
494
|
+
extractSegment(path, start) {
|
|
495
|
+
const slashPos = path.indexOf("/", start);
|
|
496
|
+
if (slashPos === -1) {
|
|
497
|
+
return [
|
|
498
|
+
path.slice(start),
|
|
499
|
+
path.length
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
return [
|
|
503
|
+
path.slice(start, slashPos),
|
|
504
|
+
slashPos + 1
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Index-based recursive node matching (avoids array allocation)
|
|
509
|
+
*/
|
|
510
|
+
matchNodeIndexed(node, path, pos, params, method, originalPath) {
|
|
511
|
+
if (pos >= path.length) {
|
|
512
|
+
return node.handlers.get(method) ?? null;
|
|
513
|
+
}
|
|
514
|
+
const [segment, nextPos] = this.extractSegment(path, pos);
|
|
515
|
+
if (segment === "") return node.handlers.get(method) ?? null;
|
|
516
|
+
const staticChild = node.children.get(segment);
|
|
517
|
+
if (staticChild) {
|
|
518
|
+
const result = this.matchNodeIndexed(staticChild, path, nextPos, params, method, originalPath);
|
|
519
|
+
if (result) return result;
|
|
520
|
+
}
|
|
521
|
+
if (node.paramChild) {
|
|
522
|
+
const paramName = node.paramChild.paramName;
|
|
523
|
+
if (paramName === void 0) return null;
|
|
524
|
+
if (originalPath) {
|
|
525
|
+
const [origSeg] = this.extractSegment(originalPath, pos);
|
|
526
|
+
params[paramName] = origSeg;
|
|
527
|
+
} else {
|
|
528
|
+
params[paramName] = segment;
|
|
529
|
+
}
|
|
530
|
+
const result = this.matchNodeIndexed(node.paramChild, path, nextPos, params, method, originalPath);
|
|
531
|
+
if (result) return result;
|
|
532
|
+
Reflect.deleteProperty(params, paramName);
|
|
533
|
+
}
|
|
534
|
+
if (node.wildcardChild) {
|
|
535
|
+
const src = originalPath ?? path;
|
|
536
|
+
params["*"] = src.slice(pos);
|
|
537
|
+
return node.wildcardChild.handlers.get(method) ?? null;
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
// ===========================================================================
|
|
542
|
+
// Middleware Generation
|
|
543
|
+
// ===========================================================================
|
|
544
|
+
/**
|
|
545
|
+
* Get routes middleware function
|
|
546
|
+
* Mount this on the application
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* ```typescript
|
|
550
|
+
* app.use(router.routes());
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
553
|
+
routes() {
|
|
554
|
+
const hasRouterMiddleware = this.routerMiddleware.length > 0;
|
|
555
|
+
if (hasRouterMiddleware) {
|
|
556
|
+
this.sealRouterMiddleware();
|
|
557
|
+
}
|
|
558
|
+
return async (ctx, next) => {
|
|
559
|
+
const match = this.match(ctx.method, ctx.path);
|
|
560
|
+
if (!match) {
|
|
561
|
+
ctx.status = 404;
|
|
562
|
+
if (next) await next();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
ctx.params = match.params;
|
|
566
|
+
if (match.executor) {
|
|
567
|
+
await match.executor(ctx);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
await match.handler(ctx, NOOP_NEXT);
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Re-compile all route executors to include router-level middleware.
|
|
575
|
+
* Called once when routes() is invoked, not per-request.
|
|
576
|
+
*/
|
|
577
|
+
sealRouterMiddleware() {
|
|
578
|
+
const routerMw = [
|
|
579
|
+
...this.routerMiddleware
|
|
580
|
+
];
|
|
581
|
+
const walk = /* @__PURE__ */ __name((node) => {
|
|
582
|
+
for (const [method, entry] of node.handlers) {
|
|
583
|
+
const combinedMw = [
|
|
584
|
+
...routerMw,
|
|
585
|
+
...entry.middleware
|
|
586
|
+
];
|
|
587
|
+
entry.executor = compileExecutor(entry.handler, combinedMw);
|
|
588
|
+
node.handlers.set(method, entry);
|
|
589
|
+
}
|
|
590
|
+
for (const [, child] of node.children) {
|
|
591
|
+
walk(child);
|
|
592
|
+
}
|
|
593
|
+
if (node.paramChild) walk(node.paramChild);
|
|
594
|
+
if (node.wildcardChild) walk(node.wildcardChild);
|
|
595
|
+
}, "walk");
|
|
596
|
+
walk(this.root);
|
|
597
|
+
for (const [key, entry] of this.staticRoutes) {
|
|
598
|
+
const combinedMw = [
|
|
599
|
+
...routerMw,
|
|
600
|
+
...entry.middleware
|
|
601
|
+
];
|
|
602
|
+
entry.executor = compileExecutor(entry.handler, combinedMw);
|
|
603
|
+
this.staticRoutes.set(key, entry);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Generate allowed methods middleware
|
|
608
|
+
* Responds to OPTIONS and sets Allow header
|
|
609
|
+
*/
|
|
610
|
+
allowedMethods() {
|
|
611
|
+
return async (ctx, next) => {
|
|
612
|
+
if (next) {
|
|
613
|
+
await next();
|
|
614
|
+
}
|
|
615
|
+
if (ctx.status !== 404) return;
|
|
616
|
+
const allowed = this.findAllowedMethods(ctx.path);
|
|
617
|
+
if (allowed.length === 0) return;
|
|
618
|
+
const allowHeader = allowed.join(", ");
|
|
619
|
+
if (ctx.method === "OPTIONS") {
|
|
620
|
+
ctx.status = 200;
|
|
621
|
+
ctx.set("Allow", allowHeader);
|
|
622
|
+
ctx.body = "";
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
ctx.status = 405;
|
|
626
|
+
ctx.set("Allow", allowHeader);
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Find all HTTP methods registered for a given path via single tree walk
|
|
631
|
+
* @internal
|
|
632
|
+
*/
|
|
633
|
+
findAllowedMethods(path) {
|
|
634
|
+
let normalized = this.opts.caseSensitive ? path : path.toLowerCase();
|
|
635
|
+
if (normalized.includes("//")) {
|
|
636
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
637
|
+
}
|
|
638
|
+
if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
|
|
639
|
+
normalized = normalized.slice(0, -1);
|
|
640
|
+
}
|
|
641
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
642
|
+
const node = this.findNode(this.root, segments, 0);
|
|
643
|
+
if (!node || node.handlers.size === 0) return [];
|
|
644
|
+
return Array.from(node.handlers.keys());
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Walk the tree to find the node matching a path (ignoring HTTP method)
|
|
648
|
+
* @internal
|
|
649
|
+
*/
|
|
650
|
+
findNode(node, segments, index) {
|
|
651
|
+
if (index === segments.length) {
|
|
652
|
+
return node;
|
|
653
|
+
}
|
|
654
|
+
const segment = segments[index];
|
|
655
|
+
if (segment === void 0) return null;
|
|
656
|
+
const staticChild = node.children.get(segment);
|
|
657
|
+
if (staticChild) {
|
|
658
|
+
const result = this.findNode(staticChild, segments, index + 1);
|
|
659
|
+
if (result) return result;
|
|
660
|
+
}
|
|
661
|
+
if (node.paramChild) {
|
|
662
|
+
const result = this.findNode(node.paramChild, segments, index + 1);
|
|
663
|
+
if (result) return result;
|
|
664
|
+
}
|
|
665
|
+
if (node.wildcardChild) {
|
|
666
|
+
return node.wildcardChild;
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
// ===========================================================================
|
|
671
|
+
// Route Groups
|
|
672
|
+
// ===========================================================================
|
|
673
|
+
/**
|
|
674
|
+
* Create a route group with shared prefix and middleware
|
|
675
|
+
*
|
|
676
|
+
* @param prefix - Path prefix for all routes in the group
|
|
677
|
+
* @param middlewareOrCallback - Middleware array or callback function
|
|
678
|
+
* @param callback - Callback function if middleware is provided
|
|
679
|
+
* @returns this for chaining
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```typescript
|
|
683
|
+
* // Simple group with prefix
|
|
684
|
+
* router.group('/api', (r) => {
|
|
685
|
+
* r.get('/users', listUsers);
|
|
686
|
+
* r.get('/posts', listPosts);
|
|
687
|
+
* });
|
|
688
|
+
*
|
|
689
|
+
* // Group with middleware
|
|
690
|
+
* router.group('/admin', [authMiddleware], (r) => {
|
|
691
|
+
* r.get('/dashboard', dashboard);
|
|
692
|
+
* r.post('/settings', updateSettings);
|
|
693
|
+
* });
|
|
694
|
+
*
|
|
695
|
+
* // Nested groups
|
|
696
|
+
* router.group('/api/v1', (r) => {
|
|
697
|
+
* r.group('/users', [rateLimit], (ur) => {
|
|
698
|
+
* ur.get('/', listUsers);
|
|
699
|
+
* ur.get('/:id', getUser);
|
|
700
|
+
* });
|
|
701
|
+
* });
|
|
702
|
+
* ```
|
|
703
|
+
*/
|
|
704
|
+
group(prefix, middlewareOrCallback, callback) {
|
|
705
|
+
let middleware = [];
|
|
706
|
+
let cb;
|
|
707
|
+
if (Array.isArray(middlewareOrCallback)) {
|
|
708
|
+
middleware = middlewareOrCallback;
|
|
709
|
+
if (!callback) {
|
|
710
|
+
throw new Error("Callback function is required when providing middleware array");
|
|
711
|
+
}
|
|
712
|
+
cb = callback;
|
|
713
|
+
} else {
|
|
714
|
+
cb = middlewareOrCallback;
|
|
715
|
+
}
|
|
716
|
+
const groupRouter = new GroupRouter(this, prefix, middleware);
|
|
717
|
+
cb(groupRouter);
|
|
718
|
+
return this;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Remove all registered routes and middleware, resetting the router to its initial state.
|
|
722
|
+
* Useful for plugin `destroy()` to cleanly un-register routes.
|
|
723
|
+
*/
|
|
724
|
+
reset() {
|
|
725
|
+
this.root.children.clear();
|
|
726
|
+
this.root.handlers.clear();
|
|
727
|
+
this.root.paramChild = void 0;
|
|
728
|
+
this.root.wildcardChild = void 0;
|
|
729
|
+
this.staticRoutes.clear();
|
|
730
|
+
this.routerMiddleware.length = 0;
|
|
731
|
+
this.hasParamRoutes = false;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Internal method to add route with group context
|
|
735
|
+
* @internal
|
|
736
|
+
*/
|
|
737
|
+
_addGroupRoute(method, path, handlers, groupMiddleware) {
|
|
738
|
+
this.addRoute(method, path, handlers, groupMiddleware);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
var GroupRouter = class GroupRouter2 {
|
|
742
|
+
static {
|
|
743
|
+
__name(this, "GroupRouter");
|
|
744
|
+
}
|
|
745
|
+
parent;
|
|
746
|
+
prefix;
|
|
747
|
+
middleware;
|
|
748
|
+
constructor(parent, prefix, middleware) {
|
|
749
|
+
this.parent = parent;
|
|
750
|
+
this.prefix = prefix;
|
|
751
|
+
this.middleware = middleware;
|
|
752
|
+
}
|
|
753
|
+
fullPath(path) {
|
|
754
|
+
if (path === "/" || path === "") {
|
|
755
|
+
return this.prefix;
|
|
756
|
+
}
|
|
757
|
+
const cleanPrefix = this.prefix.endsWith("/") ? this.prefix.slice(0, -1) : this.prefix;
|
|
758
|
+
const cleanPath = path.startsWith("/") ? path : "/" + path;
|
|
759
|
+
return cleanPrefix + cleanPath;
|
|
760
|
+
}
|
|
761
|
+
get(path, ...handlers) {
|
|
762
|
+
this.parent._addGroupRoute("GET", this.fullPath(path), handlers, this.middleware);
|
|
763
|
+
return this;
|
|
764
|
+
}
|
|
765
|
+
post(path, ...handlers) {
|
|
766
|
+
this.parent._addGroupRoute("POST", this.fullPath(path), handlers, this.middleware);
|
|
767
|
+
return this;
|
|
768
|
+
}
|
|
769
|
+
put(path, ...handlers) {
|
|
770
|
+
this.parent._addGroupRoute("PUT", this.fullPath(path), handlers, this.middleware);
|
|
771
|
+
return this;
|
|
772
|
+
}
|
|
773
|
+
delete(path, ...handlers) {
|
|
774
|
+
this.parent._addGroupRoute("DELETE", this.fullPath(path), handlers, this.middleware);
|
|
775
|
+
return this;
|
|
776
|
+
}
|
|
777
|
+
patch(path, ...handlers) {
|
|
778
|
+
this.parent._addGroupRoute("PATCH", this.fullPath(path), handlers, this.middleware);
|
|
779
|
+
return this;
|
|
780
|
+
}
|
|
781
|
+
head(path, ...handlers) {
|
|
782
|
+
this.parent._addGroupRoute("HEAD", this.fullPath(path), handlers, this.middleware);
|
|
783
|
+
return this;
|
|
784
|
+
}
|
|
785
|
+
options(path, ...handlers) {
|
|
786
|
+
this.parent._addGroupRoute("OPTIONS", this.fullPath(path), handlers, this.middleware);
|
|
787
|
+
return this;
|
|
788
|
+
}
|
|
789
|
+
all(path, ...handlers) {
|
|
790
|
+
for (const method of HTTP_METHODS) {
|
|
791
|
+
this.parent._addGroupRoute(method, this.fullPath(path), handlers, this.middleware);
|
|
792
|
+
}
|
|
793
|
+
return this;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Register a redirect within the group
|
|
797
|
+
*/
|
|
798
|
+
redirect(from, to, status = 301) {
|
|
799
|
+
const redirectHandler = /* @__PURE__ */ __name((ctx) => {
|
|
800
|
+
let targetPath = to;
|
|
801
|
+
if (targetPath.includes(":")) {
|
|
802
|
+
const entries = Object.entries(ctx.params).sort((a, b) => b[0].length - a[0].length);
|
|
803
|
+
for (const [key, value] of entries) {
|
|
804
|
+
targetPath = targetPath.replaceAll(`:${key}`, value);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
ctx.status = status;
|
|
808
|
+
ctx.set("Location", targetPath);
|
|
809
|
+
ctx.body = "";
|
|
810
|
+
}, "redirectHandler");
|
|
811
|
+
this.parent._addGroupRoute("GET", this.fullPath(from), [
|
|
812
|
+
redirectHandler
|
|
813
|
+
], this.middleware);
|
|
814
|
+
this.parent._addGroupRoute("HEAD", this.fullPath(from), [
|
|
815
|
+
redirectHandler
|
|
816
|
+
], this.middleware);
|
|
817
|
+
return this;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Nested group support
|
|
821
|
+
*/
|
|
822
|
+
group(prefix, middlewareOrCallback, callback) {
|
|
823
|
+
let nestedMiddleware = [];
|
|
824
|
+
let cb;
|
|
825
|
+
if (Array.isArray(middlewareOrCallback)) {
|
|
826
|
+
nestedMiddleware = middlewareOrCallback;
|
|
827
|
+
if (!callback) {
|
|
828
|
+
throw new Error("Callback function is required when providing middleware array");
|
|
829
|
+
}
|
|
830
|
+
cb = callback;
|
|
831
|
+
} else {
|
|
832
|
+
cb = middlewareOrCallback;
|
|
833
|
+
}
|
|
834
|
+
const nestedRouter = new GroupRouter2(this.parent, this.fullPath(prefix), [
|
|
835
|
+
...this.middleware,
|
|
836
|
+
...nestedMiddleware
|
|
837
|
+
]);
|
|
838
|
+
cb(nestedRouter);
|
|
839
|
+
return this;
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
function createRouter(options) {
|
|
843
|
+
return new Router(options);
|
|
844
|
+
}
|
|
845
|
+
__name(createRouter, "createRouter");
|
|
846
|
+
export {
|
|
847
|
+
NodeType,
|
|
848
|
+
Router,
|
|
849
|
+
createNode,
|
|
850
|
+
createRouter,
|
|
851
|
+
parseSegments
|
|
852
|
+
};
|
|
853
|
+
//# sourceMappingURL=index.js.map
|