@nextrush/router 3.0.6 → 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.js CHANGED
@@ -2,9 +2,9 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/router.ts
5
- import { HTTP_METHODS } from "@nextrush/types";
5
+ import { HTTP_METHODS as HTTP_METHODS3 } from "@nextrush/types";
6
6
 
7
- // src/radix-tree.ts
7
+ // src/segment-trie.ts
8
8
  var NodeType = /* @__PURE__ */ (function(NodeType2) {
9
9
  NodeType2[NodeType2["STATIC"] = 0] = "STATIC";
10
10
  NodeType2[NodeType2["PARAM"] = 1] = "PARAM";
@@ -16,43 +16,41 @@ var NOOP_NEXT = /* @__PURE__ */ __name(() => RESOLVED_PROMISE, "NOOP_NEXT");
16
16
  function compileExecutor(handler, middleware) {
17
17
  const len = middleware.length;
18
18
  if (len === 0) {
19
- return async (ctx) => {
20
- await handler(ctx, NOOP_NEXT);
21
- };
22
- }
23
- if (len === 1) {
24
- const mw = middleware[0];
25
- if (mw === void 0) throw new Error("middleware length mismatch");
26
- return async (ctx) => {
27
- await mw(ctx, async () => {
28
- await handler(ctx, NOOP_NEXT);
29
- });
30
- };
31
- }
32
- if (len === 2) {
33
- const mw0 = middleware[0];
34
- const mw1 = middleware[1];
35
- if (mw0 === void 0 || mw1 === void 0) throw new Error("middleware length mismatch");
36
- return async (ctx) => {
37
- await mw0(ctx, async () => {
38
- await mw1(ctx, async () => {
39
- await handler(ctx, NOOP_NEXT);
40
- });
41
- });
19
+ return (ctx) => {
20
+ if (ctx.setNext) ctx.setNext(NOOP_NEXT);
21
+ try {
22
+ return Promise.resolve(handler(ctx, NOOP_NEXT));
23
+ } catch (err) {
24
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
25
+ }
42
26
  };
43
27
  }
44
- return async (ctx) => {
45
- let index = 0;
46
- const dispatch = /* @__PURE__ */ __name(async () => {
47
- if (index < len) {
48
- const mw = middleware[index++];
49
- if (mw === void 0) throw new Error("middleware length mismatch");
50
- await mw(ctx, dispatch);
51
- } else {
52
- await handler(ctx, NOOP_NEXT);
28
+ return (ctx) => {
29
+ let index = -1;
30
+ const dispatch = /* @__PURE__ */ __name((i) => {
31
+ if (i <= index) {
32
+ return Promise.reject(new Error("next() called multiple times"));
33
+ }
34
+ index = i;
35
+ if (i < len) {
36
+ const mw = middleware[i];
37
+ if (mw === void 0) return Promise.reject(new Error("middleware length mismatch"));
38
+ const next = /* @__PURE__ */ __name(() => dispatch(i + 1), "next");
39
+ if (ctx.setNext) ctx.setNext(next);
40
+ try {
41
+ return Promise.resolve(mw(ctx, next));
42
+ } catch (err) {
43
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
44
+ }
45
+ }
46
+ if (ctx.setNext) ctx.setNext(NOOP_NEXT);
47
+ try {
48
+ return Promise.resolve(handler(ctx, NOOP_NEXT));
49
+ } catch (err) {
50
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
53
51
  }
54
52
  }, "dispatch");
55
- await dispatch();
53
+ return dispatch(0);
56
54
  };
57
55
  }
58
56
  __name(compileExecutor, "compileExecutor");
@@ -65,6 +63,13 @@ function createNode(segment, type = 0) {
65
63
  };
66
64
  }
67
65
  __name(createNode, "createNode");
66
+ function clearNode(node) {
67
+ node.children.clear();
68
+ node.handlers.clear();
69
+ node.paramChild = void 0;
70
+ node.wildcardChild = void 0;
71
+ }
72
+ __name(clearNode, "clearNode");
68
73
  function parseSegments(path, caseSensitive = true) {
69
74
  const normalized = path.startsWith("/") ? path.slice(1) : path;
70
75
  if (normalized === "") return [];
@@ -95,749 +100,894 @@ function parseSegments(path, caseSensitive = true) {
95
100
  }
96
101
  __name(parseSegments, "parseSegments");
97
102
 
98
- // src/router.ts
99
- var EMPTY_PARAMS = Object.freeze(/* @__PURE__ */ Object.create(null));
100
- var Router = class _Router {
101
- static {
102
- __name(this, "Router");
103
- }
104
- root;
105
- opts;
106
- routerMiddleware = [];
107
- /**
108
- * Static route hash map for O(1) lookup.
109
- * Key: "METHOD path" (e.g. "GET /users"), Value: HandlerEntry
110
- */
111
- staticRoutes = /* @__PURE__ */ new Map();
112
- /** Whether any routes have params or wildcards (disables static-only fast path) */
113
- hasParamRoutes = false;
114
- constructor(options = {}) {
115
- this.root = createNode("");
116
- this.opts = {
117
- prefix: options.prefix ?? "",
118
- caseSensitive: options.caseSensitive ?? false,
119
- strict: options.strict ?? false
120
- };
121
- }
122
- /**
123
- * Normalize path based on router options
124
- */
125
- normalizePath(path) {
126
- let prefix = this.opts.prefix;
127
- if (prefix.endsWith("/") && path.startsWith("/")) {
128
- prefix = prefix.slice(0, -1);
129
- }
130
- let normalized = prefix + path;
131
- if (normalized.includes("//")) {
132
- normalized = normalized.replace(/\/+/g, "/");
103
+ // src/group-router.ts
104
+ import { HTTP_METHODS } from "@nextrush/types";
105
+
106
+ // src/redirect.ts
107
+ function compileRedirectTarget(to) {
108
+ const parts = [];
109
+ let pos = 0;
110
+ let found = false;
111
+ while (pos < to.length) {
112
+ let idx = -1;
113
+ for (let i = pos; i < to.length; i++) {
114
+ if (to[i] === ":" && (i === 0 || to[i - 1] === "/") && i + 1 < to.length && to[i + 1] !== "/") {
115
+ idx = i;
116
+ break;
117
+ }
133
118
  }
134
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
135
- normalized = normalized.slice(0, -1);
119
+ if (idx === -1) break;
120
+ found = true;
121
+ parts.push(to.slice(pos, idx));
122
+ const end = to.indexOf("/", idx + 1);
123
+ if (end === -1) {
124
+ parts.push(to.slice(idx + 1));
125
+ pos = to.length;
126
+ } else {
127
+ parts.push(to.slice(idx + 1, end));
128
+ pos = end;
136
129
  }
137
- return normalized.startsWith("/") ? normalized : "/" + normalized;
138
130
  }
139
- /**
140
- * Add a route to the radix tree
141
- */
142
- addRoute(method, path, handlers, middleware = []) {
143
- const normalized = this.normalizePath(path);
144
- const segments = parseSegments(normalized, this.opts.caseSensitive);
145
- let node = this.root;
146
- for (const seg of segments) {
147
- if (seg.type === NodeType.PARAM) {
148
- if (!node.paramChild) {
149
- node.paramChild = createNode(seg.segment, NodeType.PARAM);
150
- node.paramChild.paramName = seg.paramName;
151
- } else if (node.paramChild.paramName !== seg.paramName) {
152
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
153
- const existing = node.paramChild.paramName;
154
- console.warn(`[nextrush:router] Route param name conflict at "${normalized}": ":${String(seg.paramName)}" conflicts with existing ":${String(existing)}". The existing name ":${String(existing)}" will be used.`);
155
- }
156
- }
157
- node = node.paramChild;
158
- } else if (seg.type === NodeType.WILDCARD) {
159
- node.wildcardChild ??= createNode("*", NodeType.WILDCARD);
160
- node = node.wildcardChild;
161
- break;
131
+ if (found) {
132
+ parts.push(to.slice(pos));
133
+ return parts;
134
+ }
135
+ return void 0;
136
+ }
137
+ __name(compileRedirectTarget, "compileRedirectTarget");
138
+ function createRedirectHandler(to, status) {
139
+ const compiledParts = compileRedirectTarget(to);
140
+ return (ctx) => {
141
+ let targetPath;
142
+ if (compiledParts) {
143
+ const params = ctx.params;
144
+ const head = compiledParts[0];
145
+ if (head === void 0) {
146
+ targetPath = to;
162
147
  } else {
163
- const key = seg.segment;
164
- let child = node.children.get(key);
165
- if (!child) {
166
- child = createNode(seg.segment, NodeType.STATIC);
167
- node.children.set(key, child);
148
+ let result = head;
149
+ for (let i = 1; i < compiledParts.length - 1; i += 2) {
150
+ const paramKey = compiledParts[i];
151
+ const tail = compiledParts[i + 1];
152
+ if (paramKey === void 0 || tail === void 0) break;
153
+ result += (params[paramKey] ?? "") + tail;
168
154
  }
169
- node = child;
155
+ targetPath = result;
170
156
  }
171
- }
172
- const combinedMiddleware = [
173
- ...middleware
174
- ];
175
- const finalHandler = handlers[handlers.length - 1];
176
- if (!finalHandler) {
177
- throw new Error("At least one handler is required");
178
- }
179
- const inlineMiddleware = handlers.slice(0, -1);
180
- for (const mw of inlineMiddleware) {
181
- combinedMiddleware.push(mw);
182
- }
183
- const executor = compileExecutor(finalHandler, combinedMiddleware);
184
- const entry = {
185
- handler: finalHandler,
186
- middleware: combinedMiddleware,
187
- executor
188
- };
189
- if (node.handlers.has(method)) {
190
- throw new Error(`Route conflict: ${method} ${normalized} is already registered. Remove the duplicate or use a different path.`);
191
- }
192
- node.handlers.set(method, entry);
193
- const hasParams = segments.some((s) => s.type === NodeType.PARAM || s.type === NodeType.WILDCARD);
194
- if (hasParams) {
195
- this.hasParamRoutes = true;
196
157
  } else {
197
- const normalizedKey = this.opts.caseSensitive ? normalized : normalized.toLowerCase();
198
- this.staticRoutes.set(`${method} ${normalizedKey}`, entry);
158
+ targetPath = to;
199
159
  }
160
+ ctx.status = status;
161
+ ctx.set("Location", targetPath);
162
+ ctx.body = "";
163
+ };
164
+ }
165
+ __name(createRedirectHandler, "createRedirectHandler");
166
+
167
+ // src/group-router.ts
168
+ var GroupRouter = class {
169
+ static {
170
+ __name(this, "GroupRouter");
171
+ }
172
+ parent;
173
+ prefix;
174
+ middleware;
175
+ constructor(parent, prefix, middleware) {
176
+ this.parent = parent;
177
+ this.prefix = prefix;
178
+ this.middleware = middleware;
179
+ }
180
+ fullPath(path) {
181
+ if (path === "/" || path === "") {
182
+ return this.prefix;
183
+ }
184
+ const cleanPrefix = this.prefix.endsWith("/") ? this.prefix.slice(0, -1) : this.prefix;
185
+ const cleanPath = path.startsWith("/") ? path : "/" + path;
186
+ return cleanPrefix + cleanPath;
200
187
  }
201
- // ===========================================================================
202
- // HTTP Method Shortcuts
203
- // ===========================================================================
204
188
  get(path, ...handlers) {
205
- this.addRoute("GET", path, handlers);
189
+ this.parent._addGroupRoute("GET", this.fullPath(path), handlers, this.middleware);
206
190
  return this;
207
191
  }
208
192
  post(path, ...handlers) {
209
- this.addRoute("POST", path, handlers);
193
+ this.parent._addGroupRoute("POST", this.fullPath(path), handlers, this.middleware);
210
194
  return this;
211
195
  }
212
196
  put(path, ...handlers) {
213
- this.addRoute("PUT", path, handlers);
197
+ this.parent._addGroupRoute("PUT", this.fullPath(path), handlers, this.middleware);
214
198
  return this;
215
199
  }
216
200
  delete(path, ...handlers) {
217
- this.addRoute("DELETE", path, handlers);
201
+ this.parent._addGroupRoute("DELETE", this.fullPath(path), handlers, this.middleware);
218
202
  return this;
219
203
  }
220
204
  patch(path, ...handlers) {
221
- this.addRoute("PATCH", path, handlers);
205
+ this.parent._addGroupRoute("PATCH", this.fullPath(path), handlers, this.middleware);
222
206
  return this;
223
207
  }
224
208
  head(path, ...handlers) {
225
- this.addRoute("HEAD", path, handlers);
209
+ this.parent._addGroupRoute("HEAD", this.fullPath(path), handlers, this.middleware);
226
210
  return this;
227
211
  }
228
212
  options(path, ...handlers) {
229
- this.addRoute("OPTIONS", path, handlers);
213
+ this.parent._addGroupRoute("OPTIONS", this.fullPath(path), handlers, this.middleware);
230
214
  return this;
231
215
  }
216
+ /**
217
+ * Register a route matching every HTTP method under a single introspection
218
+ * entry, mirroring `Router.all()` (T016) — a group's `.all()` must not
219
+ * regress to the pre-T016 one-row-per-method shape just because
220
+ * registration is routed through `_addGroupRoute` instead of `addRoute`
221
+ * directly.
222
+ */
232
223
  all(path, ...handlers) {
233
224
  for (const method of HTTP_METHODS) {
234
- this.addRoute(method, path, handlers);
225
+ this.parent._addGroupRoute(method, this.fullPath(path), handlers, this.middleware, false);
235
226
  }
236
- return this;
237
- }
238
- route(method, path, ...handlers) {
239
- this.addRoute(method, path, handlers);
227
+ this.parent._pushAnyMethodRouteDefinition(this.fullPath(path));
240
228
  return this;
241
229
  }
242
230
  /**
243
- * Register a redirect route from one path to another
244
- *
245
- * @param from - Source path to redirect from
246
- * @param to - Target path or URL to redirect to
247
- * @param status - HTTP status code (default: 301 permanent redirect)
248
- * @returns this for chaining
249
- *
250
- * @example
251
- * ```typescript
252
- * // Permanent redirect (301)
253
- * router.redirect('/old-page', '/new-page');
254
- *
255
- * // Temporary redirect (302)
256
- * router.redirect('/temp', '/destination', 302);
257
- *
258
- * // Redirect to external URL
259
- * router.redirect('/docs', 'https://docs.example.com');
260
- *
261
- * // With parameters - redirects /users/:id to /profiles/:id
262
- * router.redirect('/users/:id', '/profiles/:id');
263
- * ```
231
+ * Register a redirect within the group (uses the shared redirect handler,
232
+ * audit RT-4 — no more naive replaceAll param substitution).
264
233
  */
265
234
  redirect(from, to, status = 301) {
266
- let compiledParts;
267
- {
268
- const parts = [];
269
- let pos = 0;
270
- let found = false;
271
- while (pos < to.length) {
272
- let idx = -1;
273
- for (let i = pos; i < to.length; i++) {
274
- if (to[i] === ":" && (i === 0 || to[i - 1] === "/") && i + 1 < to.length && to[i + 1] !== "/") {
275
- idx = i;
276
- break;
277
- }
278
- }
279
- if (idx === -1) break;
280
- found = true;
281
- parts.push(to.slice(pos, idx));
282
- const end = to.indexOf("/", idx + 1);
283
- if (end === -1) {
284
- parts.push(to.slice(idx + 1));
285
- pos = to.length;
286
- } else {
287
- parts.push(to.slice(idx + 1, end));
288
- pos = end;
289
- }
290
- }
291
- if (found) {
292
- parts.push(to.slice(pos));
293
- compiledParts = parts;
294
- }
295
- }
296
- const redirectHandler = /* @__PURE__ */ __name((ctx) => {
297
- let targetPath;
298
- if (compiledParts) {
299
- const params = ctx.params;
300
- const parts = compiledParts;
301
- const head = parts[0];
302
- if (head === void 0) {
303
- targetPath = to;
304
- } else {
305
- let result = head;
306
- for (let i = 1; i < parts.length - 1; i += 2) {
307
- const paramKey = parts[i];
308
- const tail = parts[i + 1];
309
- if (paramKey === void 0 || tail === void 0) break;
310
- result += (params[paramKey] ?? "") + tail;
311
- }
312
- targetPath = result;
313
- }
314
- } else {
315
- targetPath = to;
316
- }
317
- ctx.status = status;
318
- ctx.set("Location", targetPath);
319
- ctx.body = "";
320
- }, "redirectHandler");
321
- this.addRoute("GET", from, [
235
+ const redirectHandler = createRedirectHandler(to, status);
236
+ this.parent._addGroupRoute("GET", this.fullPath(from), [
322
237
  redirectHandler
323
- ]);
324
- this.addRoute("HEAD", from, [
238
+ ], this.middleware);
239
+ this.parent._addGroupRoute("HEAD", this.fullPath(from), [
325
240
  redirectHandler
326
- ]);
327
- if (status === 307 || status === 308) {
328
- this.addRoute("POST", from, [
329
- redirectHandler
330
- ]);
331
- this.addRoute("PUT", from, [
332
- redirectHandler
333
- ]);
334
- this.addRoute("PATCH", from, [
335
- redirectHandler
336
- ]);
337
- this.addRoute("DELETE", from, [
338
- redirectHandler
339
- ]);
340
- }
341
- return this;
342
- }
343
- // ===========================================================================
344
- // Router Composition
345
- // ===========================================================================
346
- use(pathOrMiddleware, routerOrUndefined) {
347
- if (typeof pathOrMiddleware === "function") {
348
- this.routerMiddleware.push(pathOrMiddleware);
349
- } else if (typeof pathOrMiddleware === "string" && routerOrUndefined instanceof _Router) {
350
- this.mountRouter(pathOrMiddleware, routerOrUndefined);
351
- } else if (typeof pathOrMiddleware === "string") {
352
- throw new Error(`router.use('${pathOrMiddleware}', ...) requires a Router instance as the second argument. Use router.group(prefix, callback) for prefix-scoped middleware, or router.use(middlewareFn) to register middleware without a prefix.`);
353
- } else if (pathOrMiddleware instanceof _Router) {
354
- this.mountRouter("", pathOrMiddleware);
355
- }
241
+ ], this.middleware);
356
242
  return this;
357
243
  }
358
244
  /**
359
- * Mount a sub-router at a path prefix (Hono-style)
360
- *
361
- * This is the explicit API for mounting sub-routers.
362
- * Equivalent to `router.use(path, subRouter)` but more semantic.
363
- *
364
- * @param path - Path prefix for the sub-router
365
- * @param router - Router instance to mount
366
- * @returns this for chaining
367
- *
368
- * @example
369
- * ```typescript
370
- * // Create modular routers
371
- * const users = createRouter();
372
- * users.get('/', listUsers);
373
- * users.get('/:id', getUser);
374
- *
375
- * const posts = createRouter();
376
- * posts.get('/', listPosts);
377
- *
378
- * // Mount sub-routers
379
- * const api = createRouter();
380
- * api.mount('/users', users);
381
- * api.mount('/posts', posts);
382
- *
383
- * // Or use on main router
384
- * const router = createRouter();
385
- * router.mount('/api', api);
386
- *
387
- * app.use(router.routes());
388
- * ```
245
+ * Nested group support combines this group's prefix + middleware with the
246
+ * nested group's.
389
247
  */
390
- mount(path, router) {
391
- this.mountRouter(path, router);
248
+ group(prefix, middlewareOrCallback, callback) {
249
+ runRouteGroup(this.parent, this.fullPath(prefix), middlewareOrCallback, callback, this.middleware);
392
250
  return this;
393
251
  }
394
- /**
395
- * Mount a sub-router (internal)
396
- *
397
- * Carries the sub-router's own `routerMiddleware` forward so that
398
- * `subrouter.use(mw)` middleware applies to every copied route.
399
- */
400
- mountRouter(prefix, router) {
401
- this.copyRoutes(router.root, prefix, [], router.routerMiddleware);
252
+ };
253
+ function runRouteGroup(host, prefix, middlewareOrCallback, callback, inheritedMiddleware = []) {
254
+ let middleware = [];
255
+ let cb;
256
+ if (Array.isArray(middlewareOrCallback)) {
257
+ middleware = middlewareOrCallback;
258
+ if (!callback) {
259
+ throw new Error("Callback function is required when providing middleware array");
260
+ }
261
+ cb = callback;
262
+ } else {
263
+ cb = middlewareOrCallback;
264
+ }
265
+ const combined = inheritedMiddleware.length > 0 ? [
266
+ ...inheritedMiddleware,
267
+ ...middleware
268
+ ] : middleware;
269
+ cb(new GroupRouter(host, prefix, combined));
270
+ }
271
+ __name(runRouteGroup, "runRouteGroup");
272
+
273
+ // src/constants.ts
274
+ var EMPTY_PARAMS = Object.freeze(/* @__PURE__ */ Object.create(null));
275
+
276
+ // src/matching.ts
277
+ function decodeParam(value, decode) {
278
+ if (!decode || !value.includes("%")) return value;
279
+ try {
280
+ return decodeURIComponent(value);
281
+ } catch {
282
+ return value;
402
283
  }
403
- /**
404
- * Recursively copy routes from another router
405
- */
406
- copyRoutes(node, prefix, segments, subRouterMiddleware) {
407
- for (const [method, entry] of node.handlers) {
408
- const path = prefix + "/" + segments.join("/");
409
- const combined = subRouterMiddleware.length > 0 ? [
410
- ...subRouterMiddleware,
411
- ...entry.middleware
412
- ] : entry.middleware;
413
- this.addRoute(method, path || "/", [
414
- entry.handler
415
- ], combined);
416
- }
417
- for (const [, child] of node.children) {
418
- this.copyRoutes(child, prefix, [
419
- ...segments,
420
- child.segment
421
- ], subRouterMiddleware);
284
+ }
285
+ __name(decodeParam, "decodeParam");
286
+ function segmentAt(path, start) {
287
+ const slashPos = path.indexOf("/", start);
288
+ return slashPos === -1 ? path.slice(start) : path.slice(start, slashPos);
289
+ }
290
+ __name(segmentAt, "segmentAt");
291
+ function isProvablyLowerAscii(path) {
292
+ for (let i = 0; i < path.length; i++) {
293
+ const c = path.charCodeAt(i);
294
+ if (c >= 65 && c <= 90 || c > 127) return false;
295
+ }
296
+ return true;
297
+ }
298
+ __name(isProvablyLowerAscii, "isProvablyLowerAscii");
299
+ function collapseAndStrip(path, strict) {
300
+ let normalized = path;
301
+ if (normalized.includes("//")) {
302
+ normalized = normalized.replace(/\/+/g, "/");
303
+ }
304
+ if (!strict && normalized.length > 1 && normalized.endsWith("/")) {
305
+ normalized = normalized.slice(0, -1);
306
+ }
307
+ return normalized;
308
+ }
309
+ __name(collapseAndStrip, "collapseAndStrip");
310
+ function normalizePathForMatch(path, caseSensitive, strict) {
311
+ const folded = caseSensitive || isProvablyLowerAscii(path) ? path : path.toLowerCase();
312
+ return collapseAndStrip(folded, strict);
313
+ }
314
+ __name(normalizePathForMatch, "normalizePathForMatch");
315
+ function matchNodeIndexed(root, path, startPos, bindNames, bindValues, method, decode, originalPath) {
316
+ const stack = [
317
+ {
318
+ node: root,
319
+ pos: startPos,
320
+ stage: 0,
321
+ seg: "",
322
+ next: 0,
323
+ bound: false
324
+ }
325
+ ];
326
+ while (stack.length > 0) {
327
+ const frame = stack[stack.length - 1];
328
+ if (frame === void 0) break;
329
+ if (frame.stage === 0) {
330
+ if (frame.pos >= path.length) {
331
+ const handler = frame.node.handlers.get(method);
332
+ if (handler) return handler;
333
+ stack.pop();
334
+ continue;
335
+ }
336
+ const slashPos = path.indexOf("/", frame.pos);
337
+ if (slashPos === -1) {
338
+ frame.seg = path.slice(frame.pos);
339
+ frame.next = path.length;
340
+ } else {
341
+ frame.seg = path.slice(frame.pos, slashPos);
342
+ frame.next = slashPos + 1;
343
+ }
344
+ if (frame.seg === "") {
345
+ const handler = frame.node.handlers.get(method);
346
+ if (handler) return handler;
347
+ stack.pop();
348
+ continue;
349
+ }
350
+ frame.stage = 1;
351
+ const staticChild = frame.node.children.get(frame.seg);
352
+ if (staticChild) {
353
+ stack.push({
354
+ node: staticChild,
355
+ pos: frame.next,
356
+ stage: 0,
357
+ seg: "",
358
+ next: 0,
359
+ bound: false
360
+ });
361
+ }
362
+ continue;
363
+ }
364
+ if (frame.stage === 1) {
365
+ frame.stage = 2;
366
+ const paramChild = frame.node.paramChild;
367
+ if (paramChild) {
368
+ const paramName = paramChild.paramName;
369
+ if (paramName === void 0) return null;
370
+ const value = originalPath !== void 0 ? decodeParam(segmentAt(originalPath, frame.pos), decode) : decodeParam(frame.seg, decode);
371
+ bindNames.push(paramName);
372
+ bindValues.push(value);
373
+ frame.bound = true;
374
+ stack.push({
375
+ node: paramChild,
376
+ pos: frame.next,
377
+ stage: 0,
378
+ seg: "",
379
+ next: 0,
380
+ bound: false
381
+ });
382
+ }
383
+ continue;
422
384
  }
423
- if (node.paramChild) {
424
- this.copyRoutes(node.paramChild, prefix, [
425
- ...segments,
426
- node.paramChild.segment
427
- ], subRouterMiddleware);
385
+ if (frame.bound) {
386
+ bindNames.pop();
387
+ bindValues.pop();
388
+ frame.bound = false;
428
389
  }
429
- if (node.wildcardChild) {
430
- this.copyRoutes(node.wildcardChild, prefix, [
431
- ...segments,
432
- "*"
433
- ], subRouterMiddleware);
390
+ const wildcardChild = frame.node.wildcardChild;
391
+ if (wildcardChild) {
392
+ const src = originalPath ?? path;
393
+ bindNames.push("*");
394
+ bindValues.push(decodeParam(src.slice(frame.pos), decode));
395
+ const handler = wildcardChild.handlers.get(method);
396
+ if (handler) return handler;
397
+ bindNames.pop();
398
+ bindValues.pop();
434
399
  }
400
+ stack.pop();
435
401
  }
436
- // ===========================================================================
437
- // Route Matching
438
- // ===========================================================================
439
- /**
440
- * Match a route and return handler + params
441
- */
442
- match(method, path) {
443
- const isCaseInsensitive = !this.opts.caseSensitive;
444
- let normalized = isCaseInsensitive ? path.toLowerCase() : path;
445
- if (normalized.includes("//")) {
446
- normalized = normalized.replace(/\/+/g, "/");
447
- }
448
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
449
- normalized = normalized.slice(0, -1);
450
- }
451
- const staticKey = normalized.length > 1 && normalized.endsWith("/") ? `${method} ${normalized.slice(0, -1)}` : `${method} ${normalized}`;
452
- const staticEntry = this.staticRoutes.get(staticKey);
402
+ return null;
403
+ }
404
+ __name(matchNodeIndexed, "matchNodeIndexed");
405
+
406
+ // src/match-route.ts
407
+ function matchRoute(method, rawPath, root, staticRoutes, hasParamRoutes, caseSensitive, strict, decode, routerMiddleware) {
408
+ let path = rawPath;
409
+ const queryIdx = path.indexOf("?");
410
+ if (queryIdx !== -1) path = path.slice(0, queryIdx);
411
+ const caseStable = caseSensitive || isProvablyLowerAscii(path);
412
+ const folded = caseStable ? path : path.toLowerCase();
413
+ const normalized = collapseAndStrip(folded, strict);
414
+ const methodMap = staticRoutes.get(method);
415
+ if (methodMap) {
416
+ const staticKey = normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
417
+ const staticEntry = methodMap.get(staticKey);
453
418
  if (staticEntry) {
454
419
  return {
455
420
  handler: staticEntry.handler,
456
421
  params: EMPTY_PARAMS,
457
- middleware: this.routerMiddleware,
422
+ middleware: routerMiddleware,
458
423
  executor: staticEntry.executor
459
424
  };
460
425
  }
461
- if (!this.hasParamRoutes) return null;
462
- const params = {};
463
- let originalPath;
464
- if (isCaseInsensitive) {
465
- originalPath = path;
466
- if (originalPath.includes("//")) {
467
- originalPath = originalPath.replace(/\/+/g, "/");
468
- }
469
- if (!this.opts.strict && originalPath.length > 1 && originalPath.endsWith("/")) {
470
- originalPath = originalPath.slice(0, -1);
471
- }
472
- }
473
- const result = this.matchNodeIndexed(this.root, normalized, 1, params, method, originalPath);
474
- if (!result) return null;
475
- let hasParams = false;
476
- for (const key of Object.keys(params)) {
477
- if (params[key] === void 0) {
478
- Reflect.deleteProperty(params, key);
479
- } else {
480
- hasParams = true;
481
- }
482
- }
483
- return {
484
- handler: result.handler,
485
- params: hasParams ? params : EMPTY_PARAMS,
486
- middleware: this.routerMiddleware,
487
- executor: result.executor
488
- };
489
426
  }
490
- /**
491
- * Extract the next segment from path at given position without allocating arrays.
492
- * Returns [segment, nextIndex] where nextIndex is position after the trailing '/'.
493
- */
494
- extractSegment(path, start) {
495
- const slashPos = path.indexOf("/", start);
496
- if (slashPos === -1) {
497
- return [
498
- path.slice(start),
499
- path.length
500
- ];
427
+ if (!hasParamRoutes) return null;
428
+ const originalPath = caseStable ? void 0 : collapseAndStrip(path, strict);
429
+ const bindNames = [];
430
+ const bindValues = [];
431
+ const entry = matchNodeIndexed(root, normalized, 1, bindNames, bindValues, method, decode, originalPath);
432
+ if (!entry) return null;
433
+ const count = bindNames.length;
434
+ let params;
435
+ if (count === 0) {
436
+ params = EMPTY_PARAMS;
437
+ } else {
438
+ params = /* @__PURE__ */ Object.create(null);
439
+ for (let i = 0; i < count; i++) {
440
+ const name = bindNames[i];
441
+ const value = bindValues[i];
442
+ if (name !== void 0 && value !== void 0) params[name] = value;
501
443
  }
502
- return [
503
- path.slice(start, slashPos),
504
- slashPos + 1
505
- ];
506
444
  }
507
- /**
508
- * Index-based recursive node matching (avoids array allocation)
509
- */
510
- matchNodeIndexed(node, path, pos, params, method, originalPath) {
511
- if (pos >= path.length) {
512
- return node.handlers.get(method) ?? null;
513
- }
514
- const [segment, nextPos] = this.extractSegment(path, pos);
515
- if (segment === "") return node.handlers.get(method) ?? null;
516
- const staticChild = node.children.get(segment);
517
- if (staticChild) {
518
- const result = this.matchNodeIndexed(staticChild, path, nextPos, params, method, originalPath);
519
- if (result) return result;
520
- }
521
- if (node.paramChild) {
522
- const paramName = node.paramChild.paramName;
523
- if (paramName === void 0) return null;
524
- if (originalPath) {
525
- const [origSeg] = this.extractSegment(originalPath, pos);
526
- params[paramName] = origSeg;
527
- } else {
528
- params[paramName] = segment;
529
- }
530
- const result = this.matchNodeIndexed(node.paramChild, path, nextPos, params, method, originalPath);
531
- if (result) return result;
532
- Reflect.deleteProperty(params, paramName);
533
- }
534
- if (node.wildcardChild) {
535
- const src = originalPath ?? path;
536
- params["*"] = src.slice(pos);
537
- return node.wildcardChild.handlers.get(method) ?? null;
538
- }
539
- return null;
445
+ return {
446
+ handler: entry.handler,
447
+ params,
448
+ middleware: routerMiddleware,
449
+ executor: entry.executor
450
+ };
451
+ }
452
+ __name(matchRoute, "matchRoute");
453
+ function resolveMatch(state, hasParamRoutes, method, path) {
454
+ return matchRoute(method, path, state.root, state.staticRoutes, hasParamRoutes, state.caseSensitive, state.strict, state.decode, state.routerMiddleware);
455
+ }
456
+ __name(resolveMatch, "resolveMatch");
457
+
458
+ // src/composition.ts
459
+ function copyRoutes(node, prefix, segments, subRouterMiddleware, addRoute2) {
460
+ for (const [method, entry] of node.handlers) {
461
+ const path = prefix + "/" + segments.join("/");
462
+ const combined = subRouterMiddleware.length > 0 ? [
463
+ ...subRouterMiddleware,
464
+ ...entry.middleware
465
+ ] : entry.middleware;
466
+ addRoute2(method, path || "/", [
467
+ entry.handler
468
+ ], combined);
469
+ }
470
+ for (const [, child] of node.children) {
471
+ copyRoutes(child, prefix, [
472
+ ...segments,
473
+ child.segment
474
+ ], subRouterMiddleware, addRoute2);
475
+ }
476
+ if (node.paramChild) {
477
+ copyRoutes(node.paramChild, prefix, [
478
+ ...segments,
479
+ node.paramChild.segment
480
+ ], subRouterMiddleware, addRoute2);
481
+ }
482
+ if (node.wildcardChild) {
483
+ copyRoutes(node.wildcardChild, prefix, [
484
+ ...segments,
485
+ "*"
486
+ ], subRouterMiddleware, addRoute2);
540
487
  }
541
- // ===========================================================================
542
- // Middleware Generation
543
- // ===========================================================================
544
- /**
545
- * Get routes middleware function
546
- * Mount this on the application
547
- *
548
- * @example
549
- * ```typescript
550
- * app.use(router.routes());
551
- * ```
552
- */
553
- routes() {
554
- const hasRouterMiddleware = this.routerMiddleware.length > 0;
555
- if (hasRouterMiddleware) {
556
- this.sealRouterMiddleware();
488
+ }
489
+ __name(copyRoutes, "copyRoutes");
490
+
491
+ // src/middleware-adapter.ts
492
+ function sealRouterMiddleware(root, staticRoutes, routerMiddleware) {
493
+ const routerMw = [
494
+ ...routerMiddleware
495
+ ];
496
+ const walk = /* @__PURE__ */ __name((node) => {
497
+ for (const [method, entry] of node.handlers) {
498
+ const combinedMw = [
499
+ ...routerMw,
500
+ ...entry.middleware
501
+ ];
502
+ entry.executor = compileExecutor(entry.handler, combinedMw);
503
+ node.handlers.set(method, entry);
557
504
  }
558
- return async (ctx, next) => {
559
- const match = this.match(ctx.method, ctx.path);
560
- if (!match) {
561
- ctx.status = 404;
562
- if (next) await next();
563
- return;
564
- }
565
- ctx.params = match.params;
566
- if (match.executor) {
567
- await match.executor(ctx);
568
- return;
569
- }
570
- await match.handler(ctx, NOOP_NEXT);
571
- };
572
- }
573
- /**
574
- * Re-compile all route executors to include router-level middleware.
575
- * Called once when routes() is invoked, not per-request.
576
- */
577
- sealRouterMiddleware() {
578
- const routerMw = [
579
- ...this.routerMiddleware
580
- ];
581
- const walk = /* @__PURE__ */ __name((node) => {
582
- for (const [method, entry] of node.handlers) {
583
- const combinedMw = [
584
- ...routerMw,
585
- ...entry.middleware
586
- ];
587
- entry.executor = compileExecutor(entry.handler, combinedMw);
588
- node.handlers.set(method, entry);
589
- }
590
- for (const [, child] of node.children) {
591
- walk(child);
592
- }
593
- if (node.paramChild) walk(node.paramChild);
594
- if (node.wildcardChild) walk(node.wildcardChild);
595
- }, "walk");
596
- walk(this.root);
597
- for (const [key, entry] of this.staticRoutes) {
505
+ for (const [, child] of node.children) {
506
+ walk(child);
507
+ }
508
+ if (node.paramChild) walk(node.paramChild);
509
+ if (node.wildcardChild) walk(node.wildcardChild);
510
+ }, "walk");
511
+ walk(root);
512
+ for (const [, methodMap] of staticRoutes) {
513
+ for (const [key, entry] of methodMap) {
598
514
  const combinedMw = [
599
515
  ...routerMw,
600
516
  ...entry.middleware
601
517
  ];
602
518
  entry.executor = compileExecutor(entry.handler, combinedMw);
603
- this.staticRoutes.set(key, entry);
519
+ methodMap.set(key, entry);
604
520
  }
605
521
  }
606
- /**
607
- * Generate allowed methods middleware
608
- * Responds to OPTIONS and sets Allow header
609
- */
610
- allowedMethods() {
611
- return async (ctx, next) => {
612
- if (next) {
613
- await next();
614
- }
615
- if (ctx.status !== 404) return;
616
- const allowed = this.findAllowedMethods(ctx.path);
617
- if (allowed.length === 0) return;
618
- const allowHeader = allowed.join(", ");
619
- if (ctx.method === "OPTIONS") {
620
- ctx.status = 200;
621
- ctx.set("Allow", allowHeader);
622
- ctx.body = "";
623
- return;
624
- }
625
- ctx.status = 405;
626
- ctx.set("Allow", allowHeader);
522
+ }
523
+ __name(sealRouterMiddleware, "sealRouterMiddleware");
524
+
525
+ // src/registration.ts
526
+ import { HTTP_METHODS as HTTP_METHODS2 } from "@nextrush/types";
527
+
528
+ // src/route-metadata.ts
529
+ import { ROUTE_METADATA } from "@nextrush/types";
530
+ function endpoint(metadata) {
531
+ return {
532
+ [ROUTE_METADATA]: metadata
533
+ };
534
+ }
535
+ __name(endpoint, "endpoint");
536
+ function mergeContributions(contributions) {
537
+ if (contributions.length === 0) return void 0;
538
+ const meta = {};
539
+ for (const c of contributions) {
540
+ if (c.summary !== void 0) meta.summary = c.summary;
541
+ if (c.description !== void 0) meta.description = c.description;
542
+ if (c.deprecated !== void 0) meta.deprecated = c.deprecated;
543
+ if (c.visibility !== void 0) meta.visibility = c.visibility;
544
+ if (c.tags !== void 0) meta.tags = c.tags;
545
+ if (c.request) meta.request = {
546
+ ...meta.request,
547
+ ...c.request
548
+ };
549
+ if (c.responses) meta.responses = {
550
+ ...meta.responses,
551
+ ...c.responses
627
552
  };
628
553
  }
629
- /**
630
- * Find all HTTP methods registered for a given path via single tree walk
631
- * @internal
632
- */
633
- findAllowedMethods(path) {
634
- let normalized = this.opts.caseSensitive ? path : path.toLowerCase();
635
- if (normalized.includes("//")) {
636
- normalized = normalized.replace(/\/+/g, "/");
637
- }
638
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
639
- normalized = normalized.slice(0, -1);
640
- }
641
- const segments = normalized.split("/").filter(Boolean);
642
- const node = this.findNode(this.root, segments, 0);
643
- if (!node || node.handlers.size === 0) return [];
644
- return Array.from(node.handlers.keys());
554
+ return meta;
555
+ }
556
+ __name(mergeContributions, "mergeContributions");
557
+ function readContribution(entry) {
558
+ return entry[ROUTE_METADATA];
559
+ }
560
+ __name(readContribution, "readContribution");
561
+
562
+ // src/registration.ts
563
+ function normalizeRegistrationPath(path, prefix, strict) {
564
+ let joinedPrefix = prefix;
565
+ if (joinedPrefix.endsWith("/") && path.startsWith("/")) {
566
+ joinedPrefix = joinedPrefix.slice(0, -1);
645
567
  }
646
- /**
647
- * Walk the tree to find the node matching a path (ignoring HTTP method)
648
- * @internal
649
- */
650
- findNode(node, segments, index) {
651
- if (index === segments.length) {
652
- return node;
653
- }
654
- const segment = segments[index];
655
- if (segment === void 0) return null;
656
- const staticChild = node.children.get(segment);
657
- if (staticChild) {
658
- const result = this.findNode(staticChild, segments, index + 1);
659
- if (result) return result;
660
- }
661
- if (node.paramChild) {
662
- const result = this.findNode(node.paramChild, segments, index + 1);
663
- if (result) return result;
664
- }
665
- if (node.wildcardChild) {
666
- return node.wildcardChild;
667
- }
668
- return null;
568
+ let normalized = joinedPrefix + path;
569
+ if (normalized.includes("//")) {
570
+ normalized = normalized.replace(/\/+/g, "/");
669
571
  }
670
- // ===========================================================================
671
- // Route Groups
672
- // ===========================================================================
673
- /**
674
- * Create a route group with shared prefix and middleware
675
- *
676
- * @param prefix - Path prefix for all routes in the group
677
- * @param middlewareOrCallback - Middleware array or callback function
678
- * @param callback - Callback function if middleware is provided
679
- * @returns this for chaining
680
- *
681
- * @example
682
- * ```typescript
683
- * // Simple group with prefix
684
- * router.group('/api', (r) => {
685
- * r.get('/users', listUsers);
686
- * r.get('/posts', listPosts);
687
- * });
688
- *
689
- * // Group with middleware
690
- * router.group('/admin', [authMiddleware], (r) => {
691
- * r.get('/dashboard', dashboard);
692
- * r.post('/settings', updateSettings);
693
- * });
694
- *
695
- * // Nested groups
696
- * router.group('/api/v1', (r) => {
697
- * r.group('/users', [rateLimit], (ur) => {
698
- * ur.get('/', listUsers);
699
- * ur.get('/:id', getUser);
700
- * });
701
- * });
702
- * ```
703
- */
704
- group(prefix, middlewareOrCallback, callback) {
705
- let middleware = [];
706
- let cb;
707
- if (Array.isArray(middlewareOrCallback)) {
708
- middleware = middlewareOrCallback;
709
- if (!callback) {
710
- throw new Error("Callback function is required when providing middleware array");
572
+ if (!strict && normalized.length > 1 && normalized.endsWith("/")) {
573
+ normalized = normalized.slice(0, -1);
574
+ }
575
+ return normalized.startsWith("/") ? normalized : "/" + normalized;
576
+ }
577
+ __name(normalizeRegistrationPath, "normalizeRegistrationPath");
578
+ function addRoute(method, normalized, entries, middleware, state, recordIntrospection = true) {
579
+ const segments = parseSegments(normalized, state.caseSensitive);
580
+ let node = state.root;
581
+ for (const seg of segments) {
582
+ if (seg.type === NodeType.PARAM) {
583
+ if (!node.paramChild) {
584
+ node.paramChild = createNode(seg.segment, NodeType.PARAM);
585
+ node.paramChild.paramName = seg.paramName;
586
+ } else if (node.paramChild.paramName !== seg.paramName) {
587
+ throw new Error(`Route param name conflict at "${normalized}": ":${String(seg.paramName)}" conflicts with existing ":${String(node.paramChild.paramName)}" at the same position. Use the same param name for this segment across all routes.`);
711
588
  }
712
- cb = callback;
589
+ node = node.paramChild;
590
+ } else if (seg.type === NodeType.WILDCARD) {
591
+ node.wildcardChild ??= createNode("*", NodeType.WILDCARD);
592
+ node = node.wildcardChild;
593
+ break;
713
594
  } else {
714
- cb = middlewareOrCallback;
595
+ const key = seg.segment;
596
+ let child = node.children.get(key);
597
+ if (!child) {
598
+ child = createNode(seg.segment, NodeType.STATIC);
599
+ node.children.set(key, child);
600
+ }
601
+ node = child;
602
+ }
603
+ }
604
+ const functions = [];
605
+ const contributions = [];
606
+ for (const routeEntry of entries) {
607
+ const contribution = readContribution(routeEntry);
608
+ if (contribution) contributions.push(contribution);
609
+ if (typeof routeEntry === "function") functions.push(routeEntry);
610
+ }
611
+ const combinedMiddleware = [
612
+ ...middleware
613
+ ];
614
+ const finalHandler = functions[functions.length - 1];
615
+ if (!finalHandler) {
616
+ throw new Error("At least one handler is required");
617
+ }
618
+ for (let i = 0; i < functions.length - 1; i++) {
619
+ const fn = functions[i];
620
+ if (fn) combinedMiddleware.push(fn);
621
+ }
622
+ const executor = compileExecutor(finalHandler, combinedMiddleware);
623
+ const handlerEntry = {
624
+ handler: finalHandler,
625
+ middleware: combinedMiddleware,
626
+ executor
627
+ };
628
+ if (node.handlers.has(method)) {
629
+ throw new Error(`Route conflict: ${method} ${normalized} is already registered. Remove the duplicate or use a different path.`);
630
+ }
631
+ node.handlers.set(method, handlerEntry);
632
+ const hasParams = segments.some((s) => s.type === NodeType.PARAM || s.type === NodeType.WILDCARD);
633
+ if (!hasParams) {
634
+ const normalizedKey = state.caseSensitive ? normalized : normalized.toLowerCase();
635
+ let methodMap = state.staticRoutes.get(method);
636
+ if (!methodMap) {
637
+ methodMap = /* @__PURE__ */ new Map();
638
+ state.staticRoutes.set(method, methodMap);
639
+ }
640
+ methodMap.set(normalizedKey, handlerEntry);
641
+ }
642
+ if (recordIntrospection) {
643
+ state.routeDefinitions.push({
644
+ key: `${method} ${normalized}`,
645
+ method,
646
+ path: normalized,
647
+ metadata: mergeContributions(contributions)
648
+ });
649
+ }
650
+ return hasParams;
651
+ }
652
+ __name(addRoute, "addRoute");
653
+ function registerRedirect(from, to, status, addRoute2) {
654
+ const redirectHandler = createRedirectHandler(to, status);
655
+ addRoute2("GET", from, [
656
+ redirectHandler
657
+ ]);
658
+ addRoute2("HEAD", from, [
659
+ redirectHandler
660
+ ]);
661
+ if (status === 307 || status === 308) {
662
+ addRoute2("POST", from, [
663
+ redirectHandler
664
+ ]);
665
+ addRoute2("PUT", from, [
666
+ redirectHandler
667
+ ]);
668
+ addRoute2("PATCH", from, [
669
+ redirectHandler
670
+ ]);
671
+ addRoute2("DELETE", from, [
672
+ redirectHandler
673
+ ]);
674
+ }
675
+ }
676
+ __name(registerRedirect, "registerRedirect");
677
+ function pushAnyMethodDefinition(routeDefinitions, normalized) {
678
+ routeDefinitions.push({
679
+ key: `${HTTP_METHODS2[0]} ${normalized}`,
680
+ method: HTTP_METHODS2[0],
681
+ path: normalized,
682
+ isAnyMethod: true
683
+ });
684
+ }
685
+ __name(pushAnyMethodDefinition, "pushAnyMethodDefinition");
686
+
687
+ // src/find-node.ts
688
+ function findNode(root, path, startPos) {
689
+ const stack = [
690
+ {
691
+ node: root,
692
+ pos: startPos,
693
+ stage: 0,
694
+ next: 0
695
+ }
696
+ ];
697
+ while (stack.length > 0) {
698
+ const frame = stack[stack.length - 1];
699
+ if (frame === void 0) break;
700
+ if (frame.stage === 0) {
701
+ if (frame.pos >= path.length) {
702
+ return frame.node;
703
+ }
704
+ const seg = segmentAt(path, frame.pos);
705
+ if (seg === "") {
706
+ return frame.node;
707
+ }
708
+ const segEnd = frame.pos + seg.length;
709
+ frame.next = segEnd < path.length ? segEnd + 1 : path.length;
710
+ frame.stage = 1;
711
+ const staticChild = frame.node.children.get(seg);
712
+ if (staticChild) {
713
+ stack.push({
714
+ node: staticChild,
715
+ pos: frame.next,
716
+ stage: 0,
717
+ next: 0
718
+ });
719
+ }
720
+ continue;
721
+ }
722
+ if (frame.stage === 1) {
723
+ frame.stage = 2;
724
+ if (frame.node.paramChild) {
725
+ stack.push({
726
+ node: frame.node.paramChild,
727
+ pos: frame.next,
728
+ stage: 0,
729
+ next: 0
730
+ });
731
+ }
732
+ continue;
715
733
  }
716
- const groupRouter = new GroupRouter(this, prefix, middleware);
717
- cb(groupRouter);
718
- return this;
734
+ if (frame.node.wildcardChild) {
735
+ return frame.node.wildcardChild;
736
+ }
737
+ stack.pop();
738
+ }
739
+ return null;
740
+ }
741
+ __name(findNode, "findNode");
742
+ function findAllowedMethods(path, root, caseSensitive, strict) {
743
+ const normalized = normalizePathForMatch(path, caseSensitive, strict);
744
+ const node = findNode(root, normalized, 1);
745
+ if (!node || node.handlers.size === 0) return [];
746
+ return Array.from(node.handlers.keys());
747
+ }
748
+ __name(findAllowedMethods, "findAllowedMethods");
749
+
750
+ // src/dispatch.ts
751
+ var RESOLVED = Promise.resolve();
752
+ function createRoutesMiddleware(match) {
753
+ return (ctx, next) => {
754
+ const routeMatch = match(ctx.method, ctx.path);
755
+ if (!routeMatch) {
756
+ ctx.status = 404;
757
+ return next ? next() : RESOLVED;
758
+ }
759
+ ctx.params = routeMatch.params;
760
+ return routeMatch.executor ? routeMatch.executor(ctx) : (
761
+ // or thenable return still yields a Promise<void> and never a sync throw.
762
+ Promise.resolve(routeMatch.handler(ctx, NOOP_NEXT))
763
+ );
764
+ };
765
+ }
766
+ __name(createRoutesMiddleware, "createRoutesMiddleware");
767
+ function createAllowedMethodsMiddleware(root, caseSensitive, strict) {
768
+ return async (ctx, next) => {
769
+ if (next) {
770
+ await next();
771
+ }
772
+ if (ctx.status !== 404) return;
773
+ const allowed = findAllowedMethods(ctx.path, root, caseSensitive, strict);
774
+ if (allowed.length === 0) return;
775
+ const allowHeader = allowed.join(", ");
776
+ if (ctx.method === "OPTIONS") {
777
+ ctx.status = 200;
778
+ ctx.set("Allow", allowHeader);
779
+ ctx.body = "";
780
+ return;
781
+ }
782
+ ctx.status = 405;
783
+ ctx.set("Allow", allowHeader);
784
+ };
785
+ }
786
+ __name(createAllowedMethodsMiddleware, "createAllowedMethodsMiddleware");
787
+
788
+ // src/state.ts
789
+ function resolveRouterOptions(options) {
790
+ return {
791
+ prefix: options.prefix ?? "",
792
+ caseSensitive: options.caseSensitive ?? false,
793
+ strict: options.strict ?? false,
794
+ decode: options.decode ?? true
795
+ };
796
+ }
797
+ __name(resolveRouterOptions, "resolveRouterOptions");
798
+ function createRouterState(root, opts, staticRoutes, routeDefinitions, routerMiddleware) {
799
+ return {
800
+ root,
801
+ staticRoutes,
802
+ routeDefinitions,
803
+ caseSensitive: opts.caseSensitive,
804
+ strict: opts.strict,
805
+ decode: opts.decode,
806
+ routerMiddleware
807
+ };
808
+ }
809
+ __name(createRouterState, "createRouterState");
810
+
811
+ // src/router.ts
812
+ var Router = class _Router {
813
+ static {
814
+ __name(this, "Router");
719
815
  }
816
+ root;
817
+ opts;
818
+ routerMiddleware = [];
819
+ /** Static-route fast path: method-nested map for O(1) lookup with no per-request key string (HP-9). */
820
+ staticRoutes = /* @__PURE__ */ new Map();
720
821
  /**
721
- * Remove all registered routes and middleware, resetting the router to its initial state.
722
- * Useful for plugin `destroy()` to cleanly un-register routes.
822
+ * Introspection registry, kept SEPARATE from the hot-path trie/staticRoutes
823
+ * so request dispatch never reads metadata — only getRoutes() touches it.
723
824
  */
724
- reset() {
725
- this.root.children.clear();
726
- this.root.handlers.clear();
727
- this.root.paramChild = void 0;
728
- this.root.wildcardChild = void 0;
729
- this.staticRoutes.clear();
730
- this.routerMiddleware.length = 0;
731
- this.hasParamRoutes = false;
825
+ routeDefinitions = [];
826
+ /** Whether any routes have params or wildcards (disables static-only fast path) */
827
+ hasParamRoutes = false;
828
+ /** Whether router-level middleware has already been sealed into executors (audit RT-7) */
829
+ _sealed = false;
830
+ /** Memoized state the extracted registration/matching functions read (see {@link createRouterState}). */
831
+ state;
832
+ constructor(options = {}) {
833
+ this.root = createNode("");
834
+ this.opts = resolveRouterOptions(options);
835
+ this.state = createRouterState(this.root, this.opts, this.staticRoutes, this.routeDefinitions, this.routerMiddleware);
732
836
  }
733
837
  /**
734
- * Internal method to add route with group context
735
- * @internal
838
+ * Validate + normalize a raw path, then delegate trie insertion to the
839
+ * extracted `addRoute` (design.md D2); flips `hasParamRoutes` from its return.
736
840
  */
737
- _addGroupRoute(method, path, handlers, groupMiddleware) {
738
- this.addRoute(method, path, handlers, groupMiddleware);
841
+ addRoute(method, path, entries, middleware = [], recordIntrospection = true) {
842
+ const rawPath = path;
843
+ if (typeof rawPath !== "string") {
844
+ throw new TypeError(`Route path must be a string, received ${rawPath === null ? "null" : typeof rawPath}.`);
845
+ }
846
+ const normalized = normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict);
847
+ if (addRoute(method, normalized, entries, middleware, this.state, recordIntrospection)) {
848
+ this.hasParamRoutes = true;
849
+ }
739
850
  }
740
- };
741
- var GroupRouter = class GroupRouter2 {
742
- static {
743
- __name(this, "GroupRouter");
851
+ get(path, ...entries) {
852
+ this.addRoute("GET", path, entries);
853
+ return this;
744
854
  }
745
- parent;
746
- prefix;
747
- middleware;
748
- constructor(parent, prefix, middleware) {
749
- this.parent = parent;
750
- this.prefix = prefix;
751
- this.middleware = middleware;
855
+ post(path, ...entries) {
856
+ this.addRoute("POST", path, entries);
857
+ return this;
752
858
  }
753
- fullPath(path) {
754
- if (path === "/" || path === "") {
755
- return this.prefix;
756
- }
757
- const cleanPrefix = this.prefix.endsWith("/") ? this.prefix.slice(0, -1) : this.prefix;
758
- const cleanPath = path.startsWith("/") ? path : "/" + path;
759
- return cleanPrefix + cleanPath;
859
+ put(path, ...entries) {
860
+ this.addRoute("PUT", path, entries);
861
+ return this;
760
862
  }
761
- get(path, ...handlers) {
762
- this.parent._addGroupRoute("GET", this.fullPath(path), handlers, this.middleware);
863
+ delete(path, ...entries) {
864
+ this.addRoute("DELETE", path, entries);
763
865
  return this;
764
866
  }
765
- post(path, ...handlers) {
766
- this.parent._addGroupRoute("POST", this.fullPath(path), handlers, this.middleware);
867
+ patch(path, ...entries) {
868
+ this.addRoute("PATCH", path, entries);
767
869
  return this;
768
870
  }
769
- put(path, ...handlers) {
770
- this.parent._addGroupRoute("PUT", this.fullPath(path), handlers, this.middleware);
871
+ head(path, ...entries) {
872
+ this.addRoute("HEAD", path, entries);
771
873
  return this;
772
874
  }
773
- delete(path, ...handlers) {
774
- this.parent._addGroupRoute("DELETE", this.fullPath(path), handlers, this.middleware);
875
+ options(path, ...entries) {
876
+ this.addRoute("OPTIONS", path, entries);
775
877
  return this;
776
878
  }
777
- patch(path, ...handlers) {
778
- this.parent._addGroupRoute("PATCH", this.fullPath(path), handlers, this.middleware);
879
+ /**
880
+ * Register a route for every HTTP method under one consolidated `isAnyMethod`
881
+ * introspection row (T016) — matching is unchanged (see {@link pushAnyMethodDefinition}).
882
+ */
883
+ all(path, ...entries) {
884
+ for (const method of HTTP_METHODS3) {
885
+ this.addRoute(method, path, entries, [], false);
886
+ }
887
+ pushAnyMethodDefinition(this.routeDefinitions, normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict));
779
888
  return this;
780
889
  }
781
- head(path, ...handlers) {
782
- this.parent._addGroupRoute("HEAD", this.fullPath(path), handlers, this.middleware);
890
+ route(method, path, ...entries) {
891
+ this.addRoute(method, path, entries);
783
892
  return this;
784
893
  }
785
- options(path, ...handlers) {
786
- this.parent._addGroupRoute("OPTIONS", this.fullPath(path), handlers, this.middleware);
894
+ /**
895
+ * Every registered route as a read-only list, for renderers (`@nextrush/openapi`,
896
+ * SDK/RPC generators). Doc-generation-time only — never on the request path.
897
+ */
898
+ getRoutes() {
899
+ return this.routeDefinitions;
900
+ }
901
+ /**
902
+ * Register a redirect from one path to another (301 by default). 307/308
903
+ * additionally register POST/PUT/PATCH/DELETE to preserve the method.
904
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#redirects | README: Redirects}
905
+ */
906
+ redirect(from, to, status = 301) {
907
+ registerRedirect(from, to, status, (method, path, entries) => {
908
+ this.addRoute(method, path, entries);
909
+ });
787
910
  return this;
788
911
  }
789
- all(path, ...handlers) {
790
- for (const method of HTTP_METHODS) {
791
- this.parent._addGroupRoute(method, this.fullPath(path), handlers, this.middleware);
912
+ use(pathOrMiddleware, routerOrUndefined) {
913
+ if (typeof pathOrMiddleware === "function") {
914
+ this.routerMiddleware.push(pathOrMiddleware);
915
+ } else if (typeof pathOrMiddleware === "string" && routerOrUndefined instanceof _Router) {
916
+ this.mountRouter(pathOrMiddleware, routerOrUndefined);
917
+ } else if (typeof pathOrMiddleware === "string") {
918
+ throw new Error(`router.use('${pathOrMiddleware}', ...) requires a Router instance as the second argument. Use router.group(prefix, callback) for prefix-scoped middleware, or router.use(middlewareFn) to register middleware without a prefix.`);
919
+ } else if (pathOrMiddleware instanceof _Router) {
920
+ this.mountRouter("", pathOrMiddleware);
792
921
  }
793
922
  return this;
794
923
  }
795
924
  /**
796
- * Register a redirect within the group
925
+ * Mount a sub-router at a path prefix (Hono-style) — the explicit, more
926
+ * semantic equivalent of `router.use(path, subRouter)`.
927
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#sub-router-mounting | README: Sub-Router Mounting}
797
928
  */
798
- redirect(from, to, status = 301) {
799
- const redirectHandler = /* @__PURE__ */ __name((ctx) => {
800
- let targetPath = to;
801
- if (targetPath.includes(":")) {
802
- const entries = Object.entries(ctx.params).sort((a, b) => b[0].length - a[0].length);
803
- for (const [key, value] of entries) {
804
- targetPath = targetPath.replaceAll(`:${key}`, value);
805
- }
806
- }
807
- ctx.status = status;
808
- ctx.set("Location", targetPath);
809
- ctx.body = "";
810
- }, "redirectHandler");
811
- this.parent._addGroupRoute("GET", this.fullPath(from), [
812
- redirectHandler
813
- ], this.middleware);
814
- this.parent._addGroupRoute("HEAD", this.fullPath(from), [
815
- redirectHandler
816
- ], this.middleware);
929
+ mount(path, router) {
930
+ this.mountRouter(path, router);
817
931
  return this;
818
932
  }
933
+ /** Mount a sub-router, carrying its own `routerMiddleware` onto every copied route. */
934
+ mountRouter(prefix, router) {
935
+ copyRoutes(router.root, prefix, [], router.routerMiddleware, this.addRoute.bind(this));
936
+ }
937
+ /** Match a request to a route — delegates to {@link resolveMatch} (design.md D1). */
938
+ match(method, path) {
939
+ return resolveMatch(this.state, this.hasParamRoutes, method, path);
940
+ }
819
941
  /**
820
- * Nested group support
942
+ * Return the router's dispatch middleware — mount this on the application.
943
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#routerroutes | README: router.routes()}
821
944
  */
822
- group(prefix, middlewareOrCallback, callback) {
823
- let nestedMiddleware = [];
824
- let cb;
825
- if (Array.isArray(middlewareOrCallback)) {
826
- nestedMiddleware = middlewareOrCallback;
827
- if (!callback) {
828
- throw new Error("Callback function is required when providing middleware array");
829
- }
830
- cb = callback;
831
- } else {
832
- cb = middlewareOrCallback;
945
+ routes() {
946
+ if (this.routerMiddleware.length > 0 && !this._sealed) {
947
+ this._sealed = true;
948
+ sealRouterMiddleware(this.root, this.staticRoutes, this.routerMiddleware);
833
949
  }
834
- const nestedRouter = new GroupRouter2(this.parent, this.fullPath(prefix), [
835
- ...this.middleware,
836
- ...nestedMiddleware
837
- ]);
838
- cb(nestedRouter);
950
+ return createRoutesMiddleware((method, path) => this.match(method, path));
951
+ }
952
+ /**
953
+ * Generate allowed-methods middleware. Responds to OPTIONS with an `Allow`
954
+ * header and returns 405 for a known path hit with an unregistered method.
955
+ */
956
+ allowedMethods() {
957
+ return createAllowedMethodsMiddleware(this.root, this.opts.caseSensitive, this.opts.strict);
958
+ }
959
+ /**
960
+ * Create a route group with a shared prefix and middleware. The callback
961
+ * receives a {@link RouteGroup} to register routes against.
962
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#route-groups | README: Route Groups}
963
+ */
964
+ group(prefix, middlewareOrCallback, callback) {
965
+ runRouteGroup(this, prefix, middlewareOrCallback, callback);
839
966
  return this;
840
967
  }
968
+ /**
969
+ * Remove all routes and middleware, resetting the router to its initial
970
+ * state — for test isolation or hot-reload re-registration.
971
+ */
972
+ reset() {
973
+ clearNode(this.root);
974
+ this.staticRoutes.clear();
975
+ this.routerMiddleware.length = 0;
976
+ this.hasParamRoutes = false;
977
+ this.routeDefinitions.length = 0;
978
+ this._sealed = false;
979
+ }
980
+ /** Register a route on behalf of a {@link RouteGroup} (group context). @internal */
981
+ _addGroupRoute(method, path, handlers, groupMiddleware, recordIntrospection = true) {
982
+ this.addRoute(method, path, handlers, groupMiddleware, recordIntrospection);
983
+ }
984
+ /**
985
+ * Group-facing entry point to {@link pushAnyMethodDefinition} — a group's `.all()`
986
+ * records its consolidated row here since group routes live on the parent. @internal
987
+ */
988
+ _pushAnyMethodRouteDefinition(path) {
989
+ pushAnyMethodDefinition(this.routeDefinitions, normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict));
990
+ }
841
991
  };
842
992
  function createRouter(options) {
843
993
  return new Router(options);
@@ -848,6 +998,7 @@ export {
848
998
  Router,
849
999
  createNode,
850
1000
  createRouter,
1001
+ endpoint,
851
1002
  parseSegments
852
1003
  };
853
1004
  //# sourceMappingURL=index.js.map