@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.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,1159 @@ 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;
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;
199
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
  }
227
+ this.parent._pushAnyMethodRouteDefinition(this.fullPath(path));
236
228
  return this;
237
229
  }
238
- route(method, path, ...handlers) {
239
- this.addRoute(method, path, handlers);
230
+ /**
231
+ * Register a redirect within the group (uses the shared redirect handler,
232
+ * audit RT-4 — no more naive replaceAll param substitution).
233
+ */
234
+ redirect(from, to, status = 301) {
235
+ const redirectHandler = createRedirectHandler(to, status);
236
+ this.parent._addGroupRoute("GET", this.fullPath(from), [
237
+ redirectHandler
238
+ ], this.middleware);
239
+ this.parent._addGroupRoute("HEAD", this.fullPath(from), [
240
+ redirectHandler
241
+ ], this.middleware);
240
242
  return this;
241
243
  }
242
244
  /**
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
- * ```
245
+ * Nested group support combines this group's prefix + middleware with the
246
+ * nested group's.
264
247
  */
265
- 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
- }
248
+ group(prefix, middlewareOrCallback, callback) {
249
+ runRouteGroup(this.parent, this.fullPath(prefix), middlewareOrCallback, callback, this.middleware);
250
+ return this;
251
+ }
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 NULL_PROTO = /* @__PURE__ */ Object.create(null);
275
+ var EMPTY_PARAMS = Object.freeze(Object.create(NULL_PROTO));
276
+
277
+ // src/walk-pool.ts
278
+ function createWalkPool(maxDepth) {
279
+ const frames = [];
280
+ for (let i = 0; i < maxDepth + 1; i++) {
281
+ frames.push({
282
+ node: void 0,
283
+ pos: 0,
284
+ stage: 0,
285
+ seg: "",
286
+ next: 0,
287
+ bound: false
288
+ });
289
+ }
290
+ return {
291
+ frames,
292
+ bindNames: [],
293
+ bindValues: []
294
+ };
295
+ }
296
+ __name(createWalkPool, "createWalkPool");
297
+ function matchNodeIndexedPooled(root, path, startPos, bindNames, bindValues, method, decode, pool, originalPath) {
298
+ const { frames } = pool;
299
+ let depth = 0;
300
+ const first = frames[0];
301
+ if (first === void 0) return null;
302
+ first.node = root;
303
+ first.pos = startPos;
304
+ first.stage = 0;
305
+ first.seg = "";
306
+ first.next = 0;
307
+ first.bound = false;
308
+ while (depth >= 0) {
309
+ const frame = frames[depth];
310
+ if (frame === void 0) break;
311
+ if (frame.stage === 0) {
312
+ if (frame.pos >= path.length) {
313
+ const handler = frame.node.handlers.get(method);
314
+ if (handler) return handler;
315
+ depth--;
316
+ continue;
317
+ }
318
+ const slashPos = path.indexOf("/", frame.pos);
319
+ if (slashPos === -1) {
320
+ frame.seg = path.slice(frame.pos);
321
+ frame.next = path.length;
322
+ } else {
323
+ frame.seg = path.slice(frame.pos, slashPos);
324
+ frame.next = slashPos + 1;
325
+ }
326
+ if (frame.seg === "") {
327
+ const handler = frame.node.handlers.get(method);
328
+ if (handler) return handler;
329
+ depth--;
330
+ continue;
290
331
  }
291
- if (found) {
292
- parts.push(to.slice(pos));
293
- compiledParts = parts;
332
+ frame.stage = 1;
333
+ const staticChild = frame.node.children.get(frame.seg);
334
+ if (staticChild) {
335
+ depth++;
336
+ const next = frames[depth];
337
+ if (next === void 0) {
338
+ depth--;
339
+ continue;
340
+ }
341
+ next.node = staticChild;
342
+ next.pos = frame.next;
343
+ next.stage = 0;
344
+ next.seg = "";
345
+ next.next = 0;
346
+ next.bound = false;
294
347
  }
348
+ continue;
295
349
  }
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;
350
+ if (frame.stage === 1) {
351
+ frame.stage = 2;
352
+ const paramChild = frame.node.paramChild;
353
+ if (paramChild) {
354
+ const paramName = paramChild.paramName;
355
+ if (paramName === void 0) return null;
356
+ const value = originalPath !== void 0 ? decodeParam(segmentAt(originalPath, frame.pos), decode) : decodeParam(frame.seg, decode);
357
+ bindNames.push(paramName);
358
+ bindValues.push(value);
359
+ frame.bound = true;
360
+ depth++;
361
+ const next = frames[depth];
362
+ if (next === void 0) {
363
+ bindNames.pop();
364
+ bindValues.pop();
365
+ frame.bound = false;
366
+ depth--;
367
+ continue;
313
368
  }
314
- } else {
315
- targetPath = to;
369
+ next.node = paramChild;
370
+ next.pos = frame.next;
371
+ next.stage = 0;
372
+ next.seg = "";
373
+ next.next = 0;
374
+ next.bound = false;
316
375
  }
317
- ctx.status = status;
318
- ctx.set("Location", targetPath);
319
- ctx.body = "";
320
- }, "redirectHandler");
321
- this.addRoute("GET", from, [
322
- redirectHandler
323
- ]);
324
- this.addRoute("HEAD", from, [
325
- 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
- ]);
376
+ continue;
340
377
  }
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);
378
+ if (frame.bound) {
379
+ bindNames.pop();
380
+ bindValues.pop();
381
+ frame.bound = false;
355
382
  }
356
- return this;
383
+ const wildcardChild = frame.node.wildcardChild;
384
+ if (wildcardChild) {
385
+ const src = originalPath ?? path;
386
+ bindNames.push("*");
387
+ bindValues.push(decodeParam(src.slice(frame.pos), decode));
388
+ const handler = wildcardChild.handlers.get(method);
389
+ if (handler) return handler;
390
+ bindNames.pop();
391
+ bindValues.pop();
392
+ }
393
+ depth--;
357
394
  }
358
- /**
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
- * ```
389
- */
390
- mount(path, router) {
391
- this.mountRouter(path, router);
392
- return this;
395
+ return null;
396
+ }
397
+ __name(matchNodeIndexedPooled, "matchNodeIndexedPooled");
398
+
399
+ // src/matching.ts
400
+ function decodeParam(value, decode) {
401
+ if (!decode || !value.includes("%")) return value;
402
+ try {
403
+ return decodeURIComponent(value);
404
+ } catch {
405
+ return value;
393
406
  }
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);
407
+ }
408
+ __name(decodeParam, "decodeParam");
409
+ function segmentAt(path, start) {
410
+ const slashPos = path.indexOf("/", start);
411
+ return slashPos === -1 ? path.slice(start) : path.slice(start, slashPos);
412
+ }
413
+ __name(segmentAt, "segmentAt");
414
+ function isProvablyLowerAscii(path) {
415
+ for (let i = 0; i < path.length; i++) {
416
+ const c = path.charCodeAt(i);
417
+ if (c >= 65 && c <= 90 || c > 127) return false;
402
418
  }
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);
419
+ return true;
420
+ }
421
+ __name(isProvablyLowerAscii, "isProvablyLowerAscii");
422
+ function collapseAndStrip(path, strict) {
423
+ let normalized = path;
424
+ if (normalized.includes("//")) {
425
+ normalized = normalized.replace(/\/+/g, "/");
426
+ }
427
+ if (!strict && normalized.length > 1 && normalized.endsWith("/")) {
428
+ normalized = normalized.slice(0, -1);
429
+ }
430
+ return normalized;
431
+ }
432
+ __name(collapseAndStrip, "collapseAndStrip");
433
+ function normalizePathForMatch(path, caseSensitive, strict) {
434
+ const folded = caseSensitive || isProvablyLowerAscii(path) ? path : path.toLowerCase();
435
+ return collapseAndStrip(folded, strict);
436
+ }
437
+ __name(normalizePathForMatch, "normalizePathForMatch");
438
+ function matchNodeIndexed(root, path, startPos, bindNames, bindValues, method, decode, originalPath, pool) {
439
+ if (pool) {
440
+ return matchNodeIndexedPooled(root, path, startPos, bindNames, bindValues, method, decode, pool, originalPath);
441
+ }
442
+ const stack = [
443
+ {
444
+ node: root,
445
+ pos: startPos,
446
+ stage: 0,
447
+ seg: "",
448
+ next: 0,
449
+ bound: false
416
450
  }
417
- for (const [, child] of node.children) {
418
- this.copyRoutes(child, prefix, [
419
- ...segments,
420
- child.segment
421
- ], subRouterMiddleware);
422
- }
423
- if (node.paramChild) {
424
- this.copyRoutes(node.paramChild, prefix, [
425
- ...segments,
426
- node.paramChild.segment
427
- ], subRouterMiddleware);
428
- }
429
- if (node.wildcardChild) {
430
- this.copyRoutes(node.wildcardChild, prefix, [
431
- ...segments,
432
- "*"
433
- ], subRouterMiddleware);
434
- }
435
- }
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, "/");
451
+ ];
452
+ while (stack.length > 0) {
453
+ const frame = stack[stack.length - 1];
454
+ if (frame === void 0) break;
455
+ if (frame.stage === 0) {
456
+ if (frame.pos >= path.length) {
457
+ const handler = frame.node.handlers.get(method);
458
+ if (handler) return handler;
459
+ stack.pop();
460
+ continue;
461
+ }
462
+ const slashPos = path.indexOf("/", frame.pos);
463
+ if (slashPos === -1) {
464
+ frame.seg = path.slice(frame.pos);
465
+ frame.next = path.length;
466
+ } else {
467
+ frame.seg = path.slice(frame.pos, slashPos);
468
+ frame.next = slashPos + 1;
469
+ }
470
+ if (frame.seg === "") {
471
+ const handler = frame.node.handlers.get(method);
472
+ if (handler) return handler;
473
+ stack.pop();
474
+ continue;
475
+ }
476
+ frame.stage = 1;
477
+ const staticChild = frame.node.children.get(frame.seg);
478
+ if (staticChild) {
479
+ stack.push({
480
+ node: staticChild,
481
+ pos: frame.next,
482
+ stage: 0,
483
+ seg: "",
484
+ next: 0,
485
+ bound: false
486
+ });
487
+ }
488
+ continue;
447
489
  }
448
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
449
- normalized = normalized.slice(0, -1);
490
+ if (frame.stage === 1) {
491
+ frame.stage = 2;
492
+ const paramChild = frame.node.paramChild;
493
+ if (paramChild) {
494
+ const paramName = paramChild.paramName;
495
+ if (paramName === void 0) return null;
496
+ const value = originalPath !== void 0 ? decodeParam(segmentAt(originalPath, frame.pos), decode) : decodeParam(frame.seg, decode);
497
+ bindNames.push(paramName);
498
+ bindValues.push(value);
499
+ frame.bound = true;
500
+ stack.push({
501
+ node: paramChild,
502
+ pos: frame.next,
503
+ stage: 0,
504
+ seg: "",
505
+ next: 0,
506
+ bound: false
507
+ });
508
+ }
509
+ continue;
510
+ }
511
+ if (frame.bound) {
512
+ bindNames.pop();
513
+ bindValues.pop();
514
+ frame.bound = false;
515
+ }
516
+ const wildcardChild = frame.node.wildcardChild;
517
+ if (wildcardChild) {
518
+ const src = originalPath ?? path;
519
+ bindNames.push("*");
520
+ bindValues.push(decodeParam(src.slice(frame.pos), decode));
521
+ const handler = wildcardChild.handlers.get(method);
522
+ if (handler) return handler;
523
+ bindNames.pop();
524
+ bindValues.pop();
450
525
  }
451
- const staticKey = normalized.length > 1 && normalized.endsWith("/") ? `${method} ${normalized.slice(0, -1)}` : `${method} ${normalized}`;
452
- const staticEntry = this.staticRoutes.get(staticKey);
526
+ stack.pop();
527
+ }
528
+ return null;
529
+ }
530
+ __name(matchNodeIndexed, "matchNodeIndexed");
531
+
532
+ // src/match-route.ts
533
+ function matchRoute(method, rawPath, root, staticRoutes, hasParamRoutes, caseSensitive, strict, decode, routerMiddleware, preNormalized = false, walkPool) {
534
+ let path = rawPath;
535
+ if (!preNormalized) {
536
+ const queryIdx = path.indexOf("?");
537
+ if (queryIdx !== -1) path = path.slice(0, queryIdx);
538
+ }
539
+ let normalized;
540
+ let caseStable;
541
+ if (preNormalized) {
542
+ normalized = path;
543
+ caseStable = true;
544
+ } else {
545
+ caseStable = caseSensitive || isProvablyLowerAscii(path);
546
+ const folded = caseStable ? path : path.toLowerCase();
547
+ normalized = collapseAndStrip(folded, strict);
548
+ }
549
+ const methodMap = staticRoutes.get(method);
550
+ if (methodMap) {
551
+ const staticKey = normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
552
+ const staticEntry = methodMap.get(staticKey);
453
553
  if (staticEntry) {
454
554
  return {
455
555
  handler: staticEntry.handler,
456
556
  params: EMPTY_PARAMS,
457
- middleware: this.routerMiddleware,
557
+ middleware: routerMiddleware,
458
558
  executor: staticEntry.executor
459
559
  };
460
560
  }
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
561
  }
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
- ];
501
- }
502
- return [
503
- path.slice(start, slashPos),
504
- slashPos + 1
505
- ];
562
+ if (!hasParamRoutes) return null;
563
+ const originalPath = caseStable ? void 0 : collapseAndStrip(path, strict);
564
+ const bindNames = walkPool ? walkPool.bindNames : [];
565
+ const bindValues = walkPool ? walkPool.bindValues : [];
566
+ if (walkPool) {
567
+ bindNames.length = 0;
568
+ bindValues.length = 0;
506
569
  }
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;
570
+ const entry = matchNodeIndexed(root, normalized, 1, bindNames, bindValues, method, decode, originalPath, walkPool);
571
+ if (!entry) return null;
572
+ const count = bindNames.length;
573
+ let params;
574
+ if (count === 0) {
575
+ params = EMPTY_PARAMS;
576
+ } else {
577
+ params = Object.create(NULL_PROTO);
578
+ for (let i = 0; i < count; i++) {
579
+ const name = bindNames[i];
580
+ const value = bindValues[i];
581
+ if (name !== void 0 && value !== void 0) params[name] = value;
538
582
  }
539
- return null;
540
583
  }
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();
557
- }
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
- };
584
+ return {
585
+ handler: entry.handler,
586
+ params,
587
+ middleware: routerMiddleware,
588
+ executor: entry.executor
589
+ };
590
+ }
591
+ __name(matchRoute, "matchRoute");
592
+ function resolveMatch(state, hasParamRoutes, method, path, preNormalized = false) {
593
+ return matchRoute(method, path, state.root, state.staticRoutes, hasParamRoutes, state.caseSensitive, state.strict, state.decode, state.routerMiddleware, preNormalized, state.walkPool);
594
+ }
595
+ __name(resolveMatch, "resolveMatch");
596
+
597
+ // src/composition.ts
598
+ function copyRoutes(node, prefix, segments, subRouterMiddleware, addRoute2) {
599
+ for (const [method, entry] of node.handlers) {
600
+ if (entry.autoHead) continue;
601
+ const path = prefix + "/" + segments.join("/");
602
+ const combined = subRouterMiddleware.length > 0 ? [
603
+ ...subRouterMiddleware,
604
+ ...entry.middleware
605
+ ] : entry.middleware;
606
+ addRoute2(method, path || "/", [
607
+ entry.handler
608
+ ], combined);
572
609
  }
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) {
610
+ for (const [, child] of node.children) {
611
+ copyRoutes(child, prefix, [
612
+ ...segments,
613
+ child.segment
614
+ ], subRouterMiddleware, addRoute2);
615
+ }
616
+ if (node.paramChild) {
617
+ copyRoutes(node.paramChild, prefix, [
618
+ ...segments,
619
+ node.paramChild.segment
620
+ ], subRouterMiddleware, addRoute2);
621
+ }
622
+ if (node.wildcardChild) {
623
+ copyRoutes(node.wildcardChild, prefix, [
624
+ ...segments,
625
+ "*"
626
+ ], subRouterMiddleware, addRoute2);
627
+ }
628
+ }
629
+ __name(copyRoutes, "copyRoutes");
630
+
631
+ // src/middleware-adapter.ts
632
+ function sealRouterMiddleware(root, staticRoutes, routerMiddleware) {
633
+ const routerMw = [
634
+ ...routerMiddleware
635
+ ];
636
+ const walk = /* @__PURE__ */ __name((node) => {
637
+ for (const [method, entry] of node.handlers) {
598
638
  const combinedMw = [
599
639
  ...routerMw,
600
640
  ...entry.middleware
601
641
  ];
602
642
  entry.executor = compileExecutor(entry.handler, combinedMw);
603
- this.staticRoutes.set(key, entry);
643
+ node.handlers.set(method, entry);
644
+ }
645
+ for (const [, child] of node.children) {
646
+ walk(child);
647
+ }
648
+ if (node.paramChild) walk(node.paramChild);
649
+ if (node.wildcardChild) walk(node.wildcardChild);
650
+ }, "walk");
651
+ walk(root);
652
+ for (const [, methodMap] of staticRoutes) {
653
+ for (const [key, entry] of methodMap) {
654
+ const combinedMw = [
655
+ ...routerMw,
656
+ ...entry.middleware
657
+ ];
658
+ entry.executor = compileExecutor(entry.handler, combinedMw);
659
+ methodMap.set(key, entry);
604
660
  }
605
661
  }
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();
662
+ }
663
+ __name(sealRouterMiddleware, "sealRouterMiddleware");
664
+
665
+ // src/registration.ts
666
+ import { HTTP_METHODS as HTTP_METHODS2 } from "@nextrush/types";
667
+
668
+ // src/route-metadata.ts
669
+ import { ROUTE_METADATA } from "@nextrush/types";
670
+ function endpoint(metadata) {
671
+ return {
672
+ [ROUTE_METADATA]: metadata
673
+ };
674
+ }
675
+ __name(endpoint, "endpoint");
676
+ function mergeContributions(contributions) {
677
+ if (contributions.length === 0) return void 0;
678
+ const meta = {};
679
+ for (const c of contributions) {
680
+ if (c.summary !== void 0) meta.summary = c.summary;
681
+ if (c.description !== void 0) meta.description = c.description;
682
+ if (c.deprecated !== void 0) meta.deprecated = c.deprecated;
683
+ if (c.visibility !== void 0) meta.visibility = c.visibility;
684
+ if (c.tags !== void 0) meta.tags = c.tags;
685
+ if (c.request) meta.request = {
686
+ ...meta.request,
687
+ ...c.request
688
+ };
689
+ if (c.responses) meta.responses = {
690
+ ...meta.responses,
691
+ ...c.responses
692
+ };
693
+ }
694
+ return meta;
695
+ }
696
+ __name(mergeContributions, "mergeContributions");
697
+ function readContribution(entry) {
698
+ return entry[ROUTE_METADATA];
699
+ }
700
+ __name(readContribution, "readContribution");
701
+
702
+ // src/registration.ts
703
+ function normalizeRegistrationPath(path, prefix, strict) {
704
+ let joinedPrefix = prefix;
705
+ if (joinedPrefix.endsWith("/") && path.startsWith("/")) {
706
+ joinedPrefix = joinedPrefix.slice(0, -1);
707
+ }
708
+ let normalized = joinedPrefix + path;
709
+ if (normalized.includes("//")) {
710
+ normalized = normalized.replace(/\/+/g, "/");
711
+ }
712
+ if (!strict && normalized.length > 1 && normalized.endsWith("/")) {
713
+ normalized = normalized.slice(0, -1);
714
+ }
715
+ return normalized.startsWith("/") ? normalized : "/" + normalized;
716
+ }
717
+ __name(normalizeRegistrationPath, "normalizeRegistrationPath");
718
+ function setStaticEntry(staticRoutes, method, key, entry) {
719
+ let methodMap = staticRoutes.get(method);
720
+ if (!methodMap) {
721
+ methodMap = /* @__PURE__ */ new Map();
722
+ staticRoutes.set(method, methodMap);
723
+ }
724
+ methodMap.set(key, entry);
725
+ }
726
+ __name(setStaticEntry, "setStaticEntry");
727
+ function addRoute(method, normalized, entries, middleware, state, recordIntrospection = true) {
728
+ const segments = parseSegments(normalized, state.caseSensitive);
729
+ if (segments.length > state.maxDepth) {
730
+ state.maxDepth = segments.length;
731
+ }
732
+ let node = state.root;
733
+ for (const seg of segments) {
734
+ if (seg.type === NodeType.PARAM) {
735
+ if (!node.paramChild) {
736
+ node.paramChild = createNode(seg.segment, NodeType.PARAM);
737
+ node.paramChild.paramName = seg.paramName;
738
+ } else if (node.paramChild.paramName !== seg.paramName) {
739
+ 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.`);
614
740
  }
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;
741
+ node = node.paramChild;
742
+ } else if (seg.type === NodeType.WILDCARD) {
743
+ node.wildcardChild ??= createNode("*", NodeType.WILDCARD);
744
+ node = node.wildcardChild;
745
+ break;
746
+ } else {
747
+ const key = seg.segment;
748
+ let child = node.children.get(key);
749
+ if (!child) {
750
+ child = createNode(seg.segment, NodeType.STATIC);
751
+ node.children.set(key, child);
624
752
  }
625
- ctx.status = 405;
626
- ctx.set("Allow", allowHeader);
753
+ node = child;
754
+ }
755
+ }
756
+ const functions = [];
757
+ const contributions = [];
758
+ for (const routeEntry of entries) {
759
+ const contribution = readContribution(routeEntry);
760
+ if (contribution) contributions.push(contribution);
761
+ if (typeof routeEntry === "function") functions.push(routeEntry);
762
+ }
763
+ const combinedMiddleware = [
764
+ ...middleware
765
+ ];
766
+ const finalHandler = functions[functions.length - 1];
767
+ if (!finalHandler) {
768
+ throw new Error("At least one handler is required");
769
+ }
770
+ for (let i = 0; i < functions.length - 1; i++) {
771
+ const fn = functions[i];
772
+ if (fn) combinedMiddleware.push(fn);
773
+ }
774
+ const executor = compileExecutor(finalHandler, combinedMiddleware);
775
+ const handlerEntry = {
776
+ handler: finalHandler,
777
+ middleware: combinedMiddleware,
778
+ executor,
779
+ autoHead: false
780
+ };
781
+ const existing = node.handlers.get(method);
782
+ if (existing && !(method === "HEAD" && existing.autoHead)) {
783
+ throw new Error(`Route conflict: ${method} ${normalized} is already registered. Remove the duplicate or use a different path.`);
784
+ }
785
+ node.handlers.set(method, handlerEntry);
786
+ const hasParams = segments.some((s) => s.type === NodeType.PARAM || s.type === NodeType.WILDCARD);
787
+ const staticKey = hasParams ? void 0 : state.caseSensitive ? normalized : normalized.toLowerCase();
788
+ if (staticKey !== void 0) {
789
+ setStaticEntry(state.staticRoutes, method, staticKey, handlerEntry);
790
+ }
791
+ if (method === "GET" && !node.handlers.has("HEAD")) {
792
+ const derived = {
793
+ handler: finalHandler,
794
+ middleware: combinedMiddleware,
795
+ executor,
796
+ autoHead: true
627
797
  };
798
+ node.handlers.set("HEAD", derived);
799
+ if (staticKey !== void 0) {
800
+ setStaticEntry(state.staticRoutes, "HEAD", staticKey, derived);
801
+ }
628
802
  }
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, "/");
803
+ if (recordIntrospection) {
804
+ state.routeDefinitions.push({
805
+ key: `${method} ${normalized}`,
806
+ method,
807
+ path: normalized,
808
+ metadata: mergeContributions(contributions)
809
+ });
810
+ }
811
+ return hasParams;
812
+ }
813
+ __name(addRoute, "addRoute");
814
+ function registerRedirect(from, to, status, addRoute2) {
815
+ const redirectHandler = createRedirectHandler(to, status);
816
+ addRoute2("GET", from, [
817
+ redirectHandler
818
+ ]);
819
+ addRoute2("HEAD", from, [
820
+ redirectHandler
821
+ ]);
822
+ if (status === 307 || status === 308) {
823
+ addRoute2("POST", from, [
824
+ redirectHandler
825
+ ]);
826
+ addRoute2("PUT", from, [
827
+ redirectHandler
828
+ ]);
829
+ addRoute2("PATCH", from, [
830
+ redirectHandler
831
+ ]);
832
+ addRoute2("DELETE", from, [
833
+ redirectHandler
834
+ ]);
835
+ }
836
+ }
837
+ __name(registerRedirect, "registerRedirect");
838
+ function pushAnyMethodDefinition(routeDefinitions, normalized) {
839
+ routeDefinitions.push({
840
+ key: `${HTTP_METHODS2[0]} ${normalized}`,
841
+ method: HTTP_METHODS2[0],
842
+ path: normalized,
843
+ isAnyMethod: true
844
+ });
845
+ }
846
+ __name(pushAnyMethodDefinition, "pushAnyMethodDefinition");
847
+
848
+ // src/find-node.ts
849
+ function findNode(root, path, startPos) {
850
+ const stack = [
851
+ {
852
+ node: root,
853
+ pos: startPos,
854
+ stage: 0,
855
+ next: 0
637
856
  }
638
- if (!this.opts.strict && normalized.length > 1 && normalized.endsWith("/")) {
639
- normalized = normalized.slice(0, -1);
857
+ ];
858
+ while (stack.length > 0) {
859
+ const frame = stack[stack.length - 1];
860
+ if (frame === void 0) break;
861
+ if (frame.stage === 0) {
862
+ if (frame.pos >= path.length) {
863
+ return frame.node;
864
+ }
865
+ const seg = segmentAt(path, frame.pos);
866
+ if (seg === "") {
867
+ return frame.node;
868
+ }
869
+ const segEnd = frame.pos + seg.length;
870
+ frame.next = segEnd < path.length ? segEnd + 1 : path.length;
871
+ frame.stage = 1;
872
+ const staticChild = frame.node.children.get(seg);
873
+ if (staticChild) {
874
+ stack.push({
875
+ node: staticChild,
876
+ pos: frame.next,
877
+ stage: 0,
878
+ next: 0
879
+ });
880
+ }
881
+ continue;
640
882
  }
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());
883
+ if (frame.stage === 1) {
884
+ frame.stage = 2;
885
+ if (frame.node.paramChild) {
886
+ stack.push({
887
+ node: frame.node.paramChild,
888
+ pos: frame.next,
889
+ stage: 0,
890
+ next: 0
891
+ });
892
+ }
893
+ continue;
894
+ }
895
+ if (frame.node.wildcardChild) {
896
+ return frame.node.wildcardChild;
897
+ }
898
+ stack.pop();
645
899
  }
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;
900
+ return null;
901
+ }
902
+ __name(findNode, "findNode");
903
+ function findAllowedMethods(path, root, caseSensitive, strict) {
904
+ const normalized = normalizePathForMatch(path, caseSensitive, strict);
905
+ const node = findNode(root, normalized, 1);
906
+ if (!node || node.handlers.size === 0) return [];
907
+ return Array.from(node.handlers.keys());
908
+ }
909
+ __name(findAllowedMethods, "findAllowedMethods");
910
+
911
+ // src/canonicalize.ts
912
+ var DOT = 46;
913
+ var SLASH = 47;
914
+ function hasDotSegment(path) {
915
+ const len = path.length;
916
+ let segStart = 0;
917
+ for (let i = 0; i <= len; i++) {
918
+ const atBoundary = i === len || path.charCodeAt(i) === SLASH;
919
+ if (!atBoundary) continue;
920
+ const segLen = i - segStart;
921
+ if (segLen === 1 && path.charCodeAt(segStart) === DOT) return true;
922
+ if (segLen === 2 && path.charCodeAt(segStart) === DOT && path.charCodeAt(segStart + 1) === DOT) {
923
+ return true;
653
924
  }
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;
925
+ segStart = i + 1;
926
+ }
927
+ return false;
928
+ }
929
+ __name(hasDotSegment, "hasDotSegment");
930
+ var memoTarget = void 0;
931
+ var memoCaseSensitive = false;
932
+ var memoStrict = false;
933
+ var memoResult = {
934
+ rejected: false,
935
+ path: "/"
936
+ };
937
+ function canonicalizePath(rawTarget, caseSensitive, strict) {
938
+ if (rawTarget === memoTarget && caseSensitive === memoCaseSensitive && strict === memoStrict) {
939
+ return memoResult;
940
+ }
941
+ const queryIdx = rawTarget.indexOf("?");
942
+ const path = queryIdx === -1 ? rawTarget : rawTarget.slice(0, queryIdx);
943
+ const result = hasDotSegment(path) ? {
944
+ rejected: true,
945
+ path
946
+ } : {
947
+ rejected: false,
948
+ path: collapseAndStrip(caseSensitive || isProvablyLowerAscii(path) ? path : path.toLowerCase(), strict)
949
+ };
950
+ memoTarget = rawTarget;
951
+ memoCaseSensitive = caseSensitive;
952
+ memoStrict = strict;
953
+ memoResult = result;
954
+ return result;
955
+ }
956
+ __name(canonicalizePath, "canonicalizePath");
957
+
958
+ // src/dispatch.ts
959
+ var RESOLVED = Promise.resolve();
960
+ function createRoutesMiddleware(match, caseSensitive, strict) {
961
+ return (ctx, next) => {
962
+ const originalPath = ctx.path;
963
+ const canonical = canonicalizePath(originalPath, caseSensitive, strict);
964
+ if (canonical.rejected) {
965
+ ctx.status = 400;
966
+ ctx.originalPath = originalPath;
967
+ return RESOLVED;
660
968
  }
661
- if (node.paramChild) {
662
- const result = this.findNode(node.paramChild, segments, index + 1);
663
- if (result) return result;
969
+ ctx.path = canonical.path;
970
+ ctx.originalPath = originalPath;
971
+ const routeMatch = match(ctx.method, canonical.path);
972
+ if (!routeMatch) {
973
+ ctx.status = 404;
974
+ return next ? next() : RESOLVED;
664
975
  }
665
- if (node.wildcardChild) {
666
- return node.wildcardChild;
976
+ ctx.params = routeMatch.params;
977
+ return routeMatch.executor ? routeMatch.executor(ctx) : (
978
+ // or thenable return still yields a Promise<void> and never a sync throw.
979
+ Promise.resolve(routeMatch.handler(ctx, NOOP_NEXT))
980
+ );
981
+ };
982
+ }
983
+ __name(createRoutesMiddleware, "createRoutesMiddleware");
984
+ function createAllowedMethodsMiddleware(root, caseSensitive, strict) {
985
+ return async (ctx, next) => {
986
+ if (next) {
987
+ await next();
667
988
  }
668
- return null;
989
+ if (ctx.status !== 404) return;
990
+ const allowed = findAllowedMethods(ctx.path, root, caseSensitive, strict);
991
+ if (allowed.length === 0) return;
992
+ const allowHeader = allowed.join(", ");
993
+ if (ctx.method === "OPTIONS") {
994
+ ctx.status = 200;
995
+ ctx.set("Allow", allowHeader);
996
+ ctx.body = "";
997
+ return;
998
+ }
999
+ ctx.status = 405;
1000
+ ctx.set("Allow", allowHeader);
1001
+ };
1002
+ }
1003
+ __name(createAllowedMethodsMiddleware, "createAllowedMethodsMiddleware");
1004
+
1005
+ // src/state.ts
1006
+ function resolveRouterOptions(options) {
1007
+ return {
1008
+ prefix: options.prefix ?? "",
1009
+ caseSensitive: options.caseSensitive ?? false,
1010
+ strict: options.strict ?? false,
1011
+ decode: options.decode ?? true
1012
+ };
1013
+ }
1014
+ __name(resolveRouterOptions, "resolveRouterOptions");
1015
+ function createRouterState(root, opts, staticRoutes, routeDefinitions, routerMiddleware) {
1016
+ return {
1017
+ root,
1018
+ staticRoutes,
1019
+ routeDefinitions,
1020
+ caseSensitive: opts.caseSensitive,
1021
+ strict: opts.strict,
1022
+ decode: opts.decode,
1023
+ routerMiddleware,
1024
+ maxDepth: 0
1025
+ };
1026
+ }
1027
+ __name(createRouterState, "createRouterState");
1028
+
1029
+ // src/router.ts
1030
+ var SLASH_CHAR_CODE = 47;
1031
+ var Router = class _Router {
1032
+ static {
1033
+ __name(this, "Router");
669
1034
  }
670
- // ===========================================================================
671
- // Route Groups
672
- // ===========================================================================
1035
+ root;
1036
+ opts;
1037
+ routerMiddleware = [];
1038
+ /** Static-route fast path: method-nested map for O(1) lookup with no per-request key string (HP-9). */
1039
+ staticRoutes = /* @__PURE__ */ new Map();
673
1040
  /**
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
- * ```
1041
+ * Introspection registry, kept SEPARATE from the hot-path trie/staticRoutes
1042
+ * so request dispatch never reads metadata — only getRoutes() touches it.
703
1043
  */
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");
711
- }
712
- cb = callback;
713
- } else {
714
- cb = middlewareOrCallback;
715
- }
716
- const groupRouter = new GroupRouter(this, prefix, middleware);
717
- cb(groupRouter);
718
- return this;
719
- }
1044
+ routeDefinitions = [];
1045
+ /** Whether any routes have params or wildcards (disables static-only fast path) */
1046
+ hasParamRoutes = false;
1047
+ /** Whether router-level middleware has already been sealed into executors (audit RT-7) */
1048
+ _sealed = false;
720
1049
  /**
721
- * Remove all registered routes and middleware, resetting the router to its initial state.
722
- * Useful for plugin `destroy()` to cleanly un-register routes.
1050
+ * Single-entry memo for {@link matchesMountPrefix}'s canonicalization of the
1051
+ * mount prefix, which is fixed at registration time. Declared as two fields
1052
+ * rather than an object so a miss stores without allocating.
723
1053
  */
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;
1054
+ _prefixMemoRaw = void 0;
1055
+ _prefixMemoCanonical = "";
1056
+ /** Memoized state the extracted registration/matching functions read (see {@link createRouterState}). */
1057
+ state;
1058
+ constructor(options = {}) {
1059
+ this.root = createNode("");
1060
+ this.opts = resolveRouterOptions(options);
1061
+ this.state = createRouterState(this.root, this.opts, this.staticRoutes, this.routeDefinitions, this.routerMiddleware);
732
1062
  }
733
1063
  /**
734
- * Internal method to add route with group context
735
- * @internal
1064
+ * Validate + normalize a raw path, then delegate trie insertion to the
1065
+ * extracted `addRoute` (design.md D2); flips `hasParamRoutes` from its return.
736
1066
  */
737
- _addGroupRoute(method, path, handlers, groupMiddleware) {
738
- this.addRoute(method, path, handlers, groupMiddleware);
1067
+ addRoute(method, path, entries, middleware = [], recordIntrospection = true) {
1068
+ const rawPath = path;
1069
+ if (typeof rawPath !== "string") {
1070
+ throw new TypeError(`Route path must be a string, received ${rawPath === null ? "null" : typeof rawPath}.`);
1071
+ }
1072
+ const normalized = normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict);
1073
+ const depthBefore = this.state.maxDepth;
1074
+ if (addRoute(method, normalized, entries, middleware, this.state, recordIntrospection)) {
1075
+ this.hasParamRoutes = true;
1076
+ }
1077
+ if (this.state.maxDepth > depthBefore) {
1078
+ this.state.walkPool = createWalkPool(this.state.maxDepth);
1079
+ }
739
1080
  }
740
- };
741
- var GroupRouter = class GroupRouter2 {
742
- static {
743
- __name(this, "GroupRouter");
1081
+ get(path, ...entries) {
1082
+ this.addRoute("GET", path, entries);
1083
+ return this;
744
1084
  }
745
- parent;
746
- prefix;
747
- middleware;
748
- constructor(parent, prefix, middleware) {
749
- this.parent = parent;
750
- this.prefix = prefix;
751
- this.middleware = middleware;
1085
+ post(path, ...entries) {
1086
+ this.addRoute("POST", path, entries);
1087
+ return this;
752
1088
  }
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;
1089
+ put(path, ...entries) {
1090
+ this.addRoute("PUT", path, entries);
1091
+ return this;
760
1092
  }
761
- get(path, ...handlers) {
762
- this.parent._addGroupRoute("GET", this.fullPath(path), handlers, this.middleware);
1093
+ delete(path, ...entries) {
1094
+ this.addRoute("DELETE", path, entries);
763
1095
  return this;
764
1096
  }
765
- post(path, ...handlers) {
766
- this.parent._addGroupRoute("POST", this.fullPath(path), handlers, this.middleware);
1097
+ patch(path, ...entries) {
1098
+ this.addRoute("PATCH", path, entries);
767
1099
  return this;
768
1100
  }
769
- put(path, ...handlers) {
770
- this.parent._addGroupRoute("PUT", this.fullPath(path), handlers, this.middleware);
1101
+ head(path, ...entries) {
1102
+ this.addRoute("HEAD", path, entries);
771
1103
  return this;
772
1104
  }
773
- delete(path, ...handlers) {
774
- this.parent._addGroupRoute("DELETE", this.fullPath(path), handlers, this.middleware);
1105
+ options(path, ...entries) {
1106
+ this.addRoute("OPTIONS", path, entries);
775
1107
  return this;
776
1108
  }
777
- patch(path, ...handlers) {
778
- this.parent._addGroupRoute("PATCH", this.fullPath(path), handlers, this.middleware);
1109
+ /**
1110
+ * Register a route for every HTTP method under one consolidated `isAnyMethod`
1111
+ * introspection row (T016) — matching is unchanged (see {@link pushAnyMethodDefinition}).
1112
+ */
1113
+ all(path, ...entries) {
1114
+ for (const method of HTTP_METHODS3) {
1115
+ this.addRoute(method, path, entries, [], false);
1116
+ }
1117
+ pushAnyMethodDefinition(this.routeDefinitions, normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict));
779
1118
  return this;
780
1119
  }
781
- head(path, ...handlers) {
782
- this.parent._addGroupRoute("HEAD", this.fullPath(path), handlers, this.middleware);
1120
+ route(method, path, ...entries) {
1121
+ this.addRoute(method, path, entries);
783
1122
  return this;
784
1123
  }
785
- options(path, ...handlers) {
786
- this.parent._addGroupRoute("OPTIONS", this.fullPath(path), handlers, this.middleware);
1124
+ /**
1125
+ * Every registered route as a read-only list, for renderers (`@nextrush/openapi`,
1126
+ * SDK/RPC generators). Doc-generation-time only — never on the request path.
1127
+ */
1128
+ getRoutes() {
1129
+ return this.routeDefinitions;
1130
+ }
1131
+ /**
1132
+ * Register a redirect from one path to another (301 by default). 307/308
1133
+ * additionally register POST/PUT/PATCH/DELETE to preserve the method.
1134
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#redirects | README: Redirects}
1135
+ */
1136
+ redirect(from, to, status = 301) {
1137
+ registerRedirect(from, to, status, (method, path, entries) => {
1138
+ this.addRoute(method, path, entries);
1139
+ });
787
1140
  return this;
788
1141
  }
789
- all(path, ...handlers) {
790
- for (const method of HTTP_METHODS) {
791
- this.parent._addGroupRoute(method, this.fullPath(path), handlers, this.middleware);
1142
+ use(pathOrMiddleware, routerOrUndefined) {
1143
+ if (typeof pathOrMiddleware === "function") {
1144
+ this.routerMiddleware.push(pathOrMiddleware);
1145
+ } else if (typeof pathOrMiddleware === "string" && routerOrUndefined instanceof _Router) {
1146
+ this.mountRouter(pathOrMiddleware, routerOrUndefined);
1147
+ } else if (typeof pathOrMiddleware === "string") {
1148
+ 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.`);
1149
+ } else if (pathOrMiddleware instanceof _Router) {
1150
+ this.mountRouter("", pathOrMiddleware);
792
1151
  }
793
1152
  return this;
794
1153
  }
795
1154
  /**
796
- * Register a redirect within the group
1155
+ * Mount a sub-router at a path prefix (Hono-style) — the explicit, more
1156
+ * semantic equivalent of `router.use(path, subRouter)`.
1157
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#sub-router-mounting | README: Sub-Router Mounting}
797
1158
  */
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);
1159
+ mount(path, router) {
1160
+ this.mountRouter(path, router);
817
1161
  return this;
818
1162
  }
1163
+ /** Mount a sub-router, carrying its own `routerMiddleware` onto every copied route. */
1164
+ mountRouter(prefix, router) {
1165
+ copyRoutes(router.root, prefix, [], router.routerMiddleware, this.addRoute.bind(this));
1166
+ }
1167
+ /** Match a request to a route — delegates to {@link resolveMatch} (design.md D1). */
1168
+ match(method, path) {
1169
+ return resolveMatch(this.state, this.hasParamRoutes, method, path);
1170
+ }
819
1171
  /**
820
- * Nested group support
1172
+ * Test whether `path` falls under `prefix` using this router's OWN
1173
+ * canonicalization (case folding per `caseSensitive`, structural
1174
+ * normalization) — the mount-boundary counterpart to {@link match}, so a
1175
+ * router mounted via `Application.route()` is tested with the identical
1176
+ * rule it dispatches with (RFC-029, task 3.8). Implements the optional
1177
+ * `Routable.matchesMountPrefix` contract from `@nextrush/core`.
1178
+ *
1179
+ * @param path - The full request path being tested for this mount.
1180
+ * @param prefix - The normalized mount prefix (leading `/`, no trailing `/`).
1181
+ * @returns The path's remainder past the prefix (e.g. `/users` for a
1182
+ * `/ADMIN/Users` request mounted at `/admin`), or `undefined` when `path`
1183
+ * is not under `prefix` per this router's canonicalization.
821
1184
  */
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;
1185
+ matchesMountPrefix(path, prefix) {
1186
+ const canonical = canonicalizePath(path, this.opts.caseSensitive, this.opts.strict);
1187
+ if (canonical.rejected) return void 0;
1188
+ let canonicalPrefix;
1189
+ if (this._prefixMemoRaw === prefix) {
1190
+ canonicalPrefix = this._prefixMemoCanonical;
831
1191
  } else {
832
- cb = middlewareOrCallback;
1192
+ canonicalPrefix = canonicalizePath(prefix, this.opts.caseSensitive, this.opts.strict).path;
1193
+ this._prefixMemoRaw = prefix;
1194
+ this._prefixMemoCanonical = canonicalPrefix;
833
1195
  }
834
- const nestedRouter = new GroupRouter2(this.parent, this.fullPath(prefix), [
835
- ...this.middleware,
836
- ...nestedMiddleware
837
- ]);
838
- cb(nestedRouter);
1196
+ const prefixLen = canonicalPrefix.length;
1197
+ if (!canonical.path.startsWith(canonicalPrefix)) return void 0;
1198
+ const hasCharAfterPrefix = prefixLen < canonical.path.length;
1199
+ if (hasCharAfterPrefix && canonical.path.charCodeAt(prefixLen) !== SLASH_CHAR_CODE) {
1200
+ return void 0;
1201
+ }
1202
+ return canonical.path.slice(prefixLen) || "/";
1203
+ }
1204
+ /**
1205
+ * Return the router's dispatch middleware — mount this on the application.
1206
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#routerroutes | README: router.routes()}
1207
+ */
1208
+ routes() {
1209
+ if (this.routerMiddleware.length > 0 && !this._sealed) {
1210
+ this._sealed = true;
1211
+ sealRouterMiddleware(this.root, this.staticRoutes, this.routerMiddleware);
1212
+ }
1213
+ return createRoutesMiddleware((method, path) => resolveMatch(this.state, this.hasParamRoutes, method, path, true), this.opts.caseSensitive, this.opts.strict);
1214
+ }
1215
+ /**
1216
+ * Generate allowed-methods middleware. Responds to OPTIONS with an `Allow`
1217
+ * header and returns 405 for a known path hit with an unregistered method.
1218
+ */
1219
+ allowedMethods() {
1220
+ return createAllowedMethodsMiddleware(this.root, this.opts.caseSensitive, this.opts.strict);
1221
+ }
1222
+ /**
1223
+ * Create a route group with a shared prefix and middleware. The callback
1224
+ * receives a {@link RouteGroup} to register routes against.
1225
+ * @see {@link https://github.com/0xTanzim/nextRush/blob/main/packages/router/README.md#route-groups | README: Route Groups}
1226
+ */
1227
+ group(prefix, middlewareOrCallback, callback) {
1228
+ runRouteGroup(this, prefix, middlewareOrCallback, callback);
839
1229
  return this;
840
1230
  }
1231
+ /**
1232
+ * Remove all routes and middleware, resetting the router to its initial
1233
+ * state — for test isolation or hot-reload re-registration.
1234
+ */
1235
+ reset() {
1236
+ clearNode(this.root);
1237
+ this.staticRoutes.clear();
1238
+ this.routerMiddleware.length = 0;
1239
+ this.hasParamRoutes = false;
1240
+ this.routeDefinitions.length = 0;
1241
+ this.state.maxDepth = 0;
1242
+ this.state.walkPool = void 0;
1243
+ this._sealed = false;
1244
+ }
1245
+ /** Register a route on behalf of a {@link RouteGroup} (group context). @internal */
1246
+ _addGroupRoute(method, path, handlers, groupMiddleware, recordIntrospection = true) {
1247
+ this.addRoute(method, path, handlers, groupMiddleware, recordIntrospection);
1248
+ }
1249
+ /**
1250
+ * Group-facing entry point to {@link pushAnyMethodDefinition} — a group's `.all()`
1251
+ * records its consolidated row here since group routes live on the parent. @internal
1252
+ */
1253
+ _pushAnyMethodRouteDefinition(path) {
1254
+ pushAnyMethodDefinition(this.routeDefinitions, normalizeRegistrationPath(path, this.opts.prefix, this.opts.strict));
1255
+ }
841
1256
  };
842
1257
  function createRouter(options) {
843
1258
  return new Router(options);
@@ -846,8 +1261,11 @@ __name(createRouter, "createRouter");
846
1261
  export {
847
1262
  NodeType,
848
1263
  Router,
1264
+ canonicalizePath,
849
1265
  createNode,
850
1266
  createRouter,
1267
+ endpoint,
1268
+ hasDotSegment,
851
1269
  parseSegments
852
1270
  };
853
1271
  //# sourceMappingURL=index.js.map