@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
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @nextrush/router - Route Groups
3
+ *
4
+ * A route group applies a shared path prefix and middleware to a set of routes
5
+ * registered inside a callback. Extracted from `router.ts` (audit RT-3) and
6
+ * given a real public type (audit RT-6): `router.group()` callbacks receive a
7
+ * {@link RouteGroup}, not a mis-cast `Router`.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ import { HTTP_METHODS, type HttpMethod, type Middleware, type RouteHandler } from '@nextrush/types';
13
+ import { createRedirectHandler, type RedirectStatus } from './redirect';
14
+
15
+ /**
16
+ * The subset of the parent router a group needs to register routes into.
17
+ *
18
+ * @remarks
19
+ * Declared as an interface (rather than importing `Router`) so `group-router.ts`
20
+ * and `router.ts` don't form an import cycle. `Router` satisfies it structurally
21
+ * via its `_addGroupRoute` method.
22
+ */
23
+ export interface GroupRouterHost {
24
+ _addGroupRoute(
25
+ method: HttpMethod,
26
+ path: string,
27
+ handlers: RouteHandler[],
28
+ groupMiddleware: Middleware[],
29
+ recordIntrospection?: boolean
30
+ ): void;
31
+ /** Record a single any-method introspection row (T016) — see `Router._pushAnyMethodRouteDefinition`. */
32
+ _pushAnyMethodRouteDefinition(path: string): void;
33
+ }
34
+
35
+ /**
36
+ * The object passed to a `router.group(prefix, callback)` callback.
37
+ *
38
+ * @remarks
39
+ * Exposes only the route-registration surface that is valid inside a group —
40
+ * intentionally NOT the full {@link Router} (no `mount`/`use`/`reset`), which is
41
+ * why the previous `as unknown as Router` cast was a lie (audit RT-6).
42
+ */
43
+ export interface RouteGroup {
44
+ get(path: string, ...handlers: RouteHandler[]): this;
45
+ post(path: string, ...handlers: RouteHandler[]): this;
46
+ put(path: string, ...handlers: RouteHandler[]): this;
47
+ delete(path: string, ...handlers: RouteHandler[]): this;
48
+ patch(path: string, ...handlers: RouteHandler[]): this;
49
+ head(path: string, ...handlers: RouteHandler[]): this;
50
+ options(path: string, ...handlers: RouteHandler[]): this;
51
+ all(path: string, ...handlers: RouteHandler[]): this;
52
+ redirect(from: string, to: string, status?: RedirectStatus): this;
53
+ group(
54
+ prefix: string,
55
+ middlewareOrCallback: Middleware[] | ((router: RouteGroup) => void),
56
+ callback?: (router: RouteGroup) => void
57
+ ): this;
58
+ }
59
+
60
+ /**
61
+ * Collects routes under a shared prefix + middleware and forwards them to the
62
+ * parent router. Implements {@link RouteGroup}.
63
+ */
64
+ export class GroupRouter implements RouteGroup {
65
+ private readonly parent: GroupRouterHost;
66
+ private readonly prefix: string;
67
+ private readonly middleware: Middleware[];
68
+
69
+ constructor(parent: GroupRouterHost, prefix: string, middleware: Middleware[]) {
70
+ this.parent = parent;
71
+ this.prefix = prefix;
72
+ this.middleware = middleware;
73
+ }
74
+
75
+ private fullPath(path: string): string {
76
+ // Handle root path in group
77
+ if (path === '/' || path === '') {
78
+ return this.prefix;
79
+ }
80
+ // Combine prefix and path
81
+ const cleanPrefix = this.prefix.endsWith('/') ? this.prefix.slice(0, -1) : this.prefix;
82
+ const cleanPath = path.startsWith('/') ? path : '/' + path;
83
+ return cleanPrefix + cleanPath;
84
+ }
85
+
86
+ get(path: string, ...handlers: RouteHandler[]): this {
87
+ this.parent._addGroupRoute('GET', this.fullPath(path), handlers, this.middleware);
88
+ return this;
89
+ }
90
+
91
+ post(path: string, ...handlers: RouteHandler[]): this {
92
+ this.parent._addGroupRoute('POST', this.fullPath(path), handlers, this.middleware);
93
+ return this;
94
+ }
95
+
96
+ put(path: string, ...handlers: RouteHandler[]): this {
97
+ this.parent._addGroupRoute('PUT', this.fullPath(path), handlers, this.middleware);
98
+ return this;
99
+ }
100
+
101
+ delete(path: string, ...handlers: RouteHandler[]): this {
102
+ this.parent._addGroupRoute('DELETE', this.fullPath(path), handlers, this.middleware);
103
+ return this;
104
+ }
105
+
106
+ patch(path: string, ...handlers: RouteHandler[]): this {
107
+ this.parent._addGroupRoute('PATCH', this.fullPath(path), handlers, this.middleware);
108
+ return this;
109
+ }
110
+
111
+ head(path: string, ...handlers: RouteHandler[]): this {
112
+ this.parent._addGroupRoute('HEAD', this.fullPath(path), handlers, this.middleware);
113
+ return this;
114
+ }
115
+
116
+ options(path: string, ...handlers: RouteHandler[]): this {
117
+ this.parent._addGroupRoute('OPTIONS', this.fullPath(path), handlers, this.middleware);
118
+ return this;
119
+ }
120
+
121
+ /**
122
+ * Register a route matching every HTTP method under a single introspection
123
+ * entry, mirroring `Router.all()` (T016) — a group's `.all()` must not
124
+ * regress to the pre-T016 one-row-per-method shape just because
125
+ * registration is routed through `_addGroupRoute` instead of `addRoute`
126
+ * directly.
127
+ */
128
+ all(path: string, ...handlers: RouteHandler[]): this {
129
+ for (const method of HTTP_METHODS) {
130
+ this.parent._addGroupRoute(method, this.fullPath(path), handlers, this.middleware, false);
131
+ }
132
+ this.parent._pushAnyMethodRouteDefinition(this.fullPath(path));
133
+ return this;
134
+ }
135
+
136
+ /**
137
+ * Register a redirect within the group (uses the shared redirect handler,
138
+ * audit RT-4 — no more naive replaceAll param substitution).
139
+ */
140
+ redirect(from: string, to: string, status: RedirectStatus = 301): this {
141
+ const redirectHandler = createRedirectHandler(to, status);
142
+
143
+ this.parent._addGroupRoute('GET', this.fullPath(from), [redirectHandler], this.middleware);
144
+ this.parent._addGroupRoute('HEAD', this.fullPath(from), [redirectHandler], this.middleware);
145
+
146
+ return this;
147
+ }
148
+
149
+ /**
150
+ * Nested group support — combines this group's prefix + middleware with the
151
+ * nested group's.
152
+ */
153
+ group(
154
+ prefix: string,
155
+ middlewareOrCallback: Middleware[] | ((router: RouteGroup) => void),
156
+ callback?: (router: RouteGroup) => void
157
+ ): this {
158
+ runRouteGroup(
159
+ this.parent,
160
+ this.fullPath(prefix),
161
+ middlewareOrCallback,
162
+ callback,
163
+ this.middleware
164
+ );
165
+ return this;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Resolve a `group()` call's `(middleware[], callback)` | `(callback)` overload
171
+ * and run the callback against a fresh {@link GroupRouter} bound to `host`.
172
+ *
173
+ * @remarks
174
+ * Shared by `Router.group()` and the nested `GroupRouter.group()` so the
175
+ * overload handling and the "throw when a middleware array has no callback"
176
+ * guard have a single definition.
177
+ *
178
+ * @param host - Parent router the group registers routes into.
179
+ * @param prefix - Fully-resolved path prefix for the group.
180
+ * @param middlewareOrCallback - Either the group middleware array or the callback.
181
+ * @param callback - The callback, required when a middleware array is passed.
182
+ * @param inheritedMiddleware - Middleware from an enclosing group, prepended
183
+ * ahead of this group's own (empty for a top-level `Router.group()`).
184
+ */
185
+ export function runRouteGroup(
186
+ host: GroupRouterHost,
187
+ prefix: string,
188
+ middlewareOrCallback: Middleware[] | ((router: RouteGroup) => void),
189
+ callback: ((router: RouteGroup) => void) | undefined,
190
+ inheritedMiddleware: Middleware[] = []
191
+ ): void {
192
+ let middleware: Middleware[] = [];
193
+ let cb: (router: RouteGroup) => void;
194
+
195
+ if (Array.isArray(middlewareOrCallback)) {
196
+ middleware = middlewareOrCallback;
197
+ if (!callback) {
198
+ throw new Error('Callback function is required when providing middleware array');
199
+ }
200
+ cb = callback;
201
+ } else {
202
+ cb = middlewareOrCallback;
203
+ }
204
+
205
+ const combined =
206
+ inheritedMiddleware.length > 0 ? [...inheritedMiddleware, ...middleware] : middleware;
207
+ cb(new GroupRouter(host, prefix, combined));
208
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @nextrush/router - High-Performance Router for NextRush
3
3
  *
4
- * This package provides a radix tree based router with:
4
+ * This package provides a segment trie based router with:
5
5
  * - O(k) route matching where k is path length
6
6
  * - Named parameters (/users/:id)
7
7
  * - Wildcard routes (/files/*)
@@ -13,11 +13,20 @@
13
13
  */
14
14
 
15
15
  // Router
16
- export { createRouter, Router } from './router';
16
+ export { createRouter, endpoint, Router } from './router';
17
17
 
18
- // Radix tree internals (for advanced usage)
19
- export { createNode, NodeType, parseSegments } from './radix-tree';
20
- export type { HandlerEntry, ParsedSegment, RadixNode } from './radix-tree';
18
+ // Canonical request path (RFC-029) the single normalization owner shared by
19
+ // the router's own match, mounted-router prefix tests, and any consumer that
20
+ // needs to know "what path does the router treat this request as".
21
+ export { canonicalizePath, hasDotSegment } from './canonicalize';
22
+ export type { CanonicalPathResult } from './canonicalize';
23
+
24
+ // Route groups
25
+ export type { RouteGroup } from './group-router';
26
+
27
+ // Segment trie internals (for advanced usage)
28
+ export { createNode, NodeType, parseSegments } from './segment-trie';
29
+ export type { HandlerEntry, ParsedSegment, TrieNode } from './segment-trie';
21
30
 
22
31
  // Re-export relevant types
23
32
  export type {
@@ -0,0 +1,241 @@
1
+ /**
2
+ * @nextrush/router - Top-Level Route Match Orchestration
3
+ *
4
+ * `matchRoute` was originally part of `matching.ts` (the lookup-primitives
5
+ * module: `decodeParam`/`extractSegment`/`findNode`/`findAllowedMethods`/
6
+ * `matchNodeIndexed`), but `matching.ts` was approaching the 300-line ceiling
7
+ * itself once `matchRoute` moved in — this file separates the top-level
8
+ * "match a request" orchestration (query-strip, normalize, static fast-path,
9
+ * tree-walk delegation, param post-processing) from the lower-level lookup
10
+ * primitives it calls into.
11
+ *
12
+ * @packageDocumentation
13
+ * @internal
14
+ */
15
+
16
+ import type { HttpMethod, Middleware, RouteMatch } from '@nextrush/types';
17
+ import { EMPTY_PARAMS, NULL_PROTO } from './constants';
18
+ import { matchNodeIndexed, collapseAndStrip, isProvablyLowerAscii } from './matching';
19
+ import type { WalkPool } from './walk-pool';
20
+ import type { StaticRouteMap, TrieNode } from './segment-trie';
21
+
22
+ /**
23
+ * Match a route and return the full {@link RouteMatch} in a SINGLE allocation
24
+ * (design.md D1 / HP-10).
25
+ *
26
+ * `matchRoute` builds the final `RouteMatch` — including `middleware:
27
+ * routerMiddleware` — directly at each return site, so `Router.match()` gets
28
+ * one object per matched request instead of a bare result later re-wrapped by
29
+ * `resolveMatch`. `routerMiddleware` is threaded in as a parameter (its
30
+ * contents are never read here — only attached by reference), preserving the
31
+ * "no implicit `Router` state" property of the earlier extraction while
32
+ * removing the duplicate wrapper object.
33
+ *
34
+ * Every other piece of `Router` state it reads (`root`, `staticRoutes`,
35
+ * `hasParamRoutes`, the option flags) is read-only here (no mutation, unlike
36
+ * `addRoute` in `registration.ts`), so it is threaded as plain parameters.
37
+ */
38
+ export function matchRoute(
39
+ method: HttpMethod,
40
+ rawPath: string,
41
+ root: TrieNode,
42
+ staticRoutes: StaticRouteMap,
43
+ hasParamRoutes: boolean,
44
+ caseSensitive: boolean,
45
+ strict: boolean,
46
+ decode: boolean,
47
+ routerMiddleware: Middleware[],
48
+ /**
49
+ * When `true`, `rawPath` is trusted as already the output of
50
+ * `canonicalizePath()` (same `caseSensitive`/`strict` options) — the fold
51
+ * and structural-collapse steps below are skipped entirely rather than
52
+ * re-derived. Only valid when the caller has actually run
53
+ * `canonicalizePath()` on this exact input first (F-10); the router's own
54
+ * `routes()` dispatch path is the only caller that does. Defaults to
55
+ * `false`, preserving every other caller's existing behavior — including
56
+ * `Router.match()`, which never canonicalizes first.
57
+ */
58
+ preNormalized = false,
59
+ /**
60
+ * When supplied, the param walk reuses this router instance's pooled
61
+ * `WalkFrame[]`/binding-array scratch space instead of allocating fresh
62
+ * arrays on this call (F-02, `reduce-router-match-allocations`). Omitted,
63
+ * `matchRoute` behaves exactly as before.
64
+ */
65
+ walkPool?: WalkPool
66
+ ): RouteMatch | null {
67
+ let path = rawPath;
68
+ // Query string must not affect path matching (RFC 3986 §3.4). Strip it here,
69
+ // before normalization, so both the lookup path and extracted param values
70
+ // exclude it. This strip is caller-specific: `findAllowedMethods` receives an
71
+ // already query-free `ctx.path`, so the shared `normalizePathForMatch` does
72
+ // not strip — only `matchRoute` does.
73
+ if (!preNormalized) {
74
+ const queryIdx = path.indexOf('?');
75
+ if (queryIdx !== -1) path = path.slice(0, queryIdx);
76
+ }
77
+
78
+ // HP-12: decide case-stability ONCE. When the path is provably case-stable
79
+ // (case-sensitive router, or an all-lowercase-ASCII path), folding is a no-op
80
+ // and the original-case path equals the normalized one — so we skip both the
81
+ // `toLowerCase()` allocation and the second original-case normalize pass.
82
+ //
83
+ // F-10: when `preNormalized` is true, the caller already ran this exact
84
+ // fold+collapse via `canonicalizePath()` — trust `path` as both the
85
+ // normalized AND original-case string, skipping the fold+collapse
86
+ // re-derivation entirely rather than repeating work already done upstream.
87
+ let normalized: string;
88
+ let caseStable: boolean;
89
+ if (preNormalized) {
90
+ normalized = path;
91
+ caseStable = true;
92
+ } else {
93
+ caseStable = caseSensitive || isProvablyLowerAscii(path);
94
+ const folded = caseStable ? path : path.toLowerCase();
95
+ normalized = collapseAndStrip(folded, strict);
96
+ }
97
+
98
+ // FAST PATH: O(1) static route lookup (no tree traversal). Method-nested map
99
+ // (HP-9): select the inner map by method, then probe by the normalized path —
100
+ // no per-request `${method} ${path}` key-string allocation. For static routes
101
+ // a trailing slash is irrelevant, so always strip it for the probe.
102
+ const methodMap = staticRoutes.get(method);
103
+ if (methodMap) {
104
+ const staticKey =
105
+ normalized.length > 1 && normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
106
+ const staticEntry = methodMap.get(staticKey);
107
+ if (staticEntry) {
108
+ return {
109
+ handler: staticEntry.handler,
110
+ params: EMPTY_PARAMS,
111
+ middleware: routerMiddleware,
112
+ executor: staticEntry.executor,
113
+ };
114
+ }
115
+ }
116
+
117
+ // Only walk tree if we have param/wildcard routes
118
+ if (!hasParamRoutes) return null;
119
+
120
+ // Original-case (query-stripped) path so extracted param values keep their
121
+ // casing while lookup uses the lowercased `normalized`. Needed ONLY when a
122
+ // fold actually happened (HP-12): when case-stable, `normalized` already IS
123
+ // the original-case structure, so the walk extracts from it and no second
124
+ // normalize pass runs.
125
+ const originalPath = caseStable ? undefined : collapseAndStrip(path, strict);
126
+
127
+ // Deferred param binding (HP-11): the walk records the accepted path's
128
+ // `:param`/`*` bindings onto these parallel stacks (pushed on descent, popped
129
+ // on backtrack) so params are materialized ONCE here on the accepted terminal
130
+ // — no eager bind + backtrack `Reflect.deleteProperty`. Reused from the
131
+ // router's pool when supplied (F-02) instead of allocated fresh.
132
+ const bindNames: string[] = walkPool ? walkPool.bindNames : [];
133
+ const bindValues: string[] = walkPool ? walkPool.bindValues : [];
134
+ // A pooled bindNames/bindValues array persists across calls — clear any
135
+ // leftover entries from a prior match before this walk starts (the walk
136
+ // itself pops everything it pushes on a clean miss or match, but a defensive
137
+ // clear here costs nothing on the common empty case and closes any future
138
+ // edit that might leave a stale entry behind).
139
+ if (walkPool) {
140
+ bindNames.length = 0;
141
+ bindValues.length = 0;
142
+ }
143
+
144
+ const entry = matchNodeIndexed(
145
+ root,
146
+ normalized,
147
+ 1, // Start after leading '/'
148
+ bindNames,
149
+ bindValues,
150
+ method,
151
+ decode,
152
+ originalPath,
153
+ walkPool
154
+ );
155
+ if (!entry) return null;
156
+
157
+ // Materialize params once on a bag whose prototype chain excludes
158
+ // `Object.prototype` (design.md D8): a param named
159
+ // `__proto__`/`constructor`/`prototype` binds as an OWN key with no prototype
160
+ // mutation, and no inherited member is visible on `ctx.params`. Derived from
161
+ // `NULL_PROTO` rather than `Object.create(null)` so the object keeps V8 fast
162
+ // properties and handler reads stay inline-cacheable. The bind count replaces
163
+ // the former `Object.keys` post-loop (HP-13); zero binds returns the shared
164
+ // frozen `EMPTY_PARAMS`.
165
+ const count = bindNames.length;
166
+ let params: Record<string, string>;
167
+ if (count === 0) {
168
+ params = EMPTY_PARAMS;
169
+ } else {
170
+ params = Object.create(NULL_PROTO) as Record<string, string>;
171
+ for (let i = 0; i < count; i++) {
172
+ const name = bindNames[i];
173
+ const value = bindValues[i];
174
+ if (name !== undefined && value !== undefined) params[name] = value;
175
+ }
176
+ }
177
+
178
+ return {
179
+ handler: entry.handler,
180
+ params,
181
+ middleware: routerMiddleware,
182
+ executor: entry.executor,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Stable router state `resolveMatch` reads on every request. All fields are
188
+ * fixed references for the router's lifetime, so the caller memoizes this once
189
+ * rather than rebuilding it per request.
190
+ *
191
+ * `walkPool` is the one field that is itself mutable (its contents, not the
192
+ * reference) — the reused `WalkFrame[]`/binding-array scratch space (F-02,
193
+ * `reduce-router-match-allocations`). It is `undefined` until the router has
194
+ * at least one param/wildcard route (no pool needed for a static-only router,
195
+ * per `createWalkPool(0)` never being called) and is rebuilt whenever the
196
+ * router's `maxDepth` grows past what the current pool was sized for — see
197
+ * `Router`'s own wiring, not this interface, for when that rebuild happens.
198
+ */
199
+ export interface MatchState {
200
+ readonly root: TrieNode;
201
+ readonly staticRoutes: StaticRouteMap;
202
+ readonly caseSensitive: boolean;
203
+ readonly strict: boolean;
204
+ readonly decode: boolean;
205
+ readonly routerMiddleware: Middleware[];
206
+ walkPool?: WalkPool;
207
+ }
208
+
209
+ /**
210
+ * Resolve a request to a full {@link RouteMatch}. Thin delegator to
211
+ * {@link matchRoute}, which now builds the final `RouteMatch` (incl.
212
+ * `state.routerMiddleware`) in one allocation — so this no longer wraps a
213
+ * result in a second object (HP-10). `Router.match()` is a one-line delegator
214
+ * to this. `hasParamRoutes` is passed separately from `state` because it is
215
+ * the one piece of router state that flips after construction.
216
+ */
217
+ export function resolveMatch(
218
+ state: MatchState,
219
+ hasParamRoutes: boolean,
220
+ method: HttpMethod,
221
+ path: string,
222
+ /**
223
+ * Forwarded to {@link matchRoute} — see its own doc comment for the
224
+ * caller contract. Defaults to `false`.
225
+ */
226
+ preNormalized = false
227
+ ): RouteMatch | null {
228
+ return matchRoute(
229
+ method,
230
+ path,
231
+ state.root,
232
+ state.staticRoutes,
233
+ hasParamRoutes,
234
+ state.caseSensitive,
235
+ state.strict,
236
+ state.decode,
237
+ state.routerMiddleware,
238
+ preNormalized,
239
+ state.walkPool
240
+ );
241
+ }