@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/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