rwsdk 1.0.0-beta.5 → 1.0.0-beta.50

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 (157) hide show
  1. package/bin/rw-scripts.mjs +13 -13
  2. package/dist/lib/constants.d.mts +1 -0
  3. package/dist/lib/constants.mjs +7 -4
  4. package/dist/lib/e2e/browser.mjs +6 -2
  5. package/dist/lib/e2e/constants.d.mts +4 -0
  6. package/dist/lib/e2e/constants.mjs +49 -12
  7. package/dist/lib/e2e/dev.mjs +49 -57
  8. package/dist/lib/e2e/environment.d.mts +2 -0
  9. package/dist/lib/e2e/environment.mjs +201 -64
  10. package/dist/lib/e2e/index.d.mts +2 -0
  11. package/dist/lib/e2e/index.mjs +2 -0
  12. package/dist/lib/e2e/poll.d.mts +1 -1
  13. package/dist/lib/e2e/release.d.mts +1 -0
  14. package/dist/lib/e2e/release.mjs +57 -52
  15. package/dist/lib/e2e/tarball.mjs +2 -34
  16. package/dist/lib/e2e/testHarness.d.mts +39 -3
  17. package/dist/lib/e2e/testHarness.mjs +239 -92
  18. package/dist/lib/e2e/utils.d.mts +1 -0
  19. package/dist/lib/e2e/utils.mjs +15 -0
  20. package/dist/lib/normalizeModulePath.mjs +1 -1
  21. package/dist/runtime/client/client.d.ts +64 -2
  22. package/dist/runtime/client/client.js +156 -15
  23. package/dist/runtime/client/navigation.d.ts +45 -0
  24. package/dist/runtime/client/navigation.js +68 -14
  25. package/dist/runtime/client/navigationCache.d.ts +68 -0
  26. package/dist/runtime/client/navigationCache.js +294 -0
  27. package/dist/runtime/client/navigationCache.test.js +469 -0
  28. package/dist/runtime/client/types.d.ts +26 -5
  29. package/dist/runtime/client/types.js +8 -1
  30. package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
  31. package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
  32. package/dist/runtime/entries/no-react-server.js +3 -1
  33. package/dist/runtime/entries/react-server-only.js +1 -1
  34. package/dist/runtime/entries/router.d.ts +1 -0
  35. package/dist/runtime/entries/routerClient.d.ts +1 -0
  36. package/dist/runtime/entries/routerClient.js +1 -0
  37. package/dist/runtime/entries/worker.d.ts +4 -0
  38. package/dist/runtime/entries/worker.js +4 -0
  39. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  40. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  41. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  42. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  43. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  44. package/dist/runtime/lib/db/createDb.js +4 -0
  45. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  46. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  47. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  48. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  49. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  50. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  51. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  52. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  53. package/dist/runtime/lib/links.d.ts +21 -7
  54. package/dist/runtime/lib/links.js +84 -26
  55. package/dist/runtime/lib/links.test.d.ts +1 -0
  56. package/dist/runtime/lib/links.test.js +20 -0
  57. package/dist/runtime/lib/manifest.d.ts +1 -1
  58. package/dist/runtime/lib/manifest.js +7 -4
  59. package/dist/runtime/lib/realtime/client.js +28 -6
  60. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  61. package/dist/runtime/lib/router.d.ts +154 -35
  62. package/dist/runtime/lib/router.js +491 -105
  63. package/dist/runtime/lib/router.test.js +611 -1
  64. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  65. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  66. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  67. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  68. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  69. package/dist/runtime/lib/types.js +1 -0
  70. package/dist/runtime/register/client.d.ts +1 -1
  71. package/dist/runtime/register/client.js +10 -3
  72. package/dist/runtime/register/worker.js +13 -4
  73. package/dist/runtime/render/normalizeActionResult.js +8 -1
  74. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  75. package/dist/runtime/render/renderToStream.d.ts +4 -2
  76. package/dist/runtime/render/renderToStream.js +53 -24
  77. package/dist/runtime/render/renderToString.d.ts +3 -6
  78. package/dist/runtime/requestInfo/types.d.ts +5 -1
  79. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  80. package/dist/runtime/requestInfo/utils.js +45 -0
  81. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  82. package/dist/runtime/requestInfo/worker.js +5 -11
  83. package/dist/runtime/script.d.ts +1 -3
  84. package/dist/runtime/script.js +1 -10
  85. package/dist/runtime/server.d.ts +52 -0
  86. package/dist/runtime/server.js +88 -0
  87. package/dist/runtime/state.d.ts +3 -0
  88. package/dist/runtime/state.js +13 -0
  89. package/dist/runtime/worker.d.ts +3 -1
  90. package/dist/runtime/worker.js +45 -2
  91. package/dist/scripts/debug-sync.mjs +18 -20
  92. package/dist/scripts/worker-run.d.mts +1 -1
  93. package/dist/scripts/worker-run.mjs +59 -113
  94. package/dist/use-synced-state/SyncedStateServer.d.mts +36 -0
  95. package/dist/use-synced-state/SyncedStateServer.mjs +196 -0
  96. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  97. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +116 -0
  98. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  99. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  100. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  101. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  102. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  103. package/dist/use-synced-state/__tests__/worker.test.mjs +70 -0
  104. package/dist/use-synced-state/client-core.d.ts +29 -0
  105. package/dist/use-synced-state/client-core.js +103 -0
  106. package/dist/use-synced-state/client.d.ts +3 -0
  107. package/dist/use-synced-state/client.js +4 -0
  108. package/dist/use-synced-state/constants.d.mts +1 -0
  109. package/dist/use-synced-state/constants.mjs +1 -0
  110. package/dist/use-synced-state/useSyncedState.d.ts +21 -0
  111. package/dist/use-synced-state/useSyncedState.js +64 -0
  112. package/dist/use-synced-state/worker.d.mts +14 -0
  113. package/dist/use-synced-state/worker.mjs +135 -0
  114. package/dist/vite/buildApp.mjs +34 -2
  115. package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
  116. package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
  117. package/dist/vite/configPlugin.mjs +9 -14
  118. package/dist/vite/constants.d.mts +1 -0
  119. package/dist/vite/constants.mjs +1 -0
  120. package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
  121. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  122. package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
  123. package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
  124. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  125. package/dist/vite/directivesPlugin.mjs +4 -4
  126. package/dist/vite/envResolvers.d.mts +11 -0
  127. package/dist/vite/envResolvers.mjs +20 -0
  128. package/dist/vite/getViteEsbuild.mjs +2 -1
  129. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  130. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  131. package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
  132. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  133. package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
  134. package/dist/vite/linkerPlugin.d.mts +2 -1
  135. package/dist/vite/linkerPlugin.mjs +11 -3
  136. package/dist/vite/linkerPlugin.test.mjs +15 -0
  137. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  138. package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
  139. package/dist/vite/redwoodPlugin.mjs +9 -11
  140. package/dist/vite/redwoodPlugin.test.mjs +4 -4
  141. package/dist/vite/runDirectivesScan.mjs +75 -19
  142. package/dist/vite/ssrBridgePlugin.mjs +132 -40
  143. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  144. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  145. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  146. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  147. package/dist/vite/statePlugin.d.mts +4 -0
  148. package/dist/vite/statePlugin.mjs +62 -0
  149. package/dist/vite/transformClientComponents.test.mjs +32 -0
  150. package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
  151. package/dist/vite/transformServerFunctions.mjs +66 -4
  152. package/dist/vite/transformServerFunctions.test.mjs +35 -0
  153. package/dist/vite/virtualPlugin.mjs +6 -7
  154. package/package.json +41 -19
  155. package/dist/vite/manifestPlugin.d.mts +0 -4
  156. package/dist/vite/manifestPlugin.mjs +0 -63
  157. /package/dist/runtime/{lib/rwContext.js → client/navigationCache.test.d.ts} +0 -0
@@ -0,0 +1,469 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createDefaultNavigationCacheStorage, evictOldGenerationCaches, getCachedNavigationResponse, onNavigationCommit, preloadFromLinkTags, preloadNavigationUrl, } from "./navigationCache";
3
+ describe("navigationCache", () => {
4
+ let mockCacheStorage;
5
+ let mockCache;
6
+ let mockFetch;
7
+ let mockSessionStorage;
8
+ // Local type for requestIdleCallback to avoid depending on global declarations
9
+ let mockRequestIdleCallback;
10
+ beforeEach(() => {
11
+ // Reset module state between tests
12
+ vi.resetModules();
13
+ // Mock Cache
14
+ mockCache = {
15
+ put: vi.fn().mockResolvedValue(undefined),
16
+ match: vi.fn().mockResolvedValue(undefined),
17
+ delete: vi.fn().mockResolvedValue(true),
18
+ add: vi.fn(),
19
+ addAll: vi.fn(),
20
+ keys: vi.fn(),
21
+ matchAll: vi.fn(),
22
+ };
23
+ // Mock CacheStorage
24
+ mockCacheStorage = {
25
+ open: vi.fn().mockResolvedValue(mockCache),
26
+ delete: vi.fn().mockResolvedValue(true),
27
+ keys: vi.fn().mockResolvedValue([]),
28
+ has: vi.fn(),
29
+ match: vi.fn(),
30
+ };
31
+ // Mock fetch
32
+ mockFetch = vi.fn().mockResolvedValue(new Response("test response", {
33
+ status: 200,
34
+ headers: { "content-type": "text/html" },
35
+ }));
36
+ // Mock sessionStorage
37
+ mockSessionStorage = {
38
+ getItem: vi.fn().mockReturnValue(null),
39
+ setItem: vi.fn(),
40
+ removeItem: vi.fn(),
41
+ clear: vi.fn(),
42
+ key: vi.fn(),
43
+ length: 0,
44
+ };
45
+ // Mock requestIdleCallback to execute callback asynchronously for testing
46
+ let idleCallback = null;
47
+ mockRequestIdleCallback = vi.fn((callback) => {
48
+ idleCallback = callback;
49
+ // Execute after current I/O callbacks
50
+ setImmediate(() => {
51
+ if (idleCallback) {
52
+ idleCallback();
53
+ idleCallback = null;
54
+ }
55
+ });
56
+ return 1;
57
+ });
58
+ // Setup global mocks
59
+ globalThis.window = {
60
+ isSecureContext: true,
61
+ location: { origin: "https://example.com" },
62
+ caches: mockCacheStorage,
63
+ fetch: mockFetch,
64
+ sessionStorage: mockSessionStorage,
65
+ crypto: {
66
+ randomUUID: () => "test-uuid-123",
67
+ },
68
+ };
69
+ globalThis.requestIdleCallback = mockRequestIdleCallback;
70
+ });
71
+ describe("createDefaultNavigationCacheStorage", () => {
72
+ it("should create a cache storage wrapper", async () => {
73
+ const env = {
74
+ isSecureContext: true,
75
+ origin: "https://example.com",
76
+ caches: mockCacheStorage,
77
+ fetch: mockFetch,
78
+ };
79
+ const storage = createDefaultNavigationCacheStorage(env);
80
+ expect(storage).toBeDefined();
81
+ const cache = await storage.open("test-cache");
82
+ expect(mockCacheStorage.open).toHaveBeenCalledWith("test-cache");
83
+ expect(cache).toBeDefined();
84
+ });
85
+ it("should return undefined if no caches available", () => {
86
+ const env = {
87
+ isSecureContext: true,
88
+ origin: "https://example.com",
89
+ caches: undefined,
90
+ fetch: mockFetch,
91
+ };
92
+ const storage = createDefaultNavigationCacheStorage(env);
93
+ expect(storage).toBeUndefined();
94
+ });
95
+ });
96
+ describe("preloadNavigationUrl", () => {
97
+ it("should cache a successful response", async () => {
98
+ const env = {
99
+ isSecureContext: true,
100
+ origin: "https://example.com",
101
+ caches: mockCacheStorage,
102
+ fetch: mockFetch,
103
+ };
104
+ const url = new URL("https://example.com/test");
105
+ await preloadNavigationUrl(url, env);
106
+ expect(mockFetch).toHaveBeenCalled();
107
+ expect(mockCacheStorage.open).toHaveBeenCalled();
108
+ expect(mockCache.put).toHaveBeenCalled();
109
+ });
110
+ it("should not cache error responses (status >= 400)", async () => {
111
+ const errorFetch = vi
112
+ .fn()
113
+ .mockResolvedValue(new Response("error", { status: 404 }));
114
+ const env = {
115
+ isSecureContext: true,
116
+ origin: "https://example.com",
117
+ caches: mockCacheStorage,
118
+ fetch: errorFetch,
119
+ };
120
+ const url = new URL("https://example.com/test");
121
+ await preloadNavigationUrl(url, env);
122
+ expect(errorFetch).toHaveBeenCalled();
123
+ expect(mockCache.put).not.toHaveBeenCalled();
124
+ });
125
+ it("should skip cross-origin URLs", async () => {
126
+ const env = {
127
+ isSecureContext: true,
128
+ origin: "https://example.com",
129
+ caches: mockCacheStorage,
130
+ fetch: mockFetch,
131
+ };
132
+ const url = new URL("https://other-origin.com/test");
133
+ await preloadNavigationUrl(url, env);
134
+ expect(mockFetch).not.toHaveBeenCalled();
135
+ expect(mockCache.put).not.toHaveBeenCalled();
136
+ });
137
+ it("should add __rsc query parameter", async () => {
138
+ const env = {
139
+ isSecureContext: true,
140
+ origin: "https://example.com",
141
+ caches: mockCacheStorage,
142
+ fetch: mockFetch,
143
+ };
144
+ const url = new URL("https://example.com/test");
145
+ await preloadNavigationUrl(url, env);
146
+ const fetchCall = mockFetch.mock.calls[0];
147
+ const request = fetchCall[0];
148
+ const requestUrl = new URL(request.url);
149
+ expect(requestUrl.searchParams.has("__rsc")).toBe(true);
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
+ });
164
+ it("should use custom cacheStorage when provided", async () => {
165
+ const customCache = {
166
+ put: vi.fn().mockResolvedValue(undefined),
167
+ match: vi.fn().mockResolvedValue(undefined),
168
+ };
169
+ const customStorage = {
170
+ open: vi.fn().mockResolvedValue(customCache),
171
+ delete: vi.fn().mockResolvedValue(true),
172
+ keys: vi.fn().mockResolvedValue([]),
173
+ };
174
+ const env = {
175
+ isSecureContext: true,
176
+ origin: "https://example.com",
177
+ caches: mockCacheStorage,
178
+ fetch: mockFetch,
179
+ };
180
+ const url = new URL("https://example.com/test");
181
+ await preloadNavigationUrl(url, env, customStorage);
182
+ expect(customStorage.open).toHaveBeenCalled();
183
+ expect(customCache.put).toHaveBeenCalled();
184
+ expect(mockCacheStorage.open).not.toHaveBeenCalled();
185
+ });
186
+ it("should handle errors gracefully", async () => {
187
+ const errorFetch = vi.fn().mockRejectedValue(new Error("Network error"));
188
+ const env = {
189
+ isSecureContext: true,
190
+ origin: "https://example.com",
191
+ caches: mockCacheStorage,
192
+ fetch: errorFetch,
193
+ };
194
+ const url = new URL("https://example.com/test");
195
+ // Should not throw
196
+ await expect(preloadNavigationUrl(url, env)).resolves.toBeUndefined();
197
+ });
198
+ });
199
+ describe("getCachedNavigationResponse", () => {
200
+ it("should return cached response if found", async () => {
201
+ const cachedResponse = new Response("cached", { status: 200 });
202
+ mockCache.match.mockResolvedValue(cachedResponse);
203
+ const env = {
204
+ isSecureContext: true,
205
+ origin: "https://example.com",
206
+ caches: mockCacheStorage,
207
+ fetch: mockFetch,
208
+ };
209
+ const url = new URL("https://example.com/test");
210
+ const result = await getCachedNavigationResponse(url, env);
211
+ expect(result).toBe(cachedResponse);
212
+ expect(mockCache.match).toHaveBeenCalled();
213
+ });
214
+ it("should return undefined if not cached", async () => {
215
+ mockCache.match.mockResolvedValue(undefined);
216
+ const env = {
217
+ isSecureContext: true,
218
+ origin: "https://example.com",
219
+ caches: mockCacheStorage,
220
+ fetch: mockFetch,
221
+ };
222
+ const url = new URL("https://example.com/test");
223
+ const result = await getCachedNavigationResponse(url, env);
224
+ expect(result).toBeUndefined();
225
+ });
226
+ it("should check global cacheStorage if not provided", async () => {
227
+ const cachedResponse = new Response("cached", { status: 200 });
228
+ const customCache = {
229
+ put: vi.fn(),
230
+ match: vi.fn().mockResolvedValue(cachedResponse),
231
+ };
232
+ const customStorage = {
233
+ open: vi.fn().mockResolvedValue(customCache),
234
+ delete: vi.fn(),
235
+ keys: vi.fn().mockResolvedValue([]),
236
+ };
237
+ // Set global cache storage
238
+ globalThis.__rsc_cacheStorage = customStorage;
239
+ const url = new URL("https://example.com/test");
240
+ const result = await getCachedNavigationResponse(url);
241
+ expect(result).toBe(cachedResponse);
242
+ expect(customStorage.open).toHaveBeenCalled();
243
+ // Cleanup
244
+ delete globalThis.__rsc_cacheStorage;
245
+ });
246
+ it("should add __rsc query parameter", async () => {
247
+ const env = {
248
+ isSecureContext: true,
249
+ origin: "https://example.com",
250
+ caches: mockCacheStorage,
251
+ fetch: mockFetch,
252
+ };
253
+ const url = new URL("https://example.com/test");
254
+ await getCachedNavigationResponse(url, env);
255
+ const matchCall = mockCache.match.mock.calls[0];
256
+ const request = matchCall[0];
257
+ const requestUrl = new URL(request.url);
258
+ expect(requestUrl.searchParams.has("__rsc")).toBe(true);
259
+ });
260
+ it("should skip cross-origin URLs", async () => {
261
+ const env = {
262
+ isSecureContext: true,
263
+ origin: "https://example.com",
264
+ caches: mockCacheStorage,
265
+ fetch: mockFetch,
266
+ };
267
+ const url = new URL("https://other-origin.com/test");
268
+ const result = await getCachedNavigationResponse(url, env);
269
+ expect(result).toBeUndefined();
270
+ expect(mockCache.match).not.toHaveBeenCalled();
271
+ });
272
+ it("should handle errors gracefully", async () => {
273
+ mockCache.match.mockRejectedValue(new Error("Cache error"));
274
+ const env = {
275
+ isSecureContext: true,
276
+ origin: "https://example.com",
277
+ caches: mockCacheStorage,
278
+ fetch: mockFetch,
279
+ };
280
+ const url = new URL("https://example.com/test");
281
+ // Should not throw
282
+ const result = await getCachedNavigationResponse(url, env);
283
+ expect(result).toBeUndefined();
284
+ });
285
+ });
286
+ describe("evictOldGenerationCaches", () => {
287
+ it("should delete old generation caches", async () => {
288
+ const env = {
289
+ isSecureContext: true,
290
+ origin: "https://example.com",
291
+ caches: mockCacheStorage,
292
+ fetch: mockFetch,
293
+ };
294
+ // Get the actual tabId that will be used
295
+ const url = new URL("https://example.com/test");
296
+ await preloadNavigationUrl(url, env);
297
+ const openCall = mockCacheStorage.open.mock.calls[0];
298
+ const cacheName = openCall[0];
299
+ const tabIdMatch = cacheName.match(/^rsc-x-prefetch:rwsdk:([^:]+):\d+$/);
300
+ const tabId = tabIdMatch ? tabIdMatch[1] : "test-uuid-123";
301
+ // Increment generation to 2 by calling onNavigationCommit twice
302
+ onNavigationCommit(env);
303
+ onNavigationCommit(env);
304
+ // Mock cache names matching the actual tabId
305
+ const allCacheNames = [
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",
310
+ ];
311
+ mockCacheStorage.keys.mockResolvedValue(allCacheNames);
312
+ await evictOldGenerationCaches(env);
313
+ // Wait for the cleanup to execute
314
+ await new Promise((resolve) => setTimeout(resolve, 10));
315
+ // Should delete generations 0 and 1, but not 2 (current) or other-tab
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");
320
+ });
321
+ it("should use custom cacheStorage when provided", async () => {
322
+ // Get the actual tabId that will be used
323
+ const env = {
324
+ isSecureContext: true,
325
+ origin: "https://example.com",
326
+ caches: mockCacheStorage,
327
+ fetch: mockFetch,
328
+ };
329
+ const url = new URL("https://example.com/test");
330
+ await preloadNavigationUrl(url, env);
331
+ const openCall = mockCacheStorage.open.mock.calls[0];
332
+ const cacheName = openCall[0];
333
+ const tabIdMatch = cacheName.match(/^rsc-x-prefetch:rwsdk:([^:]+):\d+$/);
334
+ const tabId = tabIdMatch ? tabIdMatch[1] : "test-uuid-123";
335
+ const customStorage = {
336
+ open: vi.fn(),
337
+ delete: vi.fn().mockResolvedValue(true),
338
+ keys: vi
339
+ .fn()
340
+ .mockResolvedValue([
341
+ `rsc-x-prefetch:rwsdk:${tabId}:0`,
342
+ `rsc-x-prefetch:rwsdk:${tabId}:1`,
343
+ ]),
344
+ };
345
+ // Increment generation so there are old caches to delete
346
+ onNavigationCommit();
347
+ await evictOldGenerationCaches(undefined, customStorage);
348
+ // Wait for the cleanup to execute
349
+ await new Promise((resolve) => setTimeout(resolve, 10));
350
+ expect(customStorage.keys).toHaveBeenCalled();
351
+ expect(customStorage.delete).toHaveBeenCalled();
352
+ });
353
+ it("should handle errors gracefully", async () => {
354
+ mockCacheStorage.keys.mockRejectedValue(new Error("Cache error"));
355
+ const env = {
356
+ isSecureContext: true,
357
+ origin: "https://example.com",
358
+ caches: mockCacheStorage,
359
+ fetch: mockFetch,
360
+ };
361
+ // Should not throw
362
+ await expect(evictOldGenerationCaches(env)).resolves.toBeUndefined();
363
+ // Wait for requestIdleCallback to execute
364
+ await new Promise((resolve) => setTimeout(resolve, 10));
365
+ });
366
+ });
367
+ describe("onNavigationCommit", () => {
368
+ it("should increment generation and evict old caches", async () => {
369
+ const env = {
370
+ isSecureContext: true,
371
+ origin: "https://example.com",
372
+ caches: mockCacheStorage,
373
+ fetch: mockFetch,
374
+ };
375
+ mockCacheStorage.keys.mockResolvedValue([]);
376
+ onNavigationCommit(env);
377
+ // Wait for eviction to complete
378
+ await new Promise((resolve) => setTimeout(resolve, 10));
379
+ expect(mockRequestIdleCallback).toHaveBeenCalled();
380
+ });
381
+ });
382
+ describe("preloadFromLinkTags", () => {
383
+ it("should preload URLs from x-prefetch link tags", async () => {
384
+ const env = {
385
+ isSecureContext: true,
386
+ origin: "https://example.com",
387
+ caches: mockCacheStorage,
388
+ fetch: mockFetch,
389
+ };
390
+ // Create a mock document with x-prefetch links
391
+ const mockDoc = {
392
+ querySelectorAll: vi.fn().mockReturnValue([
393
+ {
394
+ getAttribute: () => "/page1",
395
+ },
396
+ {
397
+ getAttribute: () => "/page2",
398
+ },
399
+ ]),
400
+ };
401
+ await preloadFromLinkTags(mockDoc, env);
402
+ // Should have called preloadNavigationUrl for each link
403
+ expect(mockFetch).toHaveBeenCalled();
404
+ });
405
+ it("should skip non-route-like hrefs (not starting with /)", async () => {
406
+ const env = {
407
+ isSecureContext: true,
408
+ origin: "https://example.com",
409
+ caches: mockCacheStorage,
410
+ fetch: mockFetch,
411
+ };
412
+ const mockDoc = {
413
+ querySelectorAll: vi.fn().mockReturnValue([
414
+ {
415
+ getAttribute: () => "https://example.com/page1",
416
+ },
417
+ {
418
+ getAttribute: () => "/page2",
419
+ },
420
+ ]),
421
+ };
422
+ await preloadFromLinkTags(mockDoc, env);
423
+ // Should only preload /page2, not the absolute URL
424
+ expect(mockFetch).toHaveBeenCalledTimes(1);
425
+ });
426
+ it("should use custom cacheStorage when provided", async () => {
427
+ const customCache = {
428
+ put: vi.fn().mockResolvedValue(undefined),
429
+ match: vi.fn(),
430
+ };
431
+ const customStorage = {
432
+ open: vi.fn().mockResolvedValue(customCache),
433
+ delete: vi.fn(),
434
+ keys: vi.fn().mockResolvedValue([]),
435
+ };
436
+ const env = {
437
+ isSecureContext: true,
438
+ origin: "https://example.com",
439
+ caches: mockCacheStorage,
440
+ fetch: mockFetch,
441
+ };
442
+ const mockDoc = {
443
+ querySelectorAll: vi.fn().mockReturnValue([
444
+ {
445
+ getAttribute: () => "/page1",
446
+ },
447
+ ]),
448
+ };
449
+ await preloadFromLinkTags(mockDoc, env, customStorage);
450
+ expect(customStorage.open).toHaveBeenCalled();
451
+ expect(customCache.put).toHaveBeenCalled();
452
+ });
453
+ });
454
+ describe("cache name generation", () => {
455
+ it("should generate cache names with correct format", async () => {
456
+ const env = {
457
+ isSecureContext: true,
458
+ origin: "https://example.com",
459
+ caches: mockCacheStorage,
460
+ fetch: mockFetch,
461
+ };
462
+ const url = new URL("https://example.com/test");
463
+ await preloadNavigationUrl(url, env);
464
+ const openCall = mockCacheStorage.open.mock.calls[0];
465
+ const cacheName = openCall[0];
466
+ expect(cacheName).toMatch(/^rsc-x-prefetch:rwsdk:[^:]+:\d+$/);
467
+ });
468
+ });
469
+ });
@@ -1,13 +1,34 @@
1
- import type { CallServerCallback } from "react-server-dom-webpack/client.browser";
2
1
  export type { HydrationOptions } from "react-dom/client";
3
- export type { CallServerCallback } from "react-server-dom-webpack/client.browser";
4
- export type ActionResponse<Result> = {
2
+ export type CallServerCallback = <Result>(id: null | string, args: null | unknown[], source?: "action" | "navigation" | "query", method?: "GET" | "POST") => Promise<Result | undefined>;
3
+ export type RscActionResponse<Result> = {
5
4
  node: React.ReactNode;
6
5
  actionResult: Result;
7
6
  };
7
+ export type ActionResponseData = {
8
+ status: number;
9
+ headers: {
10
+ location: string | null;
11
+ };
12
+ };
13
+ export type ActionResponseMeta = {
14
+ __rw_action_response: ActionResponseData;
15
+ };
16
+ export declare function isActionResponse(value: unknown): value is ActionResponseMeta;
8
17
  export type TransportContext = {
9
- setRscPayload: <Result>(v: Promise<ActionResponse<Result>>) => void;
18
+ setRscPayload: <Result>(v: Promise<RscActionResponse<Result>>) => void;
10
19
  handleResponse?: (response: Response) => boolean;
20
+ /**
21
+ * Optional callback invoked after a new RSC payload has been committed on the client.
22
+ * This is useful for features like client-side navigation that want to run logic
23
+ * after hydration/updates, e.g. warming navigation caches.
24
+ */
25
+ onHydrated?: () => void;
26
+ /**
27
+ * Optional callback invoked when an action returns a Response.
28
+ * Return true to signal that the response has been handled and
29
+ * default behaviour (e.g. redirects) should be skipped.
30
+ */
31
+ onActionResponse?: (actionResponse: ActionResponseData) => boolean | void;
11
32
  };
12
33
  export type Transport = (context: TransportContext) => CallServerCallback;
13
- export type CreateCallServer = (context: TransportContext) => <Result>(id: null | string, args: null | unknown[]) => Promise<Result>;
34
+ export type CreateCallServer = (context: TransportContext) => <Result>(id: null | string, args: null | unknown[], source?: "action" | "navigation" | "query", method?: "GET" | "POST") => Promise<Result | undefined>;
@@ -1 +1,8 @@
1
- export {};
1
+ // import type { CallServerCallback } from "react-server-dom-webpack/client.browser";
2
+ export function isActionResponse(value) {
3
+ return (typeof value === "object" &&
4
+ value !== null &&
5
+ "__rw_action_response" in value &&
6
+ typeof value.__rw_action_response === "object" &&
7
+ value.__rw_action_response !== null);
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
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
1
  import "./types/shared";
2
2
  export * from "../lib/links";
3
3
  export * from "../lib/router";
4
+ export type { DocumentProps, LayoutProps } from "../lib/types";
@@ -0,0 +1 @@
1
+ export { defineLinks, linkFor } from "../lib/links.js";
@@ -0,0 +1 @@
1
+ export { defineLinks, linkFor } from "../lib/links.js";
@@ -1,10 +1,14 @@
1
+ import "server-only";
1
2
  import "./types/worker";
2
3
  export * from "../error";
4
+ export * from "../lib/types";
3
5
  export * from "../lib/utils";
4
6
  export * from "../register/worker";
5
7
  export * from "../render/renderToStream";
6
8
  export * from "../render/renderToString";
7
9
  export * from "../requestInfo/types";
10
+ export * from "../requestInfo/utils";
8
11
  export * from "../requestInfo/worker";
9
12
  export * from "../script";
13
+ export * from "../server";
10
14
  export * from "../worker";
@@ -1,10 +1,14 @@
1
+ import "server-only";
1
2
  import "./types/worker";
2
3
  export * from "../error";
4
+ export * from "../lib/types";
3
5
  export * from "../lib/utils";
4
6
  export * from "../register/worker";
5
7
  export * from "../render/renderToStream";
6
8
  export * from "../render/renderToString";
7
9
  export * from "../requestInfo/types";
10
+ export * from "../requestInfo/utils";
8
11
  export * from "../requestInfo/worker";
9
12
  export * from "../script";
13
+ export * from "../server";
10
14
  export * from "../worker";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mock implementation of the virtual:use-client-lookup.js module for tests.
3
+ * This provides an empty lookup object since tests don't need to actually
4
+ * load client modules - they use dependency injection for React hooks.
5
+ */
6
+ export declare const useClientLookup: Record<string, () => Promise<any>>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Mock implementation of the virtual:use-client-lookup.js module for tests.
3
+ * This provides an empty lookup object since tests don't need to actually
4
+ * load client modules - they use dependency injection for React hooks.
5
+ */
6
+ export const useClientLookup = {};
@@ -1,11 +1,11 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
- import { Kysely, QueryResult } from "kysely";
2
+ import { Kysely, KyselyPlugin, QueryResult } from "kysely";
3
3
  export declare class SqliteDurableObject<T = any> extends DurableObject {
4
4
  migrations: Record<string, any>;
5
5
  kysely: Kysely<T>;
6
6
  private initialized;
7
7
  private migrationTableName;
8
- constructor(ctx: DurableObjectState, env: any, migrations: Record<string, any>, migrationTableName?: string);
8
+ constructor(ctx: DurableObjectState, env: any, migrations: Record<string, any>, migrationTableName?: string, plugins?: KyselyPlugin[]);
9
9
  initialize(): Promise<void>;
10
10
  kyselyExecuteQuery<R>(compiledQuery: {
11
11
  sql: string;
@@ -6,14 +6,14 @@ import { createMigrator } from "./index.js";
6
6
  const log = debug("sdk:do-db");
7
7
  // Base class for Durable Objects that need Kysely database access
8
8
  export class SqliteDurableObject extends DurableObject {
9
- constructor(ctx, env, migrations, migrationTableName = "__migrations") {
9
+ constructor(ctx, env, migrations, migrationTableName = "__migrations", plugins = [new ParseJSONResultsPlugin()]) {
10
10
  super(ctx, env);
11
11
  this.initialized = false;
12
12
  this.migrations = migrations;
13
13
  this.migrationTableName = migrationTableName;
14
14
  this.kysely = new Kysely({
15
15
  dialect: new DODialect({ ctx }),
16
- plugins: [new ParseJSONResultsPlugin()],
16
+ plugins: plugins,
17
17
  });
18
18
  }
19
19
  async initialize() {
@@ -1,3 +1,2 @@
1
1
  import { Kysely } from "kysely";
2
- import { type SqliteDurableObject } from "./index.js";
3
- export declare function createDb<T>(durableObjectBinding: DurableObjectNamespace<SqliteDurableObject>, name?: string): Kysely<T>;
2
+ export declare function createDb<DatabaseType>(durableObjectBinding: DurableObjectNamespace<any>, name?: string): Kysely<DatabaseType>;
@@ -5,6 +5,10 @@ export function createDb(durableObjectBinding, name = "main") {
5
5
  dialect: new DOWorkerDialect({
6
6
  kyselyExecuteQuery: (...args) => {
7
7
  const durableObjectId = durableObjectBinding.idFromName(name);
8
+ // context(justinvdm, 2 Oct 2025): First prize would be a type parameter
9
+ // for the durable object and then use it for `durableObjectBinding`'s
10
+ // type, rather than casting like this. However, that would prevent
11
+ // users from being able to do createDb<InferredDbType> then though.
8
12
  const stub = durableObjectBinding.get(durableObjectId);
9
13
  stub.initialize();
10
14
  return stub.kyselyExecuteQuery(...args);