@rangojs/router 0.1.0 → 0.1.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 (89) hide show
  1. package/dist/bin/rango.js +11 -1
  2. package/dist/types/browser/dev-discovery.d.ts +11 -0
  3. package/dist/types/browser/navigation-store.d.ts +3 -13
  4. package/dist/types/browser/prefetch/loader.d.ts +10 -0
  5. package/dist/types/browser/prefetch/runtime.d.ts +3 -0
  6. package/dist/types/browser/types.d.ts +4 -16
  7. package/dist/types/build/generate-manifest.d.ts +6 -0
  8. package/dist/types/cache/cache-scope.d.ts +7 -5
  9. package/dist/types/cache/cf/cf-cache-store.d.ts +10 -9
  10. package/dist/types/cache/memory-segment-store.d.ts +6 -6
  11. package/dist/types/cache/shell-snapshot.d.ts +21 -8
  12. package/dist/types/cache/types.d.ts +24 -7
  13. package/dist/types/cache/vercel/vercel-cache-store.d.ts +9 -8
  14. package/dist/types/dev-discovery-protocol.d.ts +5 -0
  15. package/dist/types/prerender/store.d.ts +1 -0
  16. package/dist/types/route-map-builder.d.ts +4 -29
  17. package/dist/types/router/prerender-match.d.ts +4 -1
  18. package/dist/types/router/router-interfaces.d.ts +3 -1
  19. package/dist/types/rsc/handler-context.d.ts +1 -0
  20. package/dist/types/rsc/render-pipeline.d.ts +3 -2
  21. package/dist/types/rsc/rsc-rendering.d.ts +5 -10
  22. package/dist/types/rsc/shell-capture.d.ts +4 -8
  23. package/dist/types/rsc/types.d.ts +2 -0
  24. package/dist/types/server/context.d.ts +16 -0
  25. package/dist/types/server/request-context.d.ts +18 -8
  26. package/dist/types/server.d.ts +1 -1
  27. package/dist/types/types/route-entry.d.ts +3 -0
  28. package/dist/types/vite/discovery/state.d.ts +3 -2
  29. package/dist/types/vite/utils/manifest-utils.d.ts +1 -1
  30. package/dist/vite/index.js +194 -107
  31. package/package.json +7 -2
  32. package/skills/caching/SKILL.md +1 -1
  33. package/skills/ppr/SKILL.md +48 -4
  34. package/src/browser/dev-discovery.ts +66 -0
  35. package/src/browser/navigation-client.ts +6 -0
  36. package/src/browser/navigation-store.ts +4 -271
  37. package/src/browser/partial-update.ts +7 -14
  38. package/src/browser/prefetch/cache.ts +1 -1
  39. package/src/browser/prefetch/loader.ts +110 -0
  40. package/src/browser/prefetch/runtime.ts +7 -0
  41. package/src/browser/react/Link.tsx +7 -10
  42. package/src/browser/react/NavigationProvider.tsx +1 -1
  43. package/src/browser/react/use-router.ts +1 -1
  44. package/src/browser/rsc-router.tsx +4 -2
  45. package/src/browser/types.ts +4 -27
  46. package/src/build/generate-manifest.ts +11 -0
  47. package/src/cache/cache-scope.ts +17 -6
  48. package/src/cache/cf/cf-cache-store.ts +63 -39
  49. package/src/cache/memory-segment-store.ts +17 -6
  50. package/src/cache/shell-snapshot.ts +45 -15
  51. package/src/cache/types.ts +23 -6
  52. package/src/cache/vercel/vercel-cache-store.ts +52 -22
  53. package/src/dev-discovery-protocol.ts +11 -0
  54. package/src/prerender/build-shell-capture.ts +7 -1
  55. package/src/prerender/store.ts +5 -0
  56. package/src/route-definition/dsl-helpers.ts +7 -2
  57. package/src/route-map-builder.ts +56 -49
  58. package/src/router/handler-context.ts +13 -5
  59. package/src/router/lazy-includes.ts +1 -0
  60. package/src/router/match-api.ts +8 -4
  61. package/src/router/prerender-match.ts +19 -2
  62. package/src/router/router-interfaces.ts +4 -0
  63. package/src/router/segment-resolution/static-store.ts +36 -10
  64. package/src/router.ts +34 -3
  65. package/src/rsc/handler-context.ts +1 -0
  66. package/src/rsc/handler.ts +3 -1
  67. package/src/rsc/loader-fetch.ts +2 -2
  68. package/src/rsc/manifest-init.ts +4 -11
  69. package/src/rsc/render-pipeline.ts +10 -2
  70. package/src/rsc/rsc-rendering.ts +207 -146
  71. package/src/rsc/shell-capture.ts +34 -11
  72. package/src/rsc/types.ts +2 -0
  73. package/src/server/context.ts +28 -0
  74. package/src/server/request-context.ts +32 -10
  75. package/src/server.ts +0 -2
  76. package/src/testing/dispatch.ts +1 -0
  77. package/src/types/route-entry.ts +3 -0
  78. package/src/urls/include-helper.ts +1 -0
  79. package/src/urls/path-helper.ts +8 -4
  80. package/src/vite/discovery/bundle-postprocess.ts +10 -7
  81. package/src/vite/discovery/discover-routers.ts +7 -66
  82. package/src/vite/discovery/prerender-collection.ts +13 -2
  83. package/src/vite/discovery/state.ts +4 -4
  84. package/src/vite/discovery/virtual-module-codegen.ts +75 -42
  85. package/src/vite/plugins/version-injector.ts +0 -1
  86. package/src/vite/plugins/virtual-entries.ts +11 -1
  87. package/src/vite/router-discovery.ts +132 -22
  88. package/src/vite/utils/manifest-utils.ts +1 -4
  89. package/src/vite/utils/shared-utils.ts +7 -0
@@ -298,6 +298,10 @@ interface HelperContext {
298
298
  counters: Record<string, number>;
299
299
  forRoute?: string;
300
300
  mountIndex?: number;
301
+ /** Owning router id, when known (threaded by generateManifestFull). Scopes
302
+ * the search-schema/root-scope registries so same-named routes in
303
+ * different routers don't clobber each other (route-map-builder.ts). */
304
+ routerId?: string;
301
305
  metrics?: MetricsStore;
302
306
  /** True when rendering for SSR (document requests) */
303
307
  isSSR?: boolean;
@@ -491,6 +495,7 @@ export const getContext = (): {
491
495
  counters: store.counters,
492
496
  forRoute: store.forRoute,
493
497
  mountIndex: store.mountIndex,
498
+ routerId: store.routerId,
494
499
  metrics: store.metrics,
495
500
  isSSR: store.isSSR,
496
501
  patterns: store.patterns,
@@ -533,6 +538,7 @@ export const getContext = (): {
533
538
  counters,
534
539
  forRoute: store?.forRoute,
535
540
  mountIndex: store?.mountIndex,
541
+ routerId: store?.routerId,
536
542
  metrics: store?.metrics,
537
543
  isSSR: store?.isSSR,
538
544
  patterns,
@@ -666,6 +672,28 @@ export function getRootScoped(): boolean {
666
672
  return store?.rootScoped ?? true;
667
673
  }
668
674
 
675
+ /**
676
+ * Stamp build-time scope identity onto a Static() definition at mount time.
677
+ * The bake collector (prerender-collection.ts) renders defs OUTSIDE any
678
+ * evaluation scope and used to iterate the registry first-non-null, so these
679
+ * are the ONLY reliable carriers of the def's owning router, root-scope, and
680
+ * full route name — a name-keyed registry lookup at bake time reproduces the
681
+ * cross-router collision #757/#762 fixed (and the collector's historical
682
+ * `$$routePrefix` argument is a name PREFIX, which the root-scope registry
683
+ * never contains, silently degrading to the dot-heuristic).
684
+ * A definition mounted more than once is stamped each time; the last mount wins.
685
+ */
686
+ export function stampStaticDefScope(
687
+ handler: unknown,
688
+ routeName?: string,
689
+ ): void {
690
+ const store = RangoContext.getStore();
691
+ const def = handler as Record<string, unknown>;
692
+ if (routeName !== undefined) def.$$routeName = routeName;
693
+ def.$$rootScoped = getRootScoped();
694
+ if (store?.routerId !== undefined) def.$$routerId = store.routerId;
695
+ }
696
+
669
697
  // Export HelperContext type for use in other modules
670
698
  export type { HelperContext };
671
699
 
@@ -254,21 +254,28 @@ export interface RequestContext<
254
254
  >;
255
255
 
256
256
  /**
257
- * @internal Shell fast-path marker: makes the NEXT full match treat the whole
258
- * matched route as an implicit doc-level cache() boundary (see
257
+ * @internal Shell fast-path marker: makes the next eligible match treat the
258
+ * whole matched route as an implicit doc-level cache() boundary (see
259
259
  * resolveShellImplicitCacheScope in cache/cache-scope.ts). Set ONLY on
260
260
  * (a) the capture's derived context — with a record-only store so the
261
261
  * capture's cacheRoute write lands in the snapshot, never the real store —
262
- * and (b) a HIT tail's seeded context when the entry is eligible
263
- * (!handlerLiveHoles), where the SeededShellStore serves the recorded doc
264
- * entry and the match skips handler execution entirely. Routes with their
265
- * own cache() config (including cache(false)) are never overridden: the
266
- * marker only applies when the route tree derived NO cache scope.
262
+ * (b) a HIT tail's seeded context, and (c) an eligible normal-route partial
263
+ * replay, where `store` is a request-local segment overlay. Handler-live holes
264
+ * and conditional transitions decline replay.
265
+ * Routes with their own cache() config (including cache(false)) are never
266
+ * overridden: the marker only applies when the route tree derived NO cache
267
+ * scope.
267
268
  */
268
269
  _shellImplicitCache?: {
269
270
  ttl?: number;
270
271
  swr?: number;
271
272
  store?: SegmentCacheStore;
273
+ /**
274
+ * @internal Override the implicit scope's default cache namespace. Shell
275
+ * captures and navigation replay both consume the canonical document
276
+ * segment record even when the triggering request is partial.
277
+ */
278
+ keyPrefix?: "doc";
272
279
  };
273
280
 
274
281
  /**
@@ -606,6 +613,10 @@ export interface RequestContext<
606
613
  /** @internal Router basename for this request (used by redirect()) */
607
614
  _basename?: string;
608
615
 
616
+ /** @internal Owning router id; scopes the search-schema/root-scope registry
617
+ * lookups per router (route-map-builder.ts) */
618
+ _routerId?: string;
619
+
609
620
  /**
610
621
  * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
611
622
  * to avoid a second resolveRoute call. Cleared on HMR invalidation.
@@ -641,12 +652,19 @@ export type PublicRequestContext<
641
652
  | "_transitionWhen"
642
653
  | "_cacheStore"
643
654
  | "_shellCaptureRun"
655
+ | "_shellImplicitCache"
656
+ | "_shellLoaderSeed"
657
+ | "_shellCaptureLoaderRecords"
658
+ | "_shellCaptureHandleLiveness"
659
+ | "_shellCaptureLoaderHandleValues"
644
660
  | "_shellFragmentPayload"
661
+ | "_shellCaptureGuardTripped"
645
662
  | "_shellCaptureGuardTrippedLoaderId"
646
663
  | "_explicitTaggedStores"
647
664
  | "_requestTags"
648
665
  | "_cacheProfiles"
649
666
  | "_onResponseCallbacks"
667
+ | "_pendingBackgroundTasks"
650
668
  | "_themeConfig"
651
669
  | "_locationState"
652
670
  | "_routeName"
@@ -673,7 +691,9 @@ export type PublicRequestContext<
673
691
  | "_renderForeground"
674
692
  | "_renderDiagnosticsEnabled"
675
693
  | "_handlerStart"
694
+ | "_tracing"
676
695
  | "_basename"
696
+ | "_routerId"
677
697
  | "_setStatus"
678
698
  | "_rotateStateCookie"
679
699
  | "_setKeepCacheDirective"
@@ -765,7 +785,7 @@ export function setRequestContextParams(
765
785
  routeMap ?? getGlobalRouteMap(),
766
786
  routeName,
767
787
  params,
768
- routeName ? isRouteRootScoped(routeName) : undefined,
788
+ routeName ? isRouteRootScoped(routeName, ctx._routerId) : undefined,
769
789
  );
770
790
  }
771
791
  }
@@ -1496,7 +1516,7 @@ export function createUseFunction<TEnv>(
1496
1516
  // RequestContext carries no `search` field, so reading `(ctx as any).search`
1497
1517
  // here always yielded {} — dropping typed search for action/dispatch loaders.
1498
1518
  const searchSchema = ctx._routeName
1499
- ? getSearchSchema(ctx._routeName)
1519
+ ? getSearchSchema(ctx._routeName, ctx._routerId)
1500
1520
  : undefined;
1501
1521
  const loaderSearch = searchSchema
1502
1522
  ? parseSearchParams(ctx.searchParams, searchSchema)
@@ -1526,7 +1546,9 @@ export function createUseFunction<TEnv>(
1526
1546
  getGlobalRouteMap(),
1527
1547
  ctx._routeName,
1528
1548
  ctx.params as Record<string, string>,
1529
- ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
1549
+ ctx._routeName
1550
+ ? isRouteRootScoped(ctx._routeName, ctx._routerId)
1551
+ : undefined,
1530
1552
  ),
1531
1553
  rendered: () => {
1532
1554
  throw new Error(
package/src/server.ts CHANGED
@@ -25,8 +25,6 @@ export {
25
25
  clearAllRouterData,
26
26
  getGlobalRouteMap,
27
27
  getRouterManifest,
28
- setPrecomputedEntries,
29
- setRouteTrie,
30
28
  setManifestReadyPromise,
31
29
  setRouterManifest,
32
30
  setRouterTrie,
@@ -413,6 +413,7 @@ export async function dispatch<TEnv = any>(
413
413
  // with it. Mirror it so basename-redirect tests behave as they do in a real
414
414
  // mounted app instead of always seeing no prefix.
415
415
  requestContext._basename = router.basename;
416
+ requestContext._routerId = routerId;
416
417
 
417
418
  // Match production's response-route reverse EXACTLY: the real handler builds
418
419
  // it from the route map alone (response-route-handler.ts), with NO matched
@@ -13,6 +13,9 @@ export interface LazyIncludeContext {
13
13
  >;
14
14
  /** Root scope flag for dot-local reverse resolution */
15
15
  rootScoped?: boolean;
16
+ /** Owning router id; scopes search-schema/root-scope registration during
17
+ * lazy include evaluation (route-map-builder.ts registries) */
18
+ routerId?: string;
16
19
  /**
17
20
  * Positional include scope token composed from the parent scope plus this
18
21
  * include's sibling index (`${parentScope}I${idx}`). Applied to direct-
@@ -155,6 +155,7 @@ export function createIncludeHelper<TEnv>(): IncludeFn<TEnv> {
155
155
  counters: capturedCounters,
156
156
  cacheProfiles: ctx.cacheProfiles,
157
157
  rootScoped: capturedRootScoped,
158
+ routerId: ctx.routerId,
158
159
  includeScope,
159
160
  },
160
161
  } as IncludeItem;
@@ -6,6 +6,7 @@ import {
6
6
  getNamePrefix,
7
7
  getRootScoped,
8
8
  requireDslContext,
9
+ stampStaticDefScope,
9
10
  } from "../server/context";
10
11
  import { invariant, DataNotFoundError } from "../errors";
11
12
  import { validateUserRouteName } from "../route-name.js";
@@ -182,8 +183,11 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
182
183
  : {}),
183
184
  };
184
185
 
185
- if (isStaticHandler(handler) && handler.$$id && ctx.namePrefix) {
186
- (handler as any).$$routePrefix = ctx.namePrefix;
186
+ if (isStaticHandler(handler) && handler.$$id) {
187
+ if (ctx.namePrefix) {
188
+ (handler as any).$$routePrefix = ctx.namePrefix;
189
+ }
190
+ stampStaticDefScope(handler, routeName);
187
191
  }
188
192
 
189
193
  invariant(
@@ -193,7 +197,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
193
197
 
194
198
  ctx.manifest.set(routeName, entry);
195
199
 
196
- registerRouteRootScope(routeName, getRootScoped());
200
+ registerRouteRootScope(routeName, getRootScoped(), ctx.routerId);
197
201
 
198
202
  if (ctx.patterns) {
199
203
  ctx.patterns.set(routeName, prefixedPattern);
@@ -215,7 +219,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
215
219
  if (ctx.searchSchemas) {
216
220
  ctx.searchSchemas.set(routeName, options.search);
217
221
  }
218
- registerSearchSchema(routeName, options.search);
222
+ registerSearchSchema(routeName, options.search, ctx.routerId);
219
223
  }
220
224
 
221
225
  if (mergedUse) {
@@ -133,7 +133,7 @@ export function postprocessBundle(state: DiscoveryState): void {
133
133
  }
134
134
 
135
135
  // 3. Write static handler data as separate importable asset modules
136
- // and inject a __STATIC_MANIFEST import into the RSC entry.
136
+ // and inject a lazy loader thunk into the RSC entry.
137
137
  if (hasStaticData && existsSync(rscEntryPath)) {
138
138
  const rscCode = readFileSync(rscEntryPath, "utf-8");
139
139
  if (!rscCode.includes("__static-manifest.js")) {
@@ -152,9 +152,10 @@ export function postprocessBundle(state: DiscoveryState): void {
152
152
  );
153
153
  }
154
154
 
155
- // Set the global inside the manifest module so it is assigned
156
- // during module evaluation (before dependent modules like
157
- // segment-resolution.ts run their top-level initializers).
155
+ // The module assigns the global on evaluation; evaluation itself is
156
+ // DEFERRED behind the loader thunk below (mirroring the prerender and
157
+ // shell manifests), so the thunk table stays off the worker's eager
158
+ // cold-start path (issue #760) — it grows O(#Static handlers).
158
159
  const manifestCode = `const m={${manifestEntries.join(",")}};globalThis.__STATIC_MANIFEST=m;export default m;\n`;
159
160
  const manifestPath = resolve(
160
161
  state.projectRoot,
@@ -163,9 +164,11 @@ export function postprocessBundle(state: DiscoveryState): void {
163
164
  writeFileSync(manifestPath, manifestCode);
164
165
  totalBytes += Buffer.byteLength(manifestCode);
165
166
 
166
- // The import ensures the manifest module is evaluated early.
167
- // The global is already set inside the module itself.
168
- const injection = `import "./__static-manifest.js";\n`;
167
+ // Lazy thunk, NOT an eager import: static-store.ts resolves it on the
168
+ // first Static() lookup (a first-call latch the entry body assigning
169
+ // this global runs after hoisted imports, so an eval-time read in
170
+ // static-store.ts could never see it).
171
+ const injection = `globalThis.__loadStaticManifestModule = () => import("./__static-manifest.js");\n`;
169
172
  writeFileSync(rscEntryPath, injection + rscCode);
170
173
 
171
174
  const totalKB = (totalBytes / 1024).toFixed(1);
@@ -19,12 +19,9 @@ import {
19
19
  // boundary tree regardless of which realm imported the walker. Only
20
20
  // generateManifestFull must stay on the runner (it invokes user handlers via
21
21
  // RangoContext from the runner realm) — see the runner.import below.
22
- import { buildRouteTrie, buildPerRouterTrie } from "../../build/route-trie.js";
22
+ import { buildPerRouterTrie } from "../../build/route-trie.js";
23
23
  import { collectFallbackClientRefs } from "../../build/collect-fallback-refs.js";
24
- import {
25
- flattenLeafEntries,
26
- buildRouteToStaticPrefix,
27
- } from "../utils/manifest-utils.js";
24
+ import { flattenLeafEntries } from "../utils/manifest-utils.js";
28
25
  import type { DiscoveryState, PrecomputedEntry } from "./state.js";
29
26
  import {
30
27
  expandPrerenderRoutes,
@@ -145,12 +142,10 @@ export async function discoverRouters(
145
142
  // full pass succeeds, so a failed re-discovery preserves the last
146
143
  // known-good state instead of leaving it partially wiped.
147
144
  const newMergedRouteManifest: Record<string, string> = {};
148
- const newMergedPrecomputedEntries: PrecomputedEntry[] = [];
149
145
  const newPerRouterManifests: typeof state.perRouterManifests = [];
150
146
  const newPerRouterManifestDataMap = new Map<string, any>();
151
147
  const newPerRouterPrecomputedMap = new Map<string, PrecomputedEntry[]>();
152
148
  const newPerRouterTrieMap = new Map<string, any>();
153
- let mergedRouteTrailingSlash: Record<string, string> = {};
154
149
 
155
150
  let routerMountIndex = 0;
156
151
  // Collect all manifests for trie building (avoid re-running generateManifest)
@@ -189,6 +184,7 @@ export async function discoverRouters(
189
184
  router.urlpatterns,
190
185
  routerMountIndex,
191
186
  {
187
+ routerId: id,
192
188
  ...(router.__basename ? { urlPrefix: router.__basename } : {}),
193
189
  ...(collectClientFallbackRef ? { collectClientFallbackRef } : {}),
194
190
  },
@@ -239,29 +235,21 @@ export async function discoverRouters(
239
235
  newPerRouterManifests.push({
240
236
  id,
241
237
  routeManifest: manifest.routeManifest,
238
+ routeTrailingSlash: manifest.routeTrailingSlash,
242
239
  routeSearchSchemas: manifest.routeSearchSchemas,
243
240
  sourceFile: router.__sourceFile,
244
241
  factoryOnlyPrefixes,
245
242
  });
246
243
 
247
- // Merge trailing slash config
248
- if (manifest.routeTrailingSlash) {
249
- Object.assign(mergedRouteTrailingSlash, manifest.routeTrailingSlash);
250
- }
251
-
252
244
  // Flatten prefix tree leaf nodes into precomputed entries.
253
245
  // Leaf nodes (no children) can have their routes used directly by
254
246
  // evaluateLazyEntry() without running the handler at runtime.
255
- // Walk once into a per-router array, then fold it into the merged array;
256
- // the merged and per-router entries are identical, so a second walk is
257
- // redundant. Append order is preserved within and across routers.
258
247
  const routerPrecomputed: PrecomputedEntry[] = [];
259
248
  flattenLeafEntries(
260
249
  manifest.prefixTree,
261
250
  manifest.routeManifest,
262
251
  routerPrecomputed,
263
252
  );
264
- newMergedPrecomputedEntries.push(...routerPrecomputed);
265
253
 
266
254
  // Store per-router manifest and precomputed entries for isolated virtual modules.
267
255
  newPerRouterManifestDataMap.set(id, manifest.routeManifest);
@@ -298,53 +286,11 @@ export async function discoverRouters(
298
286
  (performance.now() - manifestGenStart).toFixed(1),
299
287
  );
300
288
 
301
- // Build route trie from merged manifest
302
- let newMergedRouteTrie: any = null;
289
+ // No merged trie is built: find-match.ts consumes per-router tries only
290
+ // (getRouterTrie(routerId)), falling back to regex over live routes on a
291
+ // gap — the global merged trie was never read by any matcher.
303
292
  const trieStart = debug ? performance.now() : 0;
304
293
  if (Object.keys(newMergedRouteManifest).length > 0) {
305
- // Build routeToStaticPrefix from saved manifests
306
- const routeToStaticPrefix: Record<string, string> = {};
307
- for (const { manifest } of allManifests) {
308
- // Root-level routes have empty static prefix
309
- for (const name of Object.keys(manifest.routeManifest)) {
310
- if (!(name in routeToStaticPrefix)) {
311
- routeToStaticPrefix[name] = "";
312
- }
313
- }
314
- buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
315
- }
316
-
317
- // Collect prerender route names and response type routes from all manifests
318
- const prerenderRouteNames = new Set<string>();
319
- const passthroughRouteNames = new Set<string>();
320
- const mergedResponseTypeRoutes: Record<string, string> = {};
321
- for (const { manifest } of allManifests) {
322
- if (manifest.prerenderRoutes) {
323
- for (const name of manifest.prerenderRoutes) {
324
- prerenderRouteNames.add(name);
325
- }
326
- }
327
- if (manifest.passthroughRoutes) {
328
- for (const name of manifest.passthroughRoutes) {
329
- passthroughRouteNames.add(name);
330
- }
331
- }
332
- if (manifest.responseTypeRoutes) {
333
- Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
334
- }
335
- }
336
-
337
- // buildRouteTrie reads these via ?.has / ?.[] — empty is observationally
338
- // identical to undefined, so no empty->undefined coercion is needed.
339
- newMergedRouteTrie = buildRouteTrie(
340
- newMergedRouteManifest,
341
- routeToStaticPrefix,
342
- mergedRouteTrailingSlash,
343
- prerenderRouteNames,
344
- passthroughRouteNames,
345
- mergedResponseTypeRoutes,
346
- );
347
-
348
294
  // Build per-router tries for multi-router isolation. Uses the single
349
295
  // shared buildPerRouterTrie so the production serialized trie is built by
350
296
  // exactly the same code as the dev/HMR runtime rebuild (manifest-init.ts).
@@ -366,12 +312,10 @@ export async function discoverRouters(
366
312
  // This ensures a failed re-discovery (e.g. from a transient module
367
313
  // evaluation error) preserves the last known-good state.
368
314
  state.mergedRouteManifest = newMergedRouteManifest;
369
- state.mergedPrecomputedEntries = newMergedPrecomputedEntries;
370
315
  state.perRouterManifests = newPerRouterManifests;
371
316
  state.perRouterManifestDataMap = newPerRouterManifestDataMap;
372
317
  state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
373
318
  state.perRouterTrieMap = newPerRouterTrieMap;
374
- state.mergedRouteTrie = newMergedRouteTrie;
375
319
 
376
320
  // Install the route tries into the RSC realm BEFORE prerender collection.
377
321
  // matchForPrerender resolves each enumerated URL via findMatch, and without
@@ -386,9 +330,6 @@ export async function discoverRouters(
386
330
  // collection was the one findMatch consumer running trieless. Mirrors the
387
331
  // dev perRouterSetters loop; deliberately does NOT markRouterTrieAuthoritative
388
332
  // so a genuine trie gap keeps the regex fallback, exactly as in dev.
389
- if (serverMod.setRouteTrie && newMergedRouteTrie) {
390
- serverMod.setRouteTrie(newMergedRouteTrie);
391
- }
392
333
  const perRouterSetters: Array<[Map<string, unknown>, string]> = [
393
334
  [newPerRouterManifestDataMap, "setRouterManifest"],
394
335
  [newPerRouterTrieMap, "setRouterTrie"],
@@ -415,15 +415,26 @@ export async function renderStaticHandlers(
415
415
 
416
416
  const startHandler = performance.now();
417
417
  let handled = false;
418
- for (const [, routerInstance] of registry) {
418
+ // Owner-first: path() stamped the mounting router's id on the def
419
+ // ($$routerId). Sweeping the whole registry hands the FIRST router's
420
+ // id and route map to every static handler — wrong scoping in
421
+ // multi-router apps. The full sweep remains only as the fallback for
422
+ // an unstamped def (evaluated outside a router scope).
423
+ const owner = (def as any).$$routerId as string | undefined;
424
+ const owned = owner !== undefined ? registry.get(owner) : undefined;
425
+ const candidates = owned ? [owned] : [...registry.values()];
426
+ for (const routerInstance of candidates) {
419
427
  if (!routerInstance.renderStaticSegment) continue;
420
428
  try {
421
429
  const result = await routerInstance.renderStaticSegment(
422
430
  def.handler,
423
431
  def.$$id,
424
- (def as any).$$routePrefix,
432
+ // Full route name when stamped; the historical $$routePrefix is a
433
+ // name PREFIX (never in the root-scope registry) kept as fallback.
434
+ (def as any).$$routeName ?? (def as any).$$routePrefix,
425
435
  state.resolvedBuildEnv,
426
436
  !state.isBuildMode,
437
+ (def as any).$$rootScoped,
427
438
  );
428
439
  if (result) {
429
440
  // result.handles is the pre-encoded handle string ("" when none).
@@ -59,6 +59,7 @@ export interface ChunkInfo {
59
59
  export interface PerRouterManifestEntry {
60
60
  id: string;
61
61
  routeManifest: Record<string, string>;
62
+ routeTrailingSlash?: Record<string, string>;
62
63
  routeSearchSchemas?: Record<string, Record<string, string>>;
63
64
  sourceFile?: string;
64
65
  factoryOnlyPrefixes?: Set<string>;
@@ -104,8 +105,6 @@ export interface DiscoveryState {
104
105
 
105
106
  mergedRouteManifest: Record<string, string> | null;
106
107
  perRouterManifests: PerRouterManifestEntry[];
107
- mergedPrecomputedEntries: PrecomputedEntry[] | null;
108
- mergedRouteTrie: any;
109
108
 
110
109
  perRouterTrieMap: Map<string, any>;
111
110
  perRouterPrecomputedMap: Map<string, PrecomputedEntry[]>;
@@ -141,6 +140,8 @@ export interface DiscoveryState {
141
140
  resolvedStaticModules: Map<string, string[]> | undefined;
142
141
 
143
142
  discoveryDone: Promise<void> | null;
143
+ /** Monotonic Cloudflare-dev generation installed by the routes manifest. */
144
+ devDiscoveryEpoch?: number;
144
145
  devServerOrigin: string | null;
145
146
  devServer: any;
146
147
  selfWrittenGenFiles: Map<string, { at: number; hash: string }>;
@@ -177,8 +178,6 @@ export function createDiscoveryState(
177
178
 
178
179
  mergedRouteManifest: null,
179
180
  perRouterManifests: [],
180
- mergedPrecomputedEntries: null,
181
- mergedRouteTrie: null,
182
181
 
183
182
  perRouterTrieMap: new Map(),
184
183
  perRouterPrecomputedMap: new Map(),
@@ -196,6 +195,7 @@ export function createDiscoveryState(
196
195
  resolvedStaticModules: undefined,
197
196
 
198
197
  discoveryDone: null,
198
+ devDiscoveryEpoch: opts?.preset === "cloudflare" ? Date.now() : undefined,
199
199
  devServerOrigin: null,
200
200
  devServer: null,
201
201
  selfWrittenGenFiles: new Map(),