@nextrush/router 3.0.7 → 4.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +250 -492
  2. package/dist/index.d.ts +227 -178
  3. package/dist/index.js +1075 -657
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -6
  6. package/src/__tests__/allowed-methods.test.ts +144 -0
  7. package/src/__tests__/audit-fixes.test.ts +59 -0
  8. package/src/__tests__/canonical-path-security.test.ts +198 -0
  9. package/src/__tests__/canonicalize-memo.test.ts +108 -0
  10. package/src/__tests__/dispatch-deasync.test.ts +292 -0
  11. package/src/__tests__/find-node-differential.test.ts +220 -0
  12. package/src/__tests__/fixtures/match-golden.json +556 -0
  13. package/src/__tests__/head-auto-registration.test.ts +197 -0
  14. package/src/__tests__/helpers/differential-corpus.ts +314 -0
  15. package/src/__tests__/match-differential.test.ts +46 -0
  16. package/src/__tests__/match-hotpath-guard.test.ts +71 -0
  17. package/src/__tests__/match-node-indexed-unpooled.test.ts +83 -0
  18. package/src/__tests__/match-normalize-fastpath.test.ts +64 -0
  19. package/src/__tests__/match-prenormalized.test.ts +95 -0
  20. package/src/__tests__/match-safety.test.ts +223 -0
  21. package/src/__tests__/match-single-alloc.test.ts +82 -0
  22. package/src/__tests__/match-walk-pool-safety.test.ts +103 -0
  23. package/src/__tests__/middleware-pipeline.test.ts +306 -0
  24. package/src/__tests__/param-decoding.test.ts +78 -0
  25. package/src/__tests__/public-surface.test.ts +72 -0
  26. package/src/__tests__/registration-max-depth.test.ts +56 -0
  27. package/src/__tests__/route-metadata.test.ts +172 -0
  28. package/src/__tests__/router-audit.test.ts +315 -0
  29. package/src/__tests__/router-edge-cases.test.ts +2 -7
  30. package/src/__tests__/router.test.ts +148 -7
  31. package/src/__tests__/static-map-reset.test.ts +48 -0
  32. package/src/__tests__/walk-pool-sizing.test.ts +72 -0
  33. package/src/__tests__/walk-pool-undersized-guard.test.ts +59 -0
  34. package/src/canonicalize.ts +137 -0
  35. package/src/composition.ts +79 -0
  36. package/src/constants.ts +43 -0
  37. package/src/dispatch.ts +145 -0
  38. package/src/find-node.ts +125 -0
  39. package/src/group-router.ts +208 -0
  40. package/src/index.ts +14 -5
  41. package/src/match-route.ts +241 -0
  42. package/src/matching.ts +246 -0
  43. package/src/middleware-adapter.ts +59 -0
  44. package/src/redirect.ts +97 -0
  45. package/src/registration.ts +343 -0
  46. package/src/route-metadata.ts +68 -0
  47. package/src/router.ts +219 -872
  48. package/src/segment-trie.ts +227 -0
  49. package/src/state.ts +53 -0
  50. package/src/walk-pool.ts +208 -0
  51. package/src/radix-tree.ts +0 -184
package/src/router.ts CHANGED
@@ -1,1028 +1,375 @@
1
1
  /**
2
2
  * @nextrush/router - Router Implementation
3
3
  *
4
- * High-performance router using a segment trie for route matching.
5
- * Routes are keyed by full path segments (e.g. "users", ":id"), not by
6
- * individual characters this is a segment-based trie, not a compressed
7
- * radix tree. Supports parameters, wildcards, and method-based routing.
4
+ * The public `Router` shell: a thin, chainable facade delegating registration,
5
+ * matching, dispatch, composition, and grouping to focused sibling modules
6
+ * (design.md D1/D2). Segment trie keyed by whole path segments, not a radix tree.
8
7
  *
9
8
  * @packageDocumentation
10
9
  */
11
10
 
12
11
  import {
13
- HTTP_METHODS,
14
- type Context,
15
- type HttpMethod,
16
- type Middleware,
17
- type RouteHandler,
18
- type RouteMatch,
19
- type RouterOptions,
12
+ HTTP_METHODS,
13
+ type HttpMethod,
14
+ type Middleware,
15
+ type RouteDefinition,
16
+ type RouteEntry,
17
+ type RouteHandler,
18
+ type RouteMatch,
19
+ type RouterOptions,
20
20
  } from '@nextrush/types';
21
+ import { clearNode, createNode, type StaticRouteMap, type TrieNode } from './segment-trie';
22
+ import { type RedirectStatus } from './redirect';
23
+ import { runRouteGroup, type RouteGroup } from './group-router';
24
+ import { resolveMatch, type MatchState } from './match-route';
25
+ import { createWalkPool } from './walk-pool';
26
+ import { copyRoutes } from './composition';
27
+ import { sealRouterMiddleware as sealRouterMiddlewareImpl } from './middleware-adapter';
21
28
  import {
22
- compileExecutor,
23
- createNode,
24
- NodeType,
25
- NOOP_NEXT,
26
- parseSegments,
27
- type HandlerEntry,
28
- type RadixNode,
29
- } from './radix-tree';
30
-
31
- /** Frozen empty params for static routes — avoids allocation per request */
32
- const EMPTY_PARAMS: Record<string, string> = Object.freeze(
33
- Object.create(null) as Record<string, string>
34
- );
29
+ addRoute as addRouteImpl,
30
+ normalizeRegistrationPath,
31
+ pushAnyMethodDefinition,
32
+ registerRedirect,
33
+ type RegistrationState,
34
+ } from './registration';
35
+ import { createAllowedMethodsMiddleware, createRoutesMiddleware } from './dispatch';
36
+ import { createRouterState, resolveRouterOptions } from './state';
37
+ import { canonicalizePath } from './canonicalize';
38
+
39
+ /** '/'.charCodeAt(0) used by {@link Router.matchesMountPrefix}'s boundary check. */
40
+ const SLASH_CHAR_CODE = 0x2f;
41
+
42
+ /** Inline route metadata declaration — re-exported from its own module (RT-3). */
43
+ export { endpoint } from './route-metadata';
35
44
 
36
45
  /**
37
- * Router class high-performance segment trie router
38
- *
39
- * Routes are indexed by path segment, giving O(d) lookup where d is the
40
- * number of segments. Static routes are additionally stored in a hash map
41
- * for O(1) fast-path lookup.
42
- *
43
- * @example
44
- * ```typescript
45
- * const router = createRouter();
46
- *
47
- * router.get('/users', listUsers);
48
- * router.get('/users/:id', getUser);
49
- * router.post('/users', createUser);
50
- *
51
- * app.use(router.routes());
52
- * ```
46
+ * High-performance segment-trie router: O(d) lookup by path segment, with a
47
+ * static-route hash map for an O(1) fast path.
48
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md | @nextrush/router README}
53
49
  */
54
50
  export class Router {
55
- private readonly root: RadixNode;
51
+ private readonly root: TrieNode;
56
52
  private readonly opts: Required<RouterOptions>;
57
53
  private readonly routerMiddleware: Middleware[] = [];
58
54
 
55
+ /** Static-route fast path: method-nested map for O(1) lookup with no per-request key string (HP-9). */
56
+ private readonly staticRoutes: StaticRouteMap = new Map();
57
+
59
58
  /**
60
- * Static route hash map for O(1) lookup.
61
- * Key: "METHOD path" (e.g. "GET /users"), Value: HandlerEntry
59
+ * Introspection registry, kept SEPARATE from the hot-path trie/staticRoutes
60
+ * so request dispatch never reads metadata — only getRoutes() touches it.
62
61
  */
63
- private readonly staticRoutes = new Map<string, HandlerEntry>();
62
+ private readonly routeDefinitions: RouteDefinition[] = [];
64
63
 
65
64
  /** Whether any routes have params or wildcards (disables static-only fast path) */
66
65
  private hasParamRoutes = false;
67
66
 
68
- constructor(options: RouterOptions = {}) {
69
- this.root = createNode('');
70
- this.opts = {
71
- prefix: options.prefix ?? '',
72
- caseSensitive: options.caseSensitive ?? false,
73
- strict: options.strict ?? false,
74
- };
75
- }
67
+ /** Whether router-level middleware has already been sealed into executors (audit RT-7) */
68
+ private _sealed = false;
76
69
 
77
70
  /**
78
- * Normalize path based on router options
71
+ * Single-entry memo for {@link matchesMountPrefix}'s canonicalization of the
72
+ * mount prefix, which is fixed at registration time. Declared as two fields
73
+ * rather than an object so a miss stores without allocating.
79
74
  */
80
- private normalizePath(path: string): string {
81
- // Handle prefix with trailing slash and path with leading slash
82
- let prefix = this.opts.prefix;
83
- if (prefix.endsWith('/') && path.startsWith('/')) {
84
- prefix = prefix.slice(0, -1);
85
- }
75
+ private _prefixMemoRaw: string | undefined = undefined;
76
+ private _prefixMemoCanonical = '';
86
77
 
87
- let normalized = prefix + path;
88
-
89
- // Fast-path: skip regex when no double slashes (99%+ of requests)
90
- if (normalized.includes('//')) {
91
- normalized = normalized.replace(/\/+/g, '/');
92
- }
78
+ /** Memoized state the extracted registration/matching functions read (see {@link createRouterState}). */
79
+ private readonly state: RegistrationState & MatchState;
93
80
 
94
- // For non-strict mode during registration, remove trailing slash
95
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith('/')) {
96
- normalized = normalized.slice(0, -1);
97
- }
98
-
99
- return normalized.startsWith('/') ? normalized : '/' + normalized;
81
+ constructor(options: RouterOptions = {}) {
82
+ this.root = createNode('');
83
+ this.opts = resolveRouterOptions(options);
84
+ this.state = createRouterState(
85
+ this.root,
86
+ this.opts,
87
+ this.staticRoutes,
88
+ this.routeDefinitions,
89
+ this.routerMiddleware
90
+ );
100
91
  }
101
92
 
102
93
  /**
103
- * Add a route to the radix tree
94
+ * Validate + normalize a raw path, then delegate trie insertion to the
95
+ * extracted `addRoute` (design.md D2); flips `hasParamRoutes` from its return.
104
96
  */
105
97
  private addRoute(
106
98
  method: HttpMethod,
107
99
  path: string,
108
- handlers: RouteHandler[],
109
- middleware: Middleware[] = []
100
+ entries: RouteEntry[],
101
+ middleware: Middleware[] = [],
102
+ recordIntrospection = true
110
103
  ): void {
111
- const normalized = this.normalizePath(path);
112
- const segments = parseSegments(normalized, this.opts.caseSensitive);
113
-
114
- let node = this.root;
115
-
116
- for (const seg of segments) {
117
- if (seg.type === NodeType.PARAM) {
118
- if (!node.paramChild) {
119
- node.paramChild = createNode(seg.segment, NodeType.PARAM);
120
- node.paramChild.paramName = seg.paramName;
121
- } else if (node.paramChild.paramName !== seg.paramName) {
122
- // Warn about param name collision — same position, different names
123
- // This helps catch accidental mismatches like :id vs :userId
124
- if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') {
125
- const existing = node.paramChild.paramName;
126
- console.warn(
127
- `[nextrush:router] Route param name conflict at "${normalized}": ` +
128
- `":${String(seg.paramName)}" conflicts with existing ":${String(existing)}". ` +
129
- `The existing name ":${String(existing)}" will be used.`
130
- );
131
- }
132
- }
133
- node = node.paramChild;
134
- } else if (seg.type === NodeType.WILDCARD) {
135
- node.wildcardChild ??= createNode('*', NodeType.WILDCARD);
136
- node = node.wildcardChild;
137
- break; // Wildcard must be last
138
- } else {
139
- const key = seg.segment;
140
- let child = node.children.get(key);
141
- if (!child) {
142
- child = createNode(seg.segment, NodeType.STATIC);
143
- node.children.set(key, child);
144
- }
145
- node = child;
146
- }
147
- }
148
-
149
- // Combine multiple handlers into single handler with inline middleware
150
- const combinedMiddleware = [...middleware];
151
- const finalHandler = handlers[handlers.length - 1];
152
-
153
- if (!finalHandler) {
154
- throw new Error('At least one handler is required');
155
- }
156
-
157
- const inlineMiddleware = handlers.slice(0, -1);
158
-
159
- // Add inline middleware (handlers before the last one)
160
- for (const mw of inlineMiddleware) {
161
- combinedMiddleware.push(mw);
162
- }
163
-
164
- // Pre-compile executor at registration time (not per-request!)
165
- const executor = compileExecutor(finalHandler, combinedMiddleware);
166
-
167
- const entry: HandlerEntry = {
168
- handler: finalHandler,
169
- middleware: combinedMiddleware,
170
- executor,
171
- };
172
-
173
- // Detect duplicate route registration
174
- if (node.handlers.has(method)) {
175
- throw new Error(
176
- `Route conflict: ${method} ${normalized} is already registered. ` +
177
- 'Remove the duplicate or use a different path.'
104
+ // Guard untyped-JS callers: a non-string path would coerce to a bogus literal route.
105
+ const rawPath: unknown = path;
106
+ if (typeof rawPath !== 'string') {
107
+ throw new TypeError(
108
+ `Route path must be a string, received ${rawPath === null ? 'null' : typeof rawPath}.`
178
109
  );
179
110
  }
180
-
181
- node.handlers.set(method, entry);
182
-
183
- // Populate static route hash map for O(1) lookup
184
- const hasParams = segments.some(
185
- (s) => s.type === NodeType.PARAM || s.type === NodeType.WILDCARD
186
- );
187
- if (hasParams) {
111
+ const normalized = normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict);
112
+ const depthBefore = this.state.maxDepth;
113
+ if (addRouteImpl(method, normalized, entries, middleware, this.state, recordIntrospection)) {
188
114
  this.hasParamRoutes = true;
189
- } else {
190
- const normalizedKey = this.opts.caseSensitive ? normalized : normalized.toLowerCase();
191
- this.staticRoutes.set(`${method} ${normalizedKey}`, entry);
115
+ }
116
+ // Rebuild the pool only when maxDepth actually grew (F-02) — a cheap,
117
+ // registration-time-only check; never runs per-request.
118
+ if (this.state.maxDepth > depthBefore) {
119
+ this.state.walkPool = createWalkPool(this.state.maxDepth);
192
120
  }
193
121
  }
194
122
 
195
- // ===========================================================================
196
- // HTTP Method Shortcuts
197
- // ===========================================================================
198
-
199
- get(path: string, ...handlers: RouteHandler[]): this {
200
- this.addRoute('GET', path, handlers);
123
+ get(path: string, ...entries: RouteEntry[]): this {
124
+ this.addRoute('GET', path, entries);
201
125
  return this;
202
126
  }
203
127
 
204
- post(path: string, ...handlers: RouteHandler[]): this {
205
- this.addRoute('POST', path, handlers);
128
+ post(path: string, ...entries: RouteEntry[]): this {
129
+ this.addRoute('POST', path, entries);
206
130
  return this;
207
131
  }
208
132
 
209
- put(path: string, ...handlers: RouteHandler[]): this {
210
- this.addRoute('PUT', path, handlers);
133
+ put(path: string, ...entries: RouteEntry[]): this {
134
+ this.addRoute('PUT', path, entries);
211
135
  return this;
212
136
  }
213
137
 
214
- delete(path: string, ...handlers: RouteHandler[]): this {
215
- this.addRoute('DELETE', path, handlers);
138
+ delete(path: string, ...entries: RouteEntry[]): this {
139
+ this.addRoute('DELETE', path, entries);
216
140
  return this;
217
141
  }
218
142
 
219
- patch(path: string, ...handlers: RouteHandler[]): this {
220
- this.addRoute('PATCH', path, handlers);
143
+ patch(path: string, ...entries: RouteEntry[]): this {
144
+ this.addRoute('PATCH', path, entries);
221
145
  return this;
222
146
  }
223
147
 
224
- head(path: string, ...handlers: RouteHandler[]): this {
225
- this.addRoute('HEAD', path, handlers);
148
+ head(path: string, ...entries: RouteEntry[]): this {
149
+ this.addRoute('HEAD', path, entries);
226
150
  return this;
227
151
  }
228
152
 
229
- options(path: string, ...handlers: RouteHandler[]): this {
230
- this.addRoute('OPTIONS', path, handlers);
153
+ options(path: string, ...entries: RouteEntry[]): this {
154
+ this.addRoute('OPTIONS', path, entries);
231
155
  return this;
232
156
  }
233
157
 
234
- all(path: string, ...handlers: RouteHandler[]): this {
158
+ /**
159
+ * Register a route for every HTTP method under one consolidated `isAnyMethod`
160
+ * introspection row (T016) — matching is unchanged (see {@link pushAnyMethodDefinition}).
161
+ */
162
+ all(path: string, ...entries: RouteEntry[]): this {
163
+ // recordIntrospection=false: insert each per-method handler without its own
164
+ // introspection row; the single consolidated row below replaces all 7.
235
165
  for (const method of HTTP_METHODS) {
236
- this.addRoute(method, path, handlers);
166
+ this.addRoute(method, path, entries, [], false);
237
167
  }
168
+ pushAnyMethodDefinition(
169
+ this.routeDefinitions,
170
+ normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict)
171
+ );
238
172
  return this;
239
173
  }
240
174
 
241
- route(method: HttpMethod, path: string, ...handlers: RouteHandler[]): this {
242
- this.addRoute(method, path, handlers);
175
+ route(method: HttpMethod, path: string, ...entries: RouteEntry[]): this {
176
+ this.addRoute(method, path, entries);
243
177
  return this;
244
178
  }
245
179
 
246
180
  /**
247
- * Register a redirect route from one path to another
248
- *
249
- * @param from - Source path to redirect from
250
- * @param to - Target path or URL to redirect to
251
- * @param status - HTTP status code (default: 301 permanent redirect)
252
- * @returns this for chaining
253
- *
254
- * @example
255
- * ```typescript
256
- * // Permanent redirect (301)
257
- * router.redirect('/old-page', '/new-page');
258
- *
259
- * // Temporary redirect (302)
260
- * router.redirect('/temp', '/destination', 302);
261
- *
262
- * // Redirect to external URL
263
- * router.redirect('/docs', 'https://docs.example.com');
264
- *
265
- * // With parameters - redirects /users/:id to /profiles/:id
266
- * router.redirect('/users/:id', '/profiles/:id');
267
- * ```
181
+ * Every registered route as a read-only list, for renderers (`@nextrush/openapi`,
182
+ * SDK/RPC generators). Doc-generation-time only — never on the request path.
268
183
  */
269
- redirect(from: string, to: string, status: 301 | 302 | 303 | 307 | 308 = 301): this {
270
- // Precompile the target template at registration time.
271
- // If `to` contains route-style `:param` placeholders, build a parts
272
- // array of alternating literal / param-name entries so the per-request
273
- // handler can substitute without sorting or scanning the string.
274
- //
275
- // Only `:` preceded by `/` or at position 0 is a param slot. This
276
- // avoids misinterpreting `https://` or other non-route colons.
277
- let compiledParts: string[] | undefined;
278
-
279
- {
280
- const parts: string[] = [];
281
- let pos = 0;
282
- let found = false;
283
-
284
- while (pos < to.length) {
285
- // Find next `:` that looks like a route param
286
- let idx = -1;
287
- for (let i = pos; i < to.length; i++) {
288
- if (
289
- to[i] === ':' &&
290
- (i === 0 || to[i - 1] === '/') &&
291
- i + 1 < to.length &&
292
- to[i + 1] !== '/'
293
- ) {
294
- idx = i;
295
- break;
296
- }
297
- }
298
- if (idx === -1) break;
299
-
300
- found = true;
301
- parts.push(to.slice(pos, idx)); // literal before ':'
302
- const end = to.indexOf('/', idx + 1);
303
- if (end === -1) {
304
- parts.push(to.slice(idx + 1)); // param name (rest of string)
305
- pos = to.length;
306
- } else {
307
- parts.push(to.slice(idx + 1, end)); // param name
308
- pos = end;
309
- }
310
- }
311
-
312
- if (found) {
313
- parts.push(to.slice(pos)); // trailing literal
314
- compiledParts = parts;
315
- }
316
- }
317
-
318
- const redirectHandler: RouteHandler = (ctx: Context) => {
319
- let targetPath: string;
320
-
321
- if (compiledParts) {
322
- // Fast path: build from precompiled template
323
- const params = ctx.params;
324
- const parts = compiledParts;
325
- const head = parts[0];
326
- if (head === undefined) {
327
- targetPath = to;
328
- } else {
329
- let result = head;
330
- for (let i = 1; i < parts.length - 1; i += 2) {
331
- const paramKey = parts[i];
332
- const tail = parts[i + 1];
333
- if (paramKey === undefined || tail === undefined) break;
334
- result += (params[paramKey] ?? '') + tail;
335
- }
336
- targetPath = result;
337
- }
338
- } else {
339
- targetPath = to;
340
- }
341
-
342
- ctx.status = status;
343
- ctx.set('Location', targetPath);
344
- ctx.body = '';
345
- };
346
-
347
- // Register for common methods. 307/308 preserve the original method,
348
- // so register all standard methods for those status codes.
349
- this.addRoute('GET', from, [redirectHandler]);
350
- this.addRoute('HEAD', from, [redirectHandler]);
351
- if (status === 307 || status === 308) {
352
- this.addRoute('POST', from, [redirectHandler]);
353
- this.addRoute('PUT', from, [redirectHandler]);
354
- this.addRoute('PATCH', from, [redirectHandler]);
355
- this.addRoute('DELETE', from, [redirectHandler]);
356
- }
184
+ getRoutes(): readonly RouteDefinition[] {
185
+ return this.routeDefinitions;
186
+ }
357
187
 
188
+ /**
189
+ * Register a redirect from one path to another (301 by default). 307/308
190
+ * additionally register POST/PUT/PATCH/DELETE to preserve the method.
191
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#redirects | README: Redirects}
192
+ */
193
+ redirect(from: string, to: string, status: RedirectStatus = 301): this {
194
+ registerRedirect(from, to, status, (method, path, entries) => {
195
+ this.addRoute(method, path, entries);
196
+ });
358
197
  return this;
359
198
  }
360
199
 
361
- // ===========================================================================
362
- // Router Composition
363
- // ===========================================================================
364
-
365
200
  use(pathOrMiddleware: string | Middleware | Router, routerOrUndefined?: Router): this {
366
201
  if (typeof pathOrMiddleware === 'function') {
367
- // Middleware function
368
202
  this.routerMiddleware.push(pathOrMiddleware);
369
203
  } else if (typeof pathOrMiddleware === 'string' && routerOrUndefined instanceof Router) {
370
- // Mount sub-router at path
371
204
  this.mountRouter(pathOrMiddleware, routerOrUndefined);
372
205
  } else if (typeof pathOrMiddleware === 'string') {
373
- // String prefix without a Router — unsupported, throw clear error
374
206
  throw new Error(
375
207
  `router.use('${pathOrMiddleware}', ...) requires a Router instance as the second argument. ` +
376
208
  'Use router.group(prefix, callback) for prefix-scoped middleware, ' +
377
209
  'or router.use(middlewareFn) to register middleware without a prefix.'
378
210
  );
379
211
  } else if (pathOrMiddleware instanceof Router) {
380
- // Mount router at root
381
212
  this.mountRouter('', pathOrMiddleware);
382
213
  }
383
214
  return this;
384
215
  }
385
216
 
386
217
  /**
387
- * Mount a sub-router at a path prefix (Hono-style)
388
- *
389
- * This is the explicit API for mounting sub-routers.
390
- * Equivalent to `router.use(path, subRouter)` but more semantic.
391
- *
392
- * @param path - Path prefix for the sub-router
393
- * @param router - Router instance to mount
394
- * @returns this for chaining
395
- *
396
- * @example
397
- * ```typescript
398
- * // Create modular routers
399
- * const users = createRouter();
400
- * users.get('/', listUsers);
401
- * users.get('/:id', getUser);
402
- *
403
- * const posts = createRouter();
404
- * posts.get('/', listPosts);
405
- *
406
- * // Mount sub-routers
407
- * const api = createRouter();
408
- * api.mount('/users', users);
409
- * api.mount('/posts', posts);
410
- *
411
- * // Or use on main router
412
- * const router = createRouter();
413
- * router.mount('/api', api);
414
- *
415
- * app.use(router.routes());
416
- * ```
218
+ * Mount a sub-router at a path prefix (Hono-style) — the explicit, more
219
+ * semantic equivalent of `router.use(path, subRouter)`.
220
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#sub-router-mounting | README: Sub-Router Mounting}
417
221
  */
418
222
  mount(path: string, router: Router): this {
419
223
  this.mountRouter(path, router);
420
224
  return this;
421
225
  }
422
226
 
423
- /**
424
- * Mount a sub-router (internal)
425
- *
426
- * Carries the sub-router's own `routerMiddleware` forward so that
427
- * `subrouter.use(mw)` middleware applies to every copied route.
428
- */
227
+ /** Mount a sub-router, carrying its own `routerMiddleware` onto every copied route. */
429
228
  private mountRouter(prefix: string, router: Router): void {
430
- this.copyRoutes(router.root, prefix, [], router.routerMiddleware);
431
- }
432
-
433
- /**
434
- * Recursively copy routes from another router
435
- */
436
- private copyRoutes(
437
- node: RadixNode,
438
- prefix: string,
439
- segments: string[],
440
- subRouterMiddleware: Middleware[]
441
- ): void {
442
- // Copy handlers at this node
443
- for (const [method, entry] of node.handlers) {
444
- const path = prefix + '/' + segments.join('/');
445
- // Prepend sub-router middleware so it runs before the route's own middleware
446
- const combined =
447
- subRouterMiddleware.length > 0
448
- ? [...subRouterMiddleware, ...entry.middleware]
449
- : entry.middleware;
450
- this.addRoute(method, path || '/', [entry.handler], combined);
451
- }
452
-
453
- // Copy static children
454
- for (const [, child] of node.children) {
455
- this.copyRoutes(child, prefix, [...segments, child.segment], subRouterMiddleware);
456
- }
457
-
458
- // Copy param child
459
- if (node.paramChild) {
460
- this.copyRoutes(
461
- node.paramChild,
462
- prefix,
463
- [...segments, node.paramChild.segment],
464
- subRouterMiddleware
465
- );
466
- }
467
-
468
- // Copy wildcard child
469
- if (node.wildcardChild) {
470
- this.copyRoutes(node.wildcardChild, prefix, [...segments, '*'], subRouterMiddleware);
471
- }
229
+ copyRoutes(router.root, prefix, [], router.routerMiddleware, this.addRoute.bind(this));
472
230
  }
473
231
 
474
- // ===========================================================================
475
- // Route Matching
476
- // ===========================================================================
477
-
478
- /**
479
- * Match a route and return handler + params
480
- */
232
+ /** Match a request to a route — delegates to {@link resolveMatch} (design.md D1). */
481
233
  match(method: HttpMethod, path: string): RouteMatch | null {
482
- const isCaseInsensitive = !this.opts.caseSensitive;
483
- let normalized = isCaseInsensitive ? path.toLowerCase() : path;
484
-
485
- // Fast-path: skip regex when no double slashes (99%+ of requests)
486
- if (normalized.includes('//')) {
487
- normalized = normalized.replace(/\/+/g, '/');
488
- }
489
-
490
- // For strict mode, keep trailing slash; otherwise remove it
491
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith('/')) {
492
- normalized = normalized.slice(0, -1);
493
- }
494
-
495
- // FAST PATH: O(1) static route lookup (no tree traversal)
496
- // For static routes, trailing slash is irrelevant — always strip for lookup
497
- const staticKey =
498
- normalized.length > 1 && normalized.endsWith('/')
499
- ? `${method} ${normalized.slice(0, -1)}`
500
- : `${method} ${normalized}`;
501
- const staticEntry = this.staticRoutes.get(staticKey);
502
- if (staticEntry) {
503
- return {
504
- handler: staticEntry.handler,
505
- params: EMPTY_PARAMS,
506
- middleware: this.routerMiddleware,
507
- executor: staticEntry.executor,
508
- };
509
- }
510
-
511
- // Only walk tree if we have param/wildcard routes
512
- if (!this.hasParamRoutes) return null;
513
-
514
- // Use index-based path scanning instead of split('/').filter(Boolean)
515
- const params: Record<string, string> = {};
516
-
517
- // For case-insensitive mode, preserve original-case path for param values
518
- let originalPath: string | undefined;
519
- if (isCaseInsensitive) {
520
- originalPath = path;
521
- if (originalPath.includes('//')) {
522
- originalPath = originalPath.replace(/\/+/g, '/');
523
- }
524
- if (!this.opts.strict && originalPath.length > 1 && originalPath.endsWith('/')) {
525
- originalPath = originalPath.slice(0, -1);
526
- }
527
- }
528
-
529
- const result = this.matchNodeIndexed(
530
- this.root,
531
- normalized,
532
- 1, // Start after leading '/'
533
- params,
534
- method,
535
- originalPath
536
- );
537
- if (!result) return null;
538
-
539
- // Check if any params were actually set
540
- let hasParams = false;
541
- for (const key of Object.keys(params)) {
542
- if (params[key] === undefined) {
543
- Reflect.deleteProperty(params, key);
544
- } else {
545
- hasParams = true;
546
- }
547
- }
548
-
549
- return {
550
- handler: result.handler,
551
- params: hasParams ? params : EMPTY_PARAMS,
552
- middleware: this.routerMiddleware,
553
- executor: result.executor,
554
- };
234
+ return resolveMatch(this.state, this.hasParamRoutes, method, path);
555
235
  }
556
236
 
557
237
  /**
558
- * Extract the next segment from path at given position without allocating arrays.
559
- * Returns [segment, nextIndex] where nextIndex is position after the trailing '/'.
560
- */
561
- private extractSegment(path: string, start: number): [segment: string, nextIndex: number] {
562
- const slashPos = path.indexOf('/', start);
563
- if (slashPos === -1) {
564
- return [path.slice(start), path.length];
565
- }
566
- return [path.slice(start, slashPos), slashPos + 1];
567
- }
568
-
569
- /**
570
- * Index-based recursive node matching (avoids array allocation)
238
+ * Test whether `path` falls under `prefix` using this router's OWN
239
+ * canonicalization (case folding per `caseSensitive`, structural
240
+ * normalization) — the mount-boundary counterpart to {@link match}, so a
241
+ * router mounted via `Application.route()` is tested with the identical
242
+ * rule it dispatches with (RFC-029, task 3.8). Implements the optional
243
+ * `Routable.matchesMountPrefix` contract from `@nextrush/core`.
244
+ *
245
+ * @param path - The full request path being tested for this mount.
246
+ * @param prefix - The normalized mount prefix (leading `/`, no trailing `/`).
247
+ * @returns The path's remainder past the prefix (e.g. `/users` for a
248
+ * `/ADMIN/Users` request mounted at `/admin`), or `undefined` when `path`
249
+ * is not under `prefix` per this router's canonicalization.
571
250
  */
572
- private matchNodeIndexed(
573
- node: RadixNode,
574
- path: string,
575
- pos: number,
576
- params: Record<string, string>,
577
- method: HttpMethod,
578
- originalPath?: string
579
- ): HandlerEntry | null {
580
- // Reached end of path
581
- if (pos >= path.length) {
582
- return node.handlers.get(method) ?? null;
583
- }
584
-
585
- const [segment, nextPos] = this.extractSegment(path, pos);
586
- if (segment === '') return node.handlers.get(method) ?? null;
587
-
588
- // Try static match first (most specific)
589
- const staticChild = node.children.get(segment);
590
- if (staticChild) {
591
- const result = this.matchNodeIndexed(
592
- staticChild,
593
- path,
594
- nextPos,
595
- params,
596
- method,
597
- originalPath
598
- );
599
- if (result) return result;
251
+ matchesMountPrefix(path: string, prefix: string): string | undefined {
252
+ const canonical = canonicalizePath(path, this.opts.caseSensitive, this.opts.strict);
253
+ if (canonical.rejected) return undefined;
254
+
255
+ // The prefix is fixed at registration time, so canonicalizing it on every
256
+ // request is pure waste — it was ~150 ns of the ~557 ns each mounted router
257
+ // added to dispatch. Memoized rather than precomputed at `route()` time so
258
+ // the `Routable.matchesMountPrefix` contract still accepts any prefix
259
+ // string from any caller. One entry is enough: a router is normally mounted
260
+ // once, and a second prefix only costs a miss.
261
+ let canonicalPrefix: string;
262
+ if (this._prefixMemoRaw === prefix) {
263
+ canonicalPrefix = this._prefixMemoCanonical;
264
+ } else {
265
+ canonicalPrefix = canonicalizePath(prefix, this.opts.caseSensitive, this.opts.strict).path;
266
+ this._prefixMemoRaw = prefix;
267
+ this._prefixMemoCanonical = canonicalPrefix;
600
268
  }
601
269
 
602
- // Try parameter match — use original-case segment for param value
603
- if (node.paramChild) {
604
- const paramName = node.paramChild.paramName;
605
- if (paramName === undefined) return null;
606
- if (originalPath) {
607
- const [origSeg] = this.extractSegment(originalPath, pos);
608
- params[paramName] = origSeg;
609
- } else {
610
- params[paramName] = segment;
611
- }
612
- const result = this.matchNodeIndexed(
613
- node.paramChild,
614
- path,
615
- nextPos,
616
- params,
617
- method,
618
- originalPath
619
- );
620
- if (result) return result;
621
- Reflect.deleteProperty(params, paramName);
622
- }
270
+ const prefixLen = canonicalPrefix.length;
271
+ if (!canonical.path.startsWith(canonicalPrefix)) return undefined;
623
272
 
624
- // Try wildcard match (catches remaining path) — use original-case path
625
- if (node.wildcardChild) {
626
- const src = originalPath ?? path;
627
- params['*'] = src.slice(pos);
628
- return node.wildcardChild.handlers.get(method) ?? null;
273
+ const hasCharAfterPrefix = prefixLen < canonical.path.length;
274
+ if (hasCharAfterPrefix && canonical.path.charCodeAt(prefixLen) !== SLASH_CHAR_CODE) {
275
+ return undefined;
629
276
  }
630
277
 
631
- return null;
278
+ return canonical.path.slice(prefixLen) || '/';
632
279
  }
633
280
 
634
- // ===========================================================================
635
- // Middleware Generation
636
- // ===========================================================================
637
-
638
281
  /**
639
- * Get routes middleware function
640
- * Mount this on the application
641
- *
642
- * @example
643
- * ```typescript
644
- * app.use(router.routes());
645
- * ```
282
+ * Return the router's dispatch middleware — mount this on the application.
283
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#routerroutes | README: router.routes()}
646
284
  */
647
285
  routes(): Middleware {
648
- // Seal router-level middleware into route executors at routes() call time
649
- // This avoids per-request closure creation
650
- const hasRouterMiddleware = this.routerMiddleware.length > 0;
651
- if (hasRouterMiddleware) {
652
- this.sealRouterMiddleware();
653
- }
654
-
655
- return async (ctx: Context, next?: () => Promise<void>): Promise<void> => {
656
- const match = this.match(ctx.method, ctx.path);
657
-
658
- if (!match) {
659
- // No route matched set 404 so allowedMethods() and notFoundHandler() can act
660
- ctx.status = 404;
661
- if (next) await next();
662
- return;
663
- }
664
-
665
- // Set params on context
666
- ctx.params = match.params;
667
-
668
- // Use pre-compiled executor (includes router middleware if any)
669
- if (match.executor) {
670
- await match.executor(ctx);
671
- return;
672
- }
673
-
674
- // Fallback: No executor (shouldn't happen but be safe)
675
- await match.handler(ctx, NOOP_NEXT);
676
- };
677
- }
678
-
679
- /**
680
- * Re-compile all route executors to include router-level middleware.
681
- * Called once when routes() is invoked, not per-request.
682
- */
683
- private sealRouterMiddleware(): void {
684
- const routerMw = [...this.routerMiddleware];
685
-
686
- // Walk the tree and re-compile every handler entry
687
- const walk = (node: RadixNode): void => {
688
- for (const [method, entry] of node.handlers) {
689
- const combinedMw = [...routerMw, ...entry.middleware];
690
- entry.executor = compileExecutor(entry.handler, combinedMw);
691
- node.handlers.set(method, entry);
692
- }
693
- for (const [, child] of node.children) {
694
- walk(child);
695
- }
696
- if (node.paramChild) walk(node.paramChild);
697
- if (node.wildcardChild) walk(node.wildcardChild);
698
- };
699
-
700
- walk(this.root);
701
-
702
- // Also update static route entries
703
- for (const [key, entry] of this.staticRoutes) {
704
- const combinedMw = [...routerMw, ...entry.middleware];
705
- entry.executor = compileExecutor(entry.handler, combinedMw);
706
- this.staticRoutes.set(key, entry);
707
- }
286
+ // Seal router middleware into every executor once (audit RT-7 idempotency):
287
+ // routes() may run more than once, so the _sealed guard prevents re-prepend.
288
+ if (this.routerMiddleware.length > 0 && !this._sealed) {
289
+ this._sealed = true;
290
+ sealRouterMiddlewareImpl(this.root, this.staticRoutes, this.routerMiddleware);
291
+ }
292
+ // F-10: the internal closure calls `resolveMatch` directly with
293
+ // `preNormalized: true` rather than going through the public `this.match()`
294
+ // this is the one call path where the caller (`createRoutesMiddleware`)
295
+ // has already run `canonicalizePath()` on `path`, so `matchRoute`'s own
296
+ // fold+collapse re-derivation is skippable. `Router.match()` itself is
297
+ // untouched and still defaults to `false` for every other caller.
298
+ return createRoutesMiddleware(
299
+ (method, path) => resolveMatch(this.state, this.hasParamRoutes, method, path, true),
300
+ this.opts.caseSensitive,
301
+ this.opts.strict
302
+ );
708
303
  }
709
304
 
710
305
  /**
711
- * Generate allowed methods middleware
712
- * Responds to OPTIONS and sets Allow header
306
+ * Generate allowed-methods middleware. Responds to OPTIONS with an `Allow`
307
+ * header and returns 405 for a known path hit with an unregistered method.
713
308
  */
714
309
  allowedMethods(): Middleware {
715
- return async (ctx: Context, next?: () => Promise<void>): Promise<void> => {
716
- if (next) {
717
- await next();
718
- }
719
-
720
- if (ctx.status !== 404) return;
721
-
722
- // Single tree walk to find all allowed methods instead of N×match()
723
- const allowed = this.findAllowedMethods(ctx.path);
724
-
725
- if (allowed.length === 0) return;
726
-
727
- const allowHeader = allowed.join(', ');
728
-
729
- // If OPTIONS request, respond with allowed methods
730
- if (ctx.method === 'OPTIONS') {
731
- ctx.status = 200;
732
- ctx.set('Allow', allowHeader);
733
- ctx.body = '';
734
- return;
735
- }
736
-
737
- // Otherwise, return 405 Method Not Allowed
738
- ctx.status = 405;
739
- ctx.set('Allow', allowHeader);
740
- };
310
+ return createAllowedMethodsMiddleware(this.root, this.opts.caseSensitive, this.opts.strict);
741
311
  }
742
312
 
743
313
  /**
744
- * Find all HTTP methods registered for a given path via single tree walk
745
- * @internal
746
- */
747
- private findAllowedMethods(path: string): HttpMethod[] {
748
- let normalized = this.opts.caseSensitive ? path : path.toLowerCase();
749
-
750
- if (normalized.includes('//')) {
751
- normalized = normalized.replace(/\/+/g, '/');
752
- }
753
-
754
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith('/')) {
755
- normalized = normalized.slice(0, -1);
756
- }
757
-
758
- const segments = normalized.split('/').filter(Boolean);
759
- const node = this.findNode(this.root, segments, 0);
760
- if (!node || node.handlers.size === 0) return [];
761
-
762
- return Array.from(node.handlers.keys());
763
- }
764
-
765
- /**
766
- * Walk the tree to find the node matching a path (ignoring HTTP method)
767
- * @internal
768
- */
769
- private findNode(node: RadixNode, segments: string[], index: number): RadixNode | null {
770
- if (index === segments.length) {
771
- return node;
772
- }
773
-
774
- const segment = segments[index];
775
- if (segment === undefined) return null;
776
-
777
- // Static match
778
- const staticChild = node.children.get(segment);
779
- if (staticChild) {
780
- const result = this.findNode(staticChild, segments, index + 1);
781
- if (result) return result;
782
- }
783
-
784
- // Param match
785
- if (node.paramChild) {
786
- const result = this.findNode(node.paramChild, segments, index + 1);
787
- if (result) return result;
788
- }
789
-
790
- // Wildcard match
791
- if (node.wildcardChild) {
792
- return node.wildcardChild;
793
- }
794
-
795
- return null;
796
- }
797
-
798
- // ===========================================================================
799
- // Route Groups
800
- // ===========================================================================
801
-
802
- /**
803
- * Create a route group with shared prefix and middleware
804
- *
805
- * @param prefix - Path prefix for all routes in the group
806
- * @param middlewareOrCallback - Middleware array or callback function
807
- * @param callback - Callback function if middleware is provided
808
- * @returns this for chaining
809
- *
810
- * @example
811
- * ```typescript
812
- * // Simple group with prefix
813
- * router.group('/api', (r) => {
814
- * r.get('/users', listUsers);
815
- * r.get('/posts', listPosts);
816
- * });
817
- *
818
- * // Group with middleware
819
- * router.group('/admin', [authMiddleware], (r) => {
820
- * r.get('/dashboard', dashboard);
821
- * r.post('/settings', updateSettings);
822
- * });
823
- *
824
- * // Nested groups
825
- * router.group('/api/v1', (r) => {
826
- * r.group('/users', [rateLimit], (ur) => {
827
- * ur.get('/', listUsers);
828
- * ur.get('/:id', getUser);
829
- * });
830
- * });
831
- * ```
314
+ * Create a route group with a shared prefix and middleware. The callback
315
+ * receives a {@link RouteGroup} to register routes against.
316
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#route-groups | README: Route Groups}
832
317
  */
833
318
  group(
834
319
  prefix: string,
835
- middlewareOrCallback: Middleware[] | ((router: Router) => void),
836
- callback?: (router: Router) => void
320
+ middlewareOrCallback: Middleware[] | ((router: RouteGroup) => void),
321
+ callback?: (router: RouteGroup) => void
837
322
  ): this {
838
- let middleware: Middleware[] = [];
839
- let cb: (router: Router) => void;
840
-
841
- if (Array.isArray(middlewareOrCallback)) {
842
- middleware = middlewareOrCallback;
843
- if (!callback) {
844
- throw new Error('Callback function is required when providing middleware array');
845
- }
846
- cb = callback;
847
- } else {
848
- cb = middlewareOrCallback;
849
- }
850
-
851
- // Create a temporary router to collect routes
852
- const groupRouter = new GroupRouter(this, prefix, middleware);
853
-
854
- // Execute callback with the group router
855
- cb(groupRouter as unknown as Router);
856
-
323
+ runRouteGroup(this, prefix, middlewareOrCallback, callback);
857
324
  return this;
858
325
  }
859
326
 
860
327
  /**
861
- * Remove all registered routes and middleware, resetting the router to its initial state.
862
- * Useful for plugin `destroy()` to cleanly un-register routes.
328
+ * Remove all routes and middleware, resetting the router to its initial
329
+ * state for test isolation or hot-reload re-registration.
863
330
  */
864
331
  reset(): void {
865
- this.root.children.clear();
866
- this.root.handlers.clear();
867
- this.root.paramChild = undefined;
868
- this.root.wildcardChild = undefined;
332
+ clearNode(this.root);
869
333
  this.staticRoutes.clear();
870
334
  this.routerMiddleware.length = 0;
871
335
  this.hasParamRoutes = false;
872
- }
873
-
874
- /**
875
- * Internal method to add route with group context
876
- * @internal
877
- */
336
+ // Clear the introspection registry too, or getRoutes()/OpenAPI would emit
337
+ // ghost routes after a reset (audit RT-1).
338
+ this.routeDefinitions.length = 0;
339
+ // Reset the walk-frame pool sizing too (F-02) — otherwise maxDepth would
340
+ // keep reporting a since-cleared route's depth, and the pool would stay
341
+ // needlessly oversized for whatever gets registered next.
342
+ this.state.maxDepth = 0;
343
+ this.state.walkPool = undefined;
344
+ this._sealed = false;
345
+ }
346
+
347
+ /** Register a route on behalf of a {@link RouteGroup} (group context). @internal */
878
348
  _addGroupRoute(
879
349
  method: HttpMethod,
880
350
  path: string,
881
351
  handlers: RouteHandler[],
882
- groupMiddleware: Middleware[]
352
+ groupMiddleware: Middleware[],
353
+ recordIntrospection = true
883
354
  ): void {
884
- this.addRoute(method, path, handlers, groupMiddleware);
885
- }
886
- }
887
-
888
- /**
889
- * Internal router class for handling route groups
890
- * Wraps the parent router and adds prefix/middleware to all routes
891
- * @internal
892
- */
893
- class GroupRouter {
894
- private readonly parent: Router;
895
- private readonly prefix: string;
896
- private readonly middleware: Middleware[];
897
-
898
- constructor(parent: Router, prefix: string, middleware: Middleware[]) {
899
- this.parent = parent;
900
- this.prefix = prefix;
901
- this.middleware = middleware;
902
- }
903
-
904
- private fullPath(path: string): string {
905
- // Handle root path in group
906
- if (path === '/' || path === '') {
907
- return this.prefix;
908
- }
909
- // Combine prefix and path
910
- const cleanPrefix = this.prefix.endsWith('/') ? this.prefix.slice(0, -1) : this.prefix;
911
- const cleanPath = path.startsWith('/') ? path : '/' + path;
912
- return cleanPrefix + cleanPath;
913
- }
914
-
915
- get(path: string, ...handlers: RouteHandler[]): this {
916
- this.parent._addGroupRoute('GET', this.fullPath(path), handlers, this.middleware);
917
- return this;
918
- }
919
-
920
- post(path: string, ...handlers: RouteHandler[]): this {
921
- this.parent._addGroupRoute('POST', this.fullPath(path), handlers, this.middleware);
922
- return this;
923
- }
924
-
925
- put(path: string, ...handlers: RouteHandler[]): this {
926
- this.parent._addGroupRoute('PUT', this.fullPath(path), handlers, this.middleware);
927
- return this;
928
- }
929
-
930
- delete(path: string, ...handlers: RouteHandler[]): this {
931
- this.parent._addGroupRoute('DELETE', this.fullPath(path), handlers, this.middleware);
932
- return this;
933
- }
934
-
935
- patch(path: string, ...handlers: RouteHandler[]): this {
936
- this.parent._addGroupRoute('PATCH', this.fullPath(path), handlers, this.middleware);
937
- return this;
938
- }
939
-
940
- head(path: string, ...handlers: RouteHandler[]): this {
941
- this.parent._addGroupRoute('HEAD', this.fullPath(path), handlers, this.middleware);
942
- return this;
943
- }
944
-
945
- options(path: string, ...handlers: RouteHandler[]): this {
946
- this.parent._addGroupRoute('OPTIONS', this.fullPath(path), handlers, this.middleware);
947
- return this;
948
- }
949
-
950
- all(path: string, ...handlers: RouteHandler[]): this {
951
- for (const method of HTTP_METHODS) {
952
- this.parent._addGroupRoute(method, this.fullPath(path), handlers, this.middleware);
953
- }
954
- return this;
355
+ this.addRoute(method, path, handlers, groupMiddleware, recordIntrospection);
955
356
  }
956
357
 
957
358
  /**
958
- * Register a redirect within the group
359
+ * Group-facing entry point to {@link pushAnyMethodDefinition} — a group's `.all()`
360
+ * records its consolidated row here since group routes live on the parent. @internal
959
361
  */
960
- redirect(from: string, to: string, status: 301 | 302 | 303 | 307 | 308 = 301): this {
961
- const redirectHandler: RouteHandler = (ctx: Context) => {
962
- let targetPath = to;
963
-
964
- if (targetPath.includes(':')) {
965
- const entries = Object.entries(ctx.params).sort((a, b) => b[0].length - a[0].length);
966
- for (const [key, value] of entries) {
967
- targetPath = targetPath.replaceAll(`:${key}`, value);
968
- }
969
- }
970
-
971
- ctx.status = status;
972
- ctx.set('Location', targetPath);
973
- ctx.body = '';
974
- };
975
-
976
- this.parent._addGroupRoute('GET', this.fullPath(from), [redirectHandler], this.middleware);
977
- this.parent._addGroupRoute('HEAD', this.fullPath(from), [redirectHandler], this.middleware);
978
-
979
- return this;
980
- }
981
-
982
- /**
983
- * Nested group support
984
- */
985
- group(
986
- prefix: string,
987
- middlewareOrCallback: Middleware[] | ((router: GroupRouter) => void),
988
- callback?: (router: GroupRouter) => void
989
- ): this {
990
- let nestedMiddleware: Middleware[] = [];
991
- let cb: (router: GroupRouter) => void;
992
-
993
- if (Array.isArray(middlewareOrCallback)) {
994
- nestedMiddleware = middlewareOrCallback;
995
- if (!callback) {
996
- throw new Error('Callback function is required when providing middleware array');
997
- }
998
- cb = callback;
999
- } else {
1000
- cb = middlewareOrCallback;
1001
- }
1002
-
1003
- // Create nested group with combined prefix and middleware
1004
- const nestedRouter = new GroupRouter(this.parent, this.fullPath(prefix), [
1005
- ...this.middleware,
1006
- ...nestedMiddleware,
1007
- ]);
1008
-
1009
- cb(nestedRouter);
1010
-
1011
- return this;
362
+ _pushAnyMethodRouteDefinition(path: string): void {
363
+ pushAnyMethodDefinition(
364
+ this.routeDefinitions,
365
+ normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict)
366
+ );
1012
367
  }
1013
368
  }
1014
369
 
1015
370
  /**
1016
- * Create a new Router instance
1017
- *
1018
- * @param options - Router options
1019
- * @returns New Router instance
1020
- *
1021
- * @example
1022
- * ```typescript
1023
- * const router = createRouter();
1024
- * const apiRouter = createRouter({ prefix: '/api/v1' });
1025
- * ```
371
+ * Create a new {@link Router} instance.
372
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#quick-start | README: Quick Start}
1026
373
  */
1027
374
  export function createRouter(options?: RouterOptions): Router {
1028
375
  return new Router(options);