@nextrush/router 3.0.7 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +250 -492
  2. package/dist/index.d.ts +142 -185
  3. package/dist/index.js +816 -665
  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 +71 -0
  8. package/src/__tests__/dispatch-deasync.test.ts +312 -0
  9. package/src/__tests__/find-node-differential.test.ts +220 -0
  10. package/src/__tests__/fixtures/match-golden.json +556 -0
  11. package/src/__tests__/helpers/differential-corpus.ts +316 -0
  12. package/src/__tests__/match-differential.test.ts +46 -0
  13. package/src/__tests__/match-hotpath-guard.test.ts +71 -0
  14. package/src/__tests__/match-normalize-fastpath.test.ts +66 -0
  15. package/src/__tests__/match-safety.test.ts +177 -0
  16. package/src/__tests__/match-single-alloc.test.ts +84 -0
  17. package/src/__tests__/middleware-pipeline.test.ts +306 -0
  18. package/src/__tests__/param-decoding.test.ts +78 -0
  19. package/src/__tests__/public-surface.test.ts +59 -0
  20. package/src/__tests__/route-metadata.test.ts +172 -0
  21. package/src/__tests__/router-audit.test.ts +326 -0
  22. package/src/__tests__/router-edge-cases.test.ts +2 -2
  23. package/src/__tests__/router.test.ts +148 -2
  24. package/src/__tests__/static-map-reset.test.ts +50 -0
  25. package/src/composition.ts +75 -0
  26. package/src/constants.ts +25 -0
  27. package/src/dispatch.ts +117 -0
  28. package/src/find-node.ts +125 -0
  29. package/src/group-router.ts +208 -0
  30. package/src/index.ts +8 -5
  31. package/src/match-route.ts +178 -0
  32. package/src/matching.ts +240 -0
  33. package/src/middleware-adapter.ts +59 -0
  34. package/src/redirect.ts +97 -0
  35. package/src/registration.ts +291 -0
  36. package/src/route-metadata.ts +68 -0
  37. package/src/router.ts +150 -881
  38. package/src/segment-trie.ts +219 -0
  39. package/src/state.ts +52 -0
  40. package/src/radix-tree.ts +0 -184
package/dist/index.d.ts CHANGED
@@ -1,230 +1,186 @@
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
17
54
  *
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.
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.
59
+ *
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;
107
+ /** Whether router-level middleware has already been sealed into executors (audit RT-7) */
108
+ private _sealed;
109
+ /** Memoized state the extracted registration/matching functions read (see {@link createRouterState}). */
110
+ private readonly state;
44
111
  constructor(options?: RouterOptions);
45
112
  /**
46
- * Normalize path based on router options
47
- */
48
- private normalizePath;
49
- /**
50
- * Add a route to the radix tree
113
+ * Validate + normalize a raw path, then delegate trie insertion to the
114
+ * extracted `addRoute` (design.md D2); flips `hasParamRoutes` from its return.
51
115
  */
52
116
  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;
117
+ get(path: string, ...entries: RouteEntry[]): this;
118
+ post(path: string, ...entries: RouteEntry[]): this;
119
+ put(path: string, ...entries: RouteEntry[]): this;
120
+ delete(path: string, ...entries: RouteEntry[]): this;
121
+ patch(path: string, ...entries: RouteEntry[]): this;
122
+ head(path: string, ...entries: RouteEntry[]): this;
123
+ options(path: string, ...entries: RouteEntry[]): this;
62
124
  /**
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
- * ```
125
+ * Register a route for every HTTP method under one consolidated `isAnyMethod`
126
+ * introspection row (T016) — matching is unchanged (see {@link pushAnyMethodDefinition}).
84
127
  */
85
- redirect(from: string, to: string, status?: 301 | 302 | 303 | 307 | 308): this;
86
- use(pathOrMiddleware: string | Middleware | Router, routerOrUndefined?: Router): this;
128
+ all(path: string, ...entries: RouteEntry[]): this;
129
+ route(method: HttpMethod, path: string, ...entries: RouteEntry[]): this;
87
130
  /**
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
- * ```
131
+ * Every registered route as a read-only list, for renderers (`@nextrush/openapi`,
132
+ * SDK/RPC generators). Doc-generation-time only — never on the request path.
118
133
  */
119
- mount(path: string, router: Router): this;
134
+ getRoutes(): readonly RouteDefinition[];
120
135
  /**
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.
136
+ * Register a redirect from one path to another (301 by default). 307/308
137
+ * additionally register POST/PUT/PATCH/DELETE to preserve the method.
138
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#redirects | README: Redirects}
125
139
  */
126
- private mountRouter;
127
- /**
128
- * Recursively copy routes from another router
129
- */
130
- private copyRoutes;
140
+ redirect(from: string, to: string, status?: RedirectStatus): this;
141
+ use(pathOrMiddleware: string | Middleware | Router, routerOrUndefined?: Router): this;
131
142
  /**
132
- * Match a route and return handler + params
143
+ * Mount a sub-router at a path prefix (Hono-style) — the explicit, more
144
+ * semantic equivalent of `router.use(path, subRouter)`.
145
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#sub-router-mounting | README: Sub-Router Mounting}
133
146
  */
147
+ mount(path: string, router: Router): this;
148
+ /** Mount a sub-router, carrying its own `routerMiddleware` onto every copied route. */
149
+ private mountRouter;
150
+ /** Match a request to a route — delegates to {@link resolveMatch} (design.md D1). */
134
151
  match(method: HttpMethod, path: string): RouteMatch | null;
135
152
  /**
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
147
- *
148
- * @example
149
- * ```typescript
150
- * app.use(router.routes());
151
- * ```
153
+ * Return the router's dispatch middleware mount this on the application.
154
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#routerroutes | README: router.routes()}
152
155
  */
153
156
  routes(): Middleware;
154
157
  /**
155
- * Re-compile all route executors to include router-level middleware.
156
- * Called once when routes() is invoked, not per-request.
157
- */
158
- private sealRouterMiddleware;
159
- /**
160
- * Generate allowed methods middleware
161
- * Responds to OPTIONS and sets Allow header
158
+ * Generate allowed-methods middleware. Responds to OPTIONS with an `Allow`
159
+ * header and returns 405 for a known path hit with an unregistered method.
162
160
  */
163
161
  allowedMethods(): Middleware;
164
162
  /**
165
- * Find all HTTP methods registered for a given path via single tree walk
166
- * @internal
163
+ * Create a route group with a shared prefix and middleware. The callback
164
+ * receives a {@link RouteGroup} to register routes against.
165
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#route-groups | README: Route Groups}
167
166
  */
168
- private findAllowedMethods;
167
+ group(prefix: string, middlewareOrCallback: Middleware[] | ((router: RouteGroup) => void), callback?: (router: RouteGroup) => void): this;
169
168
  /**
170
- * Walk the tree to find the node matching a path (ignoring HTTP method)
171
- * @internal
172
- */
173
- private findNode;
174
- /**
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
- * ```
204
- */
205
- group(prefix: string, middlewareOrCallback: Middleware[] | ((router: Router) => void), callback?: (router: Router) => void): this;
206
- /**
207
- * Remove all registered routes and middleware, resetting the router to its initial state.
208
- * Useful for plugin `destroy()` to cleanly un-register routes.
169
+ * Remove all routes and middleware, resetting the router to its initial
170
+ * state — for test isolation or hot-reload re-registration.
209
171
  */
210
172
  reset(): void;
173
+ /** Register a route on behalf of a {@link RouteGroup} (group context). @internal */
174
+ _addGroupRoute(method: HttpMethod, path: string, handlers: RouteHandler[], groupMiddleware: Middleware[], recordIntrospection?: boolean): void;
211
175
  /**
212
- * Internal method to add route with group context
213
- * @internal
176
+ * Group-facing entry point to {@link pushAnyMethodDefinition} a group's `.all()`
177
+ * records its consolidated row here since group routes live on the parent. @internal
214
178
  */
215
- _addGroupRoute(method: HttpMethod, path: string, handlers: RouteHandler[], groupMiddleware: Middleware[]): void;
179
+ _pushAnyMethodRouteDefinition(path: string): void;
216
180
  }
217
181
  /**
218
- * Create a new Router instance
219
- *
220
- * @param options - Router options
221
- * @returns New Router instance
222
- *
223
- * @example
224
- * ```typescript
225
- * const router = createRouter();
226
- * const apiRouter = createRouter({ prefix: '/api/v1' });
227
- * ```
182
+ * Create a new {@link Router} instance.
183
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#quick-start | README: Quick Start}
228
184
  */
229
185
  declare function createRouter(options?: RouterOptions): Router;
230
186
 
@@ -232,7 +188,8 @@ declare function createRouter(options?: RouterOptions): Router;
232
188
  * @nextrush/router - Segment Trie Node
233
189
  *
234
190
  * Internal segment trie implementation for high-performance route matching.
235
- * Uses a compressed trie structure for O(k) lookups where k is path length.
191
+ * Segments are split by `/` and matched one trie level per segment for O(k)
192
+ * lookups where k is path length.
236
193
  *
237
194
  * @packageDocumentation
238
195
  * @internal
@@ -250,23 +207,23 @@ declare const enum NodeType {
250
207
  WILDCARD = 2
251
208
  }
252
209
  /**
253
- * Radix tree node
210
+ * Segment trie node
254
211
  */
255
- interface RadixNode {
212
+ interface TrieNode {
256
213
  /** Path segment for this node */
257
214
  segment: string;
258
215
  /** Node type */
259
216
  type: NodeType;
260
- /** Children nodes keyed by first character */
261
- children: Map<string, RadixNode>;
217
+ /** Static child nodes keyed by whole path segment (e.g. `users`), not the first character */
218
+ children: Map<string, TrieNode>;
262
219
  /** Parameter name if this is a param node */
263
220
  paramName?: string;
264
221
  /** Handlers keyed by HTTP method */
265
222
  handlers: Map<HttpMethod, HandlerEntry>;
266
223
  /** Wildcard child if any */
267
- wildcardChild?: RadixNode;
224
+ wildcardChild?: TrieNode;
268
225
  /** Parameter child if any */
269
- paramChild?: RadixNode;
226
+ paramChild?: TrieNode;
270
227
  }
271
228
  /**
272
229
  * Handler entry with middleware and pre-compiled executor
@@ -278,9 +235,9 @@ interface HandlerEntry {
278
235
  executor?: (ctx: Context) => Promise<void>;
279
236
  }
280
237
  /**
281
- * Create a new radix node
238
+ * Create a new segment trie node
282
239
  */
283
- declare function createNode(segment: string, type?: NodeType): RadixNode;
240
+ declare function createNode(segment: string, type?: NodeType): TrieNode;
284
241
  /**
285
242
  * Parse path segments
286
243
  * Splits path into segments and identifies param/wildcard types
@@ -298,4 +255,4 @@ interface ParsedSegment {
298
255
  paramName?: string;
299
256
  }
300
257
 
301
- export { type HandlerEntry, NodeType, type ParsedSegment, type RadixNode, Router, createNode, createRouter, parseSegments };
258
+ export { type HandlerEntry, NodeType, type ParsedSegment, type RouteGroup, Router, type TrieNode, createNode, createRouter, endpoint, parseSegments };