rwsdk 1.0.0-beta.43 → 1.0.0-beta.45

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.
@@ -19,7 +19,7 @@ export declare const fetchTransport: Transport;
19
19
  * - `onCaughtError`: Handler for errors caught by error boundaries
20
20
  * - `onRecoverableError`: Handler for recoverable errors
21
21
  * @param handleResponse - Custom response handler for navigation errors (navigation GETs)
22
- * @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
22
+ * @param onHydrated - Callback invoked after a new RSC payload has been committed on the client
23
23
  * @param onActionResponse - Optional hook invoked when an action returns a Response;
24
24
  * return true to signal that the response has been handled and
25
25
  * default behaviour (e.g. redirects) should be skipped
@@ -64,10 +64,10 @@ export declare const fetchTransport: Transport;
64
64
  * },
65
65
  * });
66
66
  */
67
- export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, onHydrationUpdate, onActionResponse, }?: {
67
+ export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, onHydrated, onActionResponse, }?: {
68
68
  transport?: Transport;
69
69
  hydrateRootOptions?: HydrationOptions;
70
70
  handleResponse?: (response: Response) => boolean;
71
- onHydrationUpdate?: () => void;
71
+ onHydrated?: () => void;
72
72
  onActionResponse?: (actionResponse: ActionResponseData) => boolean | void;
73
73
  }) => Promise<void>;
@@ -111,7 +111,7 @@ export const fetchTransport = (transportContext) => {
111
111
  * - `onCaughtError`: Handler for errors caught by error boundaries
112
112
  * - `onRecoverableError`: Handler for recoverable errors
113
113
  * @param handleResponse - Custom response handler for navigation errors (navigation GETs)
114
- * @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
114
+ * @param onHydrated - Callback invoked after a new RSC payload has been committed on the client
115
115
  * @param onActionResponse - Optional hook invoked when an action returns a Response;
116
116
  * return true to signal that the response has been handled and
117
117
  * default behaviour (e.g. redirects) should be skipped
@@ -156,11 +156,11 @@ export const fetchTransport = (transportContext) => {
156
156
  * },
157
157
  * });
158
158
  */
159
- export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, onHydrationUpdate, onActionResponse, } = {}) => {
159
+ export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, onHydrated, onActionResponse, } = {}) => {
160
160
  const transportContext = {
161
161
  setRscPayload: () => { },
162
162
  handleResponse,
163
- onHydrationUpdate,
163
+ onHydrated,
164
164
  onActionResponse,
165
165
  };
166
166
  let transportCallServer = transport(transportContext);
@@ -179,7 +179,7 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
179
179
  };
180
180
  const rootEl = document.getElementById("hydrate-root");
181
181
  if (!rootEl) {
182
- throw new Error('no element with id "hydrate-root"');
182
+ throw new Error('RedwoodSDK: No element with id "hydrate-root" found in the document. This element is required for hydration. Ensure your Document component contains a <div id="hydrate-root">{children}</div>.');
183
183
  }
184
184
  let rscPayload;
185
185
  // context(justinvdm, 18 Jun 2025): We inject the RSC payload
@@ -198,7 +198,7 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
198
198
  React.useEffect(() => {
199
199
  if (!streamData)
200
200
  return;
201
- transportContext.onHydrationUpdate?.();
201
+ transportContext.onHydrated?.();
202
202
  }, [streamData]);
203
203
  return (_jsx(_Fragment, { children: streamData
204
204
  ? React.use(streamData).node
@@ -29,8 +29,8 @@ export declare function navigate(href: string, options?: NavigateOptions): Promi
29
29
  * // Basic usage
30
30
  * import { initClient, initClientNavigation } from "rwsdk/client";
31
31
  *
32
- * const { handleResponse, onHydrationUpdate } = initClientNavigation();
33
- * initClient({ handleResponse, onHydrationUpdate });
32
+ * const { handleResponse, onHydrated } = initClientNavigation();
33
+ * initClient({ handleResponse, onHydrated });
34
34
  *
35
35
  * @example
36
36
  * // With custom scroll behavior
@@ -58,5 +58,5 @@ export declare function navigate(href: string, options?: NavigateOptions): Promi
58
58
  */
59
59
  export declare function initClientNavigation(opts?: ClientNavigationOptions): {
60
60
  handleResponse: (response: Response) => boolean;
61
- onHydrationUpdate: () => void;
61
+ onHydrated: () => void;
62
62
  };
@@ -78,8 +78,8 @@ function saveScrollPosition(x, y) {
78
78
  * // Basic usage
79
79
  * import { initClient, initClientNavigation } from "rwsdk/client";
80
80
  *
81
- * const { handleResponse, onHydrationUpdate } = initClientNavigation();
82
- * initClient({ handleResponse, onHydrationUpdate });
81
+ * const { handleResponse, onHydrated } = initClientNavigation();
82
+ * initClient({ handleResponse, onHydrated });
83
83
  *
84
84
  * @example
85
85
  * // With custom scroll behavior
@@ -134,9 +134,9 @@ export function initClientNavigation(opts = {}) {
134
134
  if (opts.cacheStorage && typeof globalThis !== "undefined") {
135
135
  globalThis.__rsc_cacheStorage = opts.cacheStorage;
136
136
  }
137
- function onHydrationUpdate() {
137
+ function onHydrated() {
138
138
  // After each RSC hydration/update, increment generation and evict old caches,
139
- // then warm the navigation cache based on any <link rel="prefetch"> tags
139
+ // then warm the navigation cache based on any <link rel="x-prefetch"> tags
140
140
  // rendered for the current location.
141
141
  onNavigationCommit(undefined, opts.cacheStorage);
142
142
  void preloadFromLinkTags(undefined, undefined, opts.cacheStorage);
@@ -144,6 +144,6 @@ export function initClientNavigation(opts = {}) {
144
144
  // Return callbacks for use with initClient
145
145
  return {
146
146
  handleResponse,
147
- onHydrationUpdate,
147
+ onHydrated,
148
148
  };
149
149
  }
@@ -57,7 +57,7 @@ export declare function evictOldGenerationCaches(env?: NavigationCacheEnvironmen
57
57
  */
58
58
  export declare function onNavigationCommit(env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): void;
59
59
  /**
60
- * Scan the document for `<link rel="prefetch" href="...">` elements that point
60
+ * Scan the document for `<link rel="x-prefetch" href="...">` elements that point
61
61
  * to same-origin paths and prefetch their RSC navigation responses into the
62
62
  * Cache API.
63
63
  *
@@ -5,30 +5,22 @@ function getOrInitializeCacheState() {
5
5
  if (cacheState) {
6
6
  return cacheState;
7
7
  }
8
- // Get or generate tabId
9
- let tabId;
10
- if (typeof window !== "undefined" && window.sessionStorage) {
8
+ let tabId = null;
9
+ if (typeof window !== "undefined") {
11
10
  try {
12
- const stored = sessionStorage.getItem(TAB_ID_STORAGE_KEY);
13
- if (stored) {
14
- tabId = stored;
15
- }
16
- else {
17
- tabId = crypto.randomUUID();
11
+ tabId = sessionStorage.getItem(TAB_ID_STORAGE_KEY);
12
+ if (!tabId) {
13
+ tabId = `${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
18
14
  sessionStorage.setItem(TAB_ID_STORAGE_KEY, tabId);
19
15
  }
20
16
  }
21
17
  catch {
22
- // Fallback to in-memory tabId if sessionStorage is unavailable
23
- tabId = crypto.randomUUID();
18
+ // sessionStorage might be unavailable
19
+ tabId = tabId || `${Date.now()}-${Math.floor(Math.random() * 1000000)}`;
24
20
  }
25
21
  }
26
- else {
27
- // Fallback for non-browser environments
28
- tabId = crypto.randomUUID();
29
- }
30
22
  cacheState = {
31
- tabId,
23
+ tabId: tabId || "1",
32
24
  generation: 0,
33
25
  buildId: BUILD_ID,
34
26
  };
@@ -36,7 +28,7 @@ function getOrInitializeCacheState() {
36
28
  }
37
29
  function getCurrentCacheName() {
38
30
  const state = getOrInitializeCacheState();
39
- return `rsc-prefetch:${state.buildId}:${state.tabId}:${state.generation}`;
31
+ return `rsc-x-prefetch:${state.buildId}:${state.tabId}:${state.generation}`;
40
32
  }
41
33
  function incrementGeneration() {
42
34
  const state = getOrInitializeCacheState();
@@ -135,6 +127,9 @@ export async function preloadNavigationUrl(rawUrl, env, cacheStorage) {
135
127
  const request = new Request(url.toString(), {
136
128
  method: "GET",
137
129
  redirect: "manual",
130
+ headers: {
131
+ "x-prefetch": "true",
132
+ },
138
133
  });
139
134
  const cacheName = getCurrentCacheName();
140
135
  const cache = await storage.open(cacheName);
@@ -221,7 +216,7 @@ export async function evictOldGenerationCaches(env, cacheStorage) {
221
216
  try {
222
217
  // List all cache names
223
218
  const cacheNames = await storage.keys();
224
- const prefix = `rsc-prefetch:${buildId}:${tabId}:`;
219
+ const prefix = `rsc-x-prefetch:${buildId}:${tabId}:`;
225
220
  // Find all caches for this tab
226
221
  const tabCaches = cacheNames.filter((name) => name.startsWith(prefix));
227
222
  // Delete caches with generation numbers less than current
@@ -256,11 +251,16 @@ export async function evictOldGenerationCaches(env, cacheStorage) {
256
251
  * as complete and prepare for the next navigation cycle.
257
252
  */
258
253
  export function onNavigationCommit(env, cacheStorage) {
254
+ const runtimeEnv = env ?? getBrowserNavigationCacheEnvironment();
255
+ const storage = cacheStorage ?? createDefaultNavigationCacheStorage(runtimeEnv);
256
+ if (!storage) {
257
+ return;
258
+ }
259
259
  incrementGeneration();
260
- void evictOldGenerationCaches(env, cacheStorage);
260
+ void evictOldGenerationCaches(env, storage);
261
261
  }
262
262
  /**
263
- * Scan the document for `<link rel="prefetch" href="...">` elements that point
263
+ * Scan the document for `<link rel="x-prefetch" href="...">` elements that point
264
264
  * to same-origin paths and prefetch their RSC navigation responses into the
265
265
  * Cache API.
266
266
  *
@@ -272,7 +272,7 @@ export async function preloadFromLinkTags(doc = document, env, cacheStorage) {
272
272
  if (typeof doc === "undefined") {
273
273
  return;
274
274
  }
275
- const links = Array.from(doc.querySelectorAll('link[rel="prefetch"][href]'));
275
+ const links = Array.from(doc.querySelectorAll('link[rel="x-prefetch"][href]'));
276
276
  await Promise.all(links.map((link) => {
277
277
  const href = link.getAttribute("href");
278
278
  if (!href) {
@@ -148,6 +148,19 @@ describe("navigationCache", () => {
148
148
  const requestUrl = new URL(request.url);
149
149
  expect(requestUrl.searchParams.has("__rsc")).toBe(true);
150
150
  });
151
+ it("should add x-prefetch header", async () => {
152
+ const env = {
153
+ isSecureContext: true,
154
+ origin: "https://example.com",
155
+ caches: mockCacheStorage,
156
+ fetch: mockFetch,
157
+ };
158
+ const url = new URL("https://example.com/test");
159
+ await preloadNavigationUrl(url, env);
160
+ const fetchCall = mockFetch.mock.calls[0];
161
+ const request = fetchCall[0];
162
+ expect(request.headers.get("x-prefetch")).toBe("true");
163
+ });
151
164
  it("should use custom cacheStorage when provided", async () => {
152
165
  const customCache = {
153
166
  put: vi.fn().mockResolvedValue(undefined),
@@ -283,27 +296,27 @@ describe("navigationCache", () => {
283
296
  await preloadNavigationUrl(url, env);
284
297
  const openCall = mockCacheStorage.open.mock.calls[0];
285
298
  const cacheName = openCall[0];
286
- const tabIdMatch = cacheName.match(/^rsc-prefetch:rwsdk:([^:]+):\d+$/);
299
+ const tabIdMatch = cacheName.match(/^rsc-x-prefetch:rwsdk:([^:]+):\d+$/);
287
300
  const tabId = tabIdMatch ? tabIdMatch[1] : "test-uuid-123";
288
301
  // Increment generation to 2 by calling onNavigationCommit twice
289
302
  onNavigationCommit(env);
290
303
  onNavigationCommit(env);
291
304
  // Mock cache names matching the actual tabId
292
305
  const allCacheNames = [
293
- `rsc-prefetch:rwsdk:${tabId}:0`,
294
- `rsc-prefetch:rwsdk:${tabId}:1`,
295
- `rsc-prefetch:rwsdk:${tabId}:2`,
296
- "rsc-prefetch:rwsdk:other-tab:0",
306
+ `rsc-x-prefetch:rwsdk:${tabId}:0`,
307
+ `rsc-x-prefetch:rwsdk:${tabId}:1`,
308
+ `rsc-x-prefetch:rwsdk:${tabId}:2`,
309
+ "rsc-x-prefetch:rwsdk:other-tab:0",
297
310
  ];
298
311
  mockCacheStorage.keys.mockResolvedValue(allCacheNames);
299
312
  await evictOldGenerationCaches(env);
300
313
  // Wait for the cleanup to execute
301
314
  await new Promise((resolve) => setTimeout(resolve, 10));
302
315
  // Should delete generations 0 and 1, but not 2 (current) or other-tab
303
- expect(mockCacheStorage.delete).toHaveBeenCalledWith(`rsc-prefetch:rwsdk:${tabId}:0`);
304
- expect(mockCacheStorage.delete).toHaveBeenCalledWith(`rsc-prefetch:rwsdk:${tabId}:1`);
305
- expect(mockCacheStorage.delete).not.toHaveBeenCalledWith(`rsc-prefetch:rwsdk:${tabId}:2`);
306
- expect(mockCacheStorage.delete).not.toHaveBeenCalledWith("rsc-prefetch:rwsdk:other-tab:0");
316
+ expect(mockCacheStorage.delete).toHaveBeenCalledWith(`rsc-x-prefetch:rwsdk:${tabId}:0`);
317
+ expect(mockCacheStorage.delete).toHaveBeenCalledWith(`rsc-x-prefetch:rwsdk:${tabId}:1`);
318
+ expect(mockCacheStorage.delete).not.toHaveBeenCalledWith(`rsc-x-prefetch:rwsdk:${tabId}:2`);
319
+ expect(mockCacheStorage.delete).not.toHaveBeenCalledWith("rsc-x-prefetch:rwsdk:other-tab:0");
307
320
  });
308
321
  it("should use custom cacheStorage when provided", async () => {
309
322
  // Get the actual tabId that will be used
@@ -317,7 +330,7 @@ describe("navigationCache", () => {
317
330
  await preloadNavigationUrl(url, env);
318
331
  const openCall = mockCacheStorage.open.mock.calls[0];
319
332
  const cacheName = openCall[0];
320
- const tabIdMatch = cacheName.match(/^rsc-prefetch:rwsdk:([^:]+):\d+$/);
333
+ const tabIdMatch = cacheName.match(/^rsc-x-prefetch:rwsdk:([^:]+):\d+$/);
321
334
  const tabId = tabIdMatch ? tabIdMatch[1] : "test-uuid-123";
322
335
  const customStorage = {
323
336
  open: vi.fn(),
@@ -325,8 +338,8 @@ describe("navigationCache", () => {
325
338
  keys: vi
326
339
  .fn()
327
340
  .mockResolvedValue([
328
- `rsc-prefetch:rwsdk:${tabId}:0`,
329
- `rsc-prefetch:rwsdk:${tabId}:1`,
341
+ `rsc-x-prefetch:rwsdk:${tabId}:0`,
342
+ `rsc-x-prefetch:rwsdk:${tabId}:1`,
330
343
  ]),
331
344
  };
332
345
  // Increment generation so there are old caches to delete
@@ -367,14 +380,14 @@ describe("navigationCache", () => {
367
380
  });
368
381
  });
369
382
  describe("preloadFromLinkTags", () => {
370
- it("should preload URLs from prefetch link tags", async () => {
383
+ it("should preload URLs from x-prefetch link tags", async () => {
371
384
  const env = {
372
385
  isSecureContext: true,
373
386
  origin: "https://example.com",
374
387
  caches: mockCacheStorage,
375
388
  fetch: mockFetch,
376
389
  };
377
- // Create a mock document with prefetch links
390
+ // Create a mock document with x-prefetch links
378
391
  const mockDoc = {
379
392
  querySelectorAll: vi.fn().mockReturnValue([
380
393
  {
@@ -450,7 +463,7 @@ describe("navigationCache", () => {
450
463
  await preloadNavigationUrl(url, env);
451
464
  const openCall = mockCacheStorage.open.mock.calls[0];
452
465
  const cacheName = openCall[0];
453
- expect(cacheName).toMatch(/^rsc-prefetch:rwsdk:[^:]+:\d+$/);
466
+ expect(cacheName).toMatch(/^rsc-x-prefetch:rwsdk:[^:]+:\d+$/);
454
467
  });
455
468
  });
456
469
  });
@@ -23,7 +23,7 @@ export type TransportContext = {
23
23
  * This is useful for features like client-side navigation that want to run logic
24
24
  * after hydration/updates, e.g. warming navigation caches.
25
25
  */
26
- onHydrationUpdate?: () => void;
26
+ onHydrated?: () => void;
27
27
  /**
28
28
  * Optional callback invoked when an action returns a Response.
29
29
  * Return true to signal that the response has been handled and
@@ -1,2 +1,2 @@
1
1
  "use strict";
2
- throw new Error("rwsdk: SSR bridge was resolved with 'react-server' condition. This is a bug - the SSR bridge should be intercepted by the esbuild plugin before reaching package.json exports. Please report this issue.");
2
+ throw new Error("RedwoodSDK: SSR bridge was resolved with 'react-server' condition. This is a bug - the SSR bridge should be intercepted by the esbuild plugin before reaching package.json exports. Please report this issue at https://github.com/redwoodjs/sdk/issues.");
@@ -1,2 +1,4 @@
1
1
  "use strict";
2
- throw new Error("rwsdk: 'react-server' is not supported in this environment");
2
+ throw new Error("RedwoodSDK: A client-only module was incorrectly resolved with the 'react-server' condition.\n\n" +
3
+ "This error occurs when modules like 'rwsdk/client', 'rwsdk/__ssr', or 'rwsdk/__ssr_bridge' are being imported in a React Server Components context.\n\n" +
4
+ "For detailed troubleshooting steps, see: https://docs.rwsdk.com/guides/troubleshooting#react-server-components-configuration-errors");
@@ -1,2 +1,2 @@
1
1
  "use strict";
2
- throw new Error("rwsdk: 'react-server' import condition needs to be used in this environment");
2
+ throw new Error("RedwoodSDK: 'react-server' import condition needs to be used in this environment. This code should only run in React Server Components (RSC) context. Check that you're not importing server-only code in client components.");
@@ -1,3 +1,4 @@
1
+ import "server-only";
1
2
  import "./types/worker";
2
3
  export * from "../error";
3
4
  export * from "../lib/types";
@@ -1,3 +1,4 @@
1
+ import "server-only";
1
2
  import "./types/worker";
2
3
  export * from "../error";
3
4
  export * from "../lib/types";
@@ -19,13 +19,13 @@ export function defineLinks(routes) {
19
19
  // Original implementation for route arrays
20
20
  routes.forEach((route) => {
21
21
  if (typeof route !== "string") {
22
- throw new Error(`Invalid route: ${route}. Routes must be strings.`);
22
+ throw new Error(`RedwoodSDK: Invalid route: ${route}. Routes must be string literals. Ensure you're passing an array of route paths.`);
23
23
  }
24
24
  });
25
25
  const link = createLinkFunction();
26
26
  return ((path, params) => {
27
27
  if (!routes.includes(path)) {
28
- throw new Error(`Invalid route: ${path}`);
28
+ throw new Error(`RedwoodSDK: Invalid route: ${path}. This route is not included in the routes array passed to defineLinks(). Check for typos or ensure the route is defined in your router.`);
29
29
  }
30
30
  return link(path, params);
31
31
  });
@@ -36,7 +36,7 @@ function createLinkFunction() {
36
36
  const expectsParams = hasRouteParameters(path);
37
37
  if (!params || Object.keys(params).length === 0) {
38
38
  if (expectsParams) {
39
- throw new Error(`Route ${path} requires an object of parameters`);
39
+ throw new Error(`RedwoodSDK: Route ${path} requires an object of parameters (e.g., link("${path}", { id: "123" })).`);
40
40
  }
41
41
  return path;
42
42
  }
@@ -62,7 +62,7 @@ function interpolate(template, params) {
62
62
  const name = match[1];
63
63
  const value = params[name];
64
64
  if (value === undefined) {
65
- throw new Error(`Missing parameter "${name}" for route ${template}`);
65
+ throw new Error(`RedwoodSDK: Missing parameter "${name}" for route ${template}. Ensure you're providing all required parameters in the params object.`);
66
66
  }
67
67
  result += encodeURIComponent(value);
68
68
  consumed.add(name);
@@ -71,7 +71,7 @@ function interpolate(template, params) {
71
71
  const key = `$${wildcardIndex}`;
72
72
  const value = params[key];
73
73
  if (value === undefined) {
74
- throw new Error(`Missing parameter "${key}" for route ${template}`);
74
+ throw new Error(`RedwoodSDK: Missing parameter "${key}" for route ${template}. Wildcard routes use $0, $1, etc. as parameter keys.`);
75
75
  }
76
76
  result += encodeWildcardValue(value);
77
77
  consumed.add(key);
@@ -82,7 +82,7 @@ function interpolate(template, params) {
82
82
  result += template.slice(lastIndex);
83
83
  for (const key of Object.keys(params)) {
84
84
  if (!consumed.has(key)) {
85
- throw new Error(`Parameter "${key}" is not used by route ${template}`);
85
+ throw new Error(`RedwoodSDK: Parameter "${key}" is not used by route ${template}. Check your params object for typos or remove unused parameters.`);
86
86
  }
87
87
  }
88
88
  TOKEN_REGEX.lastIndex = 0;
@@ -6,6 +6,10 @@ type BivariantRouteHandler<T extends RequestInfo, R> = {
6
6
  bivarianceHack(requestInfo: T): R;
7
7
  }["bivarianceHack"];
8
8
  export type RouteMiddleware<T extends RequestInfo = RequestInfo> = BivariantRouteHandler<T, MaybePromise<React.JSX.Element | Response | void>>;
9
+ export type ExceptHandler<T extends RequestInfo = RequestInfo> = {
10
+ __rwExcept: true;
11
+ handler: (error: unknown, requestInfo: T) => MaybePromise<React.JSX.Element | Response | void>;
12
+ };
9
13
  type RouteFunction<T extends RequestInfo = RequestInfo> = BivariantRouteHandler<T, MaybePromise<Response>>;
10
14
  type RouteComponent<T extends RequestInfo = RequestInfo> = BivariantRouteHandler<T, MaybePromise<React.JSX.Element | Response | void>>;
11
15
  type RouteHandler<T extends RequestInfo = RequestInfo> = RouteFunction<T> | RouteComponent<T> | readonly [...RouteMiddleware<T>[], RouteFunction<T> | RouteComponent<T>];
@@ -22,7 +26,7 @@ export type MethodHandlers<T extends RequestInfo = RequestInfo> = {
22
26
  [method: string]: RouteHandler<T>;
23
27
  };
24
28
  };
25
- export type Route<T extends RequestInfo = RequestInfo> = RouteMiddleware<T> | RouteDefinition<string, T> | readonly Route<T>[];
29
+ export type Route<T extends RequestInfo = RequestInfo> = RouteMiddleware<T> | RouteDefinition<string, T> | ExceptHandler<T> | readonly Route<T>[];
26
30
  type NormalizedRouteDefinition<T extends RequestInfo = RequestInfo> = {
27
31
  path: string;
28
32
  handler: RouteHandler<T> | MethodHandlers<T>;
@@ -36,7 +40,7 @@ type TrimLeadingSlash<S extends string> = S extends `/${infer Rest}` ? TrimLeadi
36
40
  type NormalizePrefix<Prefix extends string> = TrimTrailingSlash<TrimLeadingSlash<Prefix>> extends "" ? "" : `/${TrimTrailingSlash<TrimLeadingSlash<Prefix>>}`;
37
41
  type NormalizePath<Path extends string> = TrimTrailingSlash<Path> extends "/" ? "/" : `/${TrimTrailingSlash<TrimLeadingSlash<Path>>}`;
38
42
  type JoinPaths<Prefix extends string, Path extends string> = NormalizePrefix<Prefix> extends "" ? NormalizePath<Path> : Path extends "/" ? NormalizePrefix<Prefix> : `${NormalizePrefix<Prefix>}${NormalizePath<Path>}`;
39
- type PrefixedRouteValue<Prefix extends string, Value> = Value extends RouteDefinition<infer Path, infer Req> ? RouteDefinition<JoinPaths<Prefix, Path>, Req> : Value extends readonly Route<any>[] ? PrefixedRouteArray<Prefix, Value> : Value;
43
+ type PrefixedRouteValue<Prefix extends string, Value> = Value extends RouteDefinition<infer Path, infer Req> ? RouteDefinition<JoinPaths<Prefix, Path>, Req> : Value extends ExceptHandler<any> ? Value : Value extends readonly Route<any>[] ? PrefixedRouteArray<Prefix, Value> : Value;
40
44
  type PrefixedRouteArray<Prefix extends string, Routes extends readonly Route<any>[]> = Routes extends readonly [] ? [] : Routes extends readonly [infer Head, ...infer Tail] ? readonly [
41
45
  PrefixedRouteValue<Prefix, Head>,
42
46
  ...PrefixedRouteArray<Prefix, Tail extends readonly Route<any>[] ? Tail : []>
@@ -104,24 +108,22 @@ export declare function route<Path extends string, T extends RequestInfo = Reque
104
108
  */
105
109
  export declare function index<T extends RequestInfo = RequestInfo>(handler: RouteHandler<T>): RouteDefinition<"/", T>;
106
110
  /**
107
- * Prefixes a group of routes with a path.
111
+ * Defines an error handler that catches errors from routes, middleware, and RSC actions.
108
112
  *
109
113
  * @example
110
- * // Organize blog routes under /blog
111
- * const blogRoutes = [
112
- * route("/", () => <BlogIndex />),
113
- * route("/post/:id", ({ params }) => <BlogPost id={params.id} />),
114
- * route("/admin", [isAuthenticated, () => <BlogAdmin />]),
115
- * ]
114
+ * // Global error handler
115
+ * except((error, requestInfo) => {
116
+ * console.error(error);
117
+ * return new Response("Internal Server Error", { status: 500 });
118
+ * })
116
119
  *
117
- * // In worker.tsx
118
- * defineApp([
119
- * render(Document, [
120
- * route("/", () => <HomePage />),
121
- * prefix("/blog", blogRoutes),
122
- * ]),
123
- * ])
120
+ * @example
121
+ * // Error handler that returns a React component
122
+ * except((error) => {
123
+ * return <ErrorPage error={error} />;
124
+ * })
124
125
  */
126
+ export declare function except<T extends RequestInfo = RequestInfo>(handler: (error: unknown, requestInfo: T) => MaybePromise<React.JSX.Element | Response | void>): ExceptHandler<T>;
125
127
  export declare function prefix<Prefix extends string, T extends RequestInfo = RequestInfo, Routes extends readonly Route<T>[] = readonly Route<T>[]>(prefixPath: Prefix, routes: Routes): PrefixedRouteArray<Prefix, Routes>;
126
128
  export declare const wrapHandlerToThrowResponses: <T extends RequestInfo = RequestInfo>(handler: RouteFunction<T> | RouteComponent<T>) => RouteHandler<T>;
127
129
  /**
@@ -129,7 +131,7 @@ export declare const wrapHandlerToThrowResponses: <T extends RequestInfo = Reque
129
131
  *
130
132
  * @example
131
133
  * // Define a layout component
132
- * function BlogLayout({ children }: { children: React.ReactNode }) {
134
+ * function BlogLayout({ children }: { children?: React.ReactNode }) {
133
135
  * return (
134
136
  * <div>
135
137
  * <nav>Blog Navigation</nav>