rwsdk 1.0.0-beta.5 → 1.0.0-beta.51
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/bin/rw-scripts.mjs +13 -13
- package/dist/lib/constants.d.mts +1 -0
- package/dist/lib/constants.mjs +7 -4
- package/dist/lib/e2e/browser.mjs +6 -2
- package/dist/lib/e2e/constants.d.mts +4 -0
- package/dist/lib/e2e/constants.mjs +49 -12
- package/dist/lib/e2e/dev.mjs +49 -57
- package/dist/lib/e2e/environment.d.mts +2 -0
- package/dist/lib/e2e/environment.mjs +201 -64
- package/dist/lib/e2e/index.d.mts +2 -0
- package/dist/lib/e2e/index.mjs +2 -0
- package/dist/lib/e2e/poll.d.mts +1 -1
- package/dist/lib/e2e/release.d.mts +1 -0
- package/dist/lib/e2e/release.mjs +57 -52
- package/dist/lib/e2e/tarball.mjs +2 -34
- package/dist/lib/e2e/testHarness.d.mts +39 -3
- package/dist/lib/e2e/testHarness.mjs +239 -92
- package/dist/lib/e2e/utils.d.mts +1 -0
- package/dist/lib/e2e/utils.mjs +15 -0
- package/dist/lib/normalizeModulePath.mjs +1 -1
- package/dist/runtime/client/client.d.ts +64 -2
- package/dist/runtime/client/client.js +156 -15
- package/dist/runtime/client/navigation.d.ts +45 -0
- package/dist/runtime/client/navigation.js +68 -14
- package/dist/runtime/client/navigationCache.d.ts +68 -0
- package/dist/runtime/client/navigationCache.js +294 -0
- package/dist/runtime/client/navigationCache.test.js +469 -0
- package/dist/runtime/client/types.d.ts +26 -5
- package/dist/runtime/client/types.js +8 -1
- package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
- package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
- package/dist/runtime/entries/no-react-server.js +3 -1
- package/dist/runtime/entries/react-server-only.js +1 -1
- package/dist/runtime/entries/router.d.ts +1 -0
- package/dist/runtime/entries/routerClient.d.ts +1 -0
- package/dist/runtime/entries/routerClient.js +1 -0
- package/dist/runtime/entries/worker.d.ts +4 -0
- package/dist/runtime/entries/worker.js +4 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
- package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
- package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
- package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
- package/dist/runtime/lib/db/createDb.d.ts +1 -2
- package/dist/runtime/lib/db/createDb.js +4 -0
- package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
- package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
- package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
- package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
- package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
- package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
- package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
- package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
- package/dist/runtime/lib/links.d.ts +21 -7
- package/dist/runtime/lib/links.js +84 -26
- package/dist/runtime/lib/links.test.d.ts +1 -0
- package/dist/runtime/lib/links.test.js +20 -0
- package/dist/runtime/lib/manifest.d.ts +1 -1
- package/dist/runtime/lib/manifest.js +7 -4
- package/dist/runtime/lib/realtime/client.js +28 -6
- package/dist/runtime/lib/realtime/worker.d.ts +1 -1
- package/dist/runtime/lib/router.d.ts +154 -35
- package/dist/runtime/lib/router.js +491 -105
- package/dist/runtime/lib/router.test.js +611 -1
- package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
- package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
- package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
- package/dist/runtime/lib/types.js +1 -0
- package/dist/runtime/register/client.d.ts +1 -1
- package/dist/runtime/register/client.js +10 -3
- package/dist/runtime/register/worker.js +13 -4
- package/dist/runtime/render/normalizeActionResult.js +8 -1
- package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
- package/dist/runtime/render/renderToStream.d.ts +4 -2
- package/dist/runtime/render/renderToStream.js +53 -24
- package/dist/runtime/render/renderToString.d.ts +3 -6
- package/dist/runtime/requestInfo/types.d.ts +5 -1
- package/dist/runtime/requestInfo/utils.d.ts +9 -0
- package/dist/runtime/requestInfo/utils.js +45 -0
- package/dist/runtime/requestInfo/worker.d.ts +0 -1
- package/dist/runtime/requestInfo/worker.js +5 -11
- package/dist/runtime/script.d.ts +1 -3
- package/dist/runtime/script.js +1 -10
- package/dist/runtime/server.d.ts +52 -0
- package/dist/runtime/server.js +88 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +13 -0
- package/dist/runtime/worker.d.ts +3 -1
- package/dist/runtime/worker.js +45 -2
- package/dist/scripts/debug-sync.mjs +18 -20
- package/dist/scripts/worker-run.d.mts +1 -1
- package/dist/scripts/worker-run.mjs +59 -113
- package/dist/use-synced-state/SyncedStateServer.d.mts +36 -0
- package/dist/use-synced-state/SyncedStateServer.mjs +196 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +116 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
- package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
- package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
- package/dist/use-synced-state/__tests__/worker.test.mjs +70 -0
- package/dist/use-synced-state/client-core.d.ts +29 -0
- package/dist/use-synced-state/client-core.js +103 -0
- package/dist/use-synced-state/client.d.ts +3 -0
- package/dist/use-synced-state/client.js +4 -0
- package/dist/use-synced-state/constants.d.mts +1 -0
- package/dist/use-synced-state/constants.mjs +1 -0
- package/dist/use-synced-state/useSyncedState.d.ts +21 -0
- package/dist/use-synced-state/useSyncedState.js +64 -0
- package/dist/use-synced-state/worker.d.mts +14 -0
- package/dist/use-synced-state/worker.mjs +135 -0
- package/dist/vite/buildApp.mjs +34 -2
- package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
- package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
- package/dist/vite/configPlugin.mjs +9 -14
- package/dist/vite/constants.d.mts +1 -0
- package/dist/vite/constants.mjs +1 -0
- package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
- package/dist/vite/devServerTimingPlugin.mjs +4 -0
- package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
- package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
- package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
- package/dist/vite/directivesPlugin.mjs +4 -4
- package/dist/vite/envResolvers.d.mts +11 -0
- package/dist/vite/envResolvers.mjs +20 -0
- package/dist/vite/getViteEsbuild.mjs +2 -1
- package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
- package/dist/vite/hmrStabilityPlugin.mjs +73 -0
- package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
- package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
- package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
- package/dist/vite/linkerPlugin.d.mts +2 -1
- package/dist/vite/linkerPlugin.mjs +11 -3
- package/dist/vite/linkerPlugin.test.mjs +15 -0
- package/dist/vite/miniflareHMRPlugin.mjs +6 -38
- package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
- package/dist/vite/redwoodPlugin.mjs +9 -11
- package/dist/vite/redwoodPlugin.test.mjs +4 -4
- package/dist/vite/runDirectivesScan.mjs +75 -19
- package/dist/vite/ssrBridgePlugin.mjs +132 -40
- package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
- package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
- package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
- package/dist/vite/staleDepRetryPlugin.mjs +74 -0
- package/dist/vite/statePlugin.d.mts +4 -0
- package/dist/vite/statePlugin.mjs +62 -0
- package/dist/vite/transformClientComponents.test.mjs +32 -0
- package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
- package/dist/vite/transformServerFunctions.mjs +66 -4
- package/dist/vite/transformServerFunctions.test.mjs +35 -0
- package/dist/vite/virtualPlugin.mjs +6 -7
- package/package.json +45 -20
- package/dist/vite/manifestPlugin.d.mts +0 -4
- package/dist/vite/manifestPlugin.mjs +0 -63
- /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
|
|
4
|
-
export type
|
|
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<
|
|
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
|
-
|
|
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
|
+
}
|
|
File without changes
|
|
@@ -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("
|
|
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.");
|
|
@@ -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:
|
|
16
|
+
plugins: plugins,
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
async initialize() {
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import { Kysely } from "kysely";
|
|
2
|
-
|
|
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);
|