@rangojs/router 0.0.0-experimental.fa8a383a → 0.0.0-experimental.fad716ff

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 (118) hide show
  1. package/README.md +76 -18
  2. package/dist/bin/rango.js +130 -47
  3. package/dist/vite/index.js +526 -168
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/package.json +2 -2
  6. package/skills/cache-guide/SKILL.md +32 -0
  7. package/skills/caching/SKILL.md +8 -0
  8. package/skills/links/SKILL.md +3 -1
  9. package/skills/loader/SKILL.md +53 -43
  10. package/skills/middleware/SKILL.md +2 -0
  11. package/skills/parallel/SKILL.md +67 -0
  12. package/skills/prerender/SKILL.md +110 -68
  13. package/skills/route/SKILL.md +31 -0
  14. package/skills/router-setup/SKILL.md +87 -2
  15. package/skills/typesafety/SKILL.md +10 -0
  16. package/src/__internal.ts +1 -1
  17. package/src/browser/app-version.ts +14 -0
  18. package/src/browser/navigation-bridge.ts +16 -3
  19. package/src/browser/navigation-client.ts +64 -40
  20. package/src/browser/navigation-store.ts +43 -8
  21. package/src/browser/partial-update.ts +37 -4
  22. package/src/browser/prefetch/fetch.ts +8 -2
  23. package/src/browser/prefetch/queue.ts +61 -29
  24. package/src/browser/prefetch/resource-ready.ts +77 -0
  25. package/src/browser/react/Link.tsx +44 -8
  26. package/src/browser/react/NavigationProvider.tsx +13 -4
  27. package/src/browser/react/context.ts +7 -2
  28. package/src/browser/react/use-handle.ts +9 -58
  29. package/src/browser/react/use-router.ts +21 -8
  30. package/src/browser/rsc-router.tsx +26 -3
  31. package/src/browser/scroll-restoration.ts +10 -8
  32. package/src/browser/server-action-bridge.ts +8 -6
  33. package/src/browser/types.ts +27 -5
  34. package/src/build/generate-manifest.ts +6 -6
  35. package/src/build/generate-route-types.ts +3 -0
  36. package/src/build/route-types/include-resolution.ts +8 -1
  37. package/src/build/route-types/router-processing.ts +211 -72
  38. package/src/build/route-types/scan-filter.ts +8 -1
  39. package/src/cache/cache-runtime.ts +15 -11
  40. package/src/cache/cache-scope.ts +46 -5
  41. package/src/cache/taint.ts +55 -0
  42. package/src/client.tsx +2 -56
  43. package/src/context-var.ts +72 -2
  44. package/src/handle.ts +40 -0
  45. package/src/index.rsc.ts +3 -1
  46. package/src/index.ts +12 -0
  47. package/src/prerender/store.ts +5 -4
  48. package/src/prerender.ts +138 -77
  49. package/src/reverse.ts +22 -1
  50. package/src/route-definition/dsl-helpers.ts +42 -19
  51. package/src/route-definition/helpers-types.ts +10 -6
  52. package/src/route-definition/index.ts +3 -0
  53. package/src/route-definition/redirect.ts +9 -1
  54. package/src/route-definition/resolve-handler-use.ts +149 -0
  55. package/src/route-types.ts +11 -0
  56. package/src/router/content-negotiation.ts +100 -1
  57. package/src/router/handler-context.ts +79 -23
  58. package/src/router/intercept-resolution.ts +9 -4
  59. package/src/router/loader-resolution.ts +156 -21
  60. package/src/router/match-api.ts +124 -189
  61. package/src/router/match-middleware/background-revalidation.ts +12 -1
  62. package/src/router/match-middleware/cache-lookup.ts +72 -13
  63. package/src/router/match-middleware/cache-store.ts +21 -4
  64. package/src/router/match-middleware/segment-resolution.ts +53 -0
  65. package/src/router/match-result.ts +11 -5
  66. package/src/router/metrics.ts +6 -1
  67. package/src/router/middleware-types.ts +6 -8
  68. package/src/router/middleware.ts +2 -5
  69. package/src/router/navigation-snapshot.ts +182 -0
  70. package/src/router/prerender-match.ts +110 -10
  71. package/src/router/preview-match.ts +30 -102
  72. package/src/router/request-classification.ts +310 -0
  73. package/src/router/route-snapshot.ts +245 -0
  74. package/src/router/router-context.ts +1 -0
  75. package/src/router/router-interfaces.ts +36 -4
  76. package/src/router/router-options.ts +37 -11
  77. package/src/router/segment-resolution/fresh.ts +101 -18
  78. package/src/router/segment-resolution/helpers.ts +29 -24
  79. package/src/router/segment-resolution/revalidation.ts +122 -26
  80. package/src/router/types.ts +1 -0
  81. package/src/router.ts +54 -5
  82. package/src/rsc/handler.ts +464 -377
  83. package/src/rsc/loader-fetch.ts +23 -3
  84. package/src/rsc/manifest-init.ts +5 -1
  85. package/src/rsc/progressive-enhancement.ts +14 -2
  86. package/src/rsc/rsc-rendering.ts +10 -1
  87. package/src/rsc/server-action.ts +8 -0
  88. package/src/rsc/ssr-setup.ts +2 -2
  89. package/src/rsc/types.ts +9 -1
  90. package/src/server/context.ts +50 -1
  91. package/src/server/handle-store.ts +19 -0
  92. package/src/server/loader-registry.ts +9 -8
  93. package/src/server/request-context.ts +175 -15
  94. package/src/ssr/index.tsx +3 -0
  95. package/src/static-handler.ts +18 -6
  96. package/src/types/cache-types.ts +4 -4
  97. package/src/types/handler-context.ts +137 -33
  98. package/src/types/loader-types.ts +36 -9
  99. package/src/types/route-entry.ts +1 -1
  100. package/src/urls/path-helper-types.ts +9 -2
  101. package/src/urls/path-helper.ts +47 -12
  102. package/src/urls/pattern-types.ts +12 -0
  103. package/src/urls/response-types.ts +16 -6
  104. package/src/use-loader.tsx +73 -4
  105. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  106. package/src/vite/discovery/discover-routers.ts +5 -1
  107. package/src/vite/discovery/prerender-collection.ts +14 -1
  108. package/src/vite/discovery/state.ts +13 -4
  109. package/src/vite/index.ts +4 -0
  110. package/src/vite/plugin-types.ts +60 -5
  111. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  112. package/src/vite/plugins/expose-internal-ids.ts +118 -39
  113. package/src/vite/plugins/performance-tracks.ts +88 -0
  114. package/src/vite/plugins/refresh-cmd.ts +88 -26
  115. package/src/vite/rango.ts +19 -2
  116. package/src/vite/router-discovery.ts +178 -37
  117. package/src/vite/utils/prerender-utils.ts +18 -0
  118. package/src/vite/utils/shared-utils.ts +3 -2
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
180
180
 
181
181
  if (error instanceof DataNotFoundError) {
182
182
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+ // Fall back to router's notFound component, then a plain default
184
+ const notFoundOption = deps.notFoundComponent;
185
+ const defaultFallback =
186
+ typeof notFoundOption === "function"
187
+ ? notFoundOption({ pathname: pathname ?? "" })
188
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
189
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
190
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
191
+ const notFoundInfo = createNotFoundInfo(
192
+ error,
193
+ entry.shortCode,
194
+ entry.type,
195
+ pathname,
196
+ );
191
197
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
198
+ reportError(true, {
199
+ notFound: true,
200
+ message: notFoundInfo.message,
201
+ });
196
202
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
203
+ debugLog("segment", "notFound boundary handled error", {
204
+ segmentId: entry.shortCode,
205
+ message: notFoundInfo.message,
206
+ });
201
207
 
202
- setResponseStatus(404);
208
+ setResponseStatus(404);
203
209
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
210
+ return createNotFoundSegment(
211
+ notFoundInfo,
212
+ effectiveNotFoundFallback,
213
+ entry,
214
+ params,
215
+ );
211
216
  }
212
217
 
213
218
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -41,7 +41,11 @@ import {
41
41
  } from "./helpers.js";
42
42
  import { getRouterContext } from "../router-context.js";
43
43
  import { resolveSink, safeEmit } from "../telemetry.js";
44
- import { track } from "../../server/context.js";
44
+ import {
45
+ track,
46
+ RSCRouterContext,
47
+ runInsideLoaderScope,
48
+ } from "../../server/context.js";
45
49
 
46
50
  // ---------------------------------------------------------------------------
47
51
  // Telemetry helpers
@@ -232,7 +236,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
232
236
  params: ctx.params,
233
237
  loaderId: loader.$$id,
234
238
  loaderData: deps.wrapLoaderPromise(
235
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
239
+ runInsideLoaderScope(() =>
240
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
241
+ ),
236
242
  entry,
237
243
  segmentId,
238
244
  ctx.pathname,
@@ -262,29 +268,46 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
262
268
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
263
269
  const allLoaderSegments: ResolvedSegment[] = [];
264
270
  const allMatchedIds: string[] = [];
271
+ const seenIds = new Set<string>();
265
272
 
266
273
  async function collectEntryLoaders(
267
274
  entry: EntryData,
268
275
  belongsToRoute: boolean,
269
276
  shortCodeOverride?: string,
270
277
  ): Promise<void> {
271
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
272
- entry,
273
- context,
274
- belongsToRoute,
275
- clientSegmentIds,
276
- prevParams,
277
- request,
278
- prevUrl,
279
- nextUrl,
280
- routeKey,
281
- deps,
282
- actionContext,
283
- shortCodeOverride,
284
- stale,
285
- );
286
- allLoaderSegments.push(...segments);
287
- allMatchedIds.push(...matchedIds);
278
+ // Skip if all loaders from this entry have already been resolved
279
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
280
+ const loaderEntries = entry.loader ?? [];
281
+ const sc = shortCodeOverride ?? entry.shortCode;
282
+ const allAlreadySeen =
283
+ loaderEntries.length > 0 &&
284
+ loaderEntries.every((le, i) =>
285
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
286
+ );
287
+ if (!allAlreadySeen) {
288
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
289
+ entry,
290
+ context,
291
+ belongsToRoute,
292
+ clientSegmentIds,
293
+ prevParams,
294
+ request,
295
+ prevUrl,
296
+ nextUrl,
297
+ routeKey,
298
+ deps,
299
+ actionContext,
300
+ shortCodeOverride,
301
+ stale,
302
+ );
303
+ for (const seg of segments) {
304
+ if (!seenIds.has(seg.id)) {
305
+ seenIds.add(seg.id);
306
+ allLoaderSegments.push(seg);
307
+ }
308
+ }
309
+ allMatchedIds.push(...matchedIds);
310
+ }
288
311
 
289
312
  const seenParallelEntryIds = new Set<string>();
290
313
  for (const parallelEntry of getParallelEntries(entry.parallel)) {
@@ -296,6 +319,38 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
296
319
  const childBelongsToRoute = belongsToRoute || entry.type === "route";
297
320
  for (const layoutEntry of entry.layout) {
298
321
  await collectEntryLoaders(layoutEntry, childBelongsToRoute);
322
+ // Inherit route loaders for orphan layouts with parallels.
323
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
324
+ // route entry, as that would re-iterate route.layout and loop.
325
+ if (
326
+ entry.type === "route" &&
327
+ entry.loader &&
328
+ entry.loader.length > 0 &&
329
+ Object.keys(layoutEntry.parallel).length > 0
330
+ ) {
331
+ const inherited = await resolveLoadersWithRevalidation(
332
+ entry,
333
+ context,
334
+ childBelongsToRoute,
335
+ clientSegmentIds,
336
+ prevParams,
337
+ request,
338
+ prevUrl,
339
+ nextUrl,
340
+ routeKey,
341
+ deps,
342
+ actionContext,
343
+ layoutEntry.shortCode,
344
+ stale,
345
+ );
346
+ for (const seg of inherited.segments) {
347
+ if (!seenIds.has(seg.id)) {
348
+ seenIds.add(seg.id);
349
+ allLoaderSegments.push(seg);
350
+ }
351
+ }
352
+ allMatchedIds.push(...inherited.matchedIds);
353
+ }
299
354
  }
300
355
  }
301
356
 
@@ -665,13 +720,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
665
720
  return staticComponent;
666
721
  }
667
722
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
723
+ // For Passthrough routes at runtime, use the live handler instead of
724
+ // the build handler. At build time (context.build === true), always
725
+ // use the build handler from routeEntry.handler.
726
+ const handler =
727
+ !context.build && routeEntry.liveHandler
728
+ ? routeEntry.liveHandler
729
+ : routeEntry.handler;
668
730
  if (!routeEntry.loading) {
669
- const result = handleHandlerResult(await routeEntry.handler(context));
731
+ const result = handleHandlerResult(await handler(context));
670
732
  doneHandler();
671
733
  return result;
672
734
  }
673
735
  if (!actionContext) {
674
- const result = handleHandlerResult(routeEntry.handler(context));
736
+ const result = handleHandlerResult(handler(context));
675
737
  if (result instanceof Promise) {
676
738
  result.finally(doneHandler).catch(() => {});
677
739
  const tracked = deps.trackHandler(result, {
@@ -694,9 +756,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
694
756
  debugLog("segment.action", "resolving action route with awaited value", {
695
757
  entryId: entry.id,
696
758
  });
697
- const actionResult = handleHandlerResult(
698
- await routeEntry.handler(context),
699
- );
759
+ const actionResult = handleHandlerResult(await handler(context));
700
760
  doneHandler();
701
761
  return {
702
762
  content: Promise.resolve(actionResult),
@@ -705,10 +765,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
705
765
  () => null,
706
766
  );
707
767
 
768
+ // Normalize void handlers (undefined) to null so the reconciler's
769
+ // component === null checks work consistently for both void and explicit null.
708
770
  const resolvedComponent =
709
771
  component && typeof component === "object" && "content" in component
710
- ? (component as { content: ReactNode }).content
711
- : component;
772
+ ? ((component as { content: ReactNode }).content ?? null)
773
+ : (component ?? null);
712
774
 
713
775
  const segment: ResolvedSegment = {
714
776
  id: entry.shortCode,
@@ -810,6 +872,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
810
872
  deps,
811
873
  actionContext,
812
874
  stale,
875
+ entry,
813
876
  );
814
877
  segments.push(...orphanResult.segments);
815
878
  matchedIds.push(...orphanResult.matchedIds);
@@ -921,6 +984,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
921
984
  deps: SegmentResolutionDeps<TEnv>,
922
985
  actionContext?: ActionContext,
923
986
  stale?: boolean,
987
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
988
+ parentRouteEntry?: EntryData,
924
989
  ): Promise<SegmentRevalidationResult> {
925
990
  invariant(
926
991
  orphan.type === "layout" || orphan.type === "cache",
@@ -948,6 +1013,33 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
948
1013
  segments.push(...loaderResult.segments);
949
1014
  matchedIds.push(...loaderResult.matchedIds);
950
1015
 
1016
+ // Inherit parent route's loaders so parallel slots inside this layout
1017
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1018
+ if (
1019
+ parentRouteEntry &&
1020
+ parentRouteEntry.loader &&
1021
+ parentRouteEntry.loader.length > 0 &&
1022
+ Object.keys(orphan.parallel).length > 0
1023
+ ) {
1024
+ const inheritedResult = await resolveLoadersWithRevalidation(
1025
+ parentRouteEntry,
1026
+ context,
1027
+ belongsToRoute,
1028
+ clientSegmentIds,
1029
+ prevParams,
1030
+ request,
1031
+ prevUrl,
1032
+ nextUrl,
1033
+ routeKey,
1034
+ deps,
1035
+ actionContext,
1036
+ orphan.shortCode,
1037
+ stale,
1038
+ );
1039
+ segments.push(...inheritedResult.segments);
1040
+ matchedIds.push(...inheritedResult.matchedIds);
1041
+ }
1042
+
951
1043
  // Handler-first: resolve orphan layout handler before its parallels
952
1044
  // so ctx.set() values are visible to parallel children.
953
1045
  matchedIds.push(orphan.shortCode);
@@ -1229,6 +1321,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1229
1321
  }
1230
1322
 
1231
1323
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1324
+ if (entry.type === "cache") {
1325
+ const store = RSCRouterContext.getStore();
1326
+ if (store) store.insideCacheScope = true;
1327
+ }
1232
1328
  const doneEntry = track(`segment:${entry.id}`, 1);
1233
1329
  const resolved = await resolveWithErrorBoundary(
1234
1330
  nonParallelEntry,
@@ -96,6 +96,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
96
96
  findNearestNotFoundBoundary: (
97
97
  entry: EntryData | null,
98
98
  ) => ReactNode | NotFoundBoundaryHandler | null;
99
+ notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
99
100
  callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
100
101
  }
101
102
 
package/src/router.ts CHANGED
@@ -19,6 +19,8 @@ import {
19
19
  import MapRootLayout from "./server/root-layout.js";
20
20
  import type { AllUseItems } from "./route-types.js";
21
21
  import type { UrlPatterns } from "./urls.js";
22
+ import type { UrlBuilder } from "./urls/pattern-types.js";
23
+ import { urls } from "./urls.js";
22
24
  import {
23
25
  EntryData,
24
26
  InterceptSelectorContext,
@@ -133,6 +135,7 @@ export function createRouter<TEnv = any>(
133
135
  const {
134
136
  id: userProvidedId,
135
137
  $$id: injectedId,
138
+ basename: basenameOption,
136
139
  debugPerformance = false,
137
140
  document: documentOption,
138
141
  defaultErrorBoundary,
@@ -158,6 +161,13 @@ export function createRouter<TEnv = any>(
158
161
  originCheck: originCheckOption,
159
162
  } = options;
160
163
 
164
+ // Normalize basename: ensure leading slash, strip trailing slash.
165
+ // A bare "/" is equivalent to no basename.
166
+ const basename =
167
+ basenameOption && basenameOption.replace(/^\/+|\/+$/g, "")
168
+ ? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
169
+ : undefined;
170
+
161
171
  // Resolve telemetry sink (no-op when not configured)
162
172
  const telemetry = resolveSink(telemetrySink);
163
173
 
@@ -526,6 +536,7 @@ export function createRouter<TEnv = any>(
526
536
  trackHandler,
527
537
  findNearestErrorBoundary,
528
538
  findNearestNotFoundBoundary,
539
+ notFoundComponent: notFound,
529
540
  callOnError,
530
541
  };
531
542
 
@@ -614,6 +625,8 @@ export function createRouter<TEnv = any>(
614
625
  params: Record<string, string>,
615
626
  buildVars?: Record<string, any>,
616
627
  isPassthroughRoute?: boolean,
628
+ buildEnv?: TEnv,
629
+ devMode?: boolean,
617
630
  ) {
618
631
  return _matchForPrerender(
619
632
  pathname,
@@ -621,6 +634,8 @@ export function createRouter<TEnv = any>(
621
634
  prerenderDeps,
622
635
  buildVars,
623
636
  isPassthroughRoute,
637
+ buildEnv,
638
+ devMode,
624
639
  );
625
640
  }
626
641
 
@@ -628,12 +643,16 @@ export function createRouter<TEnv = any>(
628
643
  handler: Function,
629
644
  handlerId: string,
630
645
  routeName?: string,
646
+ buildEnv?: TEnv,
647
+ devMode?: boolean,
631
648
  ) {
632
649
  return _renderStaticSegment<TEnv>(
633
650
  handler,
634
651
  handlerId,
635
652
  mergedRouteMap,
636
653
  routeName,
654
+ buildEnv,
655
+ devMode,
637
656
  );
638
657
  }
639
658
 
@@ -658,8 +677,15 @@ export function createRouter<TEnv = any>(
658
677
  const router: RSCRouterInternal<TEnv, {}> = {
659
678
  __brand: RSC_ROUTER_BRAND,
660
679
  id: routerId,
680
+ basename,
681
+
682
+ routes(patternsOrBuilder: UrlPatterns<TEnv> | UrlBuilder<TEnv>): any {
683
+ // Wrap builder functions in urls() automatically
684
+ const urlPatterns: UrlPatterns<TEnv> =
685
+ typeof patternsOrBuilder === "function"
686
+ ? (urls(patternsOrBuilder) as UrlPatterns<TEnv>)
687
+ : patternsOrBuilder;
661
688
 
662
- routes(urlPatterns: UrlPatterns<TEnv>): any {
663
689
  // Store reference for runtime manifest generation
664
690
  storedUrlPatterns = urlPatterns;
665
691
  const currentMountIndex = mountIndex++;
@@ -707,6 +733,10 @@ export function createRouter<TEnv = any>(
707
733
  counters: {},
708
734
  mountIndex: currentMountIndex,
709
735
  cacheProfiles: resolvedCacheProfiles,
736
+ // basename sets the initial URL prefix so all path() patterns
737
+ // are registered with the prefix (e.g. "/admin" + "/users" = "/admin/users").
738
+ // No namePrefix — route names stay unprefixed.
739
+ ...(basename ? { urlPrefix: basename } : {}),
710
740
  },
711
741
  () => {
712
742
  handlerResult = urlPatterns.handler() as AllUseItems[];
@@ -726,7 +756,7 @@ export function createRouter<TEnv = any>(
726
756
  if (entry.type === "route" && entry.isPrerender) {
727
757
  if (!prerenderRouteKeys) prerenderRouteKeys = new Set();
728
758
  prerenderRouteKeys.add(name);
729
- if (entry.prerenderDef?.options?.passthrough === true) {
759
+ if (entry.isPassthrough === true) {
730
760
  if (!passthroughRouteKeys) passthroughRouteKeys = new Set();
731
761
  passthroughRouteKeys.add(name);
732
762
  }
@@ -855,8 +885,18 @@ export function createRouter<TEnv = any>(
855
885
  patternOrMiddleware: string | MiddlewareFn<TEnv>,
856
886
  middleware?: MiddlewareFn<TEnv>,
857
887
  ): any {
858
- // Global middleware - no mount prefix
859
- addMiddleware(patternOrMiddleware, middleware, null);
888
+ // Auto-prefix pattern with basename so router-level middleware
889
+ // patterns are router-relative (e.g. "/users/*" matches "/app/users/*").
890
+ if (basename && typeof patternOrMiddleware === "string") {
891
+ const pattern = patternOrMiddleware;
892
+ const prefixed =
893
+ pattern === "/*" || pattern === "*"
894
+ ? `${basename}/*`
895
+ : `${basename}${pattern}`;
896
+ addMiddleware(prefixed, middleware, null);
897
+ } else {
898
+ addMiddleware(patternOrMiddleware, middleware, null);
899
+ }
860
900
  return router;
861
901
  },
862
902
 
@@ -957,6 +997,9 @@ export function createRouter<TEnv = any>(
957
997
  // Expose source file for per-router type generation
958
998
  __sourceFile,
959
999
 
1000
+ // Expose basename for runtime manifest generation
1001
+ __basename: basename,
1002
+
960
1003
  // RSC request handler (lazily created on first call)
961
1004
  fetch: (() => {
962
1005
  // Handler is created on first call and reused
@@ -990,6 +1033,10 @@ export function createRouter<TEnv = any>(
990
1033
  };
991
1034
  })(),
992
1035
 
1036
+ // Low-level route matching for request classification
1037
+ findMatch: (pathname: string, metricsStore?: any) =>
1038
+ findMatch(pathname, metricsStore),
1039
+
993
1040
  // Debug utility for manifest inspection
994
1041
  debugManifest: () => buildDebugManifest<TEnv>(routesEntries),
995
1042
  };
@@ -998,7 +1045,9 @@ export function createRouter<TEnv = any>(
998
1045
  RouterRegistry.set(routerId, router);
999
1046
 
1000
1047
  // If urls option was provided, auto-register them
1001
- if (urlsOption) {
1048
+ if (typeof urlsOption === "function") {
1049
+ return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1050
+ } else if (urlsOption) {
1002
1051
  return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1003
1052
  }
1004
1053