@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125

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 (260) hide show
  1. package/dist/bin/rango.js +10 -6
  2. package/dist/testing/vitest.js +82 -0
  3. package/dist/vite/index.js +55 -48
  4. package/package.json +61 -21
  5. package/skills/caching/SKILL.md +2 -1
  6. package/skills/hooks/SKILL.md +40 -29
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +3 -1
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +12 -0
  15. package/skills/route/SKILL.md +10 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/src/__internal.ts +0 -65
  32. package/src/browser/action-coordinator.ts +1 -1
  33. package/src/browser/action-fence.ts +47 -0
  34. package/src/browser/cookie-name.ts +140 -0
  35. package/src/browser/event-controller.ts +1 -83
  36. package/src/browser/invalidate-client-cache.ts +52 -0
  37. package/src/browser/navigation-bridge.ts +14 -1
  38. package/src/browser/navigation-client.ts +14 -1
  39. package/src/browser/navigation-store-handle.ts +38 -0
  40. package/src/browser/navigation-store.ts +26 -51
  41. package/src/browser/navigation-transaction.ts +0 -32
  42. package/src/browser/partial-update.ts +1 -83
  43. package/src/browser/prefetch/cache.ts +6 -45
  44. package/src/browser/prefetch/fetch.ts +7 -0
  45. package/src/browser/prefetch/queue.ts +6 -3
  46. package/src/browser/rango-state.ts +157 -99
  47. package/src/browser/react/Link.tsx +0 -2
  48. package/src/browser/react/NavigationProvider.tsx +2 -1
  49. package/src/browser/react/ScrollRestoration.tsx +10 -6
  50. package/src/browser/react/filter-segment-order.ts +0 -2
  51. package/src/browser/react/index.ts +0 -51
  52. package/src/browser/react/location-state-shared.ts +0 -13
  53. package/src/browser/react/location-state.ts +0 -1
  54. package/src/browser/react/use-action.ts +6 -15
  55. package/src/browser/react/use-handle.ts +0 -5
  56. package/src/browser/react/use-link-status.ts +0 -4
  57. package/src/browser/react/use-navigation.ts +0 -3
  58. package/src/browser/react/use-params.ts +0 -2
  59. package/src/browser/react/use-search-params.ts +0 -5
  60. package/src/browser/react/use-segments.ts +0 -13
  61. package/src/browser/rsc-router.tsx +12 -4
  62. package/src/browser/server-action-bridge.ts +77 -15
  63. package/src/browser/types.ts +7 -2
  64. package/src/browser/validate-redirect-origin.ts +4 -5
  65. package/src/build/route-trie.ts +3 -0
  66. package/src/build/route-types/param-extraction.ts +6 -3
  67. package/src/build/route-types/router-processing.ts +0 -8
  68. package/src/cache/cache-policy.ts +0 -54
  69. package/src/cache/cache-runtime.ts +27 -24
  70. package/src/cache/cache-scope.ts +0 -27
  71. package/src/cache/cache-tag.ts +0 -37
  72. package/src/cache/cf/cf-cache-store.ts +94 -46
  73. package/src/cache/cf/index.ts +0 -24
  74. package/src/cache/document-cache.ts +11 -36
  75. package/src/cache/handle-snapshot.ts +0 -40
  76. package/src/cache/index.ts +0 -27
  77. package/src/cache/memory-segment-store.ts +2 -48
  78. package/src/cache/profile-registry.ts +7 -3
  79. package/src/cache/read-through-swr.ts +41 -11
  80. package/src/cache/segment-codec.ts +0 -16
  81. package/src/cache/types.ts +0 -98
  82. package/src/client.rsc.tsx +1 -22
  83. package/src/client.tsx +14 -38
  84. package/src/component-utils.ts +19 -0
  85. package/src/deps/ssr.ts +0 -1
  86. package/src/handle.ts +28 -18
  87. package/src/handles/MetaTags.tsx +0 -14
  88. package/src/handles/meta.ts +0 -39
  89. package/src/host/cookie-handler.ts +0 -36
  90. package/src/host/errors.ts +0 -24
  91. package/src/host/index.ts +6 -0
  92. package/src/host/pattern-matcher.ts +7 -50
  93. package/src/host/router.ts +1 -65
  94. package/src/host/testing.ts +40 -27
  95. package/src/host/types.ts +6 -2
  96. package/src/href-client.ts +0 -4
  97. package/src/index.rsc.ts +42 -3
  98. package/src/index.ts +31 -1
  99. package/src/internal-debug.ts +2 -4
  100. package/src/loader.rsc.ts +19 -9
  101. package/src/loader.ts +12 -4
  102. package/src/network-error-thrower.tsx +1 -6
  103. package/src/outlet-provider.tsx +1 -5
  104. package/src/prerender/param-hash.ts +10 -11
  105. package/src/prerender/store.ts +23 -30
  106. package/src/prerender.ts +58 -3
  107. package/src/root-error-boundary.tsx +1 -19
  108. package/src/route-content-wrapper.tsx +1 -44
  109. package/src/route-definition/dsl-helpers.ts +7 -19
  110. package/src/route-definition/helpers-types.ts +3 -3
  111. package/src/route-definition/redirect.ts +11 -1
  112. package/src/route-map-builder.ts +0 -16
  113. package/src/router/basename.ts +14 -0
  114. package/src/router/content-negotiation.ts +0 -13
  115. package/src/router/error-handling.ts +12 -16
  116. package/src/router/find-match.ts +4 -30
  117. package/src/router/intercept-resolution.ts +10 -1
  118. package/src/router/lazy-includes.ts +1 -57
  119. package/src/router/loader-resolution.ts +3 -2
  120. package/src/router/logging.ts +0 -6
  121. package/src/router/manifest.ts +1 -25
  122. package/src/router/match-api.ts +0 -20
  123. package/src/router/match-context.ts +0 -22
  124. package/src/router/match-handlers.ts +57 -58
  125. package/src/router/match-middleware/background-revalidation.ts +0 -7
  126. package/src/router/match-middleware/cache-lookup.ts +1 -54
  127. package/src/router/match-middleware/cache-store.ts +0 -31
  128. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  129. package/src/router/match-middleware/segment-resolution.ts +0 -21
  130. package/src/router/match-pipelines.ts +1 -42
  131. package/src/router/match-result.ts +1 -52
  132. package/src/router/metrics.ts +0 -34
  133. package/src/router/middleware-cookies.ts +0 -13
  134. package/src/router/middleware-types.ts +0 -115
  135. package/src/router/middleware.ts +7 -30
  136. package/src/router/navigation-snapshot.ts +0 -51
  137. package/src/router/params-util.ts +23 -0
  138. package/src/router/pattern-matching.ts +1 -33
  139. package/src/router/prerender-match.ts +33 -45
  140. package/src/router/request-classification.ts +1 -38
  141. package/src/router/revalidation.ts +5 -58
  142. package/src/router/router-context.ts +0 -26
  143. package/src/router/router-interfaces.ts +7 -0
  144. package/src/router/router-options.ts +30 -0
  145. package/src/router/segment-resolution/fresh.ts +25 -57
  146. package/src/router/segment-resolution/helpers.ts +34 -0
  147. package/src/router/segment-resolution/loader-cache.ts +10 -13
  148. package/src/router/segment-resolution/revalidation.ts +5 -42
  149. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  150. package/src/router/segment-resolution.ts +4 -1
  151. package/src/router/state-cookie-name.ts +33 -0
  152. package/src/router/telemetry-otel.ts +0 -20
  153. package/src/router/telemetry.ts +96 -19
  154. package/src/router/timeout.ts +0 -20
  155. package/src/router/trie-matching.ts +63 -40
  156. package/src/router/types.ts +1 -63
  157. package/src/router/url-params.ts +0 -5
  158. package/src/router.ts +40 -9
  159. package/src/rsc/handler.ts +14 -2
  160. package/src/rsc/helpers.ts +34 -0
  161. package/src/rsc/origin-guard.ts +0 -12
  162. package/src/rsc/progressive-enhancement.ts +4 -1
  163. package/src/rsc/rsc-rendering.ts +4 -7
  164. package/src/rsc/runtime-warnings.ts +14 -0
  165. package/src/rsc/server-action.ts +30 -28
  166. package/src/rsc/types.ts +2 -1
  167. package/src/runtime-env.ts +18 -0
  168. package/src/search-params.ts +0 -16
  169. package/src/segment-loader-promise.ts +14 -2
  170. package/src/segment-system.tsx +79 -88
  171. package/src/server/cookie-store.ts +52 -1
  172. package/src/server/handle-store.ts +7 -24
  173. package/src/server/loader-registry.ts +5 -24
  174. package/src/server/request-context.ts +74 -77
  175. package/src/ssr/index.tsx +14 -14
  176. package/src/static-handler.ts +10 -13
  177. package/src/testing/cache-status.ts +119 -0
  178. package/src/testing/collect-handle.ts +40 -0
  179. package/src/testing/dispatch.ts +581 -0
  180. package/src/testing/dom.entry.ts +22 -0
  181. package/src/testing/e2e/fixture.ts +188 -0
  182. package/src/testing/e2e/index.ts +127 -0
  183. package/src/testing/e2e/matchers.ts +35 -0
  184. package/src/testing/e2e/page-helpers.ts +272 -0
  185. package/src/testing/e2e/parity.ts +387 -0
  186. package/src/testing/e2e/server.ts +195 -0
  187. package/src/testing/flight-matchers.ts +97 -0
  188. package/src/testing/flight-normalize.ts +11 -0
  189. package/src/testing/flight-runtime.d.ts +57 -0
  190. package/src/testing/flight-tree.ts +682 -0
  191. package/src/testing/flight.entry.ts +52 -0
  192. package/src/testing/flight.ts +186 -0
  193. package/src/testing/generated-routes.ts +183 -0
  194. package/src/testing/index.ts +98 -0
  195. package/src/testing/internal/context.ts +348 -0
  196. package/src/testing/internal/flight-client-globals.ts +30 -0
  197. package/src/testing/internal/seed-vars.ts +54 -0
  198. package/src/testing/render-handler.ts +311 -0
  199. package/src/testing/render-route.tsx +504 -0
  200. package/src/testing/run-loader.ts +378 -0
  201. package/src/testing/run-middleware.ts +205 -0
  202. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  203. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  204. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  205. package/src/testing/vitest-stubs/version.ts +5 -0
  206. package/src/testing/vitest.ts +305 -0
  207. package/src/theme/ThemeProvider.tsx +0 -52
  208. package/src/theme/ThemeScript.tsx +0 -6
  209. package/src/theme/constants.ts +0 -12
  210. package/src/theme/index.ts +0 -7
  211. package/src/theme/theme-context.ts +1 -5
  212. package/src/theme/theme-script.ts +0 -14
  213. package/src/theme/use-theme.ts +0 -3
  214. package/src/types/boundaries.ts +0 -35
  215. package/src/types/error-types.ts +25 -89
  216. package/src/types/global-namespace.ts +15 -15
  217. package/src/types/handler-context.ts +16 -13
  218. package/src/types/index.ts +0 -10
  219. package/src/types/request-scope.ts +0 -19
  220. package/src/types/route-config.ts +6 -50
  221. package/src/types/route-entry.ts +0 -6
  222. package/src/types/segments.ts +0 -13
  223. package/src/urls/include-helper.ts +0 -4
  224. package/src/urls/index.ts +0 -6
  225. package/src/urls/path-helper-types.ts +2 -2
  226. package/src/urls/path-helper.ts +0 -54
  227. package/src/urls/urls-function.ts +0 -13
  228. package/src/use-loader.tsx +0 -186
  229. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  230. package/src/vite/discovery/discover-routers.ts +6 -7
  231. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  232. package/src/vite/plugin-types.ts +3 -1
  233. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  234. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  235. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  236. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  237. package/src/vite/plugins/expose-action-id.ts +2 -73
  238. package/src/vite/plugins/expose-id-utils.ts +0 -55
  239. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  240. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  241. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  242. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  243. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  244. package/src/vite/plugins/performance-tracks.ts +0 -3
  245. package/src/vite/plugins/use-cache-transform.ts +0 -36
  246. package/src/vite/plugins/version-injector.ts +0 -20
  247. package/src/vite/plugins/version-plugin.ts +1 -49
  248. package/src/vite/plugins/virtual-entries.ts +0 -15
  249. package/src/vite/rango.ts +1 -108
  250. package/src/vite/router-discovery.ts +2 -1
  251. package/src/vite/utils/ast-handler-extract.ts +0 -16
  252. package/src/vite/utils/bundle-analysis.ts +6 -13
  253. package/src/vite/utils/client-chunks.ts +0 -6
  254. package/src/vite/utils/forward-user-plugins.ts +0 -22
  255. package/src/vite/utils/manifest-utils.ts +0 -4
  256. package/src/vite/utils/package-resolution.ts +1 -73
  257. package/src/vite/utils/prerender-utils.ts +0 -35
  258. package/src/vite/utils/shared-utils.ts +3 -35
  259. package/src/browser/react/use-client-cache.ts +0 -58
  260. package/src/browser/shallow.ts +0 -40
@@ -39,10 +39,6 @@ import {
39
39
  runAndValidateUseItems,
40
40
  } from "../route-definition/dsl-helpers.js";
41
41
 
42
- /**
43
- * Apply URL prefix to a pattern
44
- * Handles edge cases like "/" patterns and double slashes
45
- */
46
42
  function applyUrlPrefix(prefix: string, pattern: string): string {
47
43
  if (!prefix) return pattern;
48
44
  if (pattern === "/") return prefix;
@@ -52,29 +48,17 @@ function applyUrlPrefix(prefix: string, pattern: string): string {
52
48
  return prefix + pattern;
53
49
  }
54
50
 
55
- /**
56
- * Apply name prefix to a route name
57
- */
58
51
  function applyNamePrefix(prefix: string | undefined, name: string): string {
59
52
  if (!prefix) return name;
60
53
  return `${prefix}.${name}`;
61
54
  }
62
55
 
63
- /**
64
- * Resolve response type from path options (set by path.json(), path.text(), etc.)
65
- */
66
56
  function resolveResponseType(
67
57
  options: PathOptions | undefined,
68
58
  ): string | undefined {
69
59
  return options?.[RESPONSE_TYPE];
70
60
  }
71
61
 
72
- /**
73
- * Create path() helper
74
- *
75
- * The path() function is the key new feature - it combines URL pattern
76
- * with handler at the definition site.
77
- */
78
62
  export function createPathHelper<TEnv>(): PathFn<TEnv> {
79
63
  return ((
80
64
  pattern: string,
@@ -91,8 +75,6 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
91
75
  "path() cannot be used inside parallel()",
92
76
  );
93
77
 
94
- // Walk the parent chain to prevent path() nested under another path(),
95
- // even when separated by intermediate layouts (e.g. path(layout(path())))
96
78
  {
97
79
  let ancestor = ctx.parent;
98
80
  while (ancestor) {
@@ -104,7 +86,6 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
104
86
  }
105
87
  }
106
88
 
107
- // Determine options and use based on argument types
108
89
  let options: PathOptions | undefined;
109
90
  let use: (() => UseItems<RouteUseItem>) | undefined;
110
91
 
@@ -117,49 +98,29 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
117
98
  use = maybeUse;
118
99
  }
119
100
 
120
- // Merge handler.use() defaults with explicit use()
121
- // Response routes (path.json, path.text, etc.) only allow middleware + cache
122
101
  const handlerUseFn = resolveHandlerUse(handler);
123
102
  const mountSite = resolveResponseType(options) ? "response" : "path";
124
103
  const mergedUse = mergeHandlerUse(handlerUseFn, use, mountSite);
125
104
 
126
- // Get prefixes from context (set by include())
127
105
  const urlPrefix = getUrlPrefix();
128
106
  const namePrefix = getNamePrefix();
129
107
 
130
- // Apply URL prefix to pattern
131
108
  const prefixedPattern = applyUrlPrefix(urlPrefix, pattern);
132
109
 
133
- // Generate route name - use provided name or generate from pattern
134
110
  const localName =
135
111
  options?.name || `$path_${pattern.replace(/[/:*?]/g, "_")}`;
136
112
  if (options?.name) {
137
113
  validateUserRouteName(options.name);
138
114
  }
139
- // Apply name prefix if set (from include())
140
115
  const routeName = applyNamePrefix(namePrefix, localName);
141
116
 
142
117
  const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${routeName}`;
143
118
 
144
- // Per-request pruning: skip registration for routes that won't be rendered.
145
- // forRoute is set by loadManifest() to the matched route name. During
146
- // evaluateLazyEntry() (route matching), forRoute is unset so all routes
147
- // register normally. We still increment counters to keep shortCodes stable
148
- // across different routes (needed for segment reconciliation on navigation).
149
- //
150
- // include() does not need its own forRoute pruning. include() creates lazy
151
- // entries that defer handler execution until route matching. When the lazy
152
- // handler eventually runs inside loadManifest(), this path() check already
153
- // covers all routes defined inside the include.
154
119
  if (ctx.forRoute && routeName !== ctx.forRoute) {
155
120
  store.getShortCode("route");
156
121
  return { type: "route" } as RouteItem;
157
122
  }
158
123
 
159
- // Ensure handler is always a function (wrap ReactNode or extract from prerender/static def)
160
- // For prerender stubs (production builds where handler code is evicted),
161
- // handler.handler is undefined — provide a notFound fallback so requests
162
- // for non-prerendered params get 404 instead of "handler is not a function".
163
124
  const wrappedHandler: Handler<any, any, TEnv> =
164
125
  typeof handler === "function"
165
126
  ? (handler as Handler<any, any, TEnv>)
@@ -190,7 +151,6 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
190
151
  type: "route" as const,
191
152
  parent: ctx.parent,
192
153
  handler: wrappedHandler,
193
- // Store the PREFIXED pattern for route matching
194
154
  pattern: prefixedPattern,
195
155
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
196
156
  ...(isPassthroughHandler(handler)
@@ -217,29 +177,23 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
217
177
  : {}),
218
178
  };
219
179
 
220
- // Capture namespace prefix on static handler for build-time reverse() resolution
221
180
  if (isStaticHandler(handler) && handler.$$id && ctx.namePrefix) {
222
181
  (handler as any).$$routePrefix = ctx.namePrefix;
223
182
  }
224
183
 
225
- // Check for duplicate route names (TypeScript should catch this, but runtime check too)
226
184
  invariant(
227
185
  ctx.manifest.get(routeName) === undefined,
228
186
  `Duplicate route name: ${routeName} at ${namespace}`,
229
187
  );
230
188
 
231
- // Register route entry with prefixed name
232
189
  ctx.manifest.set(routeName, entry);
233
190
 
234
- // Register root-scope flag for dot-local reverse resolution
235
191
  registerRouteRootScope(routeName, getRootScoped());
236
192
 
237
- // Also store pattern in a separate map for URL generation
238
193
  if (ctx.patterns) {
239
194
  ctx.patterns.set(routeName, prefixedPattern);
240
195
  }
241
196
 
242
- // Store pattern grouped by URL prefix for separate entry creation
243
197
  if (ctx.patternsByPrefix) {
244
198
  const urlPrefix = getUrlPrefix() || "";
245
199
  if (!ctx.patternsByPrefix.has(urlPrefix)) {
@@ -248,12 +202,10 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
248
202
  ctx.patternsByPrefix.get(urlPrefix)!.set(routeName, prefixedPattern);
249
203
  }
250
204
 
251
- // Store trailing slash config if specified
252
205
  if (options?.trailingSlash && ctx.trailingSlash) {
253
206
  ctx.trailingSlash.set(routeName, options.trailingSlash);
254
207
  }
255
208
 
256
- // Store search schema if specified
257
209
  if (options?.search) {
258
210
  if (ctx.searchSchemas) {
259
211
  ctx.searchSchemas.set(routeName, options.search);
@@ -261,7 +213,6 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
261
213
  registerSearchSchema(routeName, options.search);
262
214
  }
263
215
 
264
- // Run merged use callback (handler.use defaults + explicit use) if present
265
216
  if (mergedUse) {
266
217
  const result = runAndValidateUseItems(
267
218
  store,
@@ -278,10 +229,6 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
278
229
  }) as PathFn<TEnv>;
279
230
  }
280
231
 
281
- /**
282
- * Attach response type tag methods (.json, .text, .html, .xml, .md, .image, .stream, .any) to a path helper.
283
- * Each tag wraps the original path() call with the RESPONSE_TYPE option set.
284
- */
285
232
  export function attachPathResponseTags<TEnv>(
286
233
  pathFn: PathFn<TEnv>,
287
234
  ): PathFn<TEnv> & {
@@ -303,7 +250,6 @@ export function attachPathResponseTags<TEnv>(
303
250
  ) => {
304
251
  let options: PathOptions;
305
252
  let use: (() => any[]) | undefined;
306
-
307
253
  if (typeof optionsOrUse === "function") {
308
254
  options = { [RESPONSE_TYPE]: responseType };
309
255
  use = optionsOrUse;
@@ -34,24 +34,18 @@ export function urls<
34
34
  >(
35
35
  builder: (helpers: PathHelpers<TEnv>) => TItems,
36
36
  ): UrlPatterns<TEnv, ExtractRoutes<TItems>, ExtractResponses<TItems>> {
37
- // Create the handler function that will be called by the router
38
37
  const handler = () => {
39
38
  invariant(
40
39
  typeof builder === "function",
41
40
  "urls() expects a builder function as its argument",
42
41
  );
43
42
 
44
- // Get base helpers from the existing route-definition module
45
43
  const baseHelpers = createRouteHelpers<any, TEnv>();
46
44
 
47
- // Create the path helper (with .json, .text, .html, .xml, .image, .stream, .any tags)
48
45
  const pathHelper = attachPathResponseTags(createPathHelper<TEnv>());
49
46
 
50
- // Create the include helper
51
47
  const includeHelper = createIncludeHelper<TEnv>();
52
48
 
53
- // Combine all helpers
54
- // Note: layout and cache are cast to their typed versions - phantom types don't affect runtime
55
49
  const helpers: PathHelpers<TEnv> = {
56
50
  path: pathHelper as any,
57
51
  include: includeHelper as any,
@@ -69,20 +63,13 @@ export function urls<
69
63
  transition: baseHelpers.transition as PathHelpers<TEnv>["transition"],
70
64
  };
71
65
 
72
- // Execute builder directly - manifest.ts handles RootLayout wrapping
73
- // for inline handlers (non-Promise results).
74
- // For nested include() calls, routes inherit the outer RootLayout.
75
66
  const builderResult = builder(helpers).flat(3) as AllUseItems[];
76
67
  return processItems(builderResult);
77
68
  };
78
69
 
79
- // trailingSlash config is populated when handler() runs
80
- // We expose it via a getter that reads from the context after handler execution
81
70
  return {
82
71
  handler,
83
72
  get trailingSlash() {
84
- // Get the trailingSlash map from the current context
85
- // This will be populated after handler() is called
86
73
  const store = getContext();
87
74
  const ctx = store.context.getStore();
88
75
  if (!ctx?.trailingSlash) {
@@ -15,14 +15,6 @@ import { OutletContext, type OutletContextValue } from "./outlet-context.js";
15
15
  import { loaderStore, type LoaderEntry } from "./loader-store.js";
16
16
  import type { LoaderDefinition, LoadOptions } from "./types.js";
17
17
 
18
- /**
19
- * A shareable GET — a `load()` call that reads data (GET or defaulted method)
20
- * with no request body. Params are allowed. This is the gate for keyed sharing:
21
- * when a hook is given an explicit `key`, every shareable GET writes to the
22
- * keyed bucket so co-keyed readers (including parameterized views) refresh
23
- * together. Non-GET methods and body-bearing calls are mutations and stay local
24
- * to the call site.
25
- */
26
18
  function isShareableGet(options: LoadOptions | undefined): boolean {
27
19
  if (!options) return true;
28
20
  if (options.method && options.method !== "GET") return false;
@@ -32,44 +24,14 @@ function isShareableGet(options: LoadOptions | undefined): boolean {
32
24
  return true;
33
25
  }
34
26
 
35
- /**
36
- * Plain route-context refetch — a `load()` call with no options or a
37
- * trivially-defaulted GET (no params, no body). Results from these are
38
- * broadcast to every component reading the same loader id via the shared
39
- * store, so a layout's refetch button updates page + parallel-slot reads
40
- * automatically.
41
- *
42
- * Calls with explicit `params`, an explicit non-GET method, or a `body`
43
- * stay local to the call site — that preserves the today-semantics of
44
- * `useFetchLoader(SearchLoader).load({ params: { q } })` style code where
45
- * each component owns its own fetched view. (An explicit `key` opts a
46
- * parameterized GET back into sharing; see `isShareableGet`.)
47
- */
48
27
  function isPlainRefetch(options: LoadOptions | undefined): boolean {
49
28
  if (!isShareableGet(options)) return false;
50
29
  if (options?.params && Object.keys(options.params).length > 0) return false;
51
30
  return true;
52
31
  }
53
32
 
54
- // Per-hook unique suffix for grouped reads that have no explicit `key`. Such a
55
- // read must NOT share the bare `loader.$$id` bucket, or a cross-loader group
56
- // refresh would leak into unrelated unkeyed readers of the same loader (which
57
- // the contract keeps local). Sharing within a group is opt-in via an explicit
58
- // `key`; without one, each grouped read gets its own private bucket. The value
59
- // is only ever used as a client-side store bucket key (never rendered), so the
60
- // counter has no SSR/hydration consistency requirement.
61
33
  let privateGroupBucketSeq = 0;
62
34
 
63
- /**
64
- * Extract a specific loader's data from a content ReactNode.
65
- *
66
- * When a route registers loaders via loader(), the resolved data lives in
67
- * the route's OutletProvider (rendered as <Outlet /> content). Parallel
68
- * slots are siblings of <Outlet />, so they can't find it by walking
69
- * the parent context chain. This helper traverses wrapper elements
70
- * (MountContextProvider, ViewTransition, etc.) to reach the OutletProvider
71
- * and extract the loader data directly.
72
- */
73
35
  const NOT_FOUND = Symbol("not-found");
74
36
 
75
37
  function extractContentLoaderData(
@@ -85,10 +47,6 @@ function extractContentLoaderData(
85
47
  return props.loaderData[loaderId];
86
48
  }
87
49
 
88
- // LoaderBoundary: loaderIds + loaderDataPromise (already resolved array).
89
- // When the segment has loading(), loaderData is resolved inside
90
- // LoaderBoundary via use(). If the promise was pre-awaited (forceAwait
91
- // or isAction), the prop is a raw array we can index into.
92
50
  if (
93
51
  props.loaderIds &&
94
52
  Array.isArray(props.loaderIds) &&
@@ -98,7 +56,6 @@ function extractContentLoaderData(
98
56
  const idx = (props.loaderIds as string[]).indexOf(loaderId);
99
57
  if (idx !== -1) {
100
58
  const data = (props.loaderDataPromise as any[])[idx];
101
- // loaderDataPromise entries may be { ok, data } result objects
102
59
  if (data && typeof data === "object" && "ok" in data) {
103
60
  return data.ok ? data.data : NOT_FOUND;
104
61
  }
@@ -106,118 +63,45 @@ function extractContentLoaderData(
106
63
  }
107
64
  }
108
65
 
109
- // Traverse into wrapper elements (MountContextProvider, ViewTransition,
110
- // Suspense wrappers, etc.)
111
66
  if (props.children) return extractContentLoaderData(props.children, loaderId);
112
67
  return NOT_FOUND;
113
68
  }
114
69
 
115
- /**
116
- * Payload returned by loader RSC requests
117
- */
118
70
  interface LoaderRscPayload<T = unknown> {
119
71
  loaderResult: T;
120
72
  loaderError?: { message: string; name: string };
121
73
  }
122
74
 
123
- /**
124
- * Load function type for fetching loader data from the client
125
- */
126
75
  export type LoadFunction<T> = (options?: LoadOptions) => Promise<T>;
127
76
 
128
- /**
129
- * Result type for useLoader hook (strict - data is required)
130
- */
131
77
  export interface UseLoaderResult<T> {
132
- /** The loaded data - guaranteed to exist when loader is registered on route */
133
78
  data: T;
134
- /** True while a load() is in progress */
135
79
  isLoading: boolean;
136
- /** Error from the most recent load attempt, null if successful */
137
80
  error: Error | null;
138
- /** Function to trigger a fetch (only works if loader is fetchable) */
139
81
  load: LoadFunction<T>;
140
- /** Alias for load */
141
82
  refetch: LoadFunction<T>;
142
83
  }
143
84
 
144
- /**
145
- * Result type for useFetchLoader hook (flexible - data is optional)
146
- */
147
85
  export interface UseFetchLoaderResult<T> {
148
- /** The loaded data - may be undefined if not yet fetched or not in context */
149
86
  data: T | undefined;
150
- /** True while a load() is in progress */
151
87
  isLoading: boolean;
152
- /** Error from the most recent load attempt, null if successful */
153
88
  error: Error | null;
154
- /** Function to trigger a fetch (only works if loader is fetchable) */
155
89
  load: LoadFunction<T>;
156
- /** Alias for load */
157
90
  refetch: LoadFunction<T>;
158
91
  }
159
92
 
160
- /**
161
- * Options for useLoader hook
162
- */
163
93
  export interface UseLoaderOptions {
164
- /**
165
- * If true (default), errors from load() will be thrown to the nearest error boundary.
166
- * If false, errors are only captured in the `error` state.
167
- * @default true
168
- */
169
94
  throwOnError?: boolean;
170
- /**
171
- * Client refresh key. Partitions the shared refresh store so that only hooks
172
- * using the same `key` refresh together when one of them calls `load()`.
173
- *
174
- * Without a `key` (default), a plain `load()` on a route-registered loader
175
- * broadcasts to every reader of that loader, and any parameterized / unregistered
176
- * load stays local to the calling hook. With a `key`:
177
- * - readers of the same loader that share a `key` form one refresh group —
178
- * a `load()` from any of them updates the whole group, including
179
- * parameterized GETs;
180
- * - readers with different keys are independent;
181
- * - it works even when the loader is NOT registered on the route (keyed
182
- * `useFetchLoader`), letting unrelated components opt into sharing.
183
- *
184
- * This is a client-side refresh identity only. It is unrelated to the server
185
- * `cache({ key })` option and to `revalidate()`; it never changes the request
186
- * sent to the server.
187
- */
188
95
  key?: string;
189
- /**
190
- * Cross-loader refresh group tag(s). Tag reads of DIFFERENT loaders with a
191
- * shared name, then call `useRefreshLoaders()(name)` to refresh the whole group
192
- * at once. Pass an array to tag one read into several groups — it is refreshed
193
- * when ANY of its groups is refreshed, so a coarse tag can cover the whole set
194
- * while a finer tag targets a subset. Each member is refreshed with a plain GET
195
- * against the current route URL — no params, no body, no mutation methods —
196
- * because a group spans heterogeneous loaders with different param/return
197
- * shapes.
198
- *
199
- * For parameterized sharing of a SINGLE loader, use `key` instead; group
200
- * members should be registered or non-parameterized-keyed reads (a plain-GET
201
- * group refresh would drop any per-call params).
202
- */
203
96
  refreshGroup?: string | string[];
204
97
  }
205
98
 
206
- /**
207
- * Internal hook implementation shared by useLoader and useFetchLoader
208
- */
209
99
  function useLoaderInternal<T>(
210
100
  loader: LoaderDefinition<T>,
211
101
  options?: UseLoaderOptions,
212
102
  ): UseFetchLoaderResult<T> {
213
103
  const context = useContext(OutletContext);
214
104
 
215
- // Get data from context (SSR/navigation). `hasContextData` distinguishes
216
- // "loader registered on the route, value happens to be undefined" from
217
- // "loader is not in any parent's context at all". The shared store is
218
- // only consulted when the loader really is in route context — that
219
- // preserves per-component isolation for ad-hoc useFetchLoader callers
220
- // who use the same fetchable loader without registering it.
221
105
  const { contextData, hasContextData } = useMemo((): {
222
106
  contextData: T | undefined;
223
107
  hasContextData: boolean;
@@ -230,9 +114,6 @@ function useLoaderInternal<T>(
230
114
  hasContextData: true,
231
115
  };
232
116
  }
233
- // Check content element — the route's OutletProvider is rendered as
234
- // <Outlet /> content (a child), so its loaderData isn't in the parent
235
- // chain. Parallel slots need to reach into it to find route-level loaders.
236
117
  const contentData = extractContentLoaderData(
237
118
  current.content,
238
119
  loader.$$id,
@@ -245,23 +126,8 @@ function useLoaderInternal<T>(
245
126
  return { contextData: undefined, hasContextData: false };
246
127
  }, [context, loader.$$id]);
247
128
 
248
- // Shared subscription: every component reading the same loader id sees
249
- // the same snapshot, so a plain refetch from one component propagates to
250
- // the others. Mirrors the convention used by useParams / useLinkStatus —
251
- // useState seeded from the store, useEffect subscribes for updates and
252
- // calls setState inside startTransition so subscriber re-renders don't
253
- // trip Suspense fallbacks during a refetch (matches the per-hook
254
- // startTransition the old code wrapped setFetchedData in).
255
129
  const loaderId = loader.$$id;
256
- // Client refresh key. The shared store is partitioned by bucket key so that
257
- // only hooks with the same `key` refresh together. Default (no key) keeps the
258
- // historical behavior: one bucket per loader id.
259
130
  const key = options?.key;
260
- // Normalize the refresh-group tag(s) to a stable, deduped, sorted list. The
261
- // joined `groupKey` string is the subscribe effect's dependency, so passing an
262
- // inline array literal (`refreshGroup={["a", "b"]}`) does not force a
263
- // resubscribe on every render. An empty list means "no groups" — identical to
264
- // omitting the option (`hasGroups` stays false, no private bucket is created).
265
131
  const refreshGroupOption = options?.refreshGroup;
266
132
  const groupKey =
267
133
  refreshGroupOption === undefined
@@ -276,10 +142,6 @@ function useLoaderInternal<T>(
276
142
  [groupKey],
277
143
  );
278
144
  const hasGroups = groupList.length > 0;
279
- // A grouped reader with no explicit key gets a private per-hook bucket so a
280
- // cross-loader group refresh cannot leak into the bare `loader.$$id` bucket
281
- // shared by unrelated unkeyed readers. Sharing within a group is opt-in via
282
- // an explicit `key`.
283
145
  const privateBucketIdRef = useRef<string | null>(null);
284
146
  if (hasGroups && key === undefined && privateBucketIdRef.current === null) {
285
147
  privateBucketIdRef.current = `__rg${privateGroupBucketSeq++}`;
@@ -289,12 +151,6 @@ function useLoaderInternal<T>(
289
151
  const bucketKey =
290
152
  effectiveKey === undefined ? loaderId : `${loaderId}::${effectiveKey}`;
291
153
 
292
- // Plain-GET refresh thunk registered with the store for cross-loader group
293
- // refresh (useRefreshLoaders). Always shares into this hook's bucket, never
294
- // touches lastSharedRequestIdRef (so a group refresh never render-throws —
295
- // errors surface via `error` and reject the refreshGroups() promise instead),
296
- // and sends no params/body. Stable across navigations (depends only on
297
- // loaderId + bucketKey), so the store keeps one current thunk per bucket.
298
154
  const groupRefetch = useCallback(async (): Promise<void> => {
299
155
  if (!loaderId) return;
300
156
  const requestId = loaderStore.reserveRequestId(bucketKey);
@@ -333,9 +189,6 @@ function useLoaderInternal<T>(
333
189
  ? sharedState.snapshot
334
190
  : loaderStore.getSnapshot(bucketKey);
335
191
  useEffect(() => {
336
- // Sync any value the store committed between this hook's lazy
337
- // initializer and effect-time (e.g. a sibling that mounted earlier
338
- // already triggered a load()).
339
192
  const initial = loaderStore.getSnapshot(bucketKey);
340
193
  if (initial !== sharedSnapshot) {
341
194
  startTransition(() => {
@@ -430,10 +283,6 @@ function useLoaderInternal<T>(
430
283
 
431
284
  const throwOnError = options?.throwOnError ?? true;
432
285
 
433
- // Refs for values used inside load() that should NOT cause callback identity
434
- // churn. loader.$$id can change if a reusable component receives a different
435
- // loader without remounting; data changes on every navigation. Refs keep the
436
- // callback stable while always reading the latest values.
437
286
  const loaderIdRef = useRef(loaderId);
438
287
  loaderIdRef.current = loaderId;
439
288
  const bucketKeyRef = useRef(bucketKey);
@@ -443,8 +292,6 @@ function useLoaderInternal<T>(
443
292
  const hasContextDataRef = useRef(hasContextData);
444
293
  hasContextDataRef.current = hasContextData;
445
294
 
446
- // Load function for fetching data via the ?_rsc_loader endpoint.
447
- // Supports GET (data fetching) and POST/PUT/PATCH/DELETE (mutations).
448
295
  const load = useCallback(
449
296
  async (loadOptions?: LoadOptions): Promise<T> => {
450
297
  const id = loaderIdRef.current;
@@ -455,20 +302,8 @@ function useLoaderInternal<T>(
455
302
  }
456
303
 
457
304
  const bucket = bucketKeyRef.current;
458
- // A dedicated bucket means this read owns a bucket distinct from the bare
459
- // loader id — either an explicit `key` (`$$id::key`) or a refreshGroup's
460
- // private bucket (`$$id::<private>`).
461
305
  const hasDedicatedBucket = bucket !== id;
462
306
 
463
- // Deciding shared vs local:
464
- // - With a dedicated bucket, every shareable GET (params allowed) writes
465
- // to that bucket — the key/group is an explicit opt-in to sharing, and
466
- // a direct load() must land in the same bucket a group refresh uses.
467
- // - On the bare loader-id bucket, sharing is only correct when the
468
- // loader is registered on the route and the call is a plain refetch —
469
- // otherwise two unrelated components calling load() on the same
470
- // fetchable loader would overwrite each other's local view.
471
- // Mutations (non-GET / body) stay local in both cases.
472
307
  const shared = hasDedicatedBucket
473
308
  ? isShareableGet(loadOptions)
474
309
  : isPlainRefetch(loadOptions) && hasContextDataRef.current;
@@ -477,9 +312,6 @@ function useLoaderInternal<T>(
477
312
  if (shared) {
478
313
  sharedRequestId = loaderStore.reserveRequestId(bucket);
479
314
  lastSharedRequestIdRef.current = sharedRequestId;
480
- // beginRequest flips loading on AND clears any prior error so a
481
- // throwOnError: false consumer doesn't keep showing the stale
482
- // error during the retry. Gated on requestId === latest.
483
315
  loaderStore.beginRequest(bucket, sharedRequestId);
484
316
  } else {
485
317
  localRequestId = ++localRequestIdRef.current;
@@ -505,8 +337,6 @@ function useLoaderInternal<T>(
505
337
  loadOptions?.params && Object.keys(loadOptions.params).length > 0;
506
338
 
507
339
  if (bodyValue instanceof FormData) {
508
- // FormData body — send as multipart/form-data (preserves File objects).
509
- // Params are appended as a JSON string in a special field.
510
340
  if (hasParams) {
511
341
  bodyValue.set(
512
342
  "_rsc_loader_params",
@@ -519,7 +349,6 @@ function useLoaderInternal<T>(
519
349
  body: bodyValue,
520
350
  };
521
351
  } else {
522
- // JSON body — send params and body as JSON
523
352
  const bodyPayload: {
524
353
  params?: Record<string, string>;
525
354
  body?: unknown;
@@ -541,7 +370,6 @@ function useLoaderInternal<T>(
541
370
  };
542
371
  }
543
372
  } else {
544
- // GET - send params in query string
545
373
  if (
546
374
  loadOptions?.params &&
547
375
  Object.keys(loadOptions.params).length > 0
@@ -571,12 +399,8 @@ function useLoaderInternal<T>(
571
399
 
572
400
  const result = payload.loaderResult;
573
401
  if (shared) {
574
- // finishData is gated on requestId; a stale response is dropped.
575
402
  loaderStore.finishData(bucket, sharedRequestId, result);
576
403
  } else if (localRequestId === localRequestIdRef.current) {
577
- // Local-branch gate, mirrors the shared-branch requestId check:
578
- // if a newer load() was issued from this hook before this one
579
- // resolved, drop the stale result.
580
404
  startTransition(() => {
581
405
  setLocalFetchedData({ has: true, value: result });
582
406
  setLocalIsLoading(false);
@@ -594,12 +418,9 @@ function useLoaderInternal<T>(
594
418
  if (throwOnError) {
595
419
  throw err;
596
420
  }
597
- // When throwOnError is false, return the latest data snapshot (previous
598
- // successful value or undefined). Caller should check error state.
599
421
  return dataRef.current as T;
600
422
  } finally {
601
423
  if (shared) {
602
- // setLoading is gated; only the latest request flips the flag off.
603
424
  loaderStore.setLoading(bucket, sharedRequestId, false);
604
425
  }
605
426
  }
@@ -607,13 +428,6 @@ function useLoaderInternal<T>(
607
428
  [throwOnError],
608
429
  );
609
430
 
610
- // Throw during render if there's an error and throwOnError is true.
611
- // - Local errors always belong to this hook, so always throw on opt-in.
612
- // - Shared errors throw only when this hook initiated the failing
613
- // request (entry.requestId matches lastSharedRequestIdRef). Sibling
614
- // readers expose the error via `error` but do not throw, so a
615
- // throwOnError: true reader never explodes because of someone else's
616
- // throwOnError: false load() failure.
617
431
  if (throwOnError) {
618
432
  if (localError) throw localError;
619
433
  if (
@@ -9,6 +9,7 @@ import { resolve } from "node:path";
9
9
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
10
10
  import { evictHandlerCode } from "../utils/bundle-analysis.js";
11
11
  import { copyStagedBuildAssets } from "../utils/prerender-utils.js";
12
+ import { jsonParseExpression } from "../utils/manifest-utils.js";
12
13
  import type { DiscoveryState } from "./state.js";
13
14
 
14
15
  /**
@@ -104,7 +105,7 @@ export function postprocessBundle(state: DiscoveryState): void {
104
105
  }
105
106
 
106
107
  const manifestCode = [
107
- `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
108
+ `const m=${jsonParseExpression(manifestMap)};`,
108
109
  `export function loadPrerenderAsset(s){return import(s)}`,
109
110
  `export default m;`,
110
111
  "",
@@ -243,20 +243,19 @@ export async function discoverRouters(
243
243
  // Flatten prefix tree leaf nodes into precomputed entries.
244
244
  // Leaf nodes (no children) can have their routes used directly by
245
245
  // evaluateLazyEntry() without running the handler at runtime.
246
+ // Walk once into a per-router array, then fold it into the merged array;
247
+ // the merged and per-router entries are identical, so a second walk is
248
+ // redundant. Append order is preserved within and across routers.
249
+ const routerPrecomputed: PrecomputedEntry[] = [];
246
250
  flattenLeafEntries(
247
251
  manifest.prefixTree,
248
252
  manifest.routeManifest,
249
- newMergedPrecomputedEntries,
253
+ routerPrecomputed,
250
254
  );
255
+ newMergedPrecomputedEntries.push(...routerPrecomputed);
251
256
 
252
257
  // Store per-router manifest and precomputed entries for isolated virtual modules.
253
258
  newPerRouterManifestDataMap.set(id, manifest.routeManifest);
254
- const routerPrecomputed: PrecomputedEntry[] = [];
255
- flattenLeafEntries(
256
- manifest.prefixTree,
257
- manifest.routeManifest,
258
- routerPrecomputed,
259
- );
260
259
  newPerRouterPrecomputedMap.set(id, routerPrecomputed);
261
260
 
262
261
  console.log(