@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
@@ -90,6 +90,34 @@ export interface HandlerErrorEvent extends BaseEvent {
90
90
  params?: Record<string, string>;
91
91
  }
92
92
 
93
+ /**
94
+ * Per-segment (or coarse route-level) cache status carried on the
95
+ * cache.decision telemetry event and the X-Rango-Cache debug header.
96
+ *
97
+ * v1 is COARSE: the router's pipeline tracks cache decisions at the
98
+ * route/entry level (cacheHit/cacheSource/shouldRevalidate), not per
99
+ * individual segment. The `segments` array therefore contains a single
100
+ * route-level entry keyed by the route key. The shape is forward-compatible
101
+ * with genuine per-segment status if the pipeline later exposes it.
102
+ */
103
+ export type CacheSegmentStatus =
104
+ | "hit"
105
+ | "miss"
106
+ | "stale"
107
+ | "prerendered"
108
+ | "passthrough";
109
+
110
+ export interface CacheSegmentSignal {
111
+ /** Segment id (v1: the route key, since status is route-level). */
112
+ id: string;
113
+ /** Segment type (v1: "route" for the coarse route-level entry). */
114
+ type: string;
115
+ /** Resolved cache status for this segment. */
116
+ cacheStatus: CacheSegmentStatus;
117
+ /** Whether stale-while-revalidate was triggered for this segment. */
118
+ shouldRevalidate?: boolean;
119
+ }
120
+
93
121
  export interface CacheDecisionEvent extends BaseEvent {
94
122
  type: "cache.decision";
95
123
  pathname: string;
@@ -98,6 +126,12 @@ export interface CacheDecisionEvent extends BaseEvent {
98
126
  /** Whether stale-while-revalidate was triggered */
99
127
  shouldRevalidate: boolean;
100
128
  source?: "runtime" | "prerender";
129
+ /**
130
+ * Optional per-segment (v1: coarse route-level) cache status. Present only
131
+ * when telemetry or the debug cache signal is enabled. Optional so existing
132
+ * sinks are unaffected.
133
+ */
134
+ segments?: CacheSegmentSignal[];
101
135
  }
102
136
 
103
137
  export interface RevalidationDecisionEvent extends BaseEvent {
@@ -140,6 +174,71 @@ export type TelemetryEvent =
140
174
  | RequestTimeoutEvent
141
175
  | OriginCheckRejectedEvent;
142
176
 
177
+ // ---------------------------------------------------------------------------
178
+ // Cache signal derivation (coarse, route-level)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ /**
182
+ * Derive the coarse, route-level cache status from pipeline cache state.
183
+ *
184
+ * v1 mapping (route-level — see CacheSegmentSignal):
185
+ * - prerender hit -> "prerendered"
186
+ * - runtime hit + shouldRevalidate (SWR) -> "stale"
187
+ * - runtime hit -> "hit"
188
+ * - no hit -> "miss"
189
+ *
190
+ * Note: "passthrough" is a build-time prerender concept (a route opts out of
191
+ * being prerendered for some params). At runtime a passthrough route renders
192
+ * fresh and is indistinguishable from a normal miss in the pipeline state, so
193
+ * v1 reports it as "miss". The "passthrough" status remains in the type union
194
+ * for forward compatibility.
195
+ */
196
+ export function deriveCacheStatus(state: {
197
+ cacheHit: boolean;
198
+ cacheSource?: "runtime" | "prerender";
199
+ shouldRevalidate?: boolean;
200
+ }): CacheSegmentStatus {
201
+ if (state.cacheHit) {
202
+ if (state.cacheSource === "prerender") return "prerendered";
203
+ if (state.shouldRevalidate) return "stale";
204
+ return "hit";
205
+ }
206
+ return "miss";
207
+ }
208
+
209
+ /**
210
+ * Build the coarse route-level cache signal array (a single entry keyed by
211
+ * the route key). Used for both the cache.decision telemetry event and the
212
+ * X-Rango-Cache debug header.
213
+ */
214
+ export function buildCacheSignalSegments(
215
+ routeKey: string,
216
+ state: {
217
+ cacheHit: boolean;
218
+ cacheSource?: "runtime" | "prerender";
219
+ shouldRevalidate?: boolean;
220
+ },
221
+ ): CacheSegmentSignal[] {
222
+ return [
223
+ {
224
+ id: routeKey,
225
+ type: "route",
226
+ cacheStatus: deriveCacheStatus(state),
227
+ shouldRevalidate: !!state.shouldRevalidate,
228
+ },
229
+ ];
230
+ }
231
+
232
+ /**
233
+ * Serialize cache signal segments into the X-Rango-Cache header value:
234
+ * `<segId>=<status>, <segId2>=<status2>`.
235
+ */
236
+ export function formatCacheSignalHeader(
237
+ segments: CacheSegmentSignal[],
238
+ ): string {
239
+ return segments.map((s) => `${s.id}=${s.cacheStatus}`).join(", ");
240
+ }
241
+
143
242
  // ---------------------------------------------------------------------------
144
243
  // Sink interface
145
244
  // ---------------------------------------------------------------------------
@@ -98,6 +98,14 @@ export interface SegmentResolutionDeps<TEnv = any> {
98
98
  ) => ReactNode | NotFoundBoundaryHandler | null;
99
99
  notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
100
100
  callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
101
+ /**
102
+ * Router-level default for the per-segment `transition({ viewTransition })`
103
+ * flag, from createRouter({ viewTransition }). Resolved into each segment's
104
+ * transition config during resolution (only `false` is stamped) so the render
105
+ * gate reads the boundary decision off the segment on both server and client.
106
+ * Undefined is treated as "auto" (wrap).
107
+ */
108
+ viewTransitionDefault?: "auto" | false;
101
109
  }
102
110
 
103
111
  /**
package/src/router.ts CHANGED
@@ -24,7 +24,7 @@ import { urls } from "./urls.js";
24
24
  import {
25
25
  type EntryData,
26
26
  getContext,
27
- RSCRouterContext,
27
+ RangoContext,
28
28
  type MetricsStore,
29
29
  } from "./server/context";
30
30
  import { createHandleStore, type HandleStore } from "./server/handle-store.js";
@@ -56,6 +56,7 @@ import { buildDebugManifest } from "./router/debug-manifest.js";
56
56
 
57
57
  import type { SegmentResolutionDeps, MatchApiDeps } from "./router/types.js";
58
58
  import { createHandlerContext } from "./router/handler-context.js";
59
+ import { normalizeBasename } from "./router/basename.js";
59
60
  import {
60
61
  setupLoaderAccess,
61
62
  setupLoaderAccessSilent,
@@ -90,13 +91,10 @@ import {
90
91
  RouterRegistry,
91
92
  nextRouterAutoId,
92
93
  } from "./router/router-registry.js";
94
+ import type { RangoOptions, RootLayoutProps } from "./router/router-options.js";
93
95
  import type {
94
- RSCRouterOptions,
95
- RootLayoutProps,
96
- } from "./router/router-options.js";
97
- import type {
98
- RSCRouter,
99
- RSCRouterInternal,
96
+ Rango,
97
+ RangoInternal,
100
98
  RouterRequestInput,
101
99
  } from "./router/router-interfaces.js";
102
100
 
@@ -115,22 +113,22 @@ import {
115
113
  // Re-export public types and values from extracted modules
116
114
  export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
117
115
  export type {
118
- RSCRouterOptions,
116
+ RangoOptions,
119
117
  RootLayoutProps,
120
118
  SSRStreamMode,
121
119
  SSROptions,
122
120
  ResolveStreamingContext,
123
121
  } from "./router/router-options.js";
124
122
  export type {
125
- RSCRouter,
126
- RSCRouterInternal,
123
+ Rango,
124
+ RangoInternal,
127
125
  RouterRequestInput,
128
126
  } from "./router/router-interfaces.js";
129
127
  export { toInternal } from "./router/router-interfaces.js";
130
128
 
131
129
  export function createRouter<TEnv = any>(
132
- options: RSCRouterOptions<TEnv> = {},
133
- ): RSCRouter<TEnv, {}> {
130
+ options: RangoOptions<TEnv> = {},
131
+ ): Rango<TEnv, {}> {
134
132
  const {
135
133
  id: userProvidedId,
136
134
  $$id: injectedId,
@@ -158,14 +156,23 @@ export function createRouter<TEnv = any>(
158
156
  timeouts: timeoutsOption,
159
157
  onTimeout,
160
158
  originCheck: originCheckOption,
159
+ viewTransition: viewTransitionOption = "auto",
160
+ debugCacheSignal: debugCacheSignalOption = false,
161
161
  } = options;
162
162
 
163
+ // Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
164
+ // debugCacheSignal option OR the RANGO_TEST_SIGNALS=1 env flag. When off,
165
+ // no X-Rango-Cache header is emitted and output is byte-identical.
166
+ const cacheSignalEnabled =
167
+ debugCacheSignalOption ||
168
+ (typeof process !== "undefined" &&
169
+ (process as { env?: Record<string, string | undefined> }).env
170
+ ?.RANGO_TEST_SIGNALS === "1");
171
+
163
172
  // Normalize basename: ensure leading slash, strip trailing slash.
164
- // A bare "/" is equivalent to no basename.
165
- const basename =
166
- basenameOption && basenameOption.replace(/^\/+|\/+$/g, "")
167
- ? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
168
- : undefined;
173
+ // A bare "/" is equivalent to no basename. Shared with the testing
174
+ // primitives via normalizeBasename so they can never drift.
175
+ const basename = normalizeBasename(basenameOption);
169
176
 
170
177
  // Resolve telemetry sink (no-op when not configured)
171
178
  const telemetry = resolveSink(telemetrySink);
@@ -537,6 +544,7 @@ export function createRouter<TEnv = any>(
537
544
  findNearestNotFoundBoundary,
538
545
  notFoundComponent: notFound,
539
546
  callOnError,
547
+ viewTransitionDefault: viewTransitionOption,
540
548
  };
541
549
 
542
550
  // Match API dependencies
@@ -664,6 +672,7 @@ export function createRouter<TEnv = any>(
664
672
  findMatch,
665
673
  findInterceptForRoute,
666
674
  telemetry: telemetrySink,
675
+ cacheSignalEnabled,
667
676
  });
668
677
 
669
678
  const { match, matchPartial, matchError, previewMatch } = matchHandlers;
@@ -673,7 +682,7 @@ export function createRouter<TEnv = any>(
673
682
  * The type system tracks accumulated routes through the builder chain
674
683
  * Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
675
684
  */
676
- const router: RSCRouterInternal<TEnv, {}> = {
685
+ const router: RangoInternal<TEnv, {}> = {
677
686
  __brand: RSC_ROUTER_BRAND,
678
687
  id: routerId,
679
688
  basename,
@@ -721,7 +730,7 @@ export function createRouter<TEnv = any>(
721
730
  };
722
731
 
723
732
  let handlerResult: AllUseItems[] = [];
724
- RSCRouterContext.run(
733
+ RangoContext.run(
725
734
  {
726
735
  manifest,
727
736
  patterns: routePatterns,
@@ -999,6 +1008,13 @@ export function createRouter<TEnv = any>(
999
1008
  // Expose basename for runtime manifest generation
1000
1009
  __basename: basename,
1001
1010
 
1011
+ // Expose router-level boundary defaults for build-time clientChunks
1012
+ // discovery (so a "use client" default boundary lands in app-fallback).
1013
+ // These are createRouter options, never pushed onto EntryData.
1014
+ __defaultErrorBoundary: defaultErrorBoundary,
1015
+ __defaultNotFoundBoundary: defaultNotFoundBoundary,
1016
+ __notFound: notFound,
1017
+
1002
1018
  // RSC request handler (lazily created on first call)
1003
1019
  fetch: (() => {
1004
1020
  // Handler is created on first call and reused
@@ -1045,9 +1061,9 @@ export function createRouter<TEnv = any>(
1045
1061
 
1046
1062
  // If urls option was provided, auto-register them
1047
1063
  if (typeof urlsOption === "function") {
1048
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1064
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1049
1065
  } else if (urlsOption) {
1050
- return router.routes(urlsOption) as RSCRouter<TEnv, {}>;
1066
+ return router.routes(urlsOption) as Rango<TEnv, {}>;
1051
1067
  }
1052
1068
 
1053
1069
  return router;
@@ -6,14 +6,14 @@
6
6
  * RSC rendering) so they can be standalone modules without closure coupling.
7
7
  */
8
8
 
9
- import type { RSCRouterInternal } from "../router/router-interfaces.js";
9
+ import type { RangoInternal } from "../router/router-interfaces.js";
10
10
  import type { ErrorPhase } from "../types.js";
11
11
  import type { InvokeOnErrorContext } from "../router/error-handling.js";
12
12
  import type { RSCDependencies, LoadSSRModule } from "./types.js";
13
13
  import type { SSRStreamMode } from "../router/router-options.js";
14
14
 
15
15
  export interface HandlerContext<TEnv = unknown> {
16
- router: RSCRouterInternal<TEnv, any>;
16
+ router: RangoInternal<TEnv, any>;
17
17
  version: string;
18
18
  renderToReadableStream: RSCDependencies["renderToReadableStream"];
19
19
  decodeReply: RSCDependencies["decodeReply"];
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { createElement } from "react";
11
- import { RouteNotFoundError } from "../errors.js";
11
+ import { isRouteNotFoundError } from "../errors.js";
12
12
  import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
13
13
  import {
14
14
  runWithRequestContext,
@@ -66,7 +66,10 @@ import {
66
66
  type ActionContinuation,
67
67
  } from "./server-action.js";
68
68
  import { handleLoaderFetch } from "./loader-fetch.js";
69
- import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
69
+ import {
70
+ checkRequestOrigin,
71
+ ORIGIN_CHECK_PHASE_BY_MODE,
72
+ } from "./origin-guard.js";
70
73
  import { handleRscRendering } from "./rsc-rendering.js";
71
74
  import {
72
75
  withTimeout,
@@ -83,6 +86,7 @@ import {
83
86
  startSSRSetup,
84
87
  getSSRSetup,
85
88
  mayNeedSSR,
89
+ isRscRequest,
86
90
  SSR_SETUP_VAR,
87
91
  } from "./ssr-setup.js";
88
92
  import {
@@ -597,10 +601,7 @@ export function createRSCHandler<
597
601
  routerId: router.id,
598
602
  });
599
603
  } catch (error) {
600
- if (
601
- error instanceof RouteNotFoundError ||
602
- (error instanceof Error && error.name === "RouteNotFoundError")
603
- ) {
604
+ if (isRouteNotFoundError(error)) {
604
605
  // Let the render path handle 404 — match()/matchPartial() will
605
606
  // re-throw RouteNotFoundError and the catch block in
606
607
  // executeRenderWithMiddleware renders the not-found page.
@@ -651,14 +652,7 @@ export function createRSCHandler<
651
652
  }
652
653
 
653
654
  // ---- 3. Origin guard (gate for action/loader/PE modes) ----
654
- const originPhase: OriginCheckPhase | null =
655
- plan.mode === "action"
656
- ? "action"
657
- : plan.mode === "loader"
658
- ? "loader"
659
- : plan.mode === "pe-render"
660
- ? "pe-form"
661
- : null;
655
+ const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
662
656
  if (originPhase) {
663
657
  const originResult = await checkRequestOrigin(
664
658
  request,
@@ -925,47 +919,17 @@ export function createRSCHandler<
925
919
  );
926
920
  }
927
921
 
928
- // ---- Full render / Partial render (or PE that fell through) ----
929
- if (plan.mode === "full-render" || plan.mode === "partial-render") {
930
- const isPartial = plan.mode === "partial-render";
931
- return executeRenderWithMiddleware(
932
- plan.route.routeMiddleware,
933
- plan.negotiated,
934
- plan.route.routeKey,
935
- routeReverse,
936
- request,
937
- env,
938
- url,
939
- variables,
940
- nonce,
941
- handleStore,
942
- isPartial,
943
- );
944
- }
945
-
946
- // PE that fell through (handleProgressiveEnhancement returned null)
947
- // falls back to full render
948
- if (plan.mode === "pe-render") {
949
- return executeRenderWithMiddleware(
950
- plan.route.routeMiddleware,
951
- false,
952
- plan.route.routeKey,
953
- routeReverse,
954
- request,
955
- env,
956
- url,
957
- variables,
958
- nonce,
959
- handleStore,
960
- false,
961
- );
962
- }
963
-
964
- // Redirect plan that wasn't handled above (full-page redirect — let
965
- // the pipeline handle it via match() which returns { redirect: url })
922
+ // Full render, partial render, fallen-through PE, and full-page redirect all
923
+ // render through the same middleware-wrapped path. Only full/partial-render
924
+ // carry negotiation + the partial flag; pe/redirect render plainly.
925
+ const isPartial = plan.mode === "partial-render";
926
+ const negotiated =
927
+ plan.mode === "full-render" || plan.mode === "partial-render"
928
+ ? plan.negotiated
929
+ : false;
966
930
  return executeRenderWithMiddleware(
967
931
  plan.route.routeMiddleware,
968
- false,
932
+ negotiated,
969
933
  plan.route.routeKey,
970
934
  routeReverse,
971
935
  request,
@@ -974,7 +938,7 @@ export function createRSCHandler<
974
938
  variables,
975
939
  nonce,
976
940
  handleStore,
977
- false,
941
+ isPartial,
978
942
  );
979
943
  }
980
944
 
@@ -1054,10 +1018,7 @@ export function createRSCHandler<
1054
1018
  }
1055
1019
 
1056
1020
  // Render 404 page for unmatched routes
1057
- const isRouteNotFound =
1058
- error instanceof RouteNotFoundError ||
1059
- (error instanceof Error && error.name === "RouteNotFoundError");
1060
- if (isRouteNotFound) {
1021
+ if (isRouteNotFoundError(error)) {
1061
1022
  callOnError(error, "routing", {
1062
1023
  request,
1063
1024
  url,
@@ -1104,13 +1065,7 @@ export function createRSCHandler<
1104
1065
  },
1105
1066
  });
1106
1067
 
1107
- const isRscRequest =
1108
- isPartial ||
1109
- (!request.headers.get("accept")?.includes("text/html") &&
1110
- !url.searchParams.has("__html")) ||
1111
- url.searchParams.has("__rsc");
1112
-
1113
- if (isRscRequest) {
1068
+ if (isRscRequest(request, url, isPartial)) {
1114
1069
  return createResponseWithMergedHeaders(rscStream, {
1115
1070
  status: 404,
1116
1071
  headers: { "content-type": "text/x-component;charset=utf-8" },
@@ -10,7 +10,26 @@ import {
10
10
  } from "../server/request-context.js";
11
11
  import type { RequestContext } from "../server/request-context.js";
12
12
  import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
13
+ import { isRedirectResponse } from "../response-utils.js";
13
14
  import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
15
+ import { formatCacheSignalHeader } from "../router/telemetry.js";
16
+
17
+ /**
18
+ * DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
19
+ * match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
20
+ * header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
21
+ * attached — output is byte-identical to the default. Header mutation failures
22
+ * are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
23
+ */
24
+ function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
25
+ const signal = ctx._cacheSignal;
26
+ if (!signal || signal.length === 0) return;
27
+ try {
28
+ target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
29
+ } catch {
30
+ // Headers immutable — skip.
31
+ }
32
+ }
14
33
 
15
34
  /**
16
35
  * Copy stub headers from the request context onto a target Headers instance:
@@ -84,6 +103,7 @@ export function createResponseWithMergedHeaders(
84
103
  const mergedHeaders = new Headers(init.headers);
85
104
  applyStubHeaders(mergedHeaders, ctx.res.headers);
86
105
  ctx.res.headers.delete("set-cookie");
106
+ applyCacheSignalHeader(mergedHeaders, ctx);
87
107
 
88
108
  // ctx.res.status overrides init.status when explicitly set (e.g. 404 for
89
109
  // notFound, 500 for error). Default ctx.res.status is 200.
@@ -145,10 +165,10 @@ export function interceptRedirectForPartial(
145
165
  locationState?: Record<string, unknown>,
146
166
  ) => Response,
147
167
  ): Response | null {
148
- const redirectUrl = response.headers.get("Location");
149
- if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
168
+ if (!isRedirectResponse(response)) {
150
169
  return null;
151
170
  }
171
+ const redirectUrl = response.headers.get("Location")!;
152
172
  const locationState = getLocationState();
153
173
  let intercepted: Response;
154
174
  if (locationState) {
package/src/rsc/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RSC Router - RSC Entry Point
2
+ * Rango - RSC Entry Point
3
3
  *
4
4
  * This module provides RSC utilities for server-side rendering,
5
5
  * server actions, loader fetching, and progressive enhancement.
@@ -9,11 +9,29 @@
9
9
  * navigations, bookmarks, and non-browser clients don't send Origin.
10
10
  */
11
11
 
12
+ import type { RequestPlan } from "../router/request-classification.js";
13
+
12
14
  /**
13
15
  * Request phase that triggered the origin check.
14
16
  */
15
17
  export type OriginCheckPhase = "action" | "loader" | "pe-form";
16
18
 
19
+ // Exhaustive over RequestPlan modes so a new mode must be classified here (the
20
+ // security gate) instead of silently falling through to no origin check.
21
+ export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
22
+ RequestPlan["mode"],
23
+ OriginCheckPhase | null
24
+ > = {
25
+ action: "action",
26
+ loader: "loader",
27
+ "pe-render": "pe-form",
28
+ "full-render": null,
29
+ "partial-render": null,
30
+ response: null,
31
+ redirect: null,
32
+ "version-mismatch": null,
33
+ };
34
+
17
35
  /**
18
36
  * Context passed to the originCheck callback.
19
37
  */
@@ -116,14 +134,15 @@ export async function checkRequestOrigin<TEnv = any>(
116
134
  // Disabled by explicit opt-out
117
135
  if (config === false) return null;
118
136
 
119
- // Default: built-in validation (config === true or undefined)
120
- if (config === true || config === undefined) {
121
- const allowed = defaultOriginCheck(request, url);
122
- if (allowed) return null;
123
- return createForbiddenResponse(request);
124
- }
137
+ // Default (true/undefined) becomes a callback returning boolean, so the
138
+ // Response|true|reject resolution below is written once.
139
+ const check: (
140
+ ctx: OriginCheckContext<TEnv>,
141
+ ) => boolean | Response | Promise<boolean | Response> =
142
+ config === true || config === undefined
143
+ ? () => defaultOriginCheck(request, url)
144
+ : config;
125
145
 
126
- // Custom function — build context and call
127
146
  const ctx: OriginCheckContext<TEnv> = {
128
147
  request,
129
148
  url,
@@ -133,9 +152,8 @@ export async function checkRequestOrigin<TEnv = any>(
133
152
  defaultCheck: () => defaultOriginCheck(request, url),
134
153
  };
135
154
 
136
- const result = await config(ctx);
155
+ const result = await check(ctx);
137
156
 
138
157
  if (result instanceof Response) return result;
139
- if (result === true) return null;
140
- return createForbiddenResponse(request);
158
+ return result === true ? null : createForbiddenResponse(request);
141
159
  }
@@ -11,6 +11,7 @@ import { requireRequestContext } from "../server/request-context.js";
11
11
  import { contextGet } from "../context-var.js";
12
12
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
13
13
  import { traverseBack } from "../router/pattern-matching.js";
14
+ import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
14
15
  import { createCacheScope } from "../cache/cache-scope.js";
15
16
  import { executeMiddleware } from "../router/middleware.js";
16
17
  import {
@@ -121,13 +122,15 @@ export async function handleResponseRoute<TEnv>(
121
122
  });
122
123
  };
123
124
 
124
- // JSON response routes: wrap in { data } / { error } envelope
125
- if (preview.responseType === "json") {
126
- try {
127
- const result = await (preview.handler as Function)(responseHandlerCtx);
128
- if (result instanceof Response) {
129
- return rewrapResponse(result);
130
- }
125
+ try {
126
+ const result = await (preview.handler as Function)(responseHandlerCtx);
127
+
128
+ if (result instanceof Response) {
129
+ return rewrapResponse(result);
130
+ }
131
+
132
+ // Handled before the MIME lookup (json is also a RESPONSE_TYPE_MIME key).
133
+ if (preview.responseType === "json") {
131
134
  return createResponseWithMergedHeaders(
132
135
  JSON.stringify({ data: result }),
133
136
  {
@@ -135,10 +138,28 @@ export async function handleResponseRoute<TEnv>(
135
138
  headers: { "content-type": "application/json;charset=utf-8" },
136
139
  },
137
140
  );
138
- } catch (error) {
139
- handlerCtx.callOnError(error, "handler", errorCtx);
140
- const isDev = process.env.NODE_ENV !== "production";
141
- const status = error instanceof RouterError ? error.status : 500;
141
+ }
142
+
143
+ // Object.hasOwn (not truthiness) so prototype names like "toString" are not
144
+ // matched; image/stream/any are absent and fall through to the throw.
145
+ if (Object.hasOwn(RESPONSE_TYPE_MIME, preview.responseType)) {
146
+ return createResponseWithMergedHeaders(String(result), {
147
+ status: 200,
148
+ headers: {
149
+ "content-type": `${RESPONSE_TYPE_MIME[preview.responseType]};charset=utf-8`,
150
+ },
151
+ });
152
+ }
153
+
154
+ throw new Error(
155
+ `Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
156
+ );
157
+ } catch (error) {
158
+ handlerCtx.callOnError(error, "handler", errorCtx);
159
+ const isDev = process.env.NODE_ENV !== "production";
160
+ const status = error instanceof RouterError ? error.status : 500;
161
+
162
+ if (preview.responseType === "json") {
142
163
  return createResponseWithMergedHeaders(
143
164
  JSON.stringify({
144
165
  error: createResponseErrorPayload(error, isDev),
@@ -149,48 +170,7 @@ export async function handleResponseRoute<TEnv>(
149
170
  },
150
171
  );
151
172
  }
152
- }
153
-
154
- // Non-JSON response routes: catch errors and return plain Response
155
- try {
156
- const result = await (preview.handler as Function)(responseHandlerCtx);
157
-
158
- if (result instanceof Response) {
159
- return rewrapResponse(result);
160
- }
161
173
 
162
- // Auto-wrap based on response type tag
163
- switch (preview.responseType) {
164
- case "text":
165
- return createResponseWithMergedHeaders(String(result), {
166
- status: 200,
167
- headers: { "content-type": "text/plain;charset=utf-8" },
168
- });
169
- case "html":
170
- return createResponseWithMergedHeaders(String(result), {
171
- status: 200,
172
- headers: { "content-type": "text/html;charset=utf-8" },
173
- });
174
- case "xml":
175
- return createResponseWithMergedHeaders(String(result), {
176
- status: 200,
177
- headers: { "content-type": "application/xml;charset=utf-8" },
178
- });
179
- case "md":
180
- return createResponseWithMergedHeaders(String(result), {
181
- status: 200,
182
- headers: { "content-type": "text/markdown;charset=utf-8" },
183
- });
184
- default:
185
- // image, stream, any -- must return Response
186
- throw new Error(
187
- `Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
188
- );
189
- }
190
- } catch (error) {
191
- handlerCtx.callOnError(error, "handler", errorCtx);
192
- const isDev = process.env.NODE_ENV !== "production";
193
- const status = error instanceof RouterError ? error.status : 500;
194
174
  const message =
195
175
  error instanceof RouterError
196
176
  ? error.message