@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1

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 (214) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +3 -1
  12. package/skills/hooks/SKILL.md +214 -18
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +26 -4
  15. package/skills/layout/SKILL.md +6 -7
  16. package/skills/links/SKILL.md +173 -17
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +5 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +13 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +599 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +294 -0
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +6 -6
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +9 -19
  45. package/src/browser/react/NavigationProvider.tsx +29 -40
  46. package/src/browser/react/index.ts +3 -0
  47. package/src/browser/react/location-state-shared.ts +175 -4
  48. package/src/browser/react/location-state.ts +39 -13
  49. package/src/browser/react/use-handle.ts +17 -9
  50. package/src/browser/react/use-params.ts +3 -4
  51. package/src/browser/react/use-reverse.ts +106 -0
  52. package/src/browser/react/use-router.ts +14 -1
  53. package/src/browser/response-adapter.ts +25 -0
  54. package/src/browser/rsc-router.tsx +30 -16
  55. package/src/browser/scroll-restoration.ts +22 -14
  56. package/src/browser/segment-structure-assert.ts +2 -2
  57. package/src/browser/server-action-bridge.ts +23 -30
  58. package/src/browser/types.ts +2 -0
  59. package/src/build/collect-fallback-refs.ts +107 -0
  60. package/src/build/generate-manifest.ts +60 -35
  61. package/src/build/generate-route-types.ts +2 -0
  62. package/src/build/index.ts +2 -0
  63. package/src/build/route-types/codegen.ts +4 -4
  64. package/src/build/route-types/include-resolution.ts +1 -1
  65. package/src/build/route-types/per-module-writer.ts +7 -4
  66. package/src/build/route-types/router-processing.ts +55 -14
  67. package/src/build/route-types/scan-filter.ts +1 -1
  68. package/src/build/route-types/source-scan.ts +118 -0
  69. package/src/build/runtime-discovery.ts +9 -20
  70. package/src/cache/cache-scope.ts +28 -42
  71. package/src/cache/cf/cf-cache-store.ts +49 -6
  72. package/src/client.rsc.tsx +3 -0
  73. package/src/client.tsx +10 -8
  74. package/src/context-var.ts +5 -5
  75. package/src/decode-loader-results.ts +36 -0
  76. package/src/errors.ts +30 -1
  77. package/src/handle.ts +26 -13
  78. package/src/host/index.ts +2 -2
  79. package/src/host/router.ts +129 -57
  80. package/src/host/types.ts +31 -2
  81. package/src/host/utils.ts +1 -1
  82. package/src/href-client.ts +140 -20
  83. package/src/index.rsc.ts +6 -4
  84. package/src/index.ts +13 -6
  85. package/src/loader-store.ts +500 -0
  86. package/src/loader.rsc.ts +2 -5
  87. package/src/loader.ts +3 -10
  88. package/src/missing-id-error.ts +68 -0
  89. package/src/prerender.ts +4 -4
  90. package/src/response-utils.ts +9 -0
  91. package/src/reverse.ts +65 -41
  92. package/src/route-content-wrapper.tsx +6 -28
  93. package/src/route-definition/dsl-helpers.ts +238 -263
  94. package/src/route-definition/helper-factories.ts +29 -139
  95. package/src/route-definition/helpers-types.ts +37 -14
  96. package/src/route-definition/use-item-types.ts +32 -0
  97. package/src/route-types.ts +19 -41
  98. package/src/router/basename.ts +14 -0
  99. package/src/router/content-negotiation.ts +15 -2
  100. package/src/router/error-handling.ts +1 -1
  101. package/src/router/handler-context.ts +4 -42
  102. package/src/router/intercept-resolution.ts +4 -18
  103. package/src/router/lazy-includes.ts +2 -2
  104. package/src/router/loader-resolution.ts +16 -2
  105. package/src/router/match-handlers.ts +62 -20
  106. package/src/router/match-middleware/cache-lookup.ts +44 -91
  107. package/src/router/match-middleware/cache-store.ts +3 -2
  108. package/src/router/match-result.ts +32 -30
  109. package/src/router/metrics.ts +1 -1
  110. package/src/router/middleware-types.ts +1 -1
  111. package/src/router/middleware.ts +46 -78
  112. package/src/router/prerender-match.ts +1 -1
  113. package/src/router/preview-match.ts +3 -1
  114. package/src/router/request-classification.ts +4 -28
  115. package/src/router/revalidation.ts +43 -1
  116. package/src/router/router-interfaces.ts +45 -28
  117. package/src/router/router-options.ts +40 -1
  118. package/src/router/router-registry.ts +2 -5
  119. package/src/router/segment-resolution/fresh.ts +19 -6
  120. package/src/router/segment-resolution/revalidation.ts +19 -6
  121. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  122. package/src/router/substitute-pattern-params.ts +56 -0
  123. package/src/router/telemetry.ts +99 -0
  124. package/src/router/types.ts +8 -0
  125. package/src/router.ts +37 -21
  126. package/src/rsc/handler-context.ts +2 -2
  127. package/src/rsc/handler.ts +20 -65
  128. package/src/rsc/helpers.ts +22 -2
  129. package/src/rsc/index.ts +1 -1
  130. package/src/rsc/origin-guard.ts +28 -10
  131. package/src/rsc/response-route-handler.ts +32 -52
  132. package/src/rsc/rsc-rendering.ts +27 -53
  133. package/src/rsc/runtime-warnings.ts +9 -10
  134. package/src/rsc/server-action.ts +13 -37
  135. package/src/rsc/ssr-setup.ts +16 -0
  136. package/src/rsc/types.ts +2 -2
  137. package/src/search-params.ts +4 -4
  138. package/src/segment-system.tsx +121 -65
  139. package/src/serialize.ts +243 -0
  140. package/src/server/context.ts +118 -51
  141. package/src/server/cookie-store.ts +28 -4
  142. package/src/server/request-context.ts +10 -0
  143. package/src/static-handler.ts +1 -1
  144. package/src/testing/cache-status.ts +166 -0
  145. package/src/testing/collect-handle.ts +63 -0
  146. package/src/testing/dispatch.ts +440 -0
  147. package/src/testing/dom.entry.ts +22 -0
  148. package/src/testing/e2e/fixture.ts +154 -0
  149. package/src/testing/e2e/index.ts +149 -0
  150. package/src/testing/e2e/matchers.ts +51 -0
  151. package/src/testing/e2e/page-helpers.ts +272 -0
  152. package/src/testing/e2e/parity.ts +306 -0
  153. package/src/testing/e2e/server.ts +183 -0
  154. package/src/testing/flight-matchers.ts +104 -0
  155. package/src/testing/flight-runtime.d.ts +21 -0
  156. package/src/testing/flight.entry.ts +22 -0
  157. package/src/testing/flight.ts +182 -0
  158. package/src/testing/generated-routes.ts +223 -0
  159. package/src/testing/index.ts +105 -0
  160. package/src/testing/internal/context.ts +193 -0
  161. package/src/testing/render-route.tsx +536 -0
  162. package/src/testing/run-loader.ts +296 -0
  163. package/src/testing/run-middleware.ts +170 -0
  164. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  165. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  166. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  167. package/src/testing/vitest-stubs/version.ts +5 -0
  168. package/src/testing/vitest.ts +183 -0
  169. package/src/types/global-namespace.ts +39 -26
  170. package/src/types/handler-context.ts +56 -11
  171. package/src/types/index.ts +1 -0
  172. package/src/types/segments.ts +18 -1
  173. package/src/urls/include-helper.ts +10 -53
  174. package/src/urls/index.ts +0 -3
  175. package/src/urls/path-helper-types.ts +11 -3
  176. package/src/urls/path-helper.ts +17 -52
  177. package/src/urls/pattern-types.ts +36 -19
  178. package/src/urls/response-types.ts +20 -19
  179. package/src/urls/type-extraction.ts +26 -116
  180. package/src/urls/urls-function.ts +1 -5
  181. package/src/use-loader.tsx +413 -42
  182. package/src/vite/debug.ts +1 -0
  183. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  184. package/src/vite/discovery/discover-routers.ts +70 -48
  185. package/src/vite/discovery/discovery-errors.ts +194 -0
  186. package/src/vite/discovery/prerender-collection.ts +19 -25
  187. package/src/vite/discovery/route-types-writer.ts +40 -84
  188. package/src/vite/discovery/state.ts +33 -0
  189. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  190. package/src/vite/index.ts +2 -0
  191. package/src/vite/plugin-types.ts +67 -0
  192. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  193. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  194. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  195. package/src/vite/plugins/expose-action-id.ts +2 -2
  196. package/src/vite/plugins/expose-id-utils.ts +12 -8
  197. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  198. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  199. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  200. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  201. package/src/vite/plugins/performance-tracks.ts +12 -16
  202. package/src/vite/plugins/use-cache-transform.ts +13 -11
  203. package/src/vite/plugins/version-injector.ts +2 -12
  204. package/src/vite/plugins/version-plugin.ts +59 -2
  205. package/src/vite/plugins/virtual-entries.ts +2 -2
  206. package/src/vite/rango.ts +67 -15
  207. package/src/vite/router-discovery.ts +208 -63
  208. package/src/vite/utils/ast-handler-extract.ts +15 -15
  209. package/src/vite/utils/bundle-analysis.ts +4 -2
  210. package/src/vite/utils/client-chunks.ts +190 -0
  211. package/src/vite/utils/forward-user-plugins.ts +193 -0
  212. package/src/vite/utils/manifest-utils.ts +21 -5
  213. package/src/vite/utils/shared-utils.ts +107 -26
  214. package/src/browser/action-response-classifier.ts +0 -99
@@ -11,122 +11,40 @@ import {
11
11
  when,
12
12
  errorBoundary,
13
13
  notFoundBoundary,
14
- loaderFn,
15
- loadingFn,
16
- transitionFn,
17
- routeFn,
14
+ route,
15
+ loader,
16
+ loading,
17
+ transition,
18
18
  } from "./dsl-helpers.js";
19
19
  import RootLayout from "../server/root-layout";
20
20
  import { invariant } from "../errors";
21
21
 
22
- /*
23
- * Create revalidate helper
24
- */
25
- const createRevalidateHelper = <TEnv>(): RouteHelpers<
26
- any,
27
- TEnv
28
- >["revalidate"] => {
29
- return revalidate as RouteHelpers<any, TEnv>["revalidate"];
30
- };
31
-
32
- /**
33
- * Create errorBoundary helper
34
- */
35
- const createErrorBoundaryHelper = <TEnv>(): RouteHelpers<
36
- any,
37
- TEnv
38
- >["errorBoundary"] => {
39
- return errorBoundary as RouteHelpers<any, TEnv>["errorBoundary"];
40
- };
41
-
42
- /**
43
- * Create notFoundBoundary helper
44
- */
45
- const createNotFoundBoundaryHelper = <TEnv>(): RouteHelpers<
46
- any,
47
- TEnv
48
- >["notFoundBoundary"] => {
49
- return notFoundBoundary as RouteHelpers<any, TEnv>["notFoundBoundary"];
50
- };
51
-
52
22
  /**
53
- * Create middleware helper
23
+ * Assemble the RouteHelpers object. The helpers are the DSL functions
24
+ * themselves; the single cast erases the phantom generics (and the extra
25
+ * `route` key) that the per-router RouteHelpers<T, TEnv> type carries but the
26
+ * runtime functions do not.
54
27
  */
55
- const createMiddlewareHelper = <TEnv>(): RouteHelpers<
56
- any,
28
+ function buildRouteHelpers<T extends RouteDefinition, TEnv>(): RouteHelpers<
29
+ T,
57
30
  TEnv
58
- >["middleware"] => {
59
- return middleware as RouteHelpers<any, TEnv>["middleware"];
60
- };
61
-
62
- /**
63
- * Create parallel helper
64
- */
65
- const createParallelHelper = <TEnv>(): RouteHelpers<any, TEnv>["parallel"] => {
66
- return parallel as RouteHelpers<any, TEnv>["parallel"];
67
- };
68
-
69
- /**
70
- * Create intercept helper
71
- */
72
- const createInterceptHelper = <
73
- const T extends RouteDefinition,
74
- TEnv,
75
- >(): RouteHelpers<T, TEnv>["intercept"] => {
76
- return intercept as RouteHelpers<T, TEnv>["intercept"];
77
- };
78
-
79
- /**
80
- * Create loader helper
81
- */
82
- const createLoaderHelper = <TEnv>(): RouteHelpers<any, TEnv>["loader"] => {
83
- return loaderFn as RouteHelpers<any, TEnv>["loader"];
84
- };
85
-
86
- /**
87
- * Create loading helper
88
- */
89
- const createLoadingHelper = (): RouteHelpers<any, any>["loading"] => {
90
- return loadingFn;
91
- };
92
-
93
- /**
94
- * Create route helper
95
- */
96
- const createRouteHelper = <
97
- const T extends RouteDefinition,
98
- TEnv,
99
- >(): RouteHelpers<T, TEnv>["route"] => {
100
- return routeFn as unknown as RouteHelpers<T, TEnv>["route"];
101
- };
102
-
103
- /**
104
- * Create layout helper
105
- */
106
- const createLayoutHelper = <TEnv>(): RouteHelpers<any, TEnv>["layout"] => {
107
- return layout as RouteHelpers<any, TEnv>["layout"];
108
- };
109
-
110
- /**
111
- * Create when helper for intercept conditions
112
- */
113
- const createWhenHelper = (): RouteHelpers<any, any>["when"] => {
114
- return when;
115
- };
116
-
117
- /**
118
- * Create cache helper for cache configuration
119
- */
120
- const createCacheHelper = (): RouteHelpers<any, any>["cache"] => {
121
- return cache;
122
- };
123
-
124
- /**
125
- * Create transition helper
126
- */
127
- const createTransitionHelper = (): RouteHelpers<any, any>["transition"] => {
128
- return transitionFn as RouteHelpers<any, any>["transition"];
129
- };
31
+ > {
32
+ return {
33
+ route,
34
+ layout,
35
+ parallel,
36
+ intercept,
37
+ middleware,
38
+ revalidate,
39
+ loader,
40
+ loading,
41
+ errorBoundary,
42
+ notFoundBoundary,
43
+ when,
44
+ cache,
45
+ transition,
46
+ } as unknown as RouteHelpers<T, TEnv>;
47
+ }
130
48
 
131
49
  /**
132
50
  * Branded type for route handlers that carries the route type info.
@@ -152,21 +70,7 @@ export function map<const T extends RouteDefinition, TEnv = DefaultEnv>(
152
70
  "map() expects a builder function as its argument",
153
71
  );
154
72
  // Create helpers
155
- const helpers: RouteHelpers<T, TEnv> = {
156
- route: createRouteHelper<T, TEnv>(),
157
- layout: createLayoutHelper<TEnv>(),
158
- parallel: createParallelHelper<TEnv>(),
159
- intercept: createInterceptHelper<T, TEnv>(),
160
- middleware: createMiddlewareHelper<TEnv>(),
161
- revalidate: createRevalidateHelper<TEnv>(),
162
- loader: createLoaderHelper<TEnv>(),
163
- loading: createLoadingHelper(),
164
- errorBoundary: createErrorBoundaryHelper<TEnv>(),
165
- notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
166
- when: createWhenHelper(),
167
- cache: createCacheHelper(),
168
- transition: createTransitionHelper(),
169
- };
73
+ const helpers = buildRouteHelpers<T, TEnv>();
170
74
 
171
75
  return [layout(RootLayout, () => builder(helpers))].flat(3);
172
76
  };
@@ -182,19 +86,5 @@ export function createRouteHelpers<
182
86
  T extends RouteDefinition,
183
87
  TEnv,
184
88
  >(): RouteHelpers<T, TEnv> {
185
- return {
186
- route: createRouteHelper<T, TEnv>(),
187
- layout: createLayoutHelper<TEnv>(),
188
- parallel: createParallelHelper<TEnv>(),
189
- intercept: createInterceptHelper<T, TEnv>(),
190
- middleware: createMiddlewareHelper<TEnv>(),
191
- revalidate: createRevalidateHelper<TEnv>(),
192
- loader: createLoaderHelper<TEnv>(),
193
- loading: createLoadingHelper(),
194
- errorBoundary: createErrorBoundaryHelper<TEnv>(),
195
- notFoundBoundary: createNotFoundBoundaryHelper<TEnv>(),
196
- when: createWhenHelper(),
197
- cache: createCacheHelper(),
198
- transition: createTransitionHelper(),
199
- };
89
+ return buildRouteHelpers<T, TEnv>();
200
90
  }
@@ -198,10 +198,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
198
198
  use?: () => UseItems<InterceptUseItem>,
199
199
  ): InterceptItem;
200
200
  // Global: unprefixed, params inferred from global route map
201
- <K extends keyof RSCRouter.GeneratedRouteMap & string>(
201
+ <K extends keyof Rango.GeneratedRouteMap & string>(
202
202
  slotName: `@${string}`,
203
203
  routeName: K,
204
- handler: ReactNode | Handler<K, RSCRouter.GeneratedRouteMap, TEnv>,
204
+ handler: ReactNode | Handler<K, Rango.GeneratedRouteMap, TEnv>,
205
205
  use?: () => UseItems<InterceptUseItem>,
206
206
  ): InterceptItem;
207
207
  };
@@ -250,8 +250,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
250
250
  * )
251
251
  *
252
252
  * // Revalidate after specific actions (actionId format: "path/to/file.ts#exportName")
253
+ * // Use `|| undefined` (defer), not `?? false` (hard short-circuit), so the
254
+ * // chain and the segment default still apply when there is no match.
253
255
  * revalidate(({ actionId }) =>
254
- * actionId?.includes("Cart") ?? false
256
+ * actionId?.includes("Cart") || undefined
255
257
  * )
256
258
  *
257
259
  * // Soft decision (suggest but allow override)
@@ -274,7 +276,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
274
276
  *
275
277
  * // With loader-specific revalidation (match by file or export name)
276
278
  * loader(CartLoader, () => [
277
- * revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
279
+ * revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
278
280
  * ])
279
281
  *
280
282
  * // Consume in client components with useLoader()
@@ -447,10 +449,30 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
447
449
  ): CacheItem;
448
450
  };
449
451
  /**
450
- * Attach a ViewTransition boundary to the current segment or a group of routes
451
- *
452
- * Wraps segment content with React's `<ViewTransition>` component.
453
- * Only takes effect when React experimental is used (no-op on stable React).
452
+ * Opt a route (or group of routes) into transition-driven navigation.
453
+ *
454
+ * `transition()` does two independent things, and you choose how far to go:
455
+ * 1. startTransition (ALL React versions): the navigation commit is driven
456
+ * through React's startTransition, so a same-route nav (same route,
457
+ * different params, e.g. /product/1 -> /product/2) holds the previous
458
+ * content while the new loader resolves instead of flashing the route's
459
+ * loading() skeleton (see segment-system.tsx inTransitionScope). This is
460
+ * also the precondition for any view-transition animation.
461
+ * 2. <ViewTransition> (experimental React only): the segment content is also
462
+ * wrapped in React's <ViewTransition>, so the held swap cross-fades/morphs.
463
+ * Layered on by default; pass { viewTransition: false } to keep #1 without
464
+ * the router boundary (and place your own <ViewTransition> instead).
465
+ *
466
+ * A view transition cannot fire without a startTransition, so the meaningful
467
+ * choices are (see skills/view-transitions for the full matrix):
468
+ * - no transition() -> neither (remount + skeleton)
469
+ * - transition({ viewTransition: false }) -> startTransition only (hold)
470
+ * - transition({}) / transition({ enter… }) -> startTransition + ViewTransition
471
+ *
472
+ * Precedence: a bare transition({}) inherits createRouter({ viewTransition })
473
+ * (default "auto"); an explicit per-route `viewTransition` always wins. So
474
+ * transition({}) is startTransition + ViewTransition under the default and
475
+ * startTransition only when the router sets viewTransition: false.
454
476
  *
455
477
  * ```typescript
456
478
  * // Attach to a single route
@@ -464,13 +486,14 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
464
486
  * path("/about", AboutPage),
465
487
  * ])
466
488
  *
467
- * // Direction-aware transitions
468
- * transition({
469
- * enter: { "navigation": "slide-right", "navigation-back": "slide-left" },
470
- * exit: { "navigation": "slide-left", "navigation-back": "slide-right" },
471
- * })
489
+ * // Hold content + drive view transitions, but place no router boundary:
490
+ * path("/product/:id", ProductPage, { name: "product" }, () => [
491
+ * transition({ viewTransition: false }),
492
+ * ])
472
493
  * ```
473
- * @param config - ViewTransition configuration (enter, exit, update, share, default, name)
494
+ * @param config - ViewTransition configuration (enter, exit, update, share,
495
+ * default, name) plus `viewTransition: "auto" | false` to toggle the router
496
+ * boundary (createRouter({ viewTransition }) sets the app-wide default)
474
497
  * @param children - Optional callback returning child routes to wrap
475
498
  */
476
499
  transition: {
@@ -0,0 +1,32 @@
1
+ import type { AllUseItems, WhenItem } from "../route-types.js";
2
+
3
+ /**
4
+ * The set of valid use-item `type` discriminants — the single runtime source of
5
+ * truth for "is this a well-formed use item?" shape validation.
6
+ *
7
+ * Declared via a `Record<...>` so that adding a member to the union without
8
+ * updating this map is a compile error. `when` is included because when() items
9
+ * are valid inside intercept() even though WhenItem is not part of AllUseItems
10
+ * (it lives only in InterceptUseItem). This is shape validation only; per-mount-
11
+ * site rules remain the narrower hand-written tables in resolve-handler-use.ts.
12
+ */
13
+ const USE_ITEM_TYPES: Record<AllUseItems["type"] | WhenItem["type"], true> = {
14
+ layout: true,
15
+ route: true,
16
+ middleware: true,
17
+ revalidate: true,
18
+ parallel: true,
19
+ intercept: true,
20
+ loader: true,
21
+ loading: true,
22
+ errorBoundary: true,
23
+ notFoundBoundary: true,
24
+ when: true,
25
+ cache: true,
26
+ transition: true,
27
+ include: true,
28
+ };
29
+
30
+ export const ALL_USE_ITEM_TYPES: ReadonlySet<string> = new Set(
31
+ Object.keys(USE_ITEM_TYPES),
32
+ );
@@ -5,47 +5,43 @@
5
5
  */
6
6
 
7
7
  /**
8
- * Branded return types for route helpers
8
+ * Brand for UrlPatterns nominal typing (see pattern-types.ts). The route-item
9
+ * types below are discriminated by their `type` literal, so they carry no brand.
9
10
  */
10
- export declare const LayoutBrand: unique symbol;
11
- export declare const RouteBrand: unique symbol;
12
- export declare const ParallelBrand: unique symbol;
13
- export declare const InterceptBrand: unique symbol;
14
- export declare const MiddlewareBrand: unique symbol;
15
- export declare const RevalidateBrand: unique symbol;
16
- export declare const LoaderBrand: unique symbol;
17
- export declare const LoadingBrand: unique symbol;
18
- export declare const ErrorBoundaryBrand: unique symbol;
19
- export declare const NotFoundBoundaryBrand: unique symbol;
20
- export declare const WhenBrand: unique symbol;
21
- export declare const CacheBrand: unique symbol;
22
- export declare const TransitionBrand: unique symbol;
23
- export declare const IncludeBrand: unique symbol;
24
11
  export declare const UrlPatternsBrand: unique symbol;
25
12
 
26
13
  export type LayoutItem = {
27
14
  name: string;
28
15
  type: "layout";
29
16
  uses?: AllUseItems[];
30
- [LayoutBrand]: void;
31
17
  };
32
18
 
33
19
  /**
34
- * Typed layout item that carries child routes as phantom type
35
- * Used for type inference in urls() API
20
+ * Phantom inference fields attached to wrapper items (layout/cache/transition)
21
+ * so the urls() type extractor can read their child routes/responses. The fields
22
+ * never exist at runtime.
36
23
  */
37
- export type TypedLayoutItem<
24
+ type WithChildren<
25
+ TBase,
38
26
  TChildRoutes extends Record<string, any> = Record<string, string>,
39
27
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
40
- > = LayoutItem & {
28
+ > = TBase & {
41
29
  readonly __childRoutes?: TChildRoutes;
42
30
  readonly __childResponses?: TChildResponses;
43
31
  };
32
+
33
+ /**
34
+ * Typed layout item that carries child routes as phantom type
35
+ * Used for type inference in urls() API
36
+ */
37
+ export type TypedLayoutItem<
38
+ TChildRoutes extends Record<string, any> = Record<string, string>,
39
+ TChildResponses extends Record<string, unknown> = Record<string, unknown>,
40
+ > = WithChildren<LayoutItem, TChildRoutes, TChildResponses>;
44
41
  export type RouteItem = {
45
42
  name: string;
46
43
  type: "route";
47
44
  uses?: AllUseItems[];
48
- [RouteBrand]: void;
49
45
  };
50
46
 
51
47
  /**
@@ -67,64 +63,53 @@ export type ParallelItem = {
67
63
  name: string;
68
64
  type: "parallel";
69
65
  uses?: ParallelUseItem[];
70
- [ParallelBrand]: void;
71
66
  };
72
67
  export type InterceptItem = {
73
68
  name: string;
74
69
  type: "intercept";
75
70
  uses?: InterceptUseItem[];
76
- [InterceptBrand]: void;
77
71
  };
78
72
  export type LoaderItem = {
79
73
  name: string;
80
74
  type: "loader";
81
75
  uses?: LoaderUseItem[];
82
- [LoaderBrand]: void;
83
76
  };
84
77
  export type MiddlewareItem = {
85
78
  name: string;
86
79
  type: "middleware";
87
80
  uses?: AllUseItems[];
88
- [MiddlewareBrand]: void;
89
81
  };
90
82
  export type RevalidateItem = {
91
83
  name: string;
92
84
  type: "revalidate";
93
85
  uses?: AllUseItems[];
94
- [RevalidateBrand]: void;
95
86
  };
96
87
  export type LoadingItem = {
97
88
  name: string;
98
89
  type: "loading";
99
- [LoadingBrand]: void;
100
90
  };
101
91
  export type ErrorBoundaryItem = {
102
92
  name: string;
103
93
  type: "errorBoundary";
104
94
  uses?: AllUseItems[];
105
- [ErrorBoundaryBrand]: void;
106
95
  };
107
96
  export type NotFoundBoundaryItem = {
108
97
  name: string;
109
98
  type: "notFoundBoundary";
110
99
  uses?: AllUseItems[];
111
- [NotFoundBoundaryBrand]: void;
112
100
  };
113
101
  export type WhenItem = {
114
102
  name: string;
115
103
  type: "when";
116
- [WhenBrand]: void;
117
104
  };
118
105
  export type CacheItem = {
119
106
  name: string;
120
107
  type: "cache";
121
108
  uses?: AllUseItems[];
122
- [CacheBrand]: void;
123
109
  };
124
110
  export type TransitionItem = {
125
111
  name: string;
126
112
  type: "transition";
127
- [TransitionBrand]: void;
128
113
  };
129
114
 
130
115
  /**
@@ -134,10 +119,7 @@ export type TransitionItem = {
134
119
  export type TypedTransitionItem<
135
120
  TChildRoutes extends Record<string, any> = Record<string, string>,
136
121
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
137
- > = TransitionItem & {
138
- readonly __childRoutes?: TChildRoutes;
139
- readonly __childResponses?: TChildResponses;
140
- };
122
+ > = WithChildren<TransitionItem, TChildRoutes, TChildResponses>;
141
123
 
142
124
  /**
143
125
  * Typed cache item that carries child routes as phantom type
@@ -146,10 +128,7 @@ export type TypedTransitionItem<
146
128
  export type TypedCacheItem<
147
129
  TChildRoutes extends Record<string, any> = Record<string, string>,
148
130
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
149
- > = CacheItem & {
150
- readonly __childRoutes?: TChildRoutes;
151
- readonly __childResponses?: TChildResponses;
152
- };
131
+ > = WithChildren<CacheItem, TChildRoutes, TChildResponses>;
153
132
 
154
133
  /**
155
134
  * Include item for URL pattern composition (used by urls() API)
@@ -184,7 +163,6 @@ export type IncludeItem = {
184
163
  */
185
164
  includeScope?: string;
186
165
  };
187
- [IncludeBrand]: void;
188
166
  };
189
167
 
190
168
  /**
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalize a router basename to its canonical form: a single leading slash,
3
+ * no trailing slash, and `undefined` for an empty or bare-"/" value.
4
+ *
5
+ * This is the single source of truth used by both createRouter() (so the RSC
6
+ * handler stores a canonical basename on the request context) and the testing
7
+ * primitives (so a consumer can pass the same un-normalized string their
8
+ * createRouter() accepts and observe the same redirect() prefixing).
9
+ */
10
+ export function normalizeBasename(basename?: string): string | undefined {
11
+ if (!basename) return undefined;
12
+ const trimmed = basename.replace(/^\/+|\/+$/g, "");
13
+ return trimmed ? "/" + trimmed : undefined;
14
+ }
@@ -135,8 +135,8 @@ export interface NegotiationResult {
135
135
  manifestEntry: EntryData;
136
136
  /** Route middleware for the winning variant */
137
137
  routeMiddleware: CollectedMiddleware[];
138
- /** Always true negotiation occurred */
139
- negotiated: true;
138
+ /** True when negotiation selected a variant; false for a plain response route. */
139
+ negotiated: boolean;
140
140
  }
141
141
 
142
142
  /**
@@ -155,6 +155,19 @@ export async function negotiateRoute(
155
155
  ): Promise<NegotiationResult | null> {
156
156
  const { matched, manifestEntry, routeMiddleware, responseType } = snapshot;
157
157
  if (!matched.negotiateVariants || matched.negotiateVariants.length === 0) {
158
+ // No variants: a plain response route still yields a result (negotiated:false)
159
+ // so callers don't re-derive it; RSC routes (no responseType/handler) -> null.
160
+ const handler =
161
+ manifestEntry.type === "route" ? manifestEntry.handler : undefined;
162
+ if (responseType && handler) {
163
+ return {
164
+ responseType,
165
+ handler: handler as Function,
166
+ manifestEntry,
167
+ routeMiddleware,
168
+ negotiated: false,
169
+ };
170
+ }
158
171
  return null;
159
172
  }
160
173
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Router Error Handling Utilities
3
3
  *
4
- * Error boundary and not-found boundary handling for RSC Router.
4
+ * Error boundary and not-found boundary handling for Rango.
5
5
  * Also includes the shared invokeOnError utility for error callback invocation.
6
6
  */
7
7
 
@@ -18,7 +18,7 @@ import { isInsideCacheScope } from "../server/context.js";
18
18
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
19
19
  import { isAutoGeneratedRouteName } from "../route-name.js";
20
20
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
21
- import { encodePathSegment } from "./url-params.js";
21
+ import { substitutePatternParams } from "./substitute-pattern-params.js";
22
22
  import { fireAndForgetWaitUntil } from "../types/request-scope.js";
23
23
 
24
24
  /**
@@ -160,52 +160,14 @@ export function createReverseFunction(
160
160
  );
161
161
  }
162
162
 
163
- let result = pattern;
164
-
165
163
  // Merge current request params as defaults, explicit params override
166
164
  const effectiveParams = currentParams
167
165
  ? { ...currentParams, ...hrefParams }
168
166
  : hrefParams;
169
167
 
170
- // Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
171
- // Optional params (:param?) are omitted when not provided
172
- if (effectiveParams) {
173
- let hadOmittedOptional = false;
174
- // First pass: optional params (trailing ?)
175
- result = result.replace(
176
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
177
- (_, key) => {
178
- const value = effectiveParams[key];
179
- // The matcher omits absent optional params (so `value` is
180
- // `undefined` here), but caller-supplied params or `getParams()`
181
- // shapes may still pass `""` explicitly. Treat both as the
182
- // absent form so the segment collapses cleanly.
183
- if (value === undefined || value === "") {
184
- hadOmittedOptional = true;
185
- return "";
186
- }
187
- return encodePathSegment(value);
188
- },
189
- );
190
- // Second pass: required params (no trailing ?)
191
- result = result.replace(
192
- /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
193
- (_, key) => {
194
- const value = effectiveParams[key];
195
- if (value === undefined) {
196
- throw new Error(`Missing param "${key}" for route "${name}"`);
197
- }
198
- return encodePathSegment(value);
199
- },
200
- );
201
- // Clean up slashes only when an optional param was actually omitted,
202
- // so intentional trailing-slash patterns like "/blog/" are preserved.
203
- if (hadOmittedOptional) {
204
- const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
205
- result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
206
- if (hadTrailingSlash && !result.endsWith("/")) result += "/";
207
- }
208
- }
168
+ let result = effectiveParams
169
+ ? substitutePatternParams(pattern, effectiveParams, name)
170
+ : pattern;
209
171
 
210
172
  // Append search params as query string
211
173
  if (search) {
@@ -66,28 +66,14 @@ export function findInterceptForRoute(
66
66
  let current: EntryData | null = fromEntry;
67
67
 
68
68
  while (current) {
69
- if (current.intercept && current.intercept.length > 0) {
70
- for (const intercept of current.intercept) {
69
+ // current first, then its sibling layouts — same order as before.
70
+ for (const source of [current, ...current.layout]) {
71
+ for (const intercept of source.intercept) {
71
72
  if (
72
73
  intercept.routeName === targetRouteKey &&
73
74
  evaluateInterceptWhen(intercept, selectorContext, isAction)
74
75
  ) {
75
- return { intercept, entry: current };
76
- }
77
- }
78
- }
79
-
80
- if (current.layout && current.layout.length > 0) {
81
- for (const siblingLayout of current.layout) {
82
- if (siblingLayout.intercept && siblingLayout.intercept.length > 0) {
83
- for (const intercept of siblingLayout.intercept) {
84
- if (
85
- intercept.routeName === targetRouteKey &&
86
- evaluateInterceptWhen(intercept, selectorContext, isAction)
87
- ) {
88
- return { intercept, entry: siblingLayout };
89
- }
90
- }
76
+ return { intercept, entry: source };
91
77
  }
92
78
  }
93
79
  }
@@ -2,7 +2,7 @@ import { registerRouteMap } from "../route-map-builder.js";
2
2
  import { extractStaticPrefix } from "./pattern-matching.js";
3
3
  import {
4
4
  type EntryData,
5
- RSCRouterContext,
5
+ RangoContext,
6
6
  runWithPrefixes,
7
7
  getIsolatedLazyParent,
8
8
  } from "../server/context";
@@ -131,7 +131,7 @@ export function evaluateLazyEntry<TEnv = any>(
131
131
  }
132
132
  }
133
133
 
134
- RSCRouterContext.run(
134
+ RangoContext.run(
135
135
  {
136
136
  manifest,
137
137
  patterns,