@rangojs/router 0.0.0-experimental.107 → 0.0.0-experimental.109

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 (83) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/rango.js +16 -16
  3. package/dist/vite/index.js +146 -150
  4. package/package.json +6 -6
  5. package/skills/hooks/SKILL.md +2 -0
  6. package/skills/links/SKILL.md +13 -1
  7. package/skills/loader/SKILL.md +1 -1
  8. package/skills/middleware/SKILL.md +3 -3
  9. package/skills/mime-routes/SKILL.md +27 -0
  10. package/skills/prerender/SKILL.md +13 -13
  11. package/skills/rango/SKILL.md +9 -0
  12. package/skills/response-routes/SKILL.md +58 -9
  13. package/skills/router-setup/SKILL.md +3 -3
  14. package/skills/typesafety/SKILL.md +273 -31
  15. package/src/__augment-tests__/augment.ts +81 -0
  16. package/src/__augment-tests__/augmented.check.ts +117 -0
  17. package/src/browser/index.ts +3 -3
  18. package/src/browser/react/location-state-shared.ts +3 -3
  19. package/src/browser/react/use-handle.ts +17 -9
  20. package/src/browser/rsc-router.tsx +14 -14
  21. package/src/browser/segment-structure-assert.ts +2 -2
  22. package/src/build/generate-manifest.ts +3 -3
  23. package/src/build/route-types/codegen.ts +4 -4
  24. package/src/build/route-types/include-resolution.ts +1 -1
  25. package/src/build/route-types/per-module-writer.ts +3 -3
  26. package/src/build/route-types/router-processing.ts +4 -4
  27. package/src/build/route-types/scan-filter.ts +1 -1
  28. package/src/client.tsx +4 -7
  29. package/src/errors.ts +1 -1
  30. package/src/handle.ts +2 -2
  31. package/src/href-client.ts +136 -19
  32. package/src/index.rsc.ts +4 -4
  33. package/src/index.ts +2 -2
  34. package/src/loader.rsc.ts +1 -1
  35. package/src/loader.ts +1 -1
  36. package/src/prerender.ts +4 -4
  37. package/src/route-definition/dsl-helpers.ts +2 -2
  38. package/src/route-definition/helpers-types.ts +2 -2
  39. package/src/router/error-handling.ts +1 -1
  40. package/src/router/lazy-includes.ts +2 -2
  41. package/src/router/metrics.ts +1 -1
  42. package/src/router/middleware-types.ts +1 -1
  43. package/src/router/prerender-match.ts +1 -1
  44. package/src/router/router-interfaces.ts +34 -28
  45. package/src/router/router-options.ts +1 -1
  46. package/src/router/router-registry.ts +2 -5
  47. package/src/router/segment-resolution/fresh.ts +2 -2
  48. package/src/router/segment-resolution/revalidation.ts +2 -2
  49. package/src/router.ts +13 -16
  50. package/src/rsc/handler-context.ts +2 -2
  51. package/src/rsc/index.ts +1 -1
  52. package/src/rsc/types.ts +2 -2
  53. package/src/search-params.ts +4 -4
  54. package/src/serialize.ts +243 -0
  55. package/src/server/context.ts +16 -16
  56. package/src/static-handler.ts +1 -1
  57. package/src/types/global-namespace.ts +39 -26
  58. package/src/types/handler-context.ts +3 -3
  59. package/src/urls/path-helper-types.ts +2 -2
  60. package/src/urls/pattern-types.ts +34 -0
  61. package/src/urls/type-extraction.ts +6 -1
  62. package/src/use-loader.tsx +6 -4
  63. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  64. package/src/vite/discovery/discover-routers.ts +3 -3
  65. package/src/vite/discovery/discovery-errors.ts +1 -1
  66. package/src/vite/discovery/prerender-collection.ts +19 -25
  67. package/src/vite/discovery/route-types-writer.ts +3 -3
  68. package/src/vite/discovery/state.ts +4 -4
  69. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  70. package/src/vite/plugins/expose-action-id.ts +2 -2
  71. package/src/vite/plugins/expose-id-utils.ts +12 -8
  72. package/src/vite/plugins/expose-ids/export-analysis.ts +33 -9
  73. package/src/vite/plugins/expose-internal-ids.ts +1 -1
  74. package/src/vite/plugins/performance-tracks.ts +12 -16
  75. package/src/vite/plugins/use-cache-transform.ts +1 -1
  76. package/src/vite/plugins/version-plugin.ts +2 -2
  77. package/src/vite/plugins/virtual-entries.ts +2 -2
  78. package/src/vite/rango.ts +11 -11
  79. package/src/vite/router-discovery.ts +26 -29
  80. package/src/vite/utils/ast-handler-extract.ts +15 -15
  81. package/src/vite/utils/bundle-analysis.ts +4 -2
  82. package/src/vite/utils/forward-user-plugins.ts +46 -17
  83. package/src/vite/utils/shared-utils.ts +26 -22
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Wire-type serialization transforms.
3
+ *
4
+ * The type a handler or loader returns on the server is frequently NOT the type
5
+ * a client receives after serialization. These transforms model that boundary so
6
+ * consumer-facing types (e.g. `Rango.PathResponse`) describe the wire value, not
7
+ * the source value.
8
+ *
9
+ * Two serializers, two transforms — they are intentionally NOT interchangeable:
10
+ *
11
+ * - `JsonSerialize<T>` models plain `JSON.stringify` (`path.json()` /
12
+ * `fetch().then(r => r.json())`). Lossy: `Date -> string`, `undefined` /
13
+ * functions / symbols dropped, `Map`/`Set` -> `{}`. `bigint` *throws* (no wire
14
+ * value), so it collapses the whole result to `never`. Honors `toJSON()`.
15
+ * - `FlightSerialize<T>` models React RSC Flight (loaders, RSC props, cache).
16
+ * High fidelity: `Date`/`Map`/`Set`/`bigint`/typed arrays/`Promise` are
17
+ * preserved; ordinary functions and non-global symbols do not cross.
18
+ *
19
+ * ## Overriding (full-transform replacement)
20
+ *
21
+ * Because `Rango.JsonSerialize` / `Rango.FlightSerialize` are type *aliases*, TS
22
+ * cannot let you redefine them directly (aliases don't merge). Instead each alias
23
+ * consults a generic override slot — augment it with a single member that is your
24
+ * complete transform. Delegate to the built-in for the cases you don't change:
25
+ *
26
+ * ```ts
27
+ * declare global {
28
+ * namespace Rango {
29
+ * interface FlightSerializeOverride<T> {
30
+ * app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
31
+ * }
32
+ * }
33
+ * }
34
+ * // now Rango.FlightSerialize<Money> is number; everything else is the built-in.
35
+ * ```
36
+ *
37
+ * Provide exactly one member: the slot is read as `Override<T>[keyof Override<T>]`,
38
+ * so multiple members union (and conflict). The built-in recurses through the
39
+ * override-aware alias, so an override applies at every nesting level.
40
+ */
41
+
42
+ import type { ReactNode } from "react";
43
+
44
+ type JsonPrimitive = string | number | boolean | null;
45
+
46
+ type AnyFunction = (...args: never[]) => unknown;
47
+
48
+ // --- JSON ---------------------------------------------------------------------
49
+
50
+ /**
51
+ * Internal marker for a value that makes `JSON.stringify` throw (`bigint`, or a
52
+ * `toJSON()` returning one). Distinct from `never`, which means "omitted":
53
+ * `undefined`/function/symbol-valued keys are dropped, and such array slots
54
+ * become `null`. A throwing value has no valid JSON wire form, so it propagates
55
+ * up through every container and is excluded at the public boundary (`bigint`
56
+ * alone -> `never`; `{ id: bigint }` -> `never`).
57
+ */
58
+ declare const JSON_THROWS: unique symbol;
59
+ type JsonThrows = typeof JSON_THROWS;
60
+
61
+ /** True if union `U` contains the throw marker. */
62
+ type HasThrow<U> = [Extract<U, JsonThrows>] extends [never] ? false : true;
63
+
64
+ /** Map a JSON array/tuple: propagate a throw; else omitted elements become null. */
65
+ type JsonSerializeArray<T extends readonly unknown[]> =
66
+ HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[number]> extends true
67
+ ? JsonThrows
68
+ : {
69
+ [K in keyof T]: [JsonRawResolve<T[K]>] extends [never]
70
+ ? null
71
+ : JsonRawResolve<T[K]>;
72
+ };
73
+
74
+ /** Map a JSON object: propagate a throw; else drop omitted keys. */
75
+ type JsonSerializeObject<T> =
76
+ HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[keyof T]> extends true
77
+ ? JsonThrows
78
+ : {
79
+ [K in keyof T as [JsonRawResolve<T[K]>] extends [never]
80
+ ? never
81
+ : K]: JsonRawResolve<T[K]>;
82
+ };
83
+
84
+ /**
85
+ * Built-in JSON rules, *raw* (may yield the throw marker). Honors `toJSON()` (so
86
+ * `Date -> string` and any class with `toJSON()` serialize correctly), preserves
87
+ * JSON primitives and literals, omits functions / symbols / `undefined`,
88
+ * collapses `Map`/`Set` to `{}`, and marks `bigint` as throwing. Recurses through
89
+ * the override-aware resolver, so registered overrides apply at every level.
90
+ */
91
+ type JsonSerializeBuiltinRaw<T> = T extends {
92
+ toJSON(...args: never[]): infer R;
93
+ }
94
+ ? JsonRawResolve<R>
95
+ : T extends JsonPrimitive
96
+ ? T
97
+ : T extends bigint
98
+ ? JsonThrows
99
+ : T extends AnyFunction
100
+ ? never
101
+ : T extends symbol
102
+ ? never
103
+ : T extends undefined
104
+ ? never
105
+ : T extends readonly unknown[]
106
+ ? JsonSerializeArray<T>
107
+ : T extends ReadonlyMap<unknown, unknown>
108
+ ? {}
109
+ : T extends ReadonlySet<unknown>
110
+ ? {}
111
+ : T extends object
112
+ ? JsonSerializeObject<T>
113
+ : never;
114
+
115
+ /** Override-aware raw JSON resolution (the recursion entry). */
116
+ type JsonRawResolve<T> = [keyof Rango.JsonSerializeOverride<T>] extends [never]
117
+ ? JsonSerializeBuiltinRaw<T>
118
+ : Rango.JsonSerializeOverride<T>[keyof Rango.JsonSerializeOverride<T>];
119
+
120
+ /**
121
+ * Model the result of round-tripping a value through `JSON.stringify` /
122
+ * `JSON.parse`. A registered `Rango.JsonSerializeOverride` replaces the transform
123
+ * wholesale; otherwise the built-in rules apply. Throwing values collapse to
124
+ * `never`.
125
+ */
126
+ export type JsonSerialize<T> = Exclude<JsonRawResolve<T>, JsonThrows>;
127
+
128
+ // --- Flight -------------------------------------------------------------------
129
+
130
+ /**
131
+ * Built-in Flight rules. Mirrors React's `ReactClientValue` contract: primitives
132
+ * including `bigint`, `undefined`, `null`, symbols, `Date`, `ArrayBuffer` and
133
+ * typed-array views, `Map`, `Set`, `FormData`, `Blob`, `Promise`,
134
+ * `ReadableStream`, and (async) iterables are preserved; ordinary functions
135
+ * resolve to `never`. JSX (`ReactNode`, and the async-node union
136
+ * `ReactNode | Promise<ReactNode>`) is preserved as-is via a non-distributive
137
+ * leaf, so handle/loader returns that carry JSX round-trip unchanged. Recurses
138
+ * through the override-aware `FlightSerialize`.
139
+ *
140
+ * The source of truth is React's own contract, which is intentionally NOT
141
+ * semver-stable across RSC framework APIs — this tracks the React version Rango
142
+ * pins. See:
143
+ * https://react.dev/reference/rsc/use-client#serializable-types-returned-by-server-components
144
+ *
145
+ * Type-level limitations (not detectable structurally, so not modeled): class
146
+ * instances and null-prototype objects are rejected by React at runtime but pass
147
+ * here as their structural shape; non-global symbols are rejected at runtime but
148
+ * `symbol` is preserved here; Server Functions would need an override to be
149
+ * distinguished from ordinary functions (which resolve to `never`).
150
+ */
151
+ type FlightSerializeBuiltinRaw<T> = [T] extends [ReactNode | Promise<ReactNode>]
152
+ ? T
153
+ : T extends string | number | boolean | bigint | symbol | null | undefined
154
+ ? T
155
+ : T extends AnyFunction
156
+ ? never
157
+ : T extends Date
158
+ ? Date
159
+ : T extends ArrayBuffer
160
+ ? ArrayBuffer
161
+ : T extends ArrayBufferView
162
+ ? T
163
+ : T extends FormData
164
+ ? FormData
165
+ : T extends Blob
166
+ ? Blob
167
+ : T extends Map<infer K, infer V>
168
+ ? Map<FlightSerialize<K>, FlightSerialize<V>>
169
+ : T extends Set<infer V>
170
+ ? Set<FlightSerialize<V>>
171
+ : T extends Promise<infer V>
172
+ ? Promise<FlightSerialize<V>>
173
+ : T extends ReadableStream<infer V>
174
+ ? ReadableStream<FlightSerialize<V>>
175
+ : T extends readonly unknown[]
176
+ ? { [K in keyof T]: FlightSerialize<T[K]> }
177
+ : T extends AsyncIterable<infer V>
178
+ ? AsyncIterable<FlightSerialize<V>>
179
+ : T extends Iterable<infer V>
180
+ ? Iterable<FlightSerialize<V>>
181
+ : T extends object
182
+ ? { [K in keyof T]: FlightSerialize<T[K]> }
183
+ : never;
184
+
185
+ /**
186
+ * Model React RSC Flight serialization. A registered `Rango.FlightSerializeOverride`
187
+ * replaces the transform wholesale; otherwise the built-in rules apply.
188
+ */
189
+ export type FlightSerialize<T> = [
190
+ keyof Rango.FlightSerializeOverride<T>,
191
+ ] extends [never]
192
+ ? FlightSerializeBuiltinRaw<T>
193
+ : Rango.FlightSerializeOverride<T>[keyof Rango.FlightSerializeOverride<T>];
194
+
195
+ // Module-scoped aliases so the ambient `Rango.*` members below can reference the
196
+ // module-level transforms without the global namespace shadowing the names.
197
+ type GlobalJsonSerialize<T> = JsonSerialize<T>;
198
+ type GlobalJsonSerializeBuiltin<T> = JsonSerializeBuiltinRaw<T>;
199
+ type GlobalFlightSerialize<T> = FlightSerialize<T>;
200
+ type GlobalFlightSerializeBuiltin<T> = FlightSerializeBuiltinRaw<T>;
201
+
202
+ /**
203
+ * Ambient serialization transforms and their override slots on the `Rango`
204
+ * namespace. Available with no import wherever the router's types are in scope,
205
+ * alongside `Rango.Path` / `Rango.PathResponse`.
206
+ *
207
+ * `Rango.JsonSerialize` is what `Rango.PathResponse` applies; `Rango.FlightSerialize`
208
+ * is exposed for RSC/loader/cache wire types and must NOT be used for `path.json()`.
209
+ * `Rango.JsonSerializeBuiltin` / `Rango.FlightSerializeBuiltin` are the defaults,
210
+ * exported so an override can delegate to them.
211
+ */
212
+ declare global {
213
+ namespace Rango {
214
+ /**
215
+ * Full-transform override slot for `Rango.JsonSerialize`. Empty by default;
216
+ * augment with one member that is your complete transform (delegate to
217
+ * `Rango.JsonSerializeBuiltin<T>` for the cases you don't change).
218
+ */
219
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
220
+ interface JsonSerializeOverride<T> {}
221
+
222
+ /**
223
+ * Full-transform override slot for `Rango.FlightSerialize`. Empty by default;
224
+ * augment with one member that is your complete transform (delegate to
225
+ * `Rango.FlightSerializeBuiltin<T>` for the cases you don't change).
226
+ */
227
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
228
+ interface FlightSerializeOverride<T> {}
229
+
230
+ /** Wire type after `JSON.stringify` (`path.json()` / `fetch().json()`). */
231
+ type JsonSerialize<T> = GlobalJsonSerialize<T>;
232
+ /**
233
+ * Built-in `JsonSerialize` rules, for an override to delegate to. Raw: a
234
+ * `bigint`-bearing type yields the internal throw marker here, which
235
+ * `Rango.JsonSerialize` excludes to `never` at the boundary.
236
+ */
237
+ type JsonSerializeBuiltin<T> = GlobalJsonSerializeBuiltin<T>;
238
+ /** Wire type after RSC Flight serialization (loaders / RSC props / cache). */
239
+ type FlightSerialize<T> = GlobalFlightSerialize<T>;
240
+ /** Built-in `FlightSerialize` rules, for an override to delegate to. */
241
+ type FlightSerializeBuiltin<T> = GlobalFlightSerializeBuiltin<T>;
242
+ }
243
+ }
@@ -40,7 +40,7 @@ export interface MetricsStore {
40
40
  metrics: PerformanceMetric[];
41
41
  }
42
42
  // ============================================================================
43
- // RSC Router Context
43
+ // Rango Context
44
44
  // ============================================================================
45
45
 
46
46
  /**
@@ -303,7 +303,7 @@ interface HelperContext {
303
303
  // hold references to the old instance — causing getStore() to return
304
304
  // undefined even inside a run() callback.
305
305
  const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
306
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
306
+ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
307
307
  globalThis as any
308
308
  )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
309
309
 
@@ -330,12 +330,12 @@ export const getContext = (): {
330
330
  callback: (...args: any[]) => T,
331
331
  ) => T;
332
332
  } => {
333
- const context = RSCRouterContext;
333
+ const context = RangoContext;
334
334
 
335
335
  return {
336
336
  context,
337
337
  getOrCreateStore: (forRoute?: string): HelperContext => {
338
- let store = RSCRouterContext.getStore();
338
+ let store = RangoContext.getStore();
339
339
  if (!store) {
340
340
  store = {
341
341
  manifest: new Map<string, EntryData>(),
@@ -355,7 +355,7 @@ export const getContext = (): {
355
355
  const store = context.getStore();
356
356
  if (!store) {
357
357
  throw new Error(
358
- "RSC Router context store is not available. Make sure to run within RSC Router context.",
358
+ "Rango context store is not available. Make sure to run within Rango context.",
359
359
  );
360
360
  }
361
361
  return store;
@@ -372,7 +372,7 @@ export const getContext = (): {
372
372
  type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
373
373
  ) => {
374
374
  const store = context.getStore();
375
- invariant(store, "No context RSCRouterContext available");
375
+ invariant(store, "No context RangoContext available");
376
376
  store.counters[type] ??= 0;
377
377
  const index = store.counters[type];
378
378
  store.counters[type] = index + 1;
@@ -382,7 +382,7 @@ export const getContext = (): {
382
382
  type: "layout" | "parallel" | "route" | "loader" | "cache",
383
383
  ) => {
384
384
  const store = context.getStore();
385
- invariant(store, "No context RSCRouterContext available");
385
+ invariant(store, "No context RangoContext available");
386
386
 
387
387
  const parent = store.parent;
388
388
  const prefix =
@@ -502,7 +502,7 @@ export function runWithPrefixes<T>(
502
502
  namePrefix: string | undefined,
503
503
  callback: () => T,
504
504
  ): T {
505
- const store = RSCRouterContext.getStore();
505
+ const store = RangoContext.getStore();
506
506
  if (!store) {
507
507
  throw new Error("runWithPrefixes must be called within router context");
508
508
  }
@@ -547,7 +547,7 @@ export function runWithPrefixes<T>(
547
547
  ? (store.rootScoped ?? false)
548
548
  : store.rootScoped;
549
549
 
550
- return RSCRouterContext.run(
550
+ return RangoContext.run(
551
551
  {
552
552
  ...store,
553
553
  urlPrefix: combinedUrlPrefix,
@@ -562,7 +562,7 @@ export function runWithPrefixes<T>(
562
562
  * Get current URL prefix from context
563
563
  */
564
564
  export function getUrlPrefix(): string {
565
- const store = RSCRouterContext.getStore();
565
+ const store = RangoContext.getStore();
566
566
  return store?.urlPrefix || "";
567
567
  }
568
568
 
@@ -570,7 +570,7 @@ export function getUrlPrefix(): string {
570
570
  * Get current name prefix from context
571
571
  */
572
572
  export function getNamePrefix(): string | undefined {
573
- const store = RSCRouterContext.getStore();
573
+ const store = RangoContext.getStore();
574
574
  return store?.namePrefix;
575
575
  }
576
576
 
@@ -579,7 +579,7 @@ export function getNamePrefix(): string | undefined {
579
579
  * Returns true at root or inside { name: "" } includes, false inside named includes.
580
580
  */
581
581
  export function getRootScoped(): boolean {
582
- const store = RSCRouterContext.getStore();
582
+ const store = RangoContext.getStore();
583
583
  return store?.rootScoped ?? true;
584
584
  }
585
585
 
@@ -676,7 +676,7 @@ export function getParallelSlotCount(
676
676
  * ```
677
677
  */
678
678
  export function track(label: string, depth?: number): () => void {
679
- const store = RSCRouterContext.getStore();
679
+ const store = RangoContext.getStore();
680
680
 
681
681
  // No-op if context unavailable or metrics not enabled
682
682
  if (!store?.metrics?.enabled) {
@@ -699,8 +699,8 @@ export function track(label: string, depth?: number): () => void {
699
699
 
700
700
  /**
701
701
  * Separate ALS for tracking loader execution scope.
702
- * Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
703
- * nested RSCRouterContext.run() calls in Vite's module runner.
702
+ * Uses a dedicated ALS (not RangoContext) to avoid issues with
703
+ * nested RangoContext.run() calls in Vite's module runner.
704
704
  */
705
705
  const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
706
706
  const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
@@ -713,7 +713,7 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
713
713
  * (never cached), so non-cacheable reads are safe.
714
714
  */
715
715
  export function isInsideCacheScope(): boolean {
716
- if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
716
+ if (RangoContext.getStore()?.insideCacheScope !== true) return false;
717
717
  // Loaders are always fresh — even inside a cache() boundary, the loader
718
718
  // function re-executes on every request. Skip the guard when running
719
719
  // inside a loader.
@@ -96,7 +96,7 @@ export function Static<TParams extends Record<string, any>>(
96
96
 
97
97
  if (!id) {
98
98
  throw new Error(
99
- "[rsc-router] Static: missing $$id. " +
99
+ "[rango] Static: missing $$id. " +
100
100
  "Ensure the exposeInternalIds Vite plugin is configured.",
101
101
  );
102
102
  }
@@ -7,7 +7,7 @@
7
7
  * ```typescript
8
8
  * // In env.ts or env.d.ts
9
9
  * declare global {
10
- * namespace RSCRouter {
10
+ * namespace Rango {
11
11
  * interface Env extends AppBindings {}
12
12
  * interface Vars extends AppVariables {}
13
13
  * }
@@ -18,7 +18,7 @@
18
18
  * ```
19
19
  */
20
20
  declare global {
21
- namespace RSCRouter {
21
+ namespace Rango {
22
22
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
23
23
  interface Env {
24
24
  // Empty by default - users augment with their bindings (e.g., { DB: D1Database })
@@ -44,13 +44,25 @@ declare global {
44
44
  }
45
45
 
46
46
  /**
47
- * Get registered routes or fallback to generic Record<string, string>
48
- * When RSCRouter.RegisteredRoutes is augmented, provides autocomplete for route names
49
- * When not augmented, allows any string (no autocomplete)
47
+ * Route map for path-validation surfaces (`href`, `ValidPaths`, `PathResponse`).
48
+ *
49
+ * Resolution order:
50
+ * 1. `RegisteredRoutes` — manual `extends typeof router.routeMap`; richest map,
51
+ * the only one carrying response-route payload metadata.
52
+ * 2. `GeneratedRouteMap` — auto-wired by `router.named-routes.gen.ts`; path +
53
+ * search only. Lets `rango generate` alone enable `href()`/`ValidPaths` path
54
+ * checking without a manual `RegisteredRoutes` declaration.
55
+ * 3. `Record<string, string>` — permissive fallback when nothing is registered.
56
+ *
57
+ * Referencing `GeneratedRouteMap` here is cycle-safe: it is declared in the
58
+ * standalone gen file with no imports, unlike `RegisteredRoutes extends typeof
59
+ * router.routeMap`.
50
60
  */
51
- export type GetRegisteredRoutes = keyof RSCRouter.RegisteredRoutes extends never
52
- ? Record<string, string>
53
- : RSCRouter.RegisteredRoutes;
61
+ export type GetRegisteredRoutes = keyof Rango.RegisteredRoutes extends never
62
+ ? keyof Rango.GeneratedRouteMap extends never
63
+ ? Record<string, string>
64
+ : Rango.GeneratedRouteMap
65
+ : Rango.RegisteredRoutes;
54
66
 
55
67
  /**
56
68
  * Default route map for reverse() surfaces.
@@ -58,12 +70,11 @@ export type GetRegisteredRoutes = keyof RSCRouter.RegisteredRoutes extends never
58
70
  * cycles, but falls back to RegisteredRoutes for manual augmentation and then to
59
71
  * a permissive record when no route types are available.
60
72
  */
61
- export type DefaultReverseRouteMap =
62
- keyof RSCRouter.GeneratedRouteMap extends never
63
- ? keyof RSCRouter.RegisteredRoutes extends never
64
- ? Record<string, string>
65
- : RSCRouter.RegisteredRoutes
66
- : RSCRouter.GeneratedRouteMap;
73
+ export type DefaultReverseRouteMap = keyof Rango.GeneratedRouteMap extends never
74
+ ? keyof Rango.RegisteredRoutes extends never
75
+ ? Record<string, string>
76
+ : Rango.RegisteredRoutes
77
+ : Rango.GeneratedRouteMap;
67
78
 
68
79
  /**
69
80
  * Default route map for Handler type.
@@ -71,30 +82,32 @@ export type DefaultReverseRouteMap =
71
82
  * circular dependencies: router.tsx -> urls.tsx -> handler.tsx -> RegisteredRoutes -> router.tsx.
72
83
  * GeneratedRouteMap is declared in a standalone gen file with no imports.
73
84
  */
74
- export type DefaultHandlerRouteMap =
75
- keyof RSCRouter.GeneratedRouteMap extends never
76
- ? {}
77
- : RSCRouter.GeneratedRouteMap;
85
+ export type DefaultHandlerRouteMap = keyof Rango.GeneratedRouteMap extends never
86
+ ? {}
87
+ : Rango.GeneratedRouteMap;
78
88
 
79
89
  /**
80
- * Default environment type - uses global augmentation if available, any otherwise
90
+ * Default environment type - uses global augmentation if available.
91
+ *
92
+ * Falls back to `unknown` (not `any`) when `Rango.Env` is not augmented, so
93
+ * an app that forgets to register its bindings gets a compile error on
94
+ * `ctx.env.SOMETHING` instead of silently losing all env type-checking. Augment
95
+ * `Rango.Env` to type `ctx.env`.
81
96
  */
82
- export type DefaultEnv = keyof RSCRouter.Env extends never
83
- ? any
84
- : RSCRouter.Env;
97
+ export type DefaultEnv = keyof Rango.Env extends never ? unknown : Rango.Env;
85
98
 
86
99
  /**
87
100
  * Default variables type - uses global augmentation if available, Record<string, any> otherwise
88
101
  */
89
- export type DefaultVars = keyof RSCRouter.Vars extends never
102
+ export type DefaultVars = keyof Rango.Vars extends never
90
103
  ? Record<string, any>
91
- : RSCRouter.Vars;
104
+ : Rango.Vars;
92
105
 
93
106
  /**
94
107
  * Default route name type for public `routeName` on contexts.
95
108
  * When GeneratedRouteMap is augmented, narrows to the known route names.
96
109
  * Otherwise falls back to `string` for untyped usage.
97
110
  */
98
- export type DefaultRouteName = keyof RSCRouter.GeneratedRouteMap extends never
111
+ export type DefaultRouteName = keyof Rango.GeneratedRouteMap extends never
99
112
  ? string
100
- : keyof RSCRouter.GeneratedRouteMap & string;
113
+ : keyof Rango.GeneratedRouteMap & string;
@@ -43,7 +43,7 @@ export type { MiddlewareFn } from "../router/middleware.js";
43
43
  */
44
44
  export type ScopedRouteMap<
45
45
  TPrefix extends string,
46
- TMap = RSCRouter.GeneratedRouteMap,
46
+ TMap = Rango.GeneratedRouteMap,
47
47
  > = {
48
48
  [K in keyof TMap as K extends `${TPrefix}.${infer Rest}`
49
49
  ? Rest
@@ -767,7 +767,7 @@ export type Revalidate<
767
767
  * Middleware function with typed params and environment
768
768
  *
769
769
  * @template TParams - Params object (defaults to generic)
770
- * @template TEnv - Environment type (defaults to global RSCRouter.Env)
770
+ * @template TEnv - Environment type (defaults to global Rango.Env)
771
771
  *
772
772
  * Note: Middleware cannot directly use route names for params typing because
773
773
  * middleware is defined during router setup, before RegisteredRoutes is populated.
@@ -775,7 +775,7 @@ export type Revalidate<
775
775
  *
776
776
  * @example
777
777
  * ```typescript
778
- * // Basic middleware (uses global RSCRouter.Env via module augmentation)
778
+ * // Basic middleware (uses global Rango.Env via module augmentation)
779
779
  * const middleware: Middleware = async (ctx, next) => {
780
780
  * ctx.set("user", { id: "123" }); // Type-safe!
781
781
  * await next();
@@ -264,7 +264,7 @@ export type PathHelpers<TEnv> = {
264
264
  * Define an intercepting route for soft navigation
265
265
  * Note: routeName must match a named path() in this urlpatterns
266
266
  */
267
- intercept: keyof RSCRouter.GeneratedRouteMap extends never
267
+ intercept: keyof Rango.GeneratedRouteMap extends never
268
268
  ? (
269
269
  slotName: `@${string}`,
270
270
  routeName: string,
@@ -273,7 +273,7 @@ export type PathHelpers<TEnv> = {
273
273
  ) => InterceptItem
274
274
  : (
275
275
  slotName: `@${string}`,
276
- routeName: (keyof RSCRouter.GeneratedRouteMap & string) | `.${string}`,
276
+ routeName: (keyof Rango.GeneratedRouteMap & string) | `.${string}`,
277
277
  handler: ReactNode | Handler<any, any, TEnv>,
278
278
  use?: () => InterceptUseItem[],
279
279
  ) => InterceptItem;
@@ -88,6 +88,40 @@ export interface UrlPatterns<
88
88
  readonly _responses?: TResponses;
89
89
  }
90
90
 
91
+ /**
92
+ * Extract the phantom env type carried by a UrlPatterns value.
93
+ */
94
+ export type UrlPatternsEnv<T> =
95
+ T extends UrlPatterns<infer TEnv, any, any> ? TEnv : never;
96
+
97
+ /**
98
+ * Guards `routes()` env compatibility without over-constraining.
99
+ *
100
+ * - An env-agnostic block (its env is `unknown` — e.g. a shared urls() module,
101
+ * or an app that does not augment `Rango.Env`) attaches to any router.
102
+ * - A block carrying a concrete env is accepted only when the router env
103
+ * (`TRouterEnv`) satisfies it; resolves to `never` otherwise, so a
104
+ * `urls<{ DB: D1Database }>()` cannot be mounted on a `createRouter<{}>()`.
105
+ *
106
+ * Use as `patterns: T & EnvCompatible<T, TEnv>` so `T` still infers from the
107
+ * argument — a bare `EnvCompatible<T, TEnv>` parameter sits in a non-inferrable
108
+ * conditional position and would collapse `T` to its constraint.
109
+ *
110
+ * Known limitation: `TRouterEnv extends ...` distributes over a union router env,
111
+ * so a `urls<A>()` block is accepted on `createRouter<A | B>()` even though the
112
+ * `B` arm cannot supply `A`'s env. Suppressing distribution with
113
+ * `[TRouterEnv] extends [...]` would close that edge but breaks the common
114
+ * generic-`TEnv` call sites (a deferred type parameter can't resolve the tuple
115
+ * conditional, so the intersection stops reducing to `T`). A router has one env,
116
+ * so a union env is not a supported pattern; the distributive form is kept.
117
+ */
118
+ export type EnvCompatible<TPatterns, TRouterEnv> =
119
+ unknown extends UrlPatternsEnv<TPatterns>
120
+ ? TPatterns
121
+ : TRouterEnv extends UrlPatternsEnv<TPatterns>
122
+ ? TPatterns
123
+ : never;
124
+
91
125
  /**
92
126
  * Options for include()
93
127
  */
@@ -1,4 +1,5 @@
1
1
  import type { ExtractParams } from "../types.js";
2
+ import type { JsonSerialize } from "../serialize.js";
2
3
  import type {
3
4
  TypedRouteItem,
4
5
  TypedIncludeItem,
@@ -362,11 +363,15 @@ export type ResponseEnvelope<T> =
362
363
  * type HealthData = RouteResponse<typeof apiPatterns, "health">;
363
364
  * // ResponseEnvelope<{ status: string; timestamp: number }>
364
365
  * ```
366
+ *
367
+ * The payload is the JSON wire shape (via `Rango.JsonSerialize`), matching
368
+ * `Rango.PathResponse` and what `fetch().then(r => r.json())` actually yields —
369
+ * e.g. a `Date` field resolves as `string`.
365
370
  */
366
371
  export type RouteResponse<TPatterns, TName extends string> = TPatterns extends {
367
372
  readonly _responses?: infer R;
368
373
  }
369
374
  ? TName extends keyof R
370
- ? ResponseEnvelope<Exclude<R[TName], Response>>
375
+ ? ResponseEnvelope<JsonSerialize<Exclude<R[TName], Response>>>
371
376
  : never
372
377
  : never;
@@ -501,7 +501,7 @@ function useLoaderInternal<T>(
501
501
  export function useLoader<T>(
502
502
  loader: LoaderDefinition<T>,
503
503
  options?: UseLoaderOptions,
504
- ): UseLoaderResult<T> {
504
+ ): UseLoaderResult<Rango.FlightSerialize<T>> {
505
505
  const result = useLoaderInternal(loader, options);
506
506
 
507
507
  // Strict mode: throw if data is not in context
@@ -513,7 +513,7 @@ export function useLoader<T>(
513
513
  );
514
514
  }
515
515
 
516
- return result as UseLoaderResult<T>;
516
+ return result as UseLoaderResult<Rango.FlightSerialize<T>>;
517
517
  }
518
518
 
519
519
  /**
@@ -565,6 +565,8 @@ export function useLoader<T>(
565
565
  export function useFetchLoader<T>(
566
566
  loader: LoaderDefinition<T>,
567
567
  options?: UseLoaderOptions,
568
- ): UseFetchLoaderResult<T> {
569
- return useLoaderInternal(loader, options);
568
+ ): UseFetchLoaderResult<Rango.FlightSerialize<T>> {
569
+ return useLoaderInternal(loader, options) as UseFetchLoaderResult<
570
+ Rango.FlightSerialize<T>
571
+ >;
570
572
  }