@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * rsc-router/loader (RSC/server version)
3
+ *
4
+ * Server-side createLoader implementation with full loader functionality.
5
+ * Only used in react-server context via export conditions.
6
+ *
7
+ * For non-fetchable loaders: returns a loader definition with fn included
8
+ * For fetchable loaders: stores fn in registry and returns a serializable loader with action
9
+ *
10
+ * The $$id is injected by the Vite exposeLoaderId plugin as a hidden parameter.
11
+ * Users don't need to pass any name - IDs are auto-generated from file path.
12
+ */
13
+
14
+ import type {
15
+ FetchableLoaderOptions,
16
+ LoaderDefinition,
17
+ LoaderFn,
18
+ } from "./types.js";
19
+ import type { MiddlewareFn } from "./router/middleware.js";
20
+ import { getRequestContext } from "./server/request-context.js";
21
+
22
+ // Internal registry for fetchable loaders (server-side only)
23
+ // Maps loader $$id to its function and middleware
24
+ //
25
+ // WHY TWO REGISTRIES?
26
+ // This registry (fetchableLoaderRegistry) is populated immediately when createLoader() runs.
27
+ // The other registry in loader-registry.ts (loaderRegistry) is a cache used by the RSC handler
28
+ // for GET-based fetching. The RSC handler calls getFetchableLoader() from here to populate
29
+ // its cache. This separation allows:
30
+ // 1. Server actions to look up loaders directly without going through lazy loading
31
+ // 2. The RSC handler to use lazy loading for production builds
32
+ // 3. Both to share the same source of truth (this registry)
33
+ const fetchableLoaderRegistry = new Map<
34
+ string,
35
+ { fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] }
36
+ >();
37
+
38
+ /**
39
+ * Register a fetchable loader's function internally
40
+ * Called during module initialization with the $$id
41
+ */
42
+ function registerFetchableLoader(
43
+ id: string,
44
+ fn: LoaderFn<any, any, any>,
45
+ middleware: MiddlewareFn[]
46
+ ): void {
47
+ fetchableLoaderRegistry.set(id, { fn, middleware });
48
+ }
49
+
50
+ /**
51
+ * Get a fetchable loader's function from the internal registry by $$id
52
+ *
53
+ * This is used internally by:
54
+ * - Server actions (loaderAction) to execute loader functions
55
+ * - loader-registry.ts to populate the main registry for GET-based fetching
56
+ *
57
+ * Loaders are registered here when createLoader() is called with fetchable: true.
58
+ * The $$id is injected by the Vite exposeLoaderId plugin.
59
+ *
60
+ * @param id - The loader's $$id (auto-generated from file path + export name)
61
+ * @returns The loader function and middleware, or undefined if not found
62
+ *
63
+ * @internal This is primarily for internal use by the router infrastructure
64
+ */
65
+ export function getFetchableLoader(
66
+ id: string
67
+ ): { fn: LoaderFn<any, any, any>; middleware: MiddlewareFn[] } | undefined {
68
+ return fetchableLoaderRegistry.get(id);
69
+ }
70
+
71
+ // Overload 1: With function only (not fetchable)
72
+ export function createLoader<T>(
73
+ fn: LoaderFn<T, Record<string, string | undefined>, any>
74
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
75
+
76
+ // Overload 2: Fetchable with `true` (no middleware)
77
+ export function createLoader<T>(
78
+ fn: LoaderFn<T, Record<string, string | undefined>, any>,
79
+ fetchable: true
80
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
81
+
82
+ // Overload 3: Fetchable with middleware options
83
+ export function createLoader<T>(
84
+ fn: LoaderFn<T, Record<string, string | undefined>, any>,
85
+ options: FetchableLoaderOptions
86
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
87
+
88
+ // Implementation - the $$id parameter is injected by Vite plugin, not user-provided
89
+ export function createLoader<T>(
90
+ fn: LoaderFn<T, Record<string, string | undefined>, any>,
91
+ fetchable?: true | FetchableLoaderOptions,
92
+ // Hidden parameter injected by Vite exposeLoaderId plugin
93
+ __injectedId?: string
94
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
95
+ // The $$id will be set on the returned object by Vite plugin
96
+ // For fetchable loaders, __injectedId is also passed as a parameter
97
+ const loaderId = __injectedId || "";
98
+
99
+ // If not fetchable, store fn in registry and return a plain object.
100
+ // Server-side code looks up fn via getFetchableLoader($$id).
101
+ if (fetchable === undefined) {
102
+ if (fn && loaderId) {
103
+ registerFetchableLoader(loaderId, fn, []);
104
+ }
105
+ return {
106
+ __brand: "loader",
107
+ $$id: loaderId,
108
+ };
109
+ }
110
+
111
+ // Fetchable loader - store fn in registry and return a serializable object
112
+ const middleware: MiddlewareFn[] =
113
+ fetchable === true ? [] : fetchable?.middleware || [];
114
+
115
+ // Register the function in the internal registry by $$id (server-side only)
116
+ // The server action will look it up by $$id when executed
117
+ if (fn && loaderId) {
118
+ registerFetchableLoader(loaderId, fn, middleware);
119
+ }
120
+
121
+ // Create server action for form-based fetching
122
+ // This action is serializable and can be passed to client components
123
+ // The loaderId is captured in closure (it's a primitive string)
124
+ //
125
+ // IMPORTANT: The signature must be (prevState, formData) for useActionState compatibility.
126
+ // When used with useActionState, React passes the previous state as the first argument.
127
+ // The prevState is ignored here since loaders are stateless data fetchers.
128
+ async function loaderAction(
129
+ _prevState: Awaited<T> | null,
130
+ formData: FormData
131
+ ): Promise<Awaited<T>> {
132
+ "use server";
133
+
134
+ // Look up the loader from registry by $$id
135
+ const registered = fetchableLoaderRegistry.get(loaderId);
136
+ if (!registered) {
137
+ throw new Error(`Loader "${loaderId}" not found in registry`);
138
+ }
139
+
140
+ // Get request context (env, request, url, variables) from the RSC handler
141
+ // This is set by runWithRequestContext in rsc/index.ts when executing actions
142
+ const requestCtx = getRequestContext();
143
+
144
+ // Convert FormData to params object
145
+ const params: Record<string, string> = {};
146
+ formData.forEach((value, key) => {
147
+ if (typeof value === "string") {
148
+ params[key] = value;
149
+ }
150
+ });
151
+
152
+ // Use real request/url from context, or fall back to synthetic for edge cases
153
+ const actionUrl = requestCtx?.url ?? new URL("http://localhost/");
154
+ const actionRequest = requestCtx?.request ?? new Request(actionUrl, { method: "POST" });
155
+ const env = requestCtx?.env ?? {};
156
+
157
+ // Merge variables from request context (app-level middleware) with loader-specific variables
158
+ // requestCtx.var is the shared variables object from the handler
159
+ const variables: Record<string, any> = { ...requestCtx?.var };
160
+
161
+ // Execute middleware for auth checks, headers, cookies
162
+ // Headers/cookies set on ctx.res will be merged into the final response
163
+ if (registered.middleware.length > 0 && requestCtx?.res) {
164
+ const { executeServerActionMiddleware } = await import(
165
+ "./router/middleware.js"
166
+ );
167
+ await executeServerActionMiddleware(
168
+ registered.middleware,
169
+ actionRequest,
170
+ env,
171
+ params,
172
+ variables,
173
+ requestCtx.res
174
+ );
175
+ }
176
+
177
+ // Build context using createHandlerContext for consistency with route handlers
178
+ // Variables are now accessed from request context via getRequestContext()
179
+ const { createHandlerContext } = await import("./router/handler-context.js");
180
+ const baseCtx = createHandlerContext(
181
+ params,
182
+ actionRequest,
183
+ actionUrl.searchParams,
184
+ actionUrl.pathname,
185
+ actionUrl,
186
+ env
187
+ );
188
+
189
+ // Extend with server action specific properties
190
+ const ctx: any = {
191
+ ...baseCtx,
192
+ method: "POST",
193
+ formData,
194
+ };
195
+
196
+ // Execute and return result
197
+ return registered.fn(ctx);
198
+ }
199
+
200
+ // Return a plain object with action for form-based fetching.
201
+ // loaderAction has "use server" so RSC Flight serializes it natively as a server action reference.
202
+ return {
203
+ __brand: "loader",
204
+ $$id: loaderId,
205
+ action: loaderAction,
206
+ };
207
+ }
package/src/loader.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * rsc-router/loader (client version)
3
+ *
4
+ * Client-only stub for createLoader. Returns a minimal loader definition
5
+ * that can be passed to hooks like useLoader. The actual loader function
6
+ * is not included - it only exists on the server.
7
+ *
8
+ * The $$id is injected by the Vite exposeLoaderId plugin.
9
+ */
10
+
11
+ import type {
12
+ FetchableLoaderOptions,
13
+ LoaderDefinition,
14
+ LoaderFn,
15
+ } from "./types.js";
16
+
17
+ // Overload 1: With function only (not fetchable)
18
+ export function createLoader<T>(
19
+ fn: LoaderFn<T, Record<string, string | undefined>, any>
20
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
21
+
22
+ // Overload 2: Fetchable with `true` (no middleware)
23
+ export function createLoader<T>(
24
+ fn: LoaderFn<T, Record<string, string | undefined>, any>,
25
+ fetchable: true
26
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
27
+
28
+ // Overload 3: Fetchable with middleware options
29
+ export function createLoader<T>(
30
+ fn: LoaderFn<T, Record<string, string | undefined>, any>,
31
+ options: FetchableLoaderOptions
32
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
33
+
34
+ // Implementation - client stub that just returns the loader definition
35
+ // The $$id parameter is injected by Vite plugin, not user-provided
36
+ export function createLoader<T>(
37
+ _fn: LoaderFn<T, Record<string, string | undefined>, any>,
38
+ _fetchable?: true | FetchableLoaderOptions,
39
+ __injectedId?: string
40
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
41
+ // Client only needs the $$id for identification
42
+ // The actual loader function is only used on the server
43
+ return {
44
+ __brand: "loader",
45
+ $$id: __injectedId || "",
46
+ };
47
+ }
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import type { NetworkError } from "./errors.js";
5
+
6
+ interface NetworkErrorThrowerProps {
7
+ error: NetworkError;
8
+ }
9
+
10
+ /**
11
+ * Client component that throws a NetworkError during render.
12
+ * Used to trigger the root error boundary when a network error occurs
13
+ * during navigation or server actions.
14
+ *
15
+ * This must be a separate component because:
16
+ * 1. Errors must be thrown during React's render phase to be caught by error boundaries
17
+ * 2. The error occurs in async code (fetch), so we need to propagate it to React's render
18
+ */
19
+ export function NetworkErrorThrower({ error }: NetworkErrorThrowerProps): ReactNode {
20
+ throw error;
21
+ }
@@ -0,0 +1,15 @@
1
+ import { Context, createContext, type ReactNode } from "react";
2
+ import type { ResolvedSegment } from "./types";
3
+
4
+ export interface OutletContextValue {
5
+ content: ReactNode;
6
+ parallel?: ResolvedSegment[];
7
+ segment?: ResolvedSegment;
8
+ loaderData?: Record<string, any>;
9
+ parent?: OutletContextValue | null;
10
+ /** Loading component for Suspense fallback (from segment's loading() definition) */
11
+ loading?: ReactNode;
12
+ }
13
+
14
+ export const OutletContext: Context<OutletContextValue | null> =
15
+ createContext<OutletContextValue | null>(null);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Deterministic param hashing for prerender storage keys.
3
+ *
4
+ * Used at build time (child process) to generate filenames and at
5
+ * runtime (worker) to look up pre-rendered data. Both environments
6
+ * must produce identical hashes for the same params.
7
+ *
8
+ * Uses a simple DJB2-based hash that works in all JS environments
9
+ * (Node.js, Cloudflare Workers, browsers) without crypto imports.
10
+ */
11
+
12
+ /**
13
+ * Compute a deterministic hash string from route params.
14
+ * For static routes (no params), returns "_".
15
+ */
16
+ export function hashParams(params: Record<string, string>): string {
17
+ const entries = Object.entries(params);
18
+ if (entries.length === 0) return "_";
19
+
20
+ const sorted = entries.sort(([a], [b]) => a.localeCompare(b));
21
+ const str = sorted.map(([k, v]) => `${k}=${v}`).join("&");
22
+ return djb2Hex(str);
23
+ }
24
+
25
+ /**
26
+ * DJB2 hash returning an 8-char hex string.
27
+ * Deterministic across all JS runtimes.
28
+ */
29
+ function djb2Hex(str: string): string {
30
+ let hash = 5381;
31
+ for (let i = 0; i < str.length; i++) {
32
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
33
+ }
34
+ return hash.toString(16).padStart(8, "0");
35
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Prerender Store
3
+ *
4
+ * Reads pre-rendered segment data injected into the worker bundle at build time.
5
+ * The data is stored as globalThis.__PRERENDER_DATA, a JSON object keyed by
6
+ * "<routeName>/<paramHash>".
7
+ */
8
+
9
+ import type { SerializedSegmentData, SegmentHandleData } from "../cache/types.js";
10
+
11
+ export interface PrerenderEntry {
12
+ segments: SerializedSegmentData[];
13
+ handles: Record<string, SegmentHandleData>;
14
+ }
15
+
16
+ export interface PrerenderStore {
17
+ get(routeName: string, paramHash: string): PrerenderEntry | null;
18
+ }
19
+
20
+ declare global {
21
+ // Injected by closeBundle post-processing
22
+ // eslint-disable-next-line no-var
23
+ var __PRERENDER_DATA: Record<string, PrerenderEntry> | undefined;
24
+ }
25
+
26
+ /**
27
+ * Create a prerender store backed by globalThis.__PRERENDER_DATA.
28
+ * Returns null if no prerender data is available (dev mode or no prerendered routes).
29
+ */
30
+ export function createPrerenderStore(): PrerenderStore | null {
31
+ const data = globalThis.__PRERENDER_DATA;
32
+ if (!data || Object.keys(data).length === 0) return null;
33
+
34
+ return {
35
+ get(routeName: string, paramHash: string): PrerenderEntry | null {
36
+ const key = `${routeName}/${paramHash}`;
37
+ return data[key] ?? null;
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Pre-render handler definition for build-time rendering of route segments.
3
+ *
4
+ * createPrerenderHandler wraps a handler so that in production (phase 2)
5
+ * it can be pre-rendered at build time and served as a static Flight payload.
6
+ * In dev mode (phase 1), it behaves as a normal handler — the handler runs
7
+ * on every request just like a regular path() handler.
8
+ *
9
+ * The $$id is auto-generated by the Vite exposePrerenderHandlerId plugin
10
+ * based on file path and export name. No manual naming required.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Static page — no params
15
+ * export const DocsPage = createPrerenderHandler(async (ctx) => {
16
+ * return <div>Documentation</div>;
17
+ * });
18
+ *
19
+ * // Dynamic page — params first, handler second
20
+ * export const DocsArticle = createPrerenderHandler(
21
+ * async () => [{ slug: "getting-started" }, { slug: "api-reference" }],
22
+ * async (ctx) => {
23
+ * return <div>{ctx.params.slug}</div>;
24
+ * }
25
+ * );
26
+ * ```
27
+ */
28
+ import type { ReactNode } from "react";
29
+ import type { Handler, HandlerContext } from "./types.js";
30
+ import type { Handle } from "./handle.js";
31
+
32
+ // -- Types ------------------------------------------------------------------
33
+
34
+ export interface PrerenderOptions {
35
+ /**
36
+ * Keep handler in server bundle for live fallback (default: false).
37
+ * false: handler replaced with stub, source-only APIs excluded from bundle.
38
+ * true: handler stays in bundle, unknown params render live at request time.
39
+ */
40
+ passthrough?: boolean;
41
+ }
42
+
43
+ export interface BuildContext<TParams> {
44
+ /** Params extracted from the route pattern (populated from getParams). */
45
+ params: TParams;
46
+
47
+ /** Push handle data (frozen into pre-rendered output at build time). */
48
+ use: <T>(handle: Handle<T>) => (data: T) => void;
49
+
50
+ /** Synthetic URL built from pattern + params (no real request). */
51
+ url: URL;
52
+
53
+ /** Pathname portion of the synthetic URL. */
54
+ pathname: string;
55
+ }
56
+
57
+ export interface PrerenderHandlerDefinition<TParams = any> {
58
+ readonly __brand: "prerenderHandler";
59
+ /** Auto-generated unique ID (injected by Vite plugin). */
60
+ $$id: string;
61
+ /** In dev mode, the actual handler function that path() can call. */
62
+ handler: Handler<TParams>;
63
+ /** Returns the list of param objects to pre-render (dynamic routes). */
64
+ getParams?: () => Promise<TParams[]> | TParams[];
65
+ /** Pre-render options. */
66
+ options?: PrerenderOptions;
67
+ }
68
+
69
+ // -- Overloads --------------------------------------------------------------
70
+
71
+ // Overload 1: Static handler (no params)
72
+ export function createPrerenderHandler<TParams = {}>(
73
+ handler: (ctx: HandlerContext<TParams>) => ReactNode | Promise<ReactNode>,
74
+ options?: PrerenderOptions,
75
+ __injectedId?: string,
76
+ ): PrerenderHandlerDefinition<TParams>;
77
+
78
+ // Overload 2: Dynamic handler (getParams + handler)
79
+ export function createPrerenderHandler<TParams>(
80
+ getParams: () => Promise<TParams[]> | TParams[],
81
+ handler: (ctx: HandlerContext<TParams>) => ReactNode | Promise<ReactNode>,
82
+ options?: PrerenderOptions,
83
+ __injectedId?: string,
84
+ ): PrerenderHandlerDefinition<TParams>;
85
+
86
+ // -- Implementation ---------------------------------------------------------
87
+
88
+ export function createPrerenderHandler<TParams>(
89
+ handlerOrGetParams: Function,
90
+ handlerOrOptions?: Function | PrerenderOptions,
91
+ optionsOrId?: PrerenderOptions | string,
92
+ maybeId?: string,
93
+ ): PrerenderHandlerDefinition<TParams> {
94
+ // Resolve overloads:
95
+ // 1 fn arg: createPrerenderHandler(handler, options?, __injectedId?)
96
+ // 2 fn args: createPrerenderHandler(getParams, handler, options?, __injectedId?)
97
+ let handler: Handler<TParams>;
98
+ let getParams: (() => Promise<TParams[]> | TParams[]) | undefined;
99
+ let options: PrerenderOptions | undefined;
100
+ let id: string;
101
+
102
+ if (typeof handlerOrOptions === "function") {
103
+ // Two function args: getParams + handler
104
+ getParams = handlerOrGetParams as () => Promise<TParams[]> | TParams[];
105
+ handler = handlerOrOptions as Handler<TParams>;
106
+ if (typeof optionsOrId === "string") {
107
+ id = optionsOrId;
108
+ } else {
109
+ options = optionsOrId as PrerenderOptions | undefined;
110
+ id = maybeId ?? "";
111
+ }
112
+ } else {
113
+ // Single function arg: handler only
114
+ handler = handlerOrGetParams as Handler<TParams>;
115
+ if (typeof handlerOrOptions === "object" && handlerOrOptions !== null) {
116
+ options = handlerOrOptions as PrerenderOptions;
117
+ }
118
+ if (typeof optionsOrId === "string") {
119
+ id = optionsOrId;
120
+ } else {
121
+ id = maybeId ?? "";
122
+ }
123
+ }
124
+
125
+ if (!id && process.env.NODE_ENV !== "production") {
126
+ console.warn(
127
+ "[rsc-router] PrerenderHandler is missing $$id. " +
128
+ "Make sure the exposePrerenderHandlerId Vite plugin is enabled and " +
129
+ "the handler is exported with: export const MyPage = createPrerenderHandler(...)"
130
+ );
131
+ }
132
+
133
+ return {
134
+ __brand: "prerenderHandler" as const,
135
+ $$id: id,
136
+ handler,
137
+ ...(getParams ? { getParams } : {}),
138
+ ...(options ? { options } : {}),
139
+ };
140
+ }
141
+
142
+ // -- Type guard -------------------------------------------------------------
143
+
144
+ /**
145
+ * Type guard to check if a value is a PrerenderHandlerDefinition.
146
+ */
147
+ export function isPrerenderHandler(
148
+ value: unknown,
149
+ ): value is PrerenderHandlerDefinition {
150
+ return (
151
+ typeof value === "object" &&
152
+ value !== null &&
153
+ "__brand" in value &&
154
+ (value as { __brand: unknown }).__brand === "prerenderHandler"
155
+ );
156
+ }