@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/dist/index.d.ts CHANGED
@@ -1,238 +1,279 @@
1
- import { RouterOptions, RouteHandler, HttpMethod, Middleware, RouteMatch, Context } from '@nextrush/types';
1
+ import { RouteHandler, Middleware, MetadataContribution, RouteMetaMarker, RouterOptions, RouteEntry, HttpMethod, RouteDefinition, RouteMatch, Context } from '@nextrush/types';
2
2
  export { HttpMethod, Middleware, Route, RouteHandler, RouteMatch, Router as RouterInterface, RouterOptions } from '@nextrush/types';
3
3
 
4
4
  /**
5
- * @nextrush/router - Router Implementation
5
+ * @nextrush/router - Redirect Handler Compilation
6
6
  *
7
- * High-performance router using a segment trie for route matching.
8
- * Routes are keyed by full path segments (e.g. "users", ":id"), not by
9
- * individual characters this is a segment-based trie, not a compressed
10
- * radix tree. Supports parameters, wildcards, and method-based routing.
7
+ * Single, shared redirect implementation used by both `Router.redirect()` and
8
+ * route groups (audit RT-4). Previously the group variant used a naive
9
+ * `replaceAll(':key', value)` that could mis-substitute overlapping param names
10
+ * and re-scanned the string per request; this precompiles the target template
11
+ * once and only substitutes route-style `:param` slots (a `:` at position 0 or
12
+ * preceded by `/`), so `https://` and other literal colons are never touched.
11
13
  *
12
14
  * @packageDocumentation
13
15
  */
14
16
 
17
+ /** HTTP redirect status codes supported by `redirect()`. */
18
+ type RedirectStatus = 301 | 302 | 303 | 307 | 308;
19
+
20
+ /**
21
+ * @nextrush/router - Route Groups
22
+ *
23
+ * A route group applies a shared path prefix and middleware to a set of routes
24
+ * registered inside a callback. Extracted from `router.ts` (audit RT-3) and
25
+ * given a real public type (audit RT-6): `router.group()` callbacks receive a
26
+ * {@link RouteGroup}, not a mis-cast `Router`.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+
31
+ /**
32
+ * The object passed to a `router.group(prefix, callback)` callback.
33
+ *
34
+ * @remarks
35
+ * Exposes only the route-registration surface that is valid inside a group —
36
+ * intentionally NOT the full {@link Router} (no `mount`/`use`/`reset`), which is
37
+ * why the previous `as unknown as Router` cast was a lie (audit RT-6).
38
+ */
39
+ interface RouteGroup {
40
+ get(path: string, ...handlers: RouteHandler[]): this;
41
+ post(path: string, ...handlers: RouteHandler[]): this;
42
+ put(path: string, ...handlers: RouteHandler[]): this;
43
+ delete(path: string, ...handlers: RouteHandler[]): this;
44
+ patch(path: string, ...handlers: RouteHandler[]): this;
45
+ head(path: string, ...handlers: RouteHandler[]): this;
46
+ options(path: string, ...handlers: RouteHandler[]): this;
47
+ all(path: string, ...handlers: RouteHandler[]): this;
48
+ redirect(from: string, to: string, status?: RedirectStatus): this;
49
+ group(prefix: string, middlewareOrCallback: Middleware[] | ((router: RouteGroup) => void), callback?: (router: RouteGroup) => void): this;
50
+ }
51
+
15
52
  /**
16
- * Router class — high-performance segment trie router
53
+ * @nextrush/router - Route Metadata Contributions
54
+ *
55
+ * Inline route metadata (`endpoint()`) and the registration-time merge of
56
+ * contributions from `validate()`, `endpoint()`, etc. Extracted from `router.ts`
57
+ * (audit RT-3). None of this is on the request hot path — it is read only at
58
+ * registration and by `getRoutes()` at doc-generation time.
17
59
  *
18
- * Routes are indexed by path segment, giving O(d) lookup where d is the
19
- * number of segments. Static routes are additionally stored in a hash map
20
- * for O(1) fast-path lookup.
60
+ * @packageDocumentation
61
+ */
62
+
63
+ /**
64
+ * Declare route metadata inline in a route's argument list. Returns a pure data
65
+ * marker (not a middleware) — the router reads its metadata at registration and
66
+ * never executes it per request.
21
67
  *
22
68
  * @example
23
69
  * ```typescript
24
- * const router = createRouter();
70
+ * router.post('/users',
71
+ * validate(User),
72
+ * endpoint({ summary: 'Create a user', responses: { 201: UserResponse } }),
73
+ * handler,
74
+ * );
75
+ * ```
76
+ */
77
+ declare function endpoint(metadata: MetadataContribution): RouteMetaMarker;
78
+
79
+ /**
80
+ * @nextrush/router - Router Implementation
25
81
  *
26
- * router.get('/users', listUsers);
27
- * router.get('/users/:id', getUser);
28
- * router.post('/users', createUser);
82
+ * The public `Router` shell: a thin, chainable facade delegating registration,
83
+ * matching, dispatch, composition, and grouping to focused sibling modules
84
+ * (design.md D1/D2). Segment trie keyed by whole path segments, not a radix tree.
29
85
  *
30
- * app.use(router.routes());
31
- * ```
86
+ * @packageDocumentation
87
+ */
88
+
89
+ /**
90
+ * High-performance segment-trie router: O(d) lookup by path segment, with a
91
+ * static-route hash map for an O(1) fast path.
92
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md | @nextrush/router README}
32
93
  */
33
94
  declare class Router {
34
95
  private readonly root;
35
96
  private readonly opts;
36
97
  private readonly routerMiddleware;
98
+ /** Static-route fast path: method-nested map for O(1) lookup with no per-request key string (HP-9). */
99
+ private readonly staticRoutes;
37
100
  /**
38
- * Static route hash map for O(1) lookup.
39
- * Key: "METHOD path" (e.g. "GET /users"), Value: HandlerEntry
101
+ * Introspection registry, kept SEPARATE from the hot-path trie/staticRoutes
102
+ * so request dispatch never reads metadata — only getRoutes() touches it.
40
103
  */
41
- private readonly staticRoutes;
104
+ private readonly routeDefinitions;
42
105
  /** Whether any routes have params or wildcards (disables static-only fast path) */
43
106
  private hasParamRoutes;
44
- constructor(options?: RouterOptions);
107
+ /** Whether router-level middleware has already been sealed into executors (audit RT-7) */
108
+ private _sealed;
45
109
  /**
46
- * Normalize path based on router options
110
+ * Single-entry memo for {@link matchesMountPrefix}'s canonicalization of the
111
+ * mount prefix, which is fixed at registration time. Declared as two fields
112
+ * rather than an object so a miss stores without allocating.
47
113
  */
48
- private normalizePath;
114
+ private _prefixMemoRaw;
115
+ private _prefixMemoCanonical;
116
+ /** Memoized state the extracted registration/matching functions read (see {@link createRouterState}). */
117
+ private readonly state;
118
+ constructor(options?: RouterOptions);
49
119
  /**
50
- * Add a route to the radix tree
120
+ * Validate + normalize a raw path, then delegate trie insertion to the
121
+ * extracted `addRoute` (design.md D2); flips `hasParamRoutes` from its return.
51
122
  */
52
123
  private addRoute;
53
- get(path: string, ...handlers: RouteHandler[]): this;
54
- post(path: string, ...handlers: RouteHandler[]): this;
55
- put(path: string, ...handlers: RouteHandler[]): this;
56
- delete(path: string, ...handlers: RouteHandler[]): this;
57
- patch(path: string, ...handlers: RouteHandler[]): this;
58
- head(path: string, ...handlers: RouteHandler[]): this;
59
- options(path: string, ...handlers: RouteHandler[]): this;
60
- all(path: string, ...handlers: RouteHandler[]): this;
61
- route(method: HttpMethod, path: string, ...handlers: RouteHandler[]): this;
124
+ get(path: string, ...entries: RouteEntry[]): this;
125
+ post(path: string, ...entries: RouteEntry[]): this;
126
+ put(path: string, ...entries: RouteEntry[]): this;
127
+ delete(path: string, ...entries: RouteEntry[]): this;
128
+ patch(path: string, ...entries: RouteEntry[]): this;
129
+ head(path: string, ...entries: RouteEntry[]): this;
130
+ options(path: string, ...entries: RouteEntry[]): this;
62
131
  /**
63
- * Register a redirect route from one path to another
64
- *
65
- * @param from - Source path to redirect from
66
- * @param to - Target path or URL to redirect to
67
- * @param status - HTTP status code (default: 301 permanent redirect)
68
- * @returns this for chaining
69
- *
70
- * @example
71
- * ```typescript
72
- * // Permanent redirect (301)
73
- * router.redirect('/old-page', '/new-page');
74
- *
75
- * // Temporary redirect (302)
76
- * router.redirect('/temp', '/destination', 302);
77
- *
78
- * // Redirect to external URL
79
- * router.redirect('/docs', 'https://docs.example.com');
80
- *
81
- * // With parameters - redirects /users/:id to /profiles/:id
82
- * router.redirect('/users/:id', '/profiles/:id');
83
- * ```
132
+ * Register a route for every HTTP method under one consolidated `isAnyMethod`
133
+ * introspection row (T016) — matching is unchanged (see {@link pushAnyMethodDefinition}).
84
134
  */
85
- redirect(from: string, to: string, status?: 301 | 302 | 303 | 307 | 308): this;
86
- use(pathOrMiddleware: string | Middleware | Router, routerOrUndefined?: Router): this;
135
+ all(path: string, ...entries: RouteEntry[]): this;
136
+ route(method: HttpMethod, path: string, ...entries: RouteEntry[]): this;
87
137
  /**
88
- * Mount a sub-router at a path prefix (Hono-style)
89
- *
90
- * This is the explicit API for mounting sub-routers.
91
- * Equivalent to `router.use(path, subRouter)` but more semantic.
92
- *
93
- * @param path - Path prefix for the sub-router
94
- * @param router - Router instance to mount
95
- * @returns this for chaining
96
- *
97
- * @example
98
- * ```typescript
99
- * // Create modular routers
100
- * const users = createRouter();
101
- * users.get('/', listUsers);
102
- * users.get('/:id', getUser);
103
- *
104
- * const posts = createRouter();
105
- * posts.get('/', listPosts);
106
- *
107
- * // Mount sub-routers
108
- * const api = createRouter();
109
- * api.mount('/users', users);
110
- * api.mount('/posts', posts);
111
- *
112
- * // Or use on main router
113
- * const router = createRouter();
114
- * router.mount('/api', api);
115
- *
116
- * app.use(router.routes());
117
- * ```
138
+ * Every registered route as a read-only list, for renderers (`@nextrush/openapi`,
139
+ * SDK/RPC generators). Doc-generation-time only — never on the request path.
118
140
  */
119
- mount(path: string, router: Router): this;
141
+ getRoutes(): readonly RouteDefinition[];
120
142
  /**
121
- * Mount a sub-router (internal)
122
- *
123
- * Carries the sub-router's own `routerMiddleware` forward so that
124
- * `subrouter.use(mw)` middleware applies to every copied route.
143
+ * Register a redirect from one path to another (301 by default). 307/308
144
+ * additionally register POST/PUT/PATCH/DELETE to preserve the method.
145
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#redirects | README: Redirects}
125
146
  */
126
- private mountRouter;
127
- /**
128
- * Recursively copy routes from another router
129
- */
130
- private copyRoutes;
147
+ redirect(from: string, to: string, status?: RedirectStatus): this;
148
+ use(pathOrMiddleware: string | Middleware | Router, routerOrUndefined?: Router): this;
131
149
  /**
132
- * Match a route and return handler + params
150
+ * Mount a sub-router at a path prefix (Hono-style) — the explicit, more
151
+ * semantic equivalent of `router.use(path, subRouter)`.
152
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#sub-router-mounting | README: Sub-Router Mounting}
133
153
  */
154
+ mount(path: string, router: Router): this;
155
+ /** Mount a sub-router, carrying its own `routerMiddleware` onto every copied route. */
156
+ private mountRouter;
157
+ /** Match a request to a route — delegates to {@link resolveMatch} (design.md D1). */
134
158
  match(method: HttpMethod, path: string): RouteMatch | null;
135
159
  /**
136
- * Extract the next segment from path at given position without allocating arrays.
137
- * Returns [segment, nextIndex] where nextIndex is position after the trailing '/'.
138
- */
139
- private extractSegment;
140
- /**
141
- * Index-based recursive node matching (avoids array allocation)
142
- */
143
- private matchNodeIndexed;
144
- /**
145
- * Get routes middleware function
146
- * Mount this on the application
160
+ * Test whether `path` falls under `prefix` using this router's OWN
161
+ * canonicalization (case folding per `caseSensitive`, structural
162
+ * normalization) — the mount-boundary counterpart to {@link match}, so a
163
+ * router mounted via `Application.route()` is tested with the identical
164
+ * rule it dispatches with (RFC-029, task 3.8). Implements the optional
165
+ * `Routable.matchesMountPrefix` contract from `@nextrush/core`.
147
166
  *
148
- * @example
149
- * ```typescript
150
- * app.use(router.routes());
151
- * ```
167
+ * @param path - The full request path being tested for this mount.
168
+ * @param prefix - The normalized mount prefix (leading `/`, no trailing `/`).
169
+ * @returns The path's remainder past the prefix (e.g. `/users` for a
170
+ * `/ADMIN/Users` request mounted at `/admin`), or `undefined` when `path`
171
+ * is not under `prefix` per this router's canonicalization.
152
172
  */
153
- routes(): Middleware;
173
+ matchesMountPrefix(path: string, prefix: string): string | undefined;
154
174
  /**
155
- * Re-compile all route executors to include router-level middleware.
156
- * Called once when routes() is invoked, not per-request.
175
+ * Return the router's dispatch middleware mount this on the application.
176
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#routerroutes | README: router.routes()}
157
177
  */
158
- private sealRouterMiddleware;
178
+ routes(): Middleware;
159
179
  /**
160
- * Generate allowed methods middleware
161
- * Responds to OPTIONS and sets Allow header
180
+ * Generate allowed-methods middleware. Responds to OPTIONS with an `Allow`
181
+ * header and returns 405 for a known path hit with an unregistered method.
162
182
  */
163
183
  allowedMethods(): Middleware;
164
184
  /**
165
- * Find all HTTP methods registered for a given path via single tree walk
166
- * @internal
185
+ * Create a route group with a shared prefix and middleware. The callback
186
+ * receives a {@link RouteGroup} to register routes against.
187
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#route-groups | README: Route Groups}
167
188
  */
168
- private findAllowedMethods;
189
+ group(prefix: string, middlewareOrCallback: Middleware[] | ((router: RouteGroup) => void), callback?: (router: RouteGroup) => void): this;
169
190
  /**
170
- * Walk the tree to find the node matching a path (ignoring HTTP method)
171
- * @internal
191
+ * Remove all routes and middleware, resetting the router to its initial
192
+ * state — for test isolation or hot-reload re-registration.
172
193
  */
173
- private findNode;
194
+ reset(): void;
195
+ /** Register a route on behalf of a {@link RouteGroup} (group context). @internal */
196
+ _addGroupRoute(method: HttpMethod, path: string, handlers: RouteHandler[], groupMiddleware: Middleware[], recordIntrospection?: boolean): void;
174
197
  /**
175
- * Create a route group with shared prefix and middleware
176
- *
177
- * @param prefix - Path prefix for all routes in the group
178
- * @param middlewareOrCallback - Middleware array or callback function
179
- * @param callback - Callback function if middleware is provided
180
- * @returns this for chaining
181
- *
182
- * @example
183
- * ```typescript
184
- * // Simple group with prefix
185
- * router.group('/api', (r) => {
186
- * r.get('/users', listUsers);
187
- * r.get('/posts', listPosts);
188
- * });
189
- *
190
- * // Group with middleware
191
- * router.group('/admin', [authMiddleware], (r) => {
192
- * r.get('/dashboard', dashboard);
193
- * r.post('/settings', updateSettings);
194
- * });
195
- *
196
- * // Nested groups
197
- * router.group('/api/v1', (r) => {
198
- * r.group('/users', [rateLimit], (ur) => {
199
- * ur.get('/', listUsers);
200
- * ur.get('/:id', getUser);
201
- * });
202
- * });
203
- * ```
198
+ * Group-facing entry point to {@link pushAnyMethodDefinition} a group's `.all()`
199
+ * records its consolidated row here since group routes live on the parent. @internal
204
200
  */
205
- group(prefix: string, middlewareOrCallback: Middleware[] | ((router: Router) => void), callback?: (router: Router) => void): this;
201
+ _pushAnyMethodRouteDefinition(path: string): void;
202
+ }
203
+ /**
204
+ * Create a new {@link Router} instance.
205
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#quick-start | README: Quick Start}
206
+ */
207
+ declare function createRouter(options?: RouterOptions): Router;
208
+
209
+ /**
210
+ * @nextrush/router - Canonical Request Path
211
+ *
212
+ * The single normalization owner (RFC-029): every consumer that needs to know
213
+ * "what path does the router treat this request as" — the router's own match,
214
+ * a mounted-router prefix test, a CSRF exclude-path match, an adapter's
215
+ * published `ctx.path` — calls {@link canonicalizePath} instead of hand-rolling
216
+ * its own fold/collapse/strip, so there is exactly one definition of "the same
217
+ * path" across the framework (SEC-02, SEC-09, SEC-15).
218
+ *
219
+ * @see docs/RFC/request-data/029-canonical-request-path.md
220
+ * @packageDocumentation
221
+ */
222
+ /** Result of {@link canonicalizePath}. */
223
+ interface CanonicalPathResult {
206
224
  /**
207
- * Remove all registered routes and middleware, resetting the router to its initial state.
208
- * Useful for plugin `destroy()` to cleanly un-register routes.
225
+ * `true` when the path contains a `.`/`..` path segment. A dot segment is
226
+ * rejected outright (400), never resolved resolving it locally would
227
+ * diverge from how a front-end proxy already resolved (or didn't resolve)
228
+ * the same segment before forwarding, reopening the desync this RFC closes.
209
229
  */
210
- reset(): void;
230
+ readonly rejected: boolean;
211
231
  /**
212
- * Internal method to add route with group context
213
- * @internal
232
+ * The canonical path: query-stripped, dot-segment-free, case-folded (unless
233
+ * `caseSensitive`), slash-collapsed, trailing-slash-stripped per `strict`.
234
+ * Meaningless when {@link rejected} is `true`.
214
235
  */
215
- _addGroupRoute(method: HttpMethod, path: string, handlers: RouteHandler[], groupMiddleware: Middleware[]): void;
236
+ readonly path: string;
216
237
  }
217
238
  /**
218
- * Create a new Router instance
239
+ * True when `path` (already query-stripped) contains a `.` or `..` segment —
240
+ * a component that is exactly `.` or `..` between slash boundaries (or at the
241
+ * start/end of the string). A single linear scan, no backtracking regex
242
+ * (task 3.5): each character is visited at most once, so a pathological input
243
+ * cannot degrade to worse than O(n).
219
244
  *
220
- * @param options - Router options
221
- * @returns New Router instance
245
+ * A dot as filename content (`archive.tar.gz`, `..hidden.txt`) is NOT a dot
246
+ * segment only a component whose entire text between slashes is `.` or `..`
247
+ * counts.
248
+ */
249
+ declare function hasDotSegment(path: string): boolean;
250
+ /**
251
+ * Canonicalize a raw request target into the one path value every consumer
252
+ * (router match, mount-prefix test, CSRF exclude match, published `ctx.path`)
253
+ * treats as "this request's path" (RFC-029).
222
254
  *
223
- * @example
224
- * ```typescript
225
- * const router = createRouter();
226
- * const apiRouter = createRouter({ prefix: '/api/v1' });
227
- * ```
255
+ * Order: strip the query string, reject a dot segment before any further
256
+ * normalization (a dot segment is a request-shape violation, not something to
257
+ * fold case on first), then fold case (unless `caseSensitive`) and collapse
258
+ * structure exactly as {@link import('./matching').normalizePathForMatch} does
259
+ * — this function IS that normalization, extended with dot-segment rejection
260
+ * and made a public, adapter-facing entry point.
261
+ *
262
+ * The most recent result is memoized; see the note above the memo fields for
263
+ * why that is sound. Treat the returned object as immutable.
264
+ *
265
+ * @param rawTarget - The raw request target, may include a query string.
266
+ * @param caseSensitive - Router case-sensitivity option.
267
+ * @param strict - Router strict-trailing-slash option.
228
268
  */
229
- declare function createRouter(options?: RouterOptions): Router;
269
+ declare function canonicalizePath(rawTarget: string, caseSensitive: boolean, strict: boolean): CanonicalPathResult;
230
270
 
231
271
  /**
232
272
  * @nextrush/router - Segment Trie Node
233
273
  *
234
274
  * Internal segment trie implementation for high-performance route matching.
235
- * Uses a compressed trie structure for O(k) lookups where k is path length.
275
+ * Segments are split by `/` and matched one trie level per segment for O(k)
276
+ * lookups where k is path length.
236
277
  *
237
278
  * @packageDocumentation
238
279
  * @internal
@@ -250,23 +291,23 @@ declare const enum NodeType {
250
291
  WILDCARD = 2
251
292
  }
252
293
  /**
253
- * Radix tree node
294
+ * Segment trie node
254
295
  */
255
- interface RadixNode {
296
+ interface TrieNode {
256
297
  /** Path segment for this node */
257
298
  segment: string;
258
299
  /** Node type */
259
300
  type: NodeType;
260
- /** Children nodes keyed by first character */
261
- children: Map<string, RadixNode>;
301
+ /** Static child nodes keyed by whole path segment (e.g. `users`), not the first character */
302
+ children: Map<string, TrieNode>;
262
303
  /** Parameter name if this is a param node */
263
304
  paramName?: string;
264
305
  /** Handlers keyed by HTTP method */
265
306
  handlers: Map<HttpMethod, HandlerEntry>;
266
307
  /** Wildcard child if any */
267
- wildcardChild?: RadixNode;
308
+ wildcardChild?: TrieNode;
268
309
  /** Parameter child if any */
269
- paramChild?: RadixNode;
310
+ paramChild?: TrieNode;
270
311
  }
271
312
  /**
272
313
  * Handler entry with middleware and pre-compiled executor
@@ -276,11 +317,19 @@ interface HandlerEntry {
276
317
  middleware: Middleware[];
277
318
  /** Pre-compiled executor for fast dispatch (no closure per request) */
278
319
  executor?: (ctx: Context) => Promise<void>;
320
+ /**
321
+ * `true` only for a `HEAD` entry derived from a `GET` registration
322
+ * (RFC 9110 §9.3.2). A derived entry is replaced by an explicit `HEAD`
323
+ * registration instead of reporting a route conflict, is absent from
324
+ * `getRoutes()`, and is skipped when copying routes into a parent router —
325
+ * which re-derives it from the `GET` it copies.
326
+ */
327
+ autoHead: boolean;
279
328
  }
280
329
  /**
281
- * Create a new radix node
330
+ * Create a new segment trie node
282
331
  */
283
- declare function createNode(segment: string, type?: NodeType): RadixNode;
332
+ declare function createNode(segment: string, type?: NodeType): TrieNode;
284
333
  /**
285
334
  * Parse path segments
286
335
  * Splits path into segments and identifies param/wildcard types
@@ -298,4 +347,4 @@ interface ParsedSegment {
298
347
  paramName?: string;
299
348
  }
300
349
 
301
- export { type HandlerEntry, NodeType, type ParsedSegment, type RadixNode, Router, createNode, createRouter, parseSegments };
350
+ export { type CanonicalPathResult, type HandlerEntry, NodeType, type ParsedSegment, type RouteGroup, Router, type TrieNode, canonicalizePath, createNode, createRouter, endpoint, hasDotSegment, parseSegments };