@rangojs/router 0.0.0-experimental.69 → 0.0.0-experimental.6c70a2ab

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 (123) hide show
  1. package/README.md +112 -17
  2. package/dist/vite/index.js +1456 -467
  3. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  4. package/package.json +7 -5
  5. package/skills/breadcrumbs/SKILL.md +3 -1
  6. package/skills/handler-use/SKILL.md +364 -0
  7. package/skills/hooks/SKILL.md +54 -20
  8. package/skills/i18n/SKILL.md +276 -0
  9. package/skills/intercept/SKILL.md +45 -0
  10. package/skills/layout/SKILL.md +24 -0
  11. package/skills/links/SKILL.md +234 -16
  12. package/skills/loader/SKILL.md +70 -3
  13. package/skills/middleware/SKILL.md +34 -3
  14. package/skills/migrate-nextjs/SKILL.md +562 -0
  15. package/skills/migrate-react-router/SKILL.md +769 -0
  16. package/skills/parallel/SKILL.md +68 -0
  17. package/skills/rango/SKILL.md +26 -22
  18. package/skills/response-routes/SKILL.md +8 -0
  19. package/skills/route/SKILL.md +48 -0
  20. package/skills/server-actions/SKILL.md +739 -0
  21. package/skills/streams-and-websockets/SKILL.md +283 -0
  22. package/skills/typesafety/SKILL.md +9 -1
  23. package/skills/view-transitions/SKILL.md +212 -0
  24. package/src/browser/app-shell.ts +52 -0
  25. package/src/browser/event-controller.ts +44 -4
  26. package/src/browser/navigation-bridge.ts +80 -5
  27. package/src/browser/navigation-client.ts +64 -13
  28. package/src/browser/navigation-store.ts +25 -1
  29. package/src/browser/partial-update.ts +58 -12
  30. package/src/browser/prefetch/cache.ts +129 -21
  31. package/src/browser/prefetch/fetch.ts +148 -16
  32. package/src/browser/prefetch/queue.ts +36 -5
  33. package/src/browser/rango-state.ts +53 -13
  34. package/src/browser/react/Link.tsx +30 -2
  35. package/src/browser/react/NavigationProvider.tsx +70 -18
  36. package/src/browser/react/filter-segment-order.ts +51 -7
  37. package/src/browser/react/index.ts +3 -0
  38. package/src/browser/react/use-navigation.ts +22 -2
  39. package/src/browser/react/use-params.ts +17 -4
  40. package/src/browser/react/use-reverse.ts +99 -0
  41. package/src/browser/react/use-router.ts +8 -1
  42. package/src/browser/react/use-segments.ts +11 -8
  43. package/src/browser/rsc-router.tsx +34 -6
  44. package/src/browser/scroll-restoration.ts +22 -14
  45. package/src/browser/segment-reconciler.ts +36 -14
  46. package/src/browser/types.ts +19 -0
  47. package/src/build/route-trie.ts +52 -25
  48. package/src/cache/cf/cf-cache-store.ts +5 -7
  49. package/src/client.rsc.tsx +3 -0
  50. package/src/client.tsx +87 -175
  51. package/src/href-client.ts +4 -1
  52. package/src/index.rsc.ts +3 -0
  53. package/src/index.ts +40 -9
  54. package/src/outlet-context.ts +1 -1
  55. package/src/response-utils.ts +28 -0
  56. package/src/reverse.ts +62 -36
  57. package/src/route-definition/dsl-helpers.ts +175 -23
  58. package/src/route-definition/helpers-types.ts +63 -14
  59. package/src/route-definition/resolve-handler-use.ts +6 -0
  60. package/src/route-types.ts +7 -0
  61. package/src/router/handler-context.ts +21 -38
  62. package/src/router/lazy-includes.ts +6 -6
  63. package/src/router/loader-resolution.ts +3 -0
  64. package/src/router/manifest.ts +22 -13
  65. package/src/router/match-api.ts +4 -3
  66. package/src/router/match-handlers.ts +1 -0
  67. package/src/router/match-middleware/cache-lookup.ts +2 -1
  68. package/src/router/match-result.ts +101 -4
  69. package/src/router/middleware-types.ts +14 -25
  70. package/src/router/middleware.ts +54 -7
  71. package/src/router/pattern-matching.ts +101 -17
  72. package/src/router/revalidation.ts +15 -1
  73. package/src/router/segment-resolution/fresh.ts +13 -0
  74. package/src/router/segment-resolution/revalidation.ts +135 -101
  75. package/src/router/substitute-pattern-params.ts +56 -0
  76. package/src/router/trie-matching.ts +18 -13
  77. package/src/router/url-params.ts +49 -0
  78. package/src/router.ts +1 -2
  79. package/src/rsc/handler.ts +16 -8
  80. package/src/rsc/helpers.ts +69 -41
  81. package/src/rsc/progressive-enhancement.ts +4 -0
  82. package/src/rsc/response-route-handler.ts +14 -1
  83. package/src/rsc/rsc-rendering.ts +10 -0
  84. package/src/rsc/server-action.ts +4 -0
  85. package/src/rsc/types.ts +6 -0
  86. package/src/segment-content-promise.ts +67 -0
  87. package/src/segment-loader-promise.ts +122 -0
  88. package/src/segment-system.tsx +71 -70
  89. package/src/server/context.ts +26 -3
  90. package/src/server/request-context.ts +10 -42
  91. package/src/ssr/index.tsx +5 -1
  92. package/src/types/handler-context.ts +12 -39
  93. package/src/types/loader-types.ts +5 -6
  94. package/src/types/request-scope.ts +126 -0
  95. package/src/types/route-entry.ts +11 -0
  96. package/src/types/segments.ts +18 -1
  97. package/src/urls/include-helper.ts +24 -14
  98. package/src/urls/path-helper-types.ts +30 -4
  99. package/src/urls/response-types.ts +2 -10
  100. package/src/use-loader.tsx +4 -1
  101. package/src/vite/debug.ts +184 -0
  102. package/src/vite/discovery/discover-routers.ts +31 -3
  103. package/src/vite/discovery/gate-state.ts +171 -0
  104. package/src/vite/discovery/prerender-collection.ts +172 -84
  105. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  106. package/src/vite/plugins/cjs-to-esm.ts +5 -0
  107. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  108. package/src/vite/plugins/client-ref-hashing.ts +16 -4
  109. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  110. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  111. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  112. package/src/vite/plugins/expose-action-id.ts +52 -28
  113. package/src/vite/plugins/expose-id-utils.ts +12 -0
  114. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  115. package/src/vite/plugins/expose-internal-ids.ts +540 -376
  116. package/src/vite/plugins/performance-tracks.ts +17 -9
  117. package/src/vite/plugins/use-cache-transform.ts +56 -43
  118. package/src/vite/plugins/version-injector.ts +37 -11
  119. package/src/vite/rango.ts +49 -14
  120. package/src/vite/router-discovery.ts +558 -53
  121. package/src/vite/utils/banner.ts +1 -1
  122. package/src/vite/utils/package-resolution.ts +41 -1
  123. package/src/vite/utils/prerender-utils.ts +21 -6
@@ -20,7 +20,8 @@ export interface TrieLeaf {
20
20
  sp: string;
21
21
  /** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
22
22
  a: string[];
23
- /** Optional param names (absent params get empty string value) */
23
+ /** Optional param names declared on the route. Absent params are
24
+ * omitted from the matched params record (read as `undefined`). */
24
25
  op?: string[];
25
26
  /** Constraint validation: paramName -> allowed values */
26
27
  cv?: Record<string, string[]>;
@@ -98,8 +99,14 @@ export function buildRouteTrie(
98
99
  }
99
100
 
100
101
  /**
101
- * Insert a route into the trie, handling optional params by forking
102
- * the insertion path (one terminal without the param, one with).
102
+ * Insert a route into the trie. Optional params expand into two branches at
103
+ * registration time (skip-first, then present), so each terminal lives at the
104
+ * correct depth for its number of bound params and carries a branch-local
105
+ * `pa` listing only those names. The trie's single-slot `node.p` is reused
106
+ * across branches because matching ignores `node.p.n` — the leaf's `pa` is
107
+ * the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
108
+ * last-wins rule produce greedy-leftmost semantics for free at any shared
109
+ * terminal depth.
103
110
  */
104
111
  function insertRoute(
105
112
  node: TrieNode,
@@ -107,14 +114,13 @@ function insertRoute(
107
114
  index: number,
108
115
  leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
109
116
  ): void {
110
- // Collect param names, optional param names, and constraints across all segments
111
- const paramNames: string[] = [];
117
+ // op (full optional list) and cv (full constraint map) are route-level and
118
+ // identical on every terminal, so compute them once on the shared base.
112
119
  const optionalParams: string[] = [];
113
120
  const constraints: Record<string, string[]> = {};
114
121
 
115
122
  for (const seg of segments) {
116
123
  if (seg.type === "param") {
117
- paramNames.push(seg.value);
118
124
  if (seg.optional) {
119
125
  optionalParams.push(seg.value);
120
126
  }
@@ -124,21 +130,15 @@ function insertRoute(
124
130
  }
125
131
  }
126
132
 
127
- const fullLeaf: TrieLeaf = {
133
+ const leafBase: Omit<TrieLeaf, "pa"> = {
128
134
  ...leaf,
129
- ...(paramNames.length > 0 ? { pa: paramNames } : {}),
130
135
  ...(optionalParams.length > 0 ? { op: optionalParams } : {}),
131
136
  ...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
132
137
  };
133
138
 
134
- insertSegments(node, segments, index, fullLeaf);
139
+ insertSegments(node, segments, index, leafBase, []);
135
140
  }
136
141
 
137
- /**
138
- * Recursively insert segments into the trie.
139
- * For optional params, we add a terminal at the current node (param absent)
140
- * AND continue inserting into the param child (param present).
141
- */
142
142
  /**
143
143
  * Extract ancestry map from a built trie by visiting all leaf nodes.
144
144
  * Returns { routeName: ancestryShortCodes[] } for every route in the trie.
@@ -218,15 +218,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
218
218
  node.r = mergeLeaves(node.r, leaf);
219
219
  }
220
220
 
221
+ function buildLeaf(
222
+ leafBase: Omit<TrieLeaf, "pa">,
223
+ paramNames: string[],
224
+ ): TrieLeaf {
225
+ return paramNames.length > 0
226
+ ? { ...leafBase, pa: [...paramNames] }
227
+ : { ...leafBase };
228
+ }
229
+
221
230
  function insertSegments(
222
231
  node: TrieNode,
223
232
  segments: ParsedSegment[],
224
233
  index: number,
225
- leaf: TrieLeaf,
234
+ leafBase: Omit<TrieLeaf, "pa">,
235
+ paramNames: string[],
226
236
  ): void {
227
- // Base case: all segments consumed, add terminal
237
+ // Base case: all segments consumed, add terminal with branch-local pa
228
238
  if (index >= segments.length) {
229
- mergeLeaf(node, leaf);
239
+ mergeLeaf(node, buildLeaf(leafBase, paramNames));
230
240
  return;
231
241
  }
232
242
 
@@ -235,12 +245,19 @@ function insertSegments(
235
245
  if (segment.type === "static") {
236
246
  if (!node.s) node.s = {};
237
247
  if (!node.s[segment.value]) node.s[segment.value] = {};
238
- insertSegments(node.s[segment.value], segments, index + 1, leaf);
248
+ insertSegments(
249
+ node.s[segment.value],
250
+ segments,
251
+ index + 1,
252
+ leafBase,
253
+ paramNames,
254
+ );
239
255
  } else if (segment.type === "param") {
240
256
  if (segment.optional) {
241
- // Optional param: add terminal at current node (param absent)
242
- mergeLeaf(node, leaf);
243
- // AND continue with param child (param present)
257
+ // SKIP first: continue at the same node without binding this name.
258
+ // Skip-first ordering means the present-branch's TAKE overwrites any
259
+ // shared terminal later, giving greedy-leftmost semantics.
260
+ insertSegments(node, segments, index + 1, leafBase, paramNames);
244
261
  }
245
262
  if (segment.suffix) {
246
263
  // Suffix param: keyed by suffix string (e.g., ".html")
@@ -248,16 +265,26 @@ function insertSegments(
248
265
  if (!node.xp[segment.suffix]) {
249
266
  node.xp[segment.suffix] = { n: segment.value, c: {} };
250
267
  }
251
- insertSegments(node.xp[segment.suffix].c, segments, index + 1, leaf);
268
+ insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
269
+ ...paramNames,
270
+ segment.value,
271
+ ]);
252
272
  } else {
253
273
  if (!node.p) {
254
274
  node.p = { n: segment.value, c: {} };
255
275
  }
256
- insertSegments(node.p.c, segments, index + 1, leaf);
276
+ insertSegments(node.p.c, segments, index + 1, leafBase, [
277
+ ...paramNames,
278
+ segment.value,
279
+ ]);
257
280
  }
258
281
  } else if (segment.type === "wildcard") {
259
- // Wildcard consumes all remaining segments
260
- const wildLeaf = { ...leaf, pn: "*" };
282
+ // Wildcard consumes all remaining segments. Carry any params bound before
283
+ // the wildcard in pa so they zip correctly against paramValues at match.
284
+ const wildLeaf: TrieLeaf & { pn: string } = {
285
+ ...buildLeaf(leafBase, paramNames),
286
+ pn: "*",
287
+ };
261
288
  const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
262
289
  const merged = mergeLeaves(existing, wildLeaf);
263
290
  node.w = merged as TrieLeaf & { pn: string };
@@ -67,13 +67,11 @@ export const MAX_REVALIDATION_INTERVAL = 30;
67
67
  // Types
68
68
  // ============================================================================
69
69
 
70
- /**
71
- * Cloudflare Workers ExecutionContext (subset we need)
72
- */
73
- export interface ExecutionContext {
74
- waitUntil(promise: Promise<any>): void;
75
- passThroughOnException(): void;
76
- }
70
+ // Re-exported from the canonical home so cf-cache-store consumers keep
71
+ // importing `ExecutionContext` from this module without a second interface
72
+ // drifting over time.
73
+ export type { ExecutionContext } from "../../types/request-scope.js";
74
+ import type { ExecutionContext } from "../../types/request-scope.js";
77
75
 
78
76
  /**
79
77
  * Minimal Cloudflare KV Namespace interface.
@@ -78,6 +78,9 @@ export {
78
78
  // Re-export useHref - it's a "use client" hook
79
79
  export { useHref } from "./browser/react/use-href.js";
80
80
 
81
+ // Re-export useReverse - it's a "use client" hook
82
+ export { useReverse } from "./browser/react/use-reverse.js";
83
+
81
84
  // Re-export useHandle - it's a "use client" hook
82
85
  export { useHandle } from "./browser/react/use-handle.js";
83
86
 
package/src/client.tsx CHANGED
@@ -21,6 +21,83 @@ import {
21
21
  } from "./route-content-wrapper.js";
22
22
  import { OutletProvider } from "./outlet-provider.js";
23
23
  import { MountContextProvider } from "./browser/react/mount-context.js";
24
+ import { getMemoizedContentPromise } from "./segment-content-promise.js";
25
+
26
+ /**
27
+ * Render the content for a named parallel/intercept slot segment.
28
+ *
29
+ * Shared by Outlet (with `name` prop) and ParallelOutlet — both resolve a
30
+ * segment from context.parallel by slot name and then render it through the
31
+ * same layout/loader/mountPath wrapping pipeline.
32
+ */
33
+ function renderSlotContent(segment: ResolvedSegment | null): ReactNode {
34
+ if (!segment) return null;
35
+
36
+ const content: ReactNode =
37
+ segment.loading || segment.component instanceof Promise ? (
38
+ <RouteContentWrapper
39
+ content={getMemoizedContentPromise(segment.component)}
40
+ fallback={segment.loading}
41
+ segmentId={segment.id}
42
+ />
43
+ ) : (
44
+ (segment.component ?? null)
45
+ );
46
+
47
+ const hasOwnLoaders = !!(segment.loaderDataPromise && segment.loaderIds);
48
+ const loaderWrapped = hasOwnLoaders ? (
49
+ <LoaderBoundary
50
+ loaderDataPromise={segment.loaderDataPromise!}
51
+ loaderIds={segment.loaderIds!}
52
+ fallback={segment.loading}
53
+ outletKey={segment.id + "-loader"}
54
+ outletContent={null}
55
+ segment={segment}
56
+ >
57
+ {content}
58
+ </LoaderBoundary>
59
+ ) : null;
60
+
61
+ let result: ReactNode;
62
+ if (segment.layout) {
63
+ // Layout renders immediately; if loaders exist, the LoaderBoundary becomes
64
+ // the outlet content so layout's <Outlet /> suspends until loaders resolve.
65
+ result = (
66
+ <OutletProvider
67
+ content={hasOwnLoaders ? loaderWrapped : content}
68
+ segment={segment}
69
+ >
70
+ {segment.layout}
71
+ </OutletProvider>
72
+ );
73
+ } else if (hasOwnLoaders) {
74
+ // No layout but has loaders — wrap content with LoaderBoundary for useLoader context.
75
+ // Common for intercept routes that use useLoader without a custom layout.
76
+ result = loaderWrapped;
77
+ } else {
78
+ result = content;
79
+ }
80
+
81
+ if (segment.mountPath) {
82
+ return (
83
+ <MountContextProvider value={segment.mountPath}>
84
+ {result}
85
+ </MountContextProvider>
86
+ );
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function useSlotSegment(
93
+ context: OutletContextValue | null,
94
+ name: `@${string}` | undefined,
95
+ ): ResolvedSegment | null {
96
+ return useMemo(() => {
97
+ if (!name || !context?.parallel) return null;
98
+ return context.parallel.find((seg) => seg.slot === name) ?? null;
99
+ }, [context, name]);
100
+ }
24
101
 
25
102
  /**
26
103
  * Outlet component - renders child content in layouts
@@ -61,95 +138,10 @@ import { MountContextProvider } from "./browser/react/mount-context.js";
61
138
  */
62
139
  export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
63
140
  const context = useContext(OutletContext);
141
+ const namedSegment = useSlotSegment(context, name);
64
142
 
65
- // If name provided, render parallel/intercept content for that slot
66
143
  if (name) {
67
- const segment = context?.parallel?.find((seg) => seg.slot === name) ?? null;
68
-
69
- if (!segment) return null;
70
-
71
- // Determine the content to render
72
- let content: ReactNode;
73
- if (segment.loading || segment.component instanceof Promise) {
74
- // Use RouteContentWrapper to handle Suspense wrapping properly
75
- content = (
76
- <RouteContentWrapper
77
- content={
78
- segment.component instanceof Promise
79
- ? segment.component
80
- : Promise.resolve(segment.component)
81
- }
82
- fallback={segment.loading}
83
- segmentId={segment.id}
84
- />
85
- );
86
- } else {
87
- content = segment.component ?? null;
88
- }
89
-
90
- let result: ReactNode;
91
-
92
- // If segment has a layout, wrap appropriately
93
- if (segment.layout) {
94
- // Check if this segment has loaders that need streaming
95
- // The layout renders immediately, LoaderBoundary becomes the outlet content
96
- // When layout renders <Outlet />, it gets the LoaderBoundary which suspends
97
- if (segment.loaderDataPromise && segment.loaderIds) {
98
- const loaderAwareContent = (
99
- <LoaderBoundary
100
- loaderDataPromise={segment.loaderDataPromise}
101
- loaderIds={segment.loaderIds}
102
- fallback={segment.loading}
103
- outletKey={segment.id + "-loader"}
104
- outletContent={null}
105
- segment={segment}
106
- >
107
- {content}
108
- </LoaderBoundary>
109
- );
110
-
111
- result = (
112
- <OutletProvider content={loaderAwareContent} segment={segment}>
113
- {segment.layout}
114
- </OutletProvider>
115
- );
116
- } else {
117
- // No loaders - wrap in OutletProvider so layout can use <Outlet />
118
- result = (
119
- <OutletProvider content={content} segment={segment}>
120
- {segment.layout}
121
- </OutletProvider>
122
- );
123
- }
124
- } else if (segment.loaderDataPromise && segment.loaderIds) {
125
- // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
126
- // This is common for intercept routes that use useLoader without a custom layout
127
- result = (
128
- <LoaderBoundary
129
- loaderDataPromise={segment.loaderDataPromise}
130
- loaderIds={segment.loaderIds}
131
- fallback={segment.loading}
132
- outletKey={segment.id + "-loader"}
133
- outletContent={null}
134
- segment={segment}
135
- >
136
- {content}
137
- </LoaderBoundary>
138
- );
139
- } else {
140
- result = content;
141
- }
142
-
143
- // Wrap with MountContextProvider for include() scoped parallel/intercept slots
144
- if (segment.mountPath) {
145
- return (
146
- <MountContextProvider value={segment.mountPath}>
147
- {result}
148
- </MountContextProvider>
149
- );
150
- }
151
-
152
- return result;
144
+ return renderSlotContent(namedSegment);
153
145
  }
154
146
 
155
147
  // Default: render child content
@@ -163,6 +155,7 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
163
155
 
164
156
  return content;
165
157
  }
158
+
166
159
  /**
167
160
  * ParallelOutlet component - renders content for a named parallel slot
168
161
  *
@@ -187,94 +180,9 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
187
180
  */
188
181
  export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
189
182
  const context = useContext(OutletContext);
190
- const segment = useMemo(() => {
191
- if (!context?.parallel) return null;
192
- return context.parallel.find((seg) => seg.slot === name) ?? null;
193
- }, [context, name]);
194
-
195
- if (!segment) return null;
196
-
197
- // Determine the content to render
198
- let content: ReactNode;
199
- if (segment.loading || segment.component instanceof Promise) {
200
- // Use RouteContentWrapper to handle Suspense wrapping properly
201
- content = (
202
- <RouteContentWrapper
203
- content={
204
- segment.component instanceof Promise
205
- ? segment.component
206
- : Promise.resolve(segment.component)
207
- }
208
- fallback={segment.loading}
209
- segmentId={segment.id}
210
- />
211
- );
212
- } else {
213
- content = segment.component ?? null;
214
- }
215
-
216
- let result: ReactNode;
217
-
218
- // If segment has a layout, wrap appropriately
219
- if (segment.layout) {
220
- // Check if this segment has loaders that need streaming
221
- // The layout renders immediately, LoaderBoundary becomes the outlet content
222
- if (segment.loaderDataPromise && segment.loaderIds) {
223
- const loaderAwareContent = (
224
- <LoaderBoundary
225
- loaderDataPromise={segment.loaderDataPromise}
226
- loaderIds={segment.loaderIds}
227
- fallback={segment.loading}
228
- outletKey={segment.id + "-loader"}
229
- outletContent={null}
230
- segment={segment}
231
- >
232
- {content}
233
- </LoaderBoundary>
234
- );
235
-
236
- result = (
237
- <OutletProvider content={loaderAwareContent} segment={segment}>
238
- {segment.layout}
239
- </OutletProvider>
240
- );
241
- } else {
242
- // No loaders - wrap in OutletProvider so layout can use <Outlet />
243
- result = (
244
- <OutletProvider content={content} segment={segment}>
245
- {segment.layout}
246
- </OutletProvider>
247
- );
248
- }
249
- } else if (segment.loaderDataPromise && segment.loaderIds) {
250
- // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
251
- // This is common for intercept routes that use useLoader without a custom layout
252
- result = (
253
- <LoaderBoundary
254
- loaderDataPromise={segment.loaderDataPromise}
255
- loaderIds={segment.loaderIds}
256
- fallback={segment.loading}
257
- outletKey={segment.id + "-loader"}
258
- outletContent={null}
259
- segment={segment}
260
- >
261
- {content}
262
- </LoaderBoundary>
263
- );
264
- } else {
265
- result = content;
266
- }
183
+ const segment = useSlotSegment(context, name);
267
184
 
268
- // Wrap with MountContextProvider for include() scoped parallel/intercept slots
269
- if (segment.mountPath) {
270
- return (
271
- <MountContextProvider value={segment.mountPath}>
272
- {result}
273
- </MountContextProvider>
274
- );
275
- }
276
-
277
- return result;
185
+ return renderSlotContent(segment);
278
186
  }
279
187
 
280
188
  // OutletProvider is defined in outlet-provider.tsx to break a circular
@@ -540,8 +448,12 @@ export { MountContext } from "./browser/react/mount-context.js";
540
448
  // Mount-aware href hook - auto-prefixes paths with include() mount
541
449
  export { useHref } from "./browser/react/use-href.js";
542
450
 
451
+ // Mount-aware reverse hook - resolves dot-prefixed names against an imported
452
+ // generated routes map (from a urls() module's .gen.ts).
453
+ export { useReverse } from "./browser/react/use-reverse.js";
454
+
543
455
  // Type-safe scoped reverse function for scopedReverse<typeof patterns>()
544
- export type { ScopedReverseFunction } from "./reverse.js";
456
+ export type { ScopedReverseFunction, LocalReverseFunction } from "./reverse.js";
545
457
 
546
458
  // Loader definition type - for typing loader props in client components
547
459
  export type { LoaderDefinition } from "./types.js";
@@ -186,7 +186,10 @@ export function href<T extends ValidPaths>(path: T, mount?: string): string {
186
186
  const normalizedMount = mount.endsWith("/") ? mount.slice(0, -1) : mount;
187
187
  return normalizedMount + path;
188
188
  }
189
- return path;
189
+ // ValidPaths is built from template literals so T does extend string at
190
+ // runtime, but the inference can fail past a certain route-union complexity
191
+ // and TypeScript reports T as not assignable to string.
192
+ return path as string;
190
193
  }
191
194
 
192
195
  /**
package/src/index.rsc.ts CHANGED
@@ -172,6 +172,9 @@ export type { PublicRequestContext as RequestContext } from "./server/request-co
172
172
  import type { PublicRequestContext } from "./server/request-context.js";
173
173
  import type { DefaultEnv } from "./types/global-namespace.js";
174
174
 
175
+ // Shared base for every user-facing request context (mirrors index.ts).
176
+ export type { RequestScope, ExecutionContext } from "./types/request-scope.js";
177
+
175
178
  export const getRequestContext: <
176
179
  TEnv = DefaultEnv,
177
180
  >() => PublicRequestContext<TEnv> = _getRequestContextInternal;
package/src/index.ts CHANGED
@@ -147,24 +147,52 @@ export { createVar, type ContextVar } from "./context-var.js";
147
147
  export { nonce } from "./rsc/nonce.js";
148
148
 
149
149
  /**
150
- * Error-throwing stub for server-only `Prerender` function.
150
+ * SSR/client stub for server-only `Prerender` function.
151
+ *
152
+ * Returns a lightweight stub object instead of throwing so that the
153
+ * production SSR build can safely bundle the RSC entry chunk — the SSR
154
+ * bundler resolves `@rangojs/router` to this (SSR) entry, so Prerender
155
+ * calls in RSC code must not crash at module-evaluation time.
151
156
  */
152
- export function Prerender(): never {
153
- throw serverOnlyStubError("Prerender");
157
+ export function Prerender(
158
+ _handler?: any,
159
+ _optionsOrId?: any,
160
+ __injectedId?: string,
161
+ ): any {
162
+ const id =
163
+ typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
164
+ return { __brand: "prerenderHandler" as const, $$id: id };
154
165
  }
155
166
 
156
167
  /**
157
- * Error-throwing stub for server-only `Passthrough` function.
168
+ * SSR/client stub for server-only `Passthrough` function.
158
169
  */
159
- export function Passthrough(): never {
160
- throw serverOnlyStubError("Passthrough");
170
+ export function Passthrough(
171
+ _handler?: any,
172
+ _optionsOrId?: any,
173
+ __injectedId?: string,
174
+ ): any {
175
+ const id =
176
+ typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
177
+ return { __brand: "passthroughHandler" as const, $$id: id };
161
178
  }
162
179
 
163
180
  /**
164
- * Error-throwing stub for server-only `Static` function.
181
+ * SSR/client stub for server-only `Static` function.
182
+ *
183
+ * Returns a lightweight stub object instead of throwing so that the
184
+ * production SSR build can safely bundle the RSC entry chunk — the SSR
185
+ * bundler resolves `@rangojs/router` to this (SSR) entry, so Static
186
+ * calls in RSC code must not crash at module-evaluation time.
165
187
  */
166
- export function Static(): never {
167
- throw serverOnlyStubError("Static");
188
+ export function Static(
189
+ _handler?: any,
190
+ _optionsOrId?: any,
191
+ __injectedId?: string,
192
+ ): any {
193
+ const id =
194
+ typeof _optionsOrId === "string" ? _optionsOrId : __injectedId || "";
195
+ return { __brand: "staticHandler" as const, $$id: id };
168
196
  }
169
197
 
170
198
  /**
@@ -236,6 +264,9 @@ export function transition(): never {
236
264
  // Request context type (safe for client)
237
265
  export type { PublicRequestContext as RequestContext } from "./server/request-context.js";
238
266
 
267
+ // Shared base for every user-facing request context.
268
+ export type { RequestScope, ExecutionContext } from "./types/request-scope.js";
269
+
239
270
  // Cookie store types (safe for client)
240
271
  export type {
241
272
  CookieStore,
@@ -1,4 +1,4 @@
1
- import { Context, createContext, type ReactNode } from "react";
1
+ import { type Context, createContext, type ReactNode } from "react";
2
2
  import type { ResolvedSegment } from "./types";
3
3
 
4
4
  export interface OutletContextValue {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Runtime-neutral Response shape utilities.
3
+ *
4
+ * Kept at the src/ root so both `router/` and `rsc/` can depend on it
5
+ * without creating a cross-layer import cycle.
6
+ */
7
+
8
+ /**
9
+ * True when a Response represents a WebSocket upgrade handoff and must not
10
+ * be reconstructed or mutated:
11
+ *
12
+ * - Status 101 (Switching Protocols) is outside the standard Response
13
+ * constructor's 200–599 range, so `new Response(body, { status: 101 })`
14
+ * throws RangeError on Node/undici and any spec-compliant runtime.
15
+ * - Cloudflare's workerd attaches a non-standard `webSocket` property on
16
+ * the upgrade Response (e.g. from `acceptWebSocket`/`handleWebSocketUpgrade`
17
+ * or the `agents` library's `routeAgentRequest`). That property is dropped
18
+ * by a `new Response(...)` copy, breaking the upgrade even on workerd
19
+ * where the status range is relaxed.
20
+ *
21
+ * Callers should short-circuit header/body merges for these responses.
22
+ */
23
+ export function isWebSocketUpgradeResponse(response: Response): boolean {
24
+ return (
25
+ response.status === 101 ||
26
+ (response as unknown as { webSocket?: unknown }).webSocket != null
27
+ );
28
+ }