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.
- package/dist/runtime/client/client.d.ts +3 -3
- package/dist/runtime/client/client.js +5 -5
- package/dist/runtime/client/navigation.d.ts +3 -3
- package/dist/runtime/client/navigation.js +5 -5
- package/dist/runtime/client/navigationCache.d.ts +1 -1
- package/dist/runtime/client/navigationCache.js +21 -21
- package/dist/runtime/client/navigationCache.test.js +28 -15
- package/dist/runtime/client/types.d.ts +1 -1
- package/dist/runtime/entries/no-react-server-ssr-bridge.js +1 -1
- package/dist/runtime/entries/no-react-server.js +3 -1
- package/dist/runtime/entries/react-server-only.js +1 -1
- package/dist/runtime/entries/worker.d.ts +1 -0
- package/dist/runtime/entries/worker.js +1 -0
- package/dist/runtime/lib/links.js +6 -6
- package/dist/runtime/lib/router.d.ts +19 -17
- package/dist/runtime/lib/router.js +346 -109
- package/dist/runtime/lib/router.test.js +370 -1
- package/dist/runtime/requestInfo/types.d.ts +1 -0
- package/dist/runtime/requestInfo/utils.js +1 -0
- package/dist/runtime/requestInfo/worker.js +2 -1
- package/dist/runtime/worker.js +6 -1
- package/dist/vite/runDirectivesScan.mjs +3 -1
- package/package.json +2 -1
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
159
|
+
export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, onHydrated, onActionResponse, } = {}) => {
|
|
160
160
|
const transportContext = {
|
|
161
161
|
setRscPayload: () => { },
|
|
162
162
|
handleResponse,
|
|
163
|
-
|
|
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('
|
|
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.
|
|
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,
|
|
33
|
-
* initClient({ handleResponse,
|
|
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
|
-
|
|
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,
|
|
82
|
-
* initClient({ handleResponse,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
8
|
+
let tabId = null;
|
|
9
|
+
if (typeof window !== "undefined") {
|
|
11
10
|
try {
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
tabId =
|
|
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
|
-
//
|
|
23
|
-
tabId =
|
|
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,
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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.");
|
|
@@ -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
|
|
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
|
-
*
|
|
111
|
+
* Defines an error handler that catches errors from routes, middleware, and RSC actions.
|
|
108
112
|
*
|
|
109
113
|
* @example
|
|
110
|
-
* //
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
|
134
|
+
* function BlogLayout({ children }: { children?: React.ReactNode }) {
|
|
133
135
|
* return (
|
|
134
136
|
* <div>
|
|
135
137
|
* <nav>Blog Navigation</nav>
|