@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
@@ -32,27 +32,35 @@ import { shallowEqual } from "./shallow-equal.js";
32
32
  * const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
33
33
  * ```
34
34
  */
35
- export function useHandle<T, A>(handle: Handle<T, A>): A;
35
+ export function useHandle<T, A>(handle: Handle<T, A>): Rango.FlightSerialize<A>;
36
36
  export function useHandle<T, A, S>(
37
37
  handle: Handle<T, A>,
38
- selector: (data: A) => S,
38
+ selector: (data: Rango.FlightSerialize<A>) => S,
39
39
  ): S;
40
40
  export function useHandle<T, A, S>(
41
41
  handle: Handle<T, A>,
42
- selector?: (data: A) => S,
43
- ): A | S {
42
+ selector?: (data: Rango.FlightSerialize<A>) => S,
43
+ ): Rango.FlightSerialize<A> | S {
44
44
  const ctx = useContext(NavigationStoreContext);
45
45
 
46
46
  // Initial state from context event controller, or empty fallback without provider.
47
- const [value, setValue] = useState<A | S>(() => {
47
+ const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
48
48
  if (!ctx) {
49
- const collected = collectHandleData(handle, {}, []);
49
+ const collected = collectHandleData(
50
+ handle,
51
+ {},
52
+ [],
53
+ ) as Rango.FlightSerialize<A>;
50
54
  return selector ? selector(collected) : collected;
51
55
  }
52
56
 
53
57
  // On client, use event controller state
54
58
  const state = ctx.eventController.getHandleState();
55
- const collected = collectHandleData(handle, state.data, state.segmentOrder);
59
+ const collected = collectHandleData(
60
+ handle,
61
+ state.data,
62
+ state.segmentOrder,
63
+ ) as Rango.FlightSerialize<A>;
56
64
  return selector ? selector(collected) : collected;
57
65
  });
58
66
  const [optimisticValue, setOptimisticValue] = useOptimistic(value);
@@ -76,7 +84,7 @@ export function useHandle<T, A, S>(
76
84
  handle,
77
85
  currentHandleState.data,
78
86
  currentHandleState.segmentOrder,
79
- );
87
+ ) as Rango.FlightSerialize<A>;
80
88
  const currentValue = selectorRef.current
81
89
  ? selectorRef.current(currentCollected)
82
90
  : currentCollected;
@@ -93,7 +101,7 @@ export function useHandle<T, A, S>(
93
101
  handle,
94
102
  state.data,
95
103
  state.segmentOrder,
96
- );
104
+ ) as Rango.FlightSerialize<A>;
97
105
  const nextValue = selectorRef.current
98
106
  ? selectorRef.current(collected)
99
107
  : collected;
@@ -128,7 +128,7 @@ export interface BrowserAppContext {
128
128
  let browserAppContext: BrowserAppContext | null = null;
129
129
 
130
130
  /**
131
- * Initialize the browser app. Must be called before rendering RSCRouter.
131
+ * Initialize the browser app. Must be called before rendering Rango.
132
132
  *
133
133
  * This function:
134
134
  * - Loads the initial RSC payload from the stream
@@ -325,11 +325,11 @@ export async function initBrowserApp(
325
325
  // full lifecycle (fetching + streaming, before commit) without
326
326
  // blocking on server actions.
327
327
  if (eventController.getState().isNavigating) {
328
- console.log("[RSCRouter] HMR: Skipping — navigation in progress");
328
+ console.log("[Rango] HMR: Skipping — navigation in progress");
329
329
  return;
330
330
  }
331
331
 
332
- console.log("[RSCRouter] HMR: Server update, refetching RSC");
332
+ console.log("[Rango] HMR: Server update, refetching RSC");
333
333
 
334
334
  const abort = new AbortController();
335
335
  hmrAbort = abort;
@@ -367,7 +367,7 @@ export async function initBrowserApp(
367
367
  const newVersion = payload.metadata.version;
368
368
  if (newVersion && newVersion !== version) {
369
369
  console.log(
370
- "[RSCRouter] HMR: version changed",
370
+ "[Rango] HMR: version changed",
371
371
  version,
372
372
  "→",
373
373
  newVersion,
@@ -415,10 +415,10 @@ export async function initBrowserApp(
415
415
 
416
416
  await streamComplete;
417
417
  handle.complete(new URL(window.location.href));
418
- console.log("[RSCRouter] HMR: RSC stream complete");
418
+ console.log("[Rango] HMR: RSC stream complete");
419
419
  } catch (err) {
420
420
  if (abort.signal.aborted) return;
421
- console.warn("[RSCRouter] HMR: Refetch failed, reloading page", err);
421
+ console.warn("[Rango] HMR: Refetch failed, reloading page", err);
422
422
  window.location.reload();
423
423
  return;
424
424
  } finally {
@@ -430,7 +430,7 @@ export async function initBrowserApp(
430
430
  });
431
431
  }
432
432
 
433
- // Store context for RSCRouter component
433
+ // Store context for Rango component
434
434
  const context: BrowserAppContext = {
435
435
  store,
436
436
  eventController,
@@ -454,7 +454,7 @@ export async function initBrowserApp(
454
454
  export function getBrowserAppContext(): BrowserAppContext {
455
455
  if (!browserAppContext) {
456
456
  throw new Error(
457
- "RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
457
+ "Rango: initBrowserApp() must be called before rendering Rango",
458
458
  );
459
459
  }
460
460
  return browserAppContext;
@@ -468,18 +468,18 @@ export function resetBrowserAppContext(): void {
468
468
  }
469
469
 
470
470
  /**
471
- * Props for the RSCRouter component
471
+ * Props for the Rango component
472
472
  */
473
- export interface RSCRouterProps {}
473
+ export interface RangoProps {}
474
474
 
475
475
  /**
476
- * RSCRouter component - renders the RSC router with all internal wiring.
476
+ * Rango component - renders the RSC router with all internal wiring.
477
477
  *
478
478
  * Must be called after initBrowserApp() has completed.
479
479
  *
480
480
  * @example
481
481
  * ```tsx
482
- * import { initBrowserApp, RSCRouter } from "rsc-router/browser";
482
+ * import { initBrowserApp, Rango } from "rsc-router/browser";
483
483
  * import { rscStream } from "rsc-html-stream/client";
484
484
  * import * as rscBrowser from "@vitejs/plugin-rsc/browser";
485
485
  *
@@ -489,14 +489,14 @@ export interface RSCRouterProps {}
489
489
  * hydrateRoot(
490
490
  * document,
491
491
  * <React.StrictMode>
492
- * <RSCRouter />
492
+ * <Rango />
493
493
  * </React.StrictMode>
494
494
  * );
495
495
  * }
496
496
  * main();
497
497
  * ```
498
498
  */
499
- export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
499
+ export function Rango(_props: RangoProps): React.ReactElement {
500
500
  const {
501
501
  store,
502
502
  eventController,
@@ -48,7 +48,7 @@ export function assertSegmentStructure(
48
48
 
49
49
  if (cachedCategory !== incomingCategory) {
50
50
  console.warn(
51
- `[RSC Router] Tree structure mismatch detected in ${context} ` +
51
+ `[Rango] Tree structure mismatch detected in ${context} ` +
52
52
  `for segment "${cached.id}": loading category changed from ` +
53
53
  `"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
54
54
  `"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
@@ -64,7 +64,7 @@ export function assertSegmentStructure(
64
64
  const incomingHasMount = !!incoming.mountPath;
65
65
  if (cachedHasMount !== incomingHasMount) {
66
66
  console.warn(
67
- `[RSC Router] MountContextProvider mismatch detected in ${context} ` +
67
+ `[Rango] MountContextProvider mismatch detected in ${context} ` +
68
68
  `for segment "${cached.id}": mountPath changed from ` +
69
69
  `${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
70
70
  `${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +
@@ -11,7 +11,7 @@
11
11
  import type { UrlPatterns } from "../urls.js";
12
12
  import type { AllUseItems } from "../route-types.js";
13
13
  import { extractStaticPrefix } from "../router/pattern-matching.js";
14
- import { RSCRouterContext, runWithPrefixes } from "../server/context.js";
14
+ import { RangoContext, runWithPrefixes } from "../server/context.js";
15
15
  import type { EntryData, TrackedInclude } from "../server/context.js";
16
16
  import type { TrailingSlashMode } from "../types.js";
17
17
  import { createRouteHelpers } from "../route-definition.js";
@@ -93,7 +93,7 @@ function buildPrefixTreeNode(
93
93
  const searchSchemasMap = new Map<string, Record<string, string>>();
94
94
  const trackedIncludes: TrackedInclude[] = [];
95
95
 
96
- RSCRouterContext.run(
96
+ RangoContext.run(
97
97
  {
98
98
  manifest,
99
99
  patterns: patternsMap,
@@ -296,7 +296,7 @@ export function generateManifestFull<TEnv>(
296
296
  const searchSchemasMap = new Map<string, Record<string, string>>();
297
297
  const trackedIncludes: TrackedInclude[] = [];
298
298
 
299
- RSCRouterContext.run(
299
+ RangoContext.run(
300
300
  {
301
301
  manifest,
302
302
  patterns: patternsMap,
@@ -23,7 +23,7 @@ export function generatePerModuleTypesSource(
23
23
  const valid = routes.filter(({ name }) => {
24
24
  if (!name || /["'\\`\n\r]/.test(name)) {
25
25
  console.warn(
26
- `[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`,
26
+ `[rango] Skipping route with invalid name: ${JSON.stringify(name)}`,
27
27
  );
28
28
  return false;
29
29
  }
@@ -42,7 +42,7 @@ export function generatePerModuleTypesSource(
42
42
  for (const { name, pattern, params, search } of valid) {
43
43
  if (deduped.has(name)) {
44
44
  console.warn(
45
- `[rsc-router] Duplicate route name "${name}" — keeping first definition`,
45
+ `[rango] Duplicate route name "${name}" — keeping first definition`,
46
46
  );
47
47
  continue;
48
48
  }
@@ -59,7 +59,7 @@ export function generatePerModuleTypesSource(
59
59
  }
60
60
 
61
61
  /**
62
- * Generates a .ts file that augments RSCRouter.GeneratedRouteMap
62
+ * Generates a .ts file that augments Rango.GeneratedRouteMap
63
63
  * with route name -> pattern mappings. This enables Handler<"routeName">
64
64
  * without circular references since the file has no imports from the app.
65
65
  */
@@ -94,7 +94,7 @@ ${objectBody}
94
94
  } as const;
95
95
 
96
96
  declare global {
97
- namespace RSCRouter {
97
+ namespace Rango {
98
98
  interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
99
99
  }
100
100
  }
@@ -376,7 +376,7 @@ export function buildCombinedRouteMapWithSearch(
376
376
  const realPath = resolve(filePath);
377
377
  const key = variableName ? `${realPath}:${variableName}` : realPath;
378
378
  if (visited.has(key)) {
379
- console.warn(`[rsc-router] Circular include detected, skipping: ${key}`);
379
+ console.warn(`[rango] Circular include detected, skipping: ${key}`);
380
380
  return { routes: {}, searchSchemas: {} };
381
381
  }
382
382
  visited.add(key);
@@ -106,7 +106,7 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
106
106
  if (varNames.length > 0 && !existsSync(genPath)) {
107
107
  writeFileSync(genPath, generatePerModuleTypesSource([]));
108
108
  console.log(
109
- `[rsc-router] Generated route types (placeholder) -> ${genPath}`,
109
+ `[rango] Generated route types (placeholder) -> ${genPath}`,
110
110
  );
111
111
  }
112
112
  return;
@@ -118,11 +118,11 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
118
118
  : null;
119
119
  if (existing !== genSource) {
120
120
  writeFileSync(genPath, genSource);
121
- console.log(`[rsc-router] Generated route types -> ${genPath}`);
121
+ console.log(`[rango] Generated route types -> ${genPath}`);
122
122
  }
123
123
  } catch (err) {
124
124
  console.warn(
125
- `[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
125
+ `[rango] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
126
126
  );
127
127
  }
128
128
  }
@@ -61,7 +61,7 @@ function findRouterFilesRecursive(
61
61
  entries = readdirSync(dir, { withFileTypes: true });
62
62
  } catch (err) {
63
63
  console.warn(
64
- `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
64
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
65
65
  );
66
66
  return;
67
67
  }
@@ -142,7 +142,7 @@ export function findNestedRouterConflict(
142
142
 
143
143
  export function formatNestedRouterConflictError(
144
144
  conflict: { ancestor: string; nested: string },
145
- prefix = "[rsc-router]",
145
+ prefix = "[rango]",
146
146
  ): string {
147
147
  return (
148
148
  `${prefix} Nested router roots are not supported.\n` +
@@ -536,7 +536,7 @@ export function writeCombinedRouteTypes(
536
536
  if (existsSync(oldCombinedPath)) {
537
537
  unlinkSync(oldCombinedPath);
538
538
  console.log(
539
- `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
539
+ `[rango] Removed stale combined route types: ${oldCombinedPath}`,
540
540
  );
541
541
  }
542
542
  } catch {}
@@ -611,7 +611,7 @@ export function writeCombinedRouteTypes(
611
611
  }
612
612
  writeFileSync(outPath, source);
613
613
  console.log(
614
- `[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
614
+ `[rango] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
615
615
  );
616
616
  }
617
617
  }
@@ -54,7 +54,7 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
54
54
  entries = readdirSync(dir, { withFileTypes: true });
55
55
  } catch (err) {
56
56
  console.warn(
57
- `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
57
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
58
58
  );
59
59
  return results;
60
60
  }
package/src/client.tsx CHANGED
@@ -409,13 +409,10 @@ export {
409
409
  type LocationStateOptions,
410
410
  } from "./browser/react/location-state.js";
411
411
 
412
- // Type-safe href for client-side path validation
413
- export {
414
- href,
415
- type ValidPaths,
416
- type PatternToPath,
417
- type PathResponse,
418
- } from "./href-client.js";
412
+ // Type-safe href for client-side path validation. The path and response types
413
+ // are ambient as `Rango.Path` / `Rango.PathResponse` (declared in
414
+ // href-client.ts) — no import needed.
415
+ export { href, type PatternToPath } from "./href-client.js";
419
416
 
420
417
  // Response envelope types for consuming JSON response routes
421
418
  export type { ResponseEnvelope, ResponseError } from "./urls.js";
package/src/errors.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Custom error classes for RSC Router
2
+ * Custom error classes for Rango
3
3
  *
4
4
  * All errors include:
5
5
  * - Descriptive names for easy identification
package/src/handle.ts CHANGED
@@ -97,7 +97,7 @@ export function createHandle<TData, TAccumulated = TData[]>(
97
97
 
98
98
  if (!handleId && process.env.NODE_ENV === "development") {
99
99
  throw new Error(
100
- "[rsc-router] Handle is missing $$id. " +
100
+ "[rango] Handle is missing $$id. " +
101
101
  "Make sure the exposeInternalIds Vite plugin is enabled and " +
102
102
  "the handle is exported with: export const MyHandle = createHandle(...)",
103
103
  );
@@ -151,7 +151,7 @@ export function collectHandleData<TData, TAccumulated>(
151
151
  const collectFn = getCollectFn(handle.$$id);
152
152
  if (!collectFn && process.env.NODE_ENV !== "production") {
153
153
  console.warn(
154
- `[rsc-router] Handle "${handle.$$id}" has no registered collect function. ` +
154
+ `[rango] Handle "${handle.$$id}" has no registered collect function. ` +
155
155
  `Falling back to flat array. Ensure the handle module is imported so ` +
156
156
  `createHandle() runs and registers the collect function.`,
157
157
  );
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { GetRegisteredRoutes } from "./types.js";
18
+ import type { JsonSerialize } from "./serialize.js";
18
19
  import type { ResponseEnvelope } from "./urls.js";
19
20
 
20
21
  /**
@@ -103,29 +104,75 @@ type NameForPattern<TPattern extends string, TRoutes = GetRegisteredRoutes> = {
103
104
  }[keyof TRoutes];
104
105
 
105
106
  /**
106
- * Look up the response data type for a route pattern from RegisteredRoutes.
107
- *
108
- * Works by reverse-looking up the route name for the given pattern,
109
- * then extracting the response type from the route entry.
107
+ * Strip a query (`?…`) and/or hash (`#…`) suffix before matching, so a concrete
108
+ * URL like `/api/health?ts=1` still resolves to its route's response. Removes
109
+ * from the earliest of `?`/`#`: a `#` before the first `?` (the query is part of
110
+ * a fragment, e.g. `/health#top?x=1`) is handled, as is a `/:` that only appears
111
+ * inside the query (e.g. `/health?next=/:id`).
112
+ */
113
+ type StripPathSuffix<T extends string> = T extends `${infer Base}?${string}`
114
+ ? Base extends `${infer Frag}#${string}`
115
+ ? Frag
116
+ : Base
117
+ : T extends `${infer Base}#${string}`
118
+ ? Base
119
+ : T;
120
+
121
+ /** Extract a route entry's response payload (or `never` for RSC routes). */
122
+ type ResponsePayloadOf<TRoutes, K extends keyof TRoutes> = TRoutes[K] extends {
123
+ readonly response: infer R;
124
+ }
125
+ ? Exclude<R, Response>
126
+ : never;
127
+
128
+ /**
129
+ * Look up the response payload for a route, keyed by either a route pattern
130
+ * (`/api/products/:id`) or a concrete path (`/api/products/123`). The same type
131
+ * serves a pattern lookup and a typed `fetch` wrapper that forwards a concrete
132
+ * `Rango.Path`:
110
133
  *
111
- * For static routes (no params), pattern === path:
112
- * PathResponse<"/api/health"> → { status: string; timestamp: number }
134
+ * PathResponse<"/api/products/:id"> Product // by pattern
135
+ * PathResponse<"/api/products/123"> → Product // by concrete path
113
136
  *
114
- * For dynamic routes, use the pattern:
115
- * PathResponse<"/api/products/:id"> Product
137
+ * The query/hash suffix is stripped first; the stripped key is then treated as a
138
+ * pattern when it contains a `/:param` segment and matched exactly (precise even
139
+ * for nested dynamic routes), otherwise as a concrete path matched against each
140
+ * route's `PatternToPath` template. Because those holes are `${string}`
141
+ * (slash-greedy), a concrete path under a *nested* dynamic route can match several
142
+ * patterns and union their responses — pattern lookups do not have this
143
+ * looseness. RSC routes (no response) and unmatched keys resolve to `never`.
144
+ */
145
+ type ResponsePayloadFor<
146
+ TPath extends string,
147
+ TRoutes = GetRegisteredRoutes,
148
+ > = ResponsePayloadForKey<StripPathSuffix<TPath>, TRoutes>;
149
+
150
+ type ResponsePayloadForKey<
151
+ TKey extends string,
152
+ TRoutes,
153
+ > = TKey extends `${string}/:${string}`
154
+ ? {
155
+ [K in keyof TRoutes]: RoutePattern<TRoutes, K> extends TKey
156
+ ? ResponsePayloadOf<TRoutes, K>
157
+ : never;
158
+ }[keyof TRoutes]
159
+ : {
160
+ [K in keyof TRoutes]: TKey extends PatternToPath<RoutePattern<TRoutes, K>>
161
+ ? ResponsePayloadOf<TRoutes, K>
162
+ : never;
163
+ }[keyof TRoutes];
164
+
165
+ /**
166
+ * Public response type for a route, keyed by pattern or concrete path. The
167
+ * payload is wrapped in `JsonSerialize` so it describes the JSON **wire** value a
168
+ * consumer receives from `fetch().then(r => r.json())`, not the handler's raw
169
+ * return type — e.g. a handler returning `{ createdAt: Date }` resolves here to
170
+ * `ResponseEnvelope<{ createdAt: string }>`.
116
171
  */
117
172
  export type PathResponse<
118
- TPattern extends string,
173
+ TPath extends string,
119
174
  TRoutes = GetRegisteredRoutes,
120
- > = ResponseEnvelope<
121
- {
122
- [K in keyof TRoutes]: RoutePattern<TRoutes, K> extends TPattern
123
- ? TRoutes[K] extends { readonly response: infer R }
124
- ? Exclude<R, Response>
125
- : never
126
- : never;
127
- }[keyof TRoutes]
128
- >;
175
+ > = ResponseEnvelope<JsonSerialize<ResponsePayloadFor<TPath, TRoutes>>>;
129
176
 
130
177
  /**
131
178
  * Strip trailing slash from a path (e.g., "/blog/" -> "/blog" | "/blog/")
@@ -140,7 +187,7 @@ type OptionalTrailingSlash<T extends string> = T extends `${infer Base}/`
140
187
  /**
141
188
  * Union of all valid paths from registered routes
142
189
  *
143
- * Generated from RSCRouter.RegisteredRoutes via module augmentation.
190
+ * Generated from Rango.RegisteredRoutes via module augmentation.
144
191
  * Allows optional query strings and hash fragments.
145
192
  */
146
193
  export type ValidPaths<TRoutes = GetRegisteredRoutes> =
@@ -154,6 +201,76 @@ export type ValidPaths<TRoutes = GetRegisteredRoutes> =
154
201
  }[keyof TRoutes]
155
202
  >;
156
203
 
204
+ // Module-scoped alias so the ambient `Rango.PathResponse` below can reference
205
+ // the module-level `PathResponse` without the global namespace shadowing the
206
+ // name when both are called `PathResponse`.
207
+ type GlobalPathResponse<
208
+ TPattern extends string,
209
+ TRoutes = GetRegisteredRoutes,
210
+ > = PathResponse<TPattern, TRoutes>;
211
+
212
+ /**
213
+ * Ambient path types on the `Rango` namespace.
214
+ *
215
+ * These live on the same global namespace consumers already augment for
216
+ * `Rango.Env` / `Rango.Vars`, so they are reachable with no import wherever the
217
+ * router's types are in scope. They are the public, recommended surface for
218
+ * typing anything that wraps `href()`. `ValidPaths` / `PathResponse` stay as the
219
+ * internal building blocks behind them.
220
+ */
221
+ declare global {
222
+ namespace Rango {
223
+ /**
224
+ * Union of every valid route path accepted by `href()`.
225
+ *
226
+ * Type a wrapper's path parameter as `Rango.Path` so it shares `href()`'s
227
+ * compile-time validation against the registered routes:
228
+ *
229
+ * ```ts
230
+ * import { href } from "@rangojs/router/client";
231
+ *
232
+ * export const appHref = (path: Rango.Path) => href(path);
233
+ * ```
234
+ *
235
+ * Resolves from `Rango.RegisteredRoutes` when augmented, otherwise the
236
+ * auto-generated `Rango.GeneratedRouteMap`, otherwise a permissive
237
+ * `/${string}` fallback.
238
+ */
239
+ type Path<TRoutes = GetRegisteredRoutes> = ValidPaths<TRoutes>;
240
+
241
+ /**
242
+ * Response payload for a route, looked up from the global route map by
243
+ * either a route pattern (`/api/products/:id`) or a concrete path
244
+ * (`/api/products/123`). Because it accepts a concrete `Rango.Path`, it
245
+ * doubles as the return type of a typed `fetch` wrapper:
246
+ *
247
+ * ```ts
248
+ * type Product = Rango.PathResponse<"/api/products/:id">; // by pattern
249
+ * type Same = Rango.PathResponse<"/api/products/42">; // by concrete path
250
+ *
251
+ * const get = async <T extends Rango.Path>(
252
+ * path: T,
253
+ * ): Promise<Rango.PathResponse<T>> =>
254
+ * fetch(href(path)).then((r) => r.json());
255
+ * ```
256
+ *
257
+ * The payload is the JSON **wire** shape (via `Rango.JsonSerialize`), not the
258
+ * handler's raw return — a handler returning `{ createdAt: Date }` resolves
259
+ * here to `ResponseEnvelope<{ createdAt: string }>`, matching what
260
+ * `fetch().then(r => r.json())` actually yields.
261
+ *
262
+ * Only resolves once `Rango.RegisteredRoutes` carries response metadata (the
263
+ * generated map has paths and search but no payloads). Pass an explicit route
264
+ * map as the second argument to look up against a non-global map (rarely
265
+ * needed in app code).
266
+ */
267
+ type PathResponse<
268
+ TPath extends string,
269
+ TRoutes = GetRegisteredRoutes,
270
+ > = GlobalPathResponse<TPath, TRoutes>;
271
+ }
272
+ }
273
+
157
274
  /**
158
275
  * Type-safe href function for client-side use
159
276
  *
package/src/index.rsc.ts CHANGED
@@ -68,7 +68,7 @@ export type {
68
68
 
69
69
  // Router options type (server-only, so import directly)
70
70
  export type {
71
- RSCRouterOptions,
71
+ RangoOptions,
72
72
  SSRStreamMode,
73
73
  SSROptions,
74
74
  ResolveStreamingContext,
@@ -152,7 +152,7 @@ export {
152
152
  // Core router (server-side)
153
153
  export {
154
154
  createRouter,
155
- type RSCRouter,
155
+ type Rango,
156
156
  type RootLayoutProps,
157
157
  type RouterRequestInput,
158
158
  } from "./router.js";
@@ -221,8 +221,8 @@ export {
221
221
  type LocationStateOptions,
222
222
  } from "./browser/react/location-state-shared.js";
223
223
 
224
- // Path-based response type lookup from RegisteredRoutes
225
- export type { PathResponse } from "./href-client.js";
224
+ // Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
225
+ // `Rango.PathResponse`, declared in href-client.ts) — no import needed.
226
226
 
227
227
  // Telemetry sink
228
228
  export { createConsoleSink } from "./router/telemetry.js";
package/src/index.ts CHANGED
@@ -303,8 +303,8 @@ export {
303
303
  type LocationStateOptions,
304
304
  } from "./browser/react/location-state-shared.js";
305
305
 
306
- // Path-based response type lookup from RegisteredRoutes
307
- export type { PathResponse } from "./href-client.js";
306
+ // Path and response types are ambient on the `Rango` namespace (`Rango.Path`,
307
+ // `Rango.PathResponse`, declared in href-client.ts) — no import needed.
308
308
 
309
309
  // Telemetry types only — the createConsoleSink/createOTelSink values are
310
310
  // server-only and live in index.rsc.ts (the `react-server` condition of the
package/src/loader.rsc.ts CHANGED
@@ -54,7 +54,7 @@ export function createLoader<T>(
54
54
 
55
55
  if (!loaderId && process.env.NODE_ENV === "development") {
56
56
  throw new Error(
57
- "[rsc-router] Loader is missing $$id. " +
57
+ "[rango] Loader is missing $$id. " +
58
58
  "Make sure the exposeInternalIds Vite plugin is enabled and " +
59
59
  "the loader is exported with: export const MyLoader = createLoader(...)",
60
60
  );
package/src/loader.ts CHANGED
@@ -51,7 +51,7 @@ export function createLoader<T>(
51
51
 
52
52
  if (!loaderId && process.env.NODE_ENV === "development") {
53
53
  throw new Error(
54
- "[rsc-router] Loader is missing $$id. " +
54
+ "[rango] Loader is missing $$id. " +
55
55
  "Make sure the exposeInternalIds Vite plugin is enabled and " +
56
56
  "the loader is exported with: export const MyLoader = createLoader(...)",
57
57
  );
package/src/prerender.ts CHANGED
@@ -69,9 +69,9 @@ type BuildReverseFunction = [DefaultReverseRouteMap] extends [
69
69
  * Default route map for Prerender named route resolution.
70
70
  * Uses GeneratedRouteMap (from gen file) to avoid circular dependencies.
71
71
  */
72
- type DefaultPrerenderRouteMap = keyof RSCRouter.GeneratedRouteMap extends never
72
+ type DefaultPrerenderRouteMap = keyof Rango.GeneratedRouteMap extends never
73
73
  ? {}
74
- : RSCRouter.GeneratedRouteMap;
74
+ : Rango.GeneratedRouteMap;
75
75
 
76
76
  /** Extract params from a route map entry (string pattern or { path } object). */
77
77
  type ExtractParamsFromEntry<TEntry> = TEntry extends string
@@ -378,7 +378,7 @@ export function Prerender<TParams extends Record<string, any>>(
378
378
 
379
379
  if (!id) {
380
380
  throw new Error(
381
- "[rsc-router] Prerender: missing $$id. " +
381
+ "[rango] Prerender: missing $$id. " +
382
382
  "Ensure the exposeInternalIds Vite plugin is configured.",
383
383
  );
384
384
  }
@@ -499,7 +499,7 @@ export function Passthrough<
499
499
  ): PassthroughHandlerDefinition<TParams, TEnv> {
500
500
  if (!isPrerenderHandler(prerenderDef)) {
501
501
  throw new Error(
502
- "[rsc-router] Passthrough: first argument must be a Prerender() definition.",
502
+ "[rango] Passthrough: first argument must be a Prerender() definition.",
503
503
  );
504
504
  }
505
505
  return {