rwsdk 1.0.0-beta.2-test.20250930092748 → 1.0.0-beta.21

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 (70) hide show
  1. package/dist/lib/constants.d.mts +1 -0
  2. package/dist/lib/constants.mjs +7 -4
  3. package/dist/lib/e2e/constants.d.mts +14 -0
  4. package/dist/lib/e2e/constants.mjs +74 -0
  5. package/dist/lib/e2e/dev.mjs +21 -34
  6. package/dist/lib/e2e/environment.d.mts +1 -1
  7. package/dist/lib/e2e/environment.mjs +118 -18
  8. package/dist/lib/e2e/index.d.mts +1 -0
  9. package/dist/lib/e2e/index.mjs +1 -0
  10. package/dist/lib/e2e/poll.d.mts +1 -1
  11. package/dist/lib/e2e/testHarness.d.mts +36 -3
  12. package/dist/lib/e2e/testHarness.mjs +196 -119
  13. package/dist/runtime/client/client.d.ts +1 -0
  14. package/dist/runtime/client/client.js +2 -0
  15. package/dist/runtime/client/navigation.d.ts +8 -0
  16. package/dist/runtime/client/navigation.js +39 -31
  17. package/dist/runtime/entries/clientSSR.d.ts +1 -0
  18. package/dist/runtime/entries/clientSSR.js +3 -0
  19. package/dist/runtime/entries/router.d.ts +1 -0
  20. package/dist/runtime/entries/worker.d.ts +2 -0
  21. package/dist/runtime/entries/worker.js +2 -0
  22. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  23. package/dist/runtime/lib/db/createDb.js +4 -0
  24. package/dist/runtime/lib/manifest.d.ts +1 -1
  25. package/dist/runtime/lib/manifest.js +7 -4
  26. package/dist/runtime/lib/realtime/client.js +8 -2
  27. package/dist/runtime/lib/router.d.ts +16 -21
  28. package/dist/runtime/lib/router.js +67 -1
  29. package/dist/runtime/lib/router.test.js +241 -0
  30. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  31. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  32. package/dist/runtime/render/renderToStream.d.ts +4 -2
  33. package/dist/runtime/render/renderToStream.js +21 -2
  34. package/dist/runtime/render/renderToString.d.ts +3 -1
  35. package/dist/runtime/requestInfo/types.d.ts +4 -1
  36. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  37. package/dist/runtime/requestInfo/utils.js +44 -0
  38. package/dist/runtime/requestInfo/worker.js +3 -2
  39. package/dist/runtime/script.d.ts +1 -3
  40. package/dist/runtime/script.js +1 -10
  41. package/dist/runtime/state.d.ts +3 -0
  42. package/dist/runtime/state.js +13 -0
  43. package/dist/runtime/worker.js +25 -0
  44. package/dist/scripts/debug-sync.mjs +18 -20
  45. package/dist/scripts/worker-run.d.mts +1 -1
  46. package/dist/scripts/worker-run.mjs +52 -113
  47. package/dist/vite/buildApp.mjs +34 -2
  48. package/dist/vite/directiveModulesDevPlugin.mjs +1 -1
  49. package/dist/vite/envResolvers.d.mts +11 -0
  50. package/dist/vite/envResolvers.mjs +20 -0
  51. package/dist/vite/getViteEsbuild.mjs +2 -1
  52. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  53. package/dist/vite/hmrStabilityPlugin.mjs +68 -0
  54. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  55. package/dist/vite/knownDepsResolverPlugin.mjs +1 -12
  56. package/dist/vite/linkerPlugin.d.mts +2 -1
  57. package/dist/vite/linkerPlugin.mjs +11 -3
  58. package/dist/vite/linkerPlugin.test.mjs +15 -0
  59. package/dist/vite/moveStaticAssetsPlugin.mjs +14 -4
  60. package/dist/vite/redwoodPlugin.mjs +7 -9
  61. package/dist/vite/runDirectivesScan.mjs +59 -14
  62. package/dist/vite/ssrBridgePlugin.mjs +104 -34
  63. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  64. package/dist/vite/staleDepRetryPlugin.mjs +69 -0
  65. package/dist/vite/statePlugin.d.mts +4 -0
  66. package/dist/vite/statePlugin.mjs +62 -0
  67. package/package.json +13 -7
  68. package/dist/vite/manifestPlugin.d.mts +0 -4
  69. package/dist/vite/manifestPlugin.mjs +0 -63
  70. /package/dist/runtime/lib/{rwContext.js → types.js} +0 -0
@@ -4,44 +4,13 @@ import path, { basename, dirname, join as pathJoin } from "path";
4
4
  import puppeteer from "puppeteer-core";
5
5
  import { afterAll, afterEach, beforeAll, beforeEach, describe, test, } from "vitest";
6
6
  import { launchBrowser } from "./browser.mjs";
7
+ import { DEPLOYMENT_CHECK_TIMEOUT, DEPLOYMENT_MIN_TRIES, DEPLOYMENT_TIMEOUT, DEV_SERVER_MIN_TRIES, DEV_SERVER_TIMEOUT, HYDRATION_TIMEOUT, INSTALL_DEPENDENCIES_RETRIES, PUPPETEER_TIMEOUT, SETUP_PLAYGROUND_ENV_TIMEOUT, SETUP_WAIT_TIMEOUT, TEST_MAX_RETRIES, TEST_MAX_RETRIES_PER_CODE, } from "./constants.mjs";
7
8
  import { runDevServer } from "./dev.mjs";
8
9
  import { poll, pollValue } from "./poll.mjs";
9
10
  import { deleteD1Database, deleteWorker, isRelatedToTest, runRelease, } from "./release.mjs";
10
11
  import { setupTarballEnvironment } from "./tarball.mjs";
11
- const SETUP_PLAYGROUND_ENV_TIMEOUT = process.env
12
- .RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT
13
- ? parseInt(process.env.RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT, 10)
14
- : 15 * 60 * 1000;
15
- const DEPLOYMENT_TIMEOUT = process.env.RWSDK_DEPLOYMENT_TIMEOUT
16
- ? parseInt(process.env.RWSDK_DEPLOYMENT_TIMEOUT, 10)
17
- : 5 * 60 * 1000;
18
- const DEPLOYMENT_MIN_TRIES = process.env.RWSDK_DEPLOYMENT_MIN_TRIES
19
- ? parseInt(process.env.RWSDK_DEPLOYMENT_MIN_TRIES, 10)
20
- : 5;
21
- const DEPLOYMENT_CHECK_TIMEOUT = process.env.RWSDK_DEPLOYMENT_CHECK_TIMEOUT
22
- ? parseInt(process.env.RWSDK_DEPLOYMENT_CHECK_TIMEOUT, 10)
23
- : 5 * 60 * 1000;
24
- const PUPPETEER_TIMEOUT = process.env.RWSDK_PUPPETEER_TIMEOUT
25
- ? parseInt(process.env.RWSDK_PUPPETEER_TIMEOUT, 10)
26
- : 60 * 1000 * 2;
27
- const HYDRATION_TIMEOUT = process.env.RWSDK_HYDRATION_TIMEOUT
28
- ? parseInt(process.env.RWSDK_HYDRATION_TIMEOUT, 10)
29
- : 5000;
30
- const DEV_SERVER_TIMEOUT = process.env.RWSDK_DEV_SERVER_TIMEOUT
31
- ? parseInt(process.env.RWSDK_DEV_SERVER_TIMEOUT, 10)
32
- : 5 * 60 * 1000;
33
- const DEV_SERVER_MIN_TRIES = process.env.RWSDK_DEV_SERVER_MIN_TRIES
34
- ? parseInt(process.env.RWSDK_DEV_SERVER_MIN_TRIES, 10)
35
- : 5;
36
- const SETUP_WAIT_TIMEOUT = process.env.RWSDK_SETUP_WAIT_TIMEOUT
37
- ? parseInt(process.env.RWSDK_SETUP_WAIT_TIMEOUT, 10)
38
- : 10 * 60 * 1000;
39
- const TEST_MAX_RETRIES = process.env.RWSDK_TEST_MAX_RETRIES
40
- ? parseInt(process.env.RWSDK_TEST_MAX_RETRIES, 10)
41
- : 10;
42
- const TEST_MAX_RETRIES_PER_CODE = process.env.RWSDK_TEST_MAX_RETRIES_PER_CODE
43
- ? parseInt(process.env.RWSDK_TEST_MAX_RETRIES_PER_CODE, 10)
44
- : 6;
12
+ import { fileURLToPath } from "url";
13
+ export { DEPLOYMENT_CHECK_TIMEOUT, DEPLOYMENT_MIN_TRIES, DEPLOYMENT_TIMEOUT, DEV_SERVER_MIN_TRIES, DEV_SERVER_TIMEOUT, HYDRATION_TIMEOUT, INSTALL_DEPENDENCIES_RETRIES, PUPPETEER_TIMEOUT, SETUP_PLAYGROUND_ENV_TIMEOUT, SETUP_WAIT_TIMEOUT, TEST_MAX_RETRIES, TEST_MAX_RETRIES_PER_CODE, };
45
14
  // Environment variable flags for skipping tests
46
15
  const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
47
16
  const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
@@ -52,6 +21,8 @@ let globalDevInstancePromise = null;
52
21
  let globalDeploymentInstancePromise = null;
53
22
  let globalDevInstance = null;
54
23
  let globalDeploymentInstance = null;
24
+ const devInstances = [];
25
+ const deploymentInstances = [];
55
26
  let hooksRegistered = false;
56
27
  /**
57
28
  * Registers global cleanup hooks automatically
@@ -62,11 +33,11 @@ function ensureHooksRegistered() {
62
33
  // Register global afterAll to clean up the playground environment
63
34
  afterAll(async () => {
64
35
  const cleanupPromises = [];
65
- if (globalDevInstance) {
66
- cleanupPromises.push(globalDevInstance.stopDev());
36
+ for (const instance of devInstances) {
37
+ cleanupPromises.push(instance.stopDev());
67
38
  }
68
- if (globalDeploymentInstance) {
69
- cleanupPromises.push(globalDeploymentInstance.cleanup());
39
+ for (const instance of deploymentInstances) {
40
+ cleanupPromises.push(instance.cleanup());
70
41
  }
71
42
  if (globalDevPlaygroundEnv) {
72
43
  cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
@@ -75,8 +46,8 @@ function ensureHooksRegistered() {
75
46
  cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
76
47
  }
77
48
  await Promise.all(cleanupPromises);
78
- globalDevInstance = null;
79
- globalDeploymentInstance = null;
49
+ devInstances.length = 0;
50
+ deploymentInstances.length = 0;
80
51
  globalDevPlaygroundEnv = null;
81
52
  globalDeployPlaygroundEnv = null;
82
53
  });
@@ -94,11 +65,11 @@ function getProjectDirectory() {
94
65
  * Derive the playground directory from import.meta.url by finding the nearest package.json
95
66
  */
96
67
  function getPlaygroundDirFromImportMeta(importMetaUrl) {
97
- const url = new URL(importMetaUrl);
98
- const testFilePath = url.pathname;
68
+ const testFilePath = fileURLToPath(importMetaUrl);
99
69
  let currentDir = dirname(testFilePath);
100
70
  // Walk up the tree from the test file's directory
101
- while (currentDir !== "/") {
71
+ // Stop when the parent directory is the same as the current directory (we've reached the root)
72
+ while (dirname(currentDir) !== currentDir) {
102
73
  // Check if a package.json exists in the current directory
103
74
  if (fs.existsSync(pathJoin(currentDir, "package.json"))) {
104
75
  return currentDir;
@@ -114,8 +85,10 @@ function getPlaygroundDirFromImportMeta(importMetaUrl) {
114
85
  * and installs dependencies using a tarball of the SDK.
115
86
  * This ensures that tests run in a clean, isolated environment.
116
87
  */
117
- export function setupPlaygroundEnvironment(options = {}) {
118
- const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
88
+ export function setupPlaygroundEnvironment(options) {
89
+ const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, autoStartDevServer = true, } = typeof options === "string"
90
+ ? { sourceProjectDir: options, autoStartDevServer: true }
91
+ : options;
119
92
  ensureHooksRegistered();
120
93
  beforeAll(async () => {
121
94
  let projectDir;
@@ -141,16 +114,19 @@ export function setupPlaygroundEnvironment(options = {}) {
141
114
  projectDir: devEnv.targetDir,
142
115
  cleanup: devEnv.cleanup,
143
116
  };
144
- globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
145
- globalDevInstance = instance;
146
- return instance;
147
- });
148
- // Prevent unhandled promise rejections. The error will be handled inside
149
- // the test's beforeEach hook where this promise is awaited.
150
- globalDevInstancePromise.catch(() => { });
117
+ if (autoStartDevServer) {
118
+ const devControl = createDevServer();
119
+ globalDevInstancePromise = devControl.start().then((instance) => {
120
+ globalDevInstance = instance;
121
+ return instance;
122
+ });
123
+ // Prevent unhandled promise rejections. The error will be handled inside
124
+ // the test's beforeEach hook where this promise is awaited.
125
+ globalDevInstancePromise.catch(() => { });
126
+ }
151
127
  }
152
128
  else {
153
- globalDevInstancePromise = Promise.resolve(null);
129
+ globalDevPlaygroundEnv = null;
154
130
  }
155
131
  if (deploy) {
156
132
  const deployEnv = await setupTarballEnvironment({
@@ -162,7 +138,10 @@ export function setupPlaygroundEnvironment(options = {}) {
162
138
  projectDir: deployEnv.targetDir,
163
139
  cleanup: deployEnv.cleanup,
164
140
  };
165
- globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
141
+ const deployControl = createDeployment();
142
+ globalDeploymentInstancePromise = deployControl
143
+ .start()
144
+ .then((instance) => {
166
145
  globalDeploymentInstance = instance;
167
146
  return instance;
168
147
  });
@@ -170,7 +149,7 @@ export function setupPlaygroundEnvironment(options = {}) {
170
149
  globalDeploymentInstancePromise.catch(() => { });
171
150
  }
172
151
  else {
173
- globalDeploymentInstancePromise = Promise.resolve(null);
152
+ globalDeployPlaygroundEnv = null;
174
153
  }
175
154
  }, SETUP_PLAYGROUND_ENV_TIMEOUT);
176
155
  }
@@ -178,82 +157,106 @@ export function setupPlaygroundEnvironment(options = {}) {
178
157
  * Creates a dev server instance using the shared playground environment.
179
158
  * Automatically registers cleanup to run after the test.
180
159
  */
181
- export async function createDevServer(projectDir) {
182
- if (SKIP_DEV_SERVER_TESTS) {
183
- throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
160
+ export function createDevServer() {
161
+ ensureHooksRegistered();
162
+ if (!globalDevPlaygroundEnv) {
163
+ throw new Error("Dev playground environment not initialized. Enable `dev: true` in setupPlaygroundEnvironment.");
184
164
  }
165
+ const { projectDir } = globalDevPlaygroundEnv;
185
166
  const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
186
- const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
187
- timeout: DEV_SERVER_TIMEOUT,
188
- minTries: DEV_SERVER_MIN_TRIES,
189
- onRetry: (error, tries) => {
190
- console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
191
- },
192
- });
167
+ let instance = null;
193
168
  return {
194
- url: devResult.url,
195
- stopDev: devResult.stopDev,
169
+ projectDir,
170
+ start: async () => {
171
+ if (instance)
172
+ return instance;
173
+ if (SKIP_DEV_SERVER_TESTS) {
174
+ throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
175
+ }
176
+ const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
177
+ timeout: DEV_SERVER_TIMEOUT,
178
+ minTries: DEV_SERVER_MIN_TRIES,
179
+ onRetry: (error, tries) => {
180
+ console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
181
+ },
182
+ });
183
+ instance = {
184
+ url: devResult.url,
185
+ projectDir,
186
+ stopDev: devResult.stopDev,
187
+ };
188
+ devInstances.push(instance);
189
+ return instance;
190
+ },
196
191
  };
197
192
  }
198
193
  /**
199
194
  * Creates a deployment instance using the shared playground environment.
200
195
  * Automatically registers cleanup to run after the test.
201
196
  */
202
- export async function createDeployment(projectDir) {
203
- if (SKIP_DEPLOYMENT_TESTS) {
204
- throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
197
+ export function createDeployment() {
198
+ ensureHooksRegistered();
199
+ if (!globalDeployPlaygroundEnv) {
200
+ throw new Error("Deploy playground environment not initialized. Enable `deploy: true` in setupPlaygroundEnvironment.");
205
201
  }
206
- return await pollValue(async () => {
207
- // Extract the unique key from the project directory name instead of generating a new one
208
- // The directory name format is: {projectName}-e2e-test-{randomId}
209
- const dirName = basename(projectDir);
210
- const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
211
- const resourceUniqueKey = match
212
- ? match[1]
213
- : Math.random().toString(36).substring(2, 15);
214
- const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
215
- // Poll the URL to ensure it's live before proceeding
216
- await poll(async () => {
217
- try {
218
- const response = await fetch(deployResult.url);
219
- // We consider any response (even 4xx or 5xx) as success,
220
- // as it means the worker is routable.
221
- return response.status > 0;
222
- }
223
- catch (e) {
224
- return false;
202
+ const { projectDir } = globalDeployPlaygroundEnv;
203
+ let instance = null;
204
+ return {
205
+ projectDir,
206
+ start: async () => {
207
+ if (instance)
208
+ return instance;
209
+ if (SKIP_DEPLOYMENT_TESTS) {
210
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
225
211
  }
226
- }, {
227
- timeout: DEPLOYMENT_CHECK_TIMEOUT,
228
- });
229
- const cleanup = async () => {
230
- // Run deployment cleanup in background without blocking
231
- const performCleanup = async () => {
232
- if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
233
- await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
234
- }
235
- await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
236
- };
237
- // Start cleanup in background and return immediately
238
- performCleanup().catch((error) => {
239
- console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
212
+ const newInstance = await pollValue(async () => {
213
+ const dirName = basename(projectDir);
214
+ const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
215
+ const resourceUniqueKey = match
216
+ ? match[1]
217
+ : Math.random().toString(36).substring(2, 15);
218
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
219
+ await poll(async () => {
220
+ try {
221
+ const response = await fetch(deployResult.url);
222
+ return response.status > 0;
223
+ }
224
+ catch (e) {
225
+ return false;
226
+ }
227
+ }, {
228
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
229
+ });
230
+ const cleanup = async () => {
231
+ const performCleanup = async () => {
232
+ if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
233
+ await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
234
+ }
235
+ await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
236
+ };
237
+ performCleanup().catch((error) => {
238
+ console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
239
+ });
240
+ return Promise.resolve();
241
+ };
242
+ return {
243
+ url: deployResult.url,
244
+ workerName: deployResult.workerName,
245
+ resourceUniqueKey,
246
+ projectDir: projectDir,
247
+ cleanup,
248
+ };
249
+ }, {
250
+ timeout: DEPLOYMENT_TIMEOUT,
251
+ minTries: DEPLOYMENT_MIN_TRIES,
252
+ onRetry: (error, tries) => {
253
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
254
+ },
240
255
  });
241
- return Promise.resolve();
242
- };
243
- return {
244
- url: deployResult.url,
245
- workerName: deployResult.workerName,
246
- resourceUniqueKey,
247
- projectDir: projectDir,
248
- cleanup,
249
- };
250
- }, {
251
- timeout: DEPLOYMENT_TIMEOUT,
252
- minTries: DEPLOYMENT_MIN_TRIES,
253
- onRetry: (error, tries) => {
254
- console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
256
+ deploymentInstances.push(newInstance);
257
+ return newInstance;
255
258
  },
256
- });
259
+ };
257
260
  }
258
261
  /**
259
262
  * Executes a test function with a retry mechanism for specific error codes.
@@ -302,7 +305,7 @@ function createTestRunner(testFn, envType) {
302
305
  return (name, testLogic) => {
303
306
  if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
304
307
  (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
305
- test.skip(name, () => { });
308
+ test.skip(`${name} (${envType})`, () => { });
306
309
  return;
307
310
  }
308
311
  describe.concurrent(name, () => {
@@ -364,12 +367,86 @@ function createTestRunner(testFn, envType) {
364
367
  browser: browser,
365
368
  page: page,
366
369
  url: instance.url,
370
+ projectDir: instance
371
+ .projectDir,
367
372
  });
368
373
  });
369
374
  });
370
375
  });
371
376
  };
372
377
  }
378
+ /**
379
+ * Creates a low-level test runner that provides utilities for creating
380
+ * tests that need to perform setup actions before the server starts.
381
+ */
382
+ function createSDKTestRunner() {
383
+ const internalRunner = (testFn) => {
384
+ return (name, testLogic) => {
385
+ describe.concurrent(name, () => {
386
+ let page;
387
+ let browser;
388
+ beforeAll(async () => {
389
+ const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
390
+ const wsEndpointFile = path.join(tempDir, "wsEndpoint");
391
+ try {
392
+ const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
393
+ browser = await puppeteer.connect({
394
+ browserWSEndpoint: wsEndpoint,
395
+ });
396
+ }
397
+ catch (error) {
398
+ console.warn("Failed to connect to existing browser instance. " +
399
+ "This might happen if you are running a single test file. " +
400
+ "Launching a new browser instance instead.");
401
+ browser = await launchBrowser();
402
+ }
403
+ }, SETUP_WAIT_TIMEOUT);
404
+ afterAll(async () => {
405
+ if (browser) {
406
+ await browser.disconnect();
407
+ }
408
+ });
409
+ beforeEach(async () => {
410
+ if (!globalDevPlaygroundEnv && !globalDeployPlaygroundEnv) {
411
+ throw new Error("Test environment not initialized. Call setupPlaygroundEnvironment() in your test file.");
412
+ }
413
+ page = await browser.newPage();
414
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
415
+ }, SETUP_WAIT_TIMEOUT);
416
+ afterEach(async () => {
417
+ if (page) {
418
+ try {
419
+ await page.close();
420
+ }
421
+ catch (error) {
422
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
423
+ }
424
+ }
425
+ });
426
+ testFn(">", async () => {
427
+ if (!browser) {
428
+ throw new Error("Test environment not ready.");
429
+ }
430
+ await runTestWithRetries(name, async () => {
431
+ await testLogic({
432
+ browser: browser,
433
+ page: page,
434
+ projectDir: globalDevPlaygroundEnv?.projectDir ||
435
+ globalDeployPlaygroundEnv?.projectDir ||
436
+ "",
437
+ });
438
+ });
439
+ });
440
+ });
441
+ };
442
+ };
443
+ const main = internalRunner(test);
444
+ return Object.assign(main, {
445
+ only: internalRunner(test.only),
446
+ skip: test.skip,
447
+ });
448
+ }
449
+ export const testSDK = createSDKTestRunner();
373
450
  /**
374
451
  * High-level test wrapper for dev server tests.
375
452
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -1,6 +1,7 @@
1
1
  import "./setWebpackRequire";
2
2
  export { default as React } from "react";
3
3
  export { ClientOnly } from "./ClientOnly.js";
4
+ export { initClientNavigation, navigate } from "./navigation.js";
4
5
  import type { HydrationOptions, Transport } from "./types";
5
6
  export declare const fetchTransport: Transport;
6
7
  export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
@@ -4,6 +4,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
4
4
  // context(justinvdm, 14 Aug 2025): `react-server-dom-webpack` uses this global
5
5
  // to load modules, so we need to define it here before importing
6
6
  // "react-server-dom-webpack."
7
+ // prettier-ignore
7
8
  import "./setWebpackRequire";
8
9
  import React from "react";
9
10
  import { hydrateRoot } from "react-dom/client";
@@ -11,6 +12,7 @@ import { createFromFetch, createFromReadableStream, encodeReply, } from "react-s
11
12
  import { rscStream } from "rsc-html-stream/client";
12
13
  export { default as React } from "react";
13
14
  export { ClientOnly } from "./ClientOnly.js";
15
+ export { initClientNavigation, navigate } from "./navigation.js";
14
16
  export const fetchTransport = (transportContext) => {
15
17
  const fetchCallServer = async (id, args) => {
16
18
  const url = new URL(window.location.href);
@@ -4,6 +4,14 @@ export interface ClientNavigationOptions {
4
4
  scrollBehavior?: "auto" | "smooth" | "instant";
5
5
  }
6
6
  export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
7
+ export interface NavigateOptions {
8
+ history?: "push" | "replace";
9
+ info?: {
10
+ scrollToTop?: boolean;
11
+ scrollBehavior?: "auto" | "smooth" | "instant";
12
+ };
13
+ }
14
+ export declare function navigate(href: string, options?: NavigateOptions): Promise<void>;
7
15
  export declare function initClientNavigation(opts?: ClientNavigationOptions): {
8
16
  handleResponse: (response: Response) => boolean;
9
17
  };
@@ -1,10 +1,3 @@
1
- function saveScrollPosition(x, y) {
2
- window.history.replaceState({
3
- ...window.history.state,
4
- scrollX: x,
5
- scrollY: y,
6
- }, "", window.location.href);
7
- }
8
1
  export function validateClickEvent(event, target) {
9
2
  // should this only work for left click?
10
3
  if (event.button !== 0) {
@@ -37,19 +30,44 @@ export function validateClickEvent(event, target) {
37
30
  }
38
31
  return true;
39
32
  }
33
+ let IS_CLIENT_NAVIGATION = false;
34
+ export async function navigate(href, options = { history: "push" }) {
35
+ if (!IS_CLIENT_NAVIGATION) {
36
+ window.location.href = href;
37
+ return;
38
+ }
39
+ saveScrollPosition(window.scrollX, window.scrollY);
40
+ const url = window.location.origin + href;
41
+ if (options.history === "push") {
42
+ window.history.pushState({ path: href }, "", url);
43
+ }
44
+ else {
45
+ window.history.replaceState({ path: href }, "", url);
46
+ }
47
+ // @ts-expect-error
48
+ await globalThis.__rsc_callServer();
49
+ const scrollToTop = options.info?.scrollToTop ?? true;
50
+ const scrollBehavior = options.info?.scrollBehavior ?? "instant";
51
+ if (scrollToTop && history.scrollRestoration === "auto") {
52
+ window.scrollTo({
53
+ top: 0,
54
+ left: 0,
55
+ behavior: scrollBehavior,
56
+ });
57
+ saveScrollPosition(0, 0);
58
+ }
59
+ }
60
+ function saveScrollPosition(x, y) {
61
+ window.history.replaceState({
62
+ ...window.history.state,
63
+ scrollX: x,
64
+ scrollY: y,
65
+ }, "", window.location.href);
66
+ }
40
67
  export function initClientNavigation(opts = {}) {
41
- const options = {
42
- onNavigate: async function onNavigate() {
43
- // @ts-expect-error
44
- await globalThis.__rsc_callServer();
45
- },
46
- scrollToTop: true,
47
- scrollBehavior: "instant",
48
- ...opts,
49
- };
68
+ IS_CLIENT_NAVIGATION = true;
50
69
  history.scrollRestoration = "auto";
51
70
  document.addEventListener("click", async function handleClickEvent(event) {
52
- // Prevent default navigation
53
71
  if (!validateClickEvent(event, event.target)) {
54
72
  return;
55
73
  }
@@ -57,28 +75,18 @@ export function initClientNavigation(opts = {}) {
57
75
  const el = event.target;
58
76
  const a = el.closest("a");
59
77
  const href = a?.getAttribute("href");
60
- saveScrollPosition(window.scrollX, window.scrollY);
61
- window.history.pushState({ path: href }, "", window.location.origin + href);
62
- await options.onNavigate();
63
- if (options.scrollToTop && history.scrollRestoration === "auto") {
64
- window.scrollTo({
65
- top: 0,
66
- left: 0,
67
- behavior: options.scrollBehavior,
68
- });
69
- saveScrollPosition(0, 0);
70
- }
71
- history.scrollRestoration = "auto";
78
+ navigate(href);
72
79
  }, true);
73
80
  window.addEventListener("popstate", async function handlePopState() {
74
- saveScrollPosition(window.scrollX, window.scrollY);
75
- await options.onNavigate();
81
+ // @ts-expect-error
82
+ await globalThis.__rsc_callServer();
76
83
  });
77
84
  // Return a handleResponse function for use with initClient
78
85
  return {
79
86
  handleResponse: function handleResponse(response) {
80
87
  if (!response.ok) {
81
88
  // Redirect to the current page (window.location) to show the error
89
+ // This means the page that produced the error is called twice.
82
90
  window.location.href = window.location.href;
83
91
  return false;
84
92
  }
@@ -1,2 +1,3 @@
1
1
  import "./types/ssr";
2
2
  export * from "../lib/streams/consumeEventStream";
3
+ export declare const navigate: () => void;
@@ -1,2 +1,5 @@
1
1
  import "./types/ssr";
2
2
  export * from "../lib/streams/consumeEventStream";
3
+ export const navigate = () => {
4
+ /* stub */
5
+ };
@@ -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";
@@ -1,10 +1,12 @@
1
1
  import "./types/worker";
2
2
  export * from "../error";
3
+ export * from "../lib/types";
3
4
  export * from "../lib/utils";
4
5
  export * from "../register/worker";
5
6
  export * from "../render/renderToStream";
6
7
  export * from "../render/renderToString";
7
8
  export * from "../requestInfo/types";
9
+ export * from "../requestInfo/utils";
8
10
  export * from "../requestInfo/worker";
9
11
  export * from "../script";
10
12
  export * from "../worker";
@@ -1,10 +1,12 @@
1
1
  import "./types/worker";
2
2
  export * from "../error";
3
+ export * from "../lib/types";
3
4
  export * from "../lib/utils";
4
5
  export * from "../register/worker";
5
6
  export * from "../render/renderToStream";
6
7
  export * from "../render/renderToString";
7
8
  export * from "../requestInfo/types";
9
+ export * from "../requestInfo/utils";
8
10
  export * from "../requestInfo/worker";
9
11
  export * from "../script";
10
12
  export * from "../worker";
@@ -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);
@@ -8,4 +8,4 @@ export interface ManifestChunk {
8
8
  css?: string[];
9
9
  assets?: string[];
10
10
  }
11
- export declare const getManifest: () => Promise<Manifest>;
11
+ export declare function getManifest(): Promise<Manifest>;
@@ -1,14 +1,17 @@
1
1
  let manifest;
2
- export const getManifest = async () => {
2
+ export async function getManifest() {
3
3
  if (manifest) {
4
4
  return manifest;
5
5
  }
6
6
  if (import.meta.env.VITE_IS_DEV_SERVER) {
7
+ // In dev, there's no manifest, so we can use an empty object.
7
8
  manifest = {};
8
9
  }
9
10
  else {
10
- const { default: prodManifest } = await import("virtual:rwsdk:manifest.js");
11
- manifest = prodManifest;
11
+ // context(justinvdm, 2 Oct 2025): In production, the manifest is a
12
+ // placeholder string that will be replaced by the linker plugin with the
13
+ // actual manifest JSON object.
14
+ manifest = "__RWSDK_MANIFEST_PLACEHOLDER__";
12
15
  }
13
16
  return manifest;
14
- };
17
+ }