@rangojs/router 0.0.0-experimental.77 → 0.0.0-experimental.77ed8945

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 (239) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2103 -861
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +66 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +26 -4
  19. package/skills/layout/SKILL.md +6 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +12 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +238 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +33 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/tailwind/SKILL.md +27 -3
  37. package/skills/typesafety/SKILL.md +319 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +116 -0
  42. package/src/browser/action-coordinator.ts +53 -36
  43. package/src/browser/app-shell.ts +39 -0
  44. package/src/browser/event-controller.ts +86 -70
  45. package/src/browser/history-state.ts +21 -0
  46. package/src/browser/index.ts +3 -3
  47. package/src/browser/navigation-bridge.ts +29 -9
  48. package/src/browser/navigation-client.ts +99 -77
  49. package/src/browser/navigation-store.ts +7 -8
  50. package/src/browser/navigation-transaction.ts +10 -28
  51. package/src/browser/partial-update.ts +60 -40
  52. package/src/browser/prefetch/cache.ts +196 -49
  53. package/src/browser/prefetch/fetch.ts +203 -59
  54. package/src/browser/prefetch/queue.ts +36 -5
  55. package/src/browser/rango-state.ts +37 -13
  56. package/src/browser/react/Link.tsx +18 -13
  57. package/src/browser/react/NavigationProvider.tsx +75 -31
  58. package/src/browser/react/filter-segment-order.ts +51 -7
  59. package/src/browser/react/index.ts +3 -0
  60. package/src/browser/react/location-state-shared.ts +175 -4
  61. package/src/browser/react/location-state.ts +39 -13
  62. package/src/browser/react/use-handle.ts +17 -9
  63. package/src/browser/react/use-navigation.ts +22 -2
  64. package/src/browser/react/use-params.ts +20 -8
  65. package/src/browser/react/use-reverse.ts +106 -0
  66. package/src/browser/react/use-router.ts +23 -2
  67. package/src/browser/react/use-segments.ts +11 -8
  68. package/src/browser/response-adapter.ts +52 -1
  69. package/src/browser/rsc-router.tsx +71 -22
  70. package/src/browser/scroll-restoration.ts +22 -14
  71. package/src/browser/segment-reconciler.ts +10 -14
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +44 -30
  74. package/src/browser/types.ts +12 -2
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +45 -1
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-runtime.ts +17 -5
  89. package/src/cache/cache-scope.ts +51 -49
  90. package/src/cache/cf/cf-cache-store.ts +502 -32
  91. package/src/cache/cf/index.ts +3 -0
  92. package/src/cache/handle-snapshot.ts +103 -0
  93. package/src/cache/index.ts +3 -0
  94. package/src/cache/memory-segment-store.ts +3 -2
  95. package/src/cache/types.ts +10 -6
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +96 -205
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -4
  101. package/src/handle.ts +4 -6
  102. package/src/host/index.ts +2 -2
  103. package/src/host/router.ts +129 -57
  104. package/src/host/types.ts +31 -2
  105. package/src/host/utils.ts +1 -1
  106. package/src/href-client.ts +140 -21
  107. package/src/index.rsc.ts +10 -6
  108. package/src/index.ts +17 -8
  109. package/src/loader-store.ts +500 -0
  110. package/src/loader.rsc.ts +2 -5
  111. package/src/loader.ts +3 -10
  112. package/src/missing-id-error.ts +68 -0
  113. package/src/outlet-context.ts +1 -1
  114. package/src/prerender/store.ts +9 -7
  115. package/src/prerender.ts +4 -4
  116. package/src/response-utils.ts +37 -0
  117. package/src/reverse.ts +65 -39
  118. package/src/route-content-wrapper.tsx +6 -28
  119. package/src/route-definition/dsl-helpers.ts +253 -265
  120. package/src/route-definition/helper-factories.ts +29 -139
  121. package/src/route-definition/helpers-types.ts +43 -15
  122. package/src/route-definition/resolve-handler-use.ts +6 -0
  123. package/src/route-definition/use-item-types.ts +32 -0
  124. package/src/route-types.ts +26 -41
  125. package/src/router/content-negotiation.ts +15 -2
  126. package/src/router/error-handling.ts +1 -1
  127. package/src/router/find-match.ts +54 -6
  128. package/src/router/handler-context.ts +21 -41
  129. package/src/router/intercept-resolution.ts +4 -18
  130. package/src/router/lazy-includes.ts +41 -22
  131. package/src/router/loader-resolution.ts +82 -36
  132. package/src/router/manifest.ts +41 -19
  133. package/src/router/match-api.ts +4 -3
  134. package/src/router/match-handlers.ts +1 -0
  135. package/src/router/match-middleware/cache-lookup.ts +57 -95
  136. package/src/router/match-middleware/cache-store.ts +3 -2
  137. package/src/router/match-result.ts +53 -32
  138. package/src/router/metrics.ts +1 -1
  139. package/src/router/middleware-types.ts +15 -26
  140. package/src/router/middleware.ts +99 -84
  141. package/src/router/pattern-matching.ts +116 -19
  142. package/src/router/prerender-match.ts +40 -15
  143. package/src/router/preview-match.ts +3 -1
  144. package/src/router/request-classification.ts +40 -37
  145. package/src/router/revalidation.ts +58 -2
  146. package/src/router/router-interfaces.ts +51 -35
  147. package/src/router/router-options.ts +25 -1
  148. package/src/router/router-registry.ts +2 -5
  149. package/src/router/segment-resolution/fresh.ts +27 -6
  150. package/src/router/segment-resolution/revalidation.ts +147 -106
  151. package/src/router/segment-resolution/static-store.ts +19 -5
  152. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  153. package/src/router/substitute-pattern-params.ts +56 -0
  154. package/src/router/trie-matching.ts +40 -16
  155. package/src/router/types.ts +8 -0
  156. package/src/router/url-params.ts +49 -0
  157. package/src/router.ts +37 -25
  158. package/src/rsc/handler-context.ts +2 -2
  159. package/src/rsc/handler.ts +58 -77
  160. package/src/rsc/helpers.ts +72 -43
  161. package/src/rsc/index.ts +1 -1
  162. package/src/rsc/manifest-init.ts +28 -41
  163. package/src/rsc/origin-guard.ts +30 -10
  164. package/src/rsc/progressive-enhancement.ts +4 -0
  165. package/src/rsc/response-error.ts +79 -12
  166. package/src/rsc/response-route-handler.ts +76 -61
  167. package/src/rsc/rsc-rendering.ts +45 -51
  168. package/src/rsc/runtime-warnings.ts +9 -10
  169. package/src/rsc/server-action.ts +33 -39
  170. package/src/rsc/ssr-setup.ts +16 -0
  171. package/src/rsc/types.ts +8 -2
  172. package/src/search-params.ts +4 -4
  173. package/src/segment-content-promise.ts +67 -0
  174. package/src/segment-loader-promise.ts +122 -0
  175. package/src/segment-system.tsx +132 -116
  176. package/src/serialize.ts +243 -0
  177. package/src/server/context.ts +175 -53
  178. package/src/server/cookie-store.ts +28 -4
  179. package/src/server/request-context.ts +57 -51
  180. package/src/ssr/index.tsx +5 -1
  181. package/src/static-handler.ts +1 -1
  182. package/src/types/global-namespace.ts +39 -26
  183. package/src/types/handler-context.ts +68 -50
  184. package/src/types/index.ts +1 -0
  185. package/src/types/loader-types.ts +11 -9
  186. package/src/types/request-scope.ts +126 -0
  187. package/src/types/route-entry.ts +11 -0
  188. package/src/types/segments.ts +35 -2
  189. package/src/urls/include-helper.ts +34 -67
  190. package/src/urls/index.ts +1 -5
  191. package/src/urls/path-helper-types.ts +17 -3
  192. package/src/urls/path-helper.ts +17 -52
  193. package/src/urls/pattern-types.ts +36 -19
  194. package/src/urls/response-types.ts +22 -29
  195. package/src/urls/type-extraction.ts +58 -139
  196. package/src/urls/urls-function.ts +1 -5
  197. package/src/use-loader.tsx +413 -42
  198. package/src/vite/debug.ts +185 -0
  199. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  200. package/src/vite/discovery/discover-routers.ts +106 -75
  201. package/src/vite/discovery/discovery-errors.ts +194 -0
  202. package/src/vite/discovery/gate-state.ts +171 -0
  203. package/src/vite/discovery/prerender-collection.ts +72 -31
  204. package/src/vite/discovery/route-types-writer.ts +40 -84
  205. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  206. package/src/vite/discovery/state.ts +33 -0
  207. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  208. package/src/vite/index.ts +2 -0
  209. package/src/vite/plugin-types.ts +67 -0
  210. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  211. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  212. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  213. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  214. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  215. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  216. package/src/vite/plugins/expose-action-id.ts +54 -30
  217. package/src/vite/plugins/expose-id-utils.ts +12 -8
  218. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  219. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  220. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  221. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  222. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  223. package/src/vite/plugins/performance-tracks.ts +29 -25
  224. package/src/vite/plugins/use-cache-transform.ts +65 -50
  225. package/src/vite/plugins/version-injector.ts +39 -23
  226. package/src/vite/plugins/version-plugin.ts +59 -2
  227. package/src/vite/plugins/virtual-entries.ts +2 -2
  228. package/src/vite/rango.ts +116 -29
  229. package/src/vite/router-discovery.ts +753 -104
  230. package/src/vite/utils/ast-handler-extract.ts +15 -15
  231. package/src/vite/utils/banner.ts +1 -1
  232. package/src/vite/utils/bundle-analysis.ts +4 -2
  233. package/src/vite/utils/client-chunks.ts +190 -0
  234. package/src/vite/utils/forward-user-plugins.ts +193 -0
  235. package/src/vite/utils/manifest-utils.ts +8 -59
  236. package/src/vite/utils/package-resolution.ts +41 -1
  237. package/src/vite/utils/prerender-utils.ts +5 -4
  238. package/src/vite/utils/shared-utils.ts +107 -26
  239. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,68 @@
1
+ // Builds the error thrown when a create*() call (createLoader / createHandle)
2
+ // reaches runtime without an injected $$id. The exposeInternalIds Vite transform
3
+ // injects $$id only for an EXPORTED const declaration, so a non-exported const,
4
+ // an `export let/var`, or an inline create*() call gets none. Previously this
5
+ // failed with a terse message and no source location; this helper adds the
6
+ // offending call site (best-effort, from the stack) and actionable guidance.
7
+ //
8
+ // The "<Kind> is missing $$id" prefix is preserved so existing tests and any
9
+ // log scrapers keep matching. Dev-only: the call sites guard on
10
+ // process.env.NODE_ENV === "development", so production builds fold the branch
11
+ // away and tree-shake this module out.
12
+
13
+ // create*() implementation files to skip when locating the user's call site.
14
+ const SELF_FILES = new Set([
15
+ "missing-id-error",
16
+ "loader",
17
+ "loader.rsc",
18
+ "handle",
19
+ ]);
20
+
21
+ /**
22
+ * Best-effort "path:line:column" of the user's create*() call, parsed from the
23
+ * current stack. Skips @rangojs/router internals and node_modules. Returns
24
+ * undefined if nothing usable is found (stack parsing is inherently fragile).
25
+ */
26
+ function findUserCallSite(): string | undefined {
27
+ try {
28
+ const stack = new Error().stack;
29
+ if (!stack) return undefined;
30
+ for (const frame of stack.split("\n").slice(1)) {
31
+ const m = frame.match(
32
+ /(?:\(|@|\s)(?:file:\/\/)?((?:\/|[A-Za-z]:[\\/])[^()\s]+?\.(?:ts|tsx|js|jsx|mts|cts)):(\d+):(\d+)\)?/,
33
+ );
34
+ if (!m) continue;
35
+ const path = m[1];
36
+ if (path.includes("node_modules") || path.includes("@rangojs/router")) {
37
+ continue;
38
+ }
39
+ const base = path
40
+ .split(/[\\/]/)
41
+ .pop()!
42
+ .replace(/\.(?:ts|tsx|js|jsx|mts|cts)$/, "");
43
+ if (SELF_FILES.has(base)) continue;
44
+ return `${path}:${m[2]}:${m[3]}`;
45
+ }
46
+ } catch {
47
+ // best-effort only
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ export function missingInjectedIdError(
53
+ kind: "Loader" | "Handle",
54
+ fnName: "createLoader" | "createHandle",
55
+ ): Error {
56
+ const site = findUserCallSite();
57
+ const at = site ? ` (created at ${site})` : "";
58
+ return new Error(
59
+ `[rango] ${kind} is missing $$id${at}.\n` +
60
+ `The @rangojs/router:expose-internal-ids Vite transform injects ${fnName}()'s ` +
61
+ `stable $$id from an EXPORTED const declaration only:\n` +
62
+ ` export const X = ${fnName}(...)\n` +
63
+ ` const X = ${fnName}(...); export { X }\n` +
64
+ `A non-exported const, an \`export let/var\`, or an inline ${fnName}(...) ` +
65
+ `call gets no $$id — export it as \`export const\`. (A matching ` +
66
+ `"Unsupported ${fnName} shape" warning names the exact file:line.)`,
67
+ );
68
+ }
@@ -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 {
@@ -8,14 +8,14 @@
8
8
  * that anchors import() resolution relative to the manifest file.
9
9
  */
10
10
 
11
- import type {
12
- SerializedSegmentData,
13
- SegmentHandleData,
14
- } from "../cache/types.js";
11
+ import type { SerializedSegmentData } from "../cache/types.js";
15
12
 
16
13
  export interface PrerenderEntry {
17
14
  segments: SerializedSegmentData[];
18
- handles: Record<string, SegmentHandleData>;
15
+ /** RSC-encoded handle map (see handle-snapshot.ts encodeHandles); "" when the
16
+ * route pushed no handles. Encoded so Promise/ReactNode handle values survive
17
+ * the JSON-serialized build artifact / dev wire, identical to the runtime cache. */
18
+ handles: string;
19
19
  }
20
20
 
21
21
  export interface PrerenderStore {
@@ -28,7 +28,9 @@ export interface PrerenderStore {
28
28
 
29
29
  export interface StaticEntry {
30
30
  encoded: string;
31
- handles: Record<string, unknown[]>;
31
+ /** RSC-encoded single-segment handle data (see encodeHandleValue); "" when the
32
+ * Static handler pushed no handles. */
33
+ handles: string;
32
34
  }
33
35
 
34
36
  export interface StaticStore {
@@ -174,7 +176,7 @@ export function createStaticStore(): StaticStore | null {
174
176
  const val = mod.default;
175
177
  // Normalize: string-only (no handles) or { encoded, handles }
176
178
  if (typeof val === "string") {
177
- return { encoded: val, handles: {} } as StaticEntry;
179
+ return { encoded: val, handles: "" } as StaticEntry;
178
180
  }
179
181
  return val as StaticEntry;
180
182
  })
package/src/prerender.ts CHANGED
@@ -69,9 +69,9 @@ type BuildReverseFunction = [DefaultReverseRouteMap] extends [
69
69
  * Default route map for Prerender named route resolution.
70
70
  * Uses GeneratedRouteMap (from gen file) to avoid circular dependencies.
71
71
  */
72
- type DefaultPrerenderRouteMap = keyof RSCRouter.GeneratedRouteMap extends never
72
+ type DefaultPrerenderRouteMap = keyof Rango.GeneratedRouteMap extends never
73
73
  ? {}
74
- : RSCRouter.GeneratedRouteMap;
74
+ : Rango.GeneratedRouteMap;
75
75
 
76
76
  /** Extract params from a route map entry (string pattern or { path } object). */
77
77
  type ExtractParamsFromEntry<TEntry> = TEntry extends string
@@ -378,7 +378,7 @@ export function Prerender<TParams extends Record<string, any>>(
378
378
 
379
379
  if (!id) {
380
380
  throw new Error(
381
- "[rsc-router] Prerender: missing $$id. " +
381
+ "[rango] Prerender: missing $$id. " +
382
382
  "Ensure the exposeInternalIds Vite plugin is configured.",
383
383
  );
384
384
  }
@@ -499,7 +499,7 @@ export function Passthrough<
499
499
  ): PassthroughHandlerDefinition<TParams, TEnv> {
500
500
  if (!isPrerenderHandler(prerenderDef)) {
501
501
  throw new Error(
502
- "[rsc-router] Passthrough: first argument must be a Prerender() definition.",
502
+ "[rango] Passthrough: first argument must be a Prerender() definition.",
503
503
  );
504
504
  }
505
505
  return {
@@ -0,0 +1,37 @@
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
+ }
29
+
30
+ // Location truthiness (not presence) so an empty `Location: ""` is not a redirect.
31
+ export function isRedirectResponse(response: Response): boolean {
32
+ return (
33
+ response.status >= 300 &&
34
+ response.status < 400 &&
35
+ Boolean(response.headers.get("Location"))
36
+ );
37
+ }
package/src/reverse.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ExtractParams } from "./types.js";
2
2
  import type { SearchSchema, ResolveSearchSchema } from "./search-params.js";
3
3
  import { serializeSearchParams } from "./search-params.js";
4
+ import { substitutePatternParams } from "./router/substitute-pattern-params.js";
4
5
 
5
6
  /**
6
7
  * Sanitize prefix string by removing leading slash
@@ -218,6 +219,67 @@ export type ExtractLocalRoutes<TPatterns> = TPatterns extends {
218
219
  ? TPatterns
219
220
  : Record<string, string>;
220
221
 
222
+ /**
223
+ * Params accepted by `useReverse(routes)`. The route's own params are
224
+ * required, and additional string keys are permitted so callers can
225
+ * override values that would otherwise be auto-filled from the matched
226
+ * route's `useParams()` (e.g. an enclosing `:tenantId` mount segment).
227
+ */
228
+ export type LocalReverseParams<TPattern extends string> =
229
+ ExtractParams<TPattern> & {
230
+ readonly [extra: string]: string | undefined;
231
+ };
232
+
233
+ /**
234
+ * Type-safe local reverse function.
235
+ *
236
+ * Returned by `useReverse(routes)` on the client. The route map is the
237
+ * exposure boundary (a generated `routes` from a `urls()` module) and the
238
+ * scope is implicit from that import. Names may be written with or without a
239
+ * leading dot — `reverse("post")` and `reverse(".post")` are identical. The dot
240
+ * is a cosmetic readability convention (and parity with `ctx.reverse(".name")`);
241
+ * there is no separate global namespace here, so it carries no meaning.
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const reverse = useReverse(blogRoutes);
246
+ * reverse("index"); // ✓ no params (dot optional)
247
+ * reverse(".index"); // ✓ identical to the above
248
+ * reverse("post", { postId: "hello" }); // ✓ with params
249
+ * reverse("search", {}, { q: "hi" }); // ✓ with search schema
250
+ * reverse("typo"); // ✗ compile error
251
+ * ```
252
+ */
253
+ export type LocalReverseFunction<TLocalRoutes> = {
254
+ /**
255
+ * Route without params (leading dot optional)
256
+ */
257
+ <TName extends keyof TLocalRoutes & string>(
258
+ name: IsEmptyObject<
259
+ ExtractParams<RoutePatternFor<TLocalRoutes, TName>>
260
+ > extends true
261
+ ? TName | `.${TName}`
262
+ : never,
263
+ ): string;
264
+
265
+ /**
266
+ * Route with params (leading dot optional)
267
+ */
268
+ <TName extends keyof TLocalRoutes & string>(
269
+ name: TName | `.${TName}`,
270
+ params: LocalReverseParams<RoutePatternFor<TLocalRoutes, TName>>,
271
+ ): string;
272
+
273
+ /**
274
+ * Route with params and search (leading dot optional)
275
+ */
276
+ <TName extends keyof TLocalRoutes & string>(
277
+ name: TName | `.${TName}`,
278
+ params: LocalReverseParams<RoutePatternFor<TLocalRoutes, TName>>,
279
+ search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>,
280
+ ): string;
281
+ };
282
+
221
283
  /**
222
284
  * Extract the response data type for a named route from a UrlPatterns instance.
223
285
  * Re-exported from urls.ts for consumer convenience.
@@ -301,45 +363,9 @@ export function createReverse<TRoutes extends Record<string, string>>(
301
363
  throw new Error(`Unknown route: ${name}`);
302
364
  }
303
365
 
304
- let result = pattern;
305
- if (params) {
306
- // Replace :param placeholders with actual values
307
- // Strip constraint syntax: :param(a|b) -> use "param" as key
308
- // Optional params (:param?) are omitted when not provided
309
- let hadOmittedOptional = false;
310
- result = result.replace(
311
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
312
- (_, key, _constraint, optional) => {
313
- const value = params[key];
314
- // Empty string is treated as omitted — the trie matcher fills
315
- // unmatched optional params with "" (not undefined), so reverse
316
- // must collapse those segments instead of leaving empty slots.
317
- if (value === undefined || value === "") {
318
- hadOmittedOptional = true;
319
- return "";
320
- }
321
- return encodeURIComponent(value);
322
- },
323
- );
324
- // Second pass: required params (no trailing ?)
325
- result = result.replace(
326
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
327
- (_, key) => {
328
- const value = params[key];
329
- if (value === undefined) {
330
- throw new Error(`Missing param "${key}" for route "${name}"`);
331
- }
332
- return encodeURIComponent(value);
333
- },
334
- );
335
- // Clean up slashes only when an optional param was actually omitted,
336
- // so intentional trailing-slash patterns like "/blog/" are preserved.
337
- if (hadOmittedOptional) {
338
- const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
339
- result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
340
- if (hadTrailingSlash && !result.endsWith("/")) result += "/";
341
- }
342
- }
366
+ let result = params
367
+ ? substitutePatternParams(pattern, params, name)
368
+ : pattern;
343
369
 
344
370
  // Append search params as query string
345
371
  if (search) {
@@ -4,7 +4,7 @@ import { Suspense, use, useId } from "react";
4
4
  import { invariant } from "./errors";
5
5
  import { OutletProvider } from "./outlet-provider.js";
6
6
  import type { ResolvedSegment } from "./types.js";
7
- import { isLoaderDataResult } from "./types.js";
7
+ import { decodeLoaderResults } from "./decode-loader-results.js";
8
8
 
9
9
  /**
10
10
  * Stable async wrapper component for route content
@@ -26,10 +26,6 @@ export function RouteContentWrapper({
26
26
  fallback?: ReactNode;
27
27
  segmentId?: string;
28
28
  }): ReactNode {
29
- if (!content) {
30
- // Already resolved
31
- return content as ReactNode;
32
- }
33
29
  return (
34
30
  <Suspense
35
31
  fallback={fallback ?? null}
@@ -159,28 +155,10 @@ function LoaderResolver({
159
155
  ? use(loaderDataPromise)
160
156
  : loaderDataPromise;
161
157
 
162
- // Build loaderData record from resolved values
163
- const loaderData: Record<string, any> = {};
164
- let loaderErrorFallback: ReactNode = null;
165
-
166
- loaderIds.forEach((id, i) => {
167
- const result = resolvedData[i];
168
-
169
- if (isLoaderDataResult(result)) {
170
- if (result.ok) {
171
- loaderData[id] = result.data;
172
- } else {
173
- if (result.fallback) {
174
- loaderErrorFallback = result.fallback;
175
- } else {
176
- throw new Error(result.error.message);
177
- }
178
- }
179
- } else {
180
- // Legacy format - direct data
181
- loaderData[id] = result;
182
- }
183
- });
158
+ const { loaderData, errorFallback } = decodeLoaderResults(
159
+ resolvedData,
160
+ loaderIds,
161
+ );
184
162
 
185
163
  return (
186
164
  <OutletProvider
@@ -190,7 +168,7 @@ function LoaderResolver({
190
168
  parallel={parallel}
191
169
  loaderData={Object.keys(loaderData).length > 0 ? loaderData : undefined}
192
170
  >
193
- {loaderErrorFallback ?? children}
171
+ {errorFallback ?? children}
194
172
  </OutletProvider>
195
173
  );
196
174
  }