rwsdk 1.0.0-beta.0 → 1.0.0-beta.10

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 (43) hide show
  1. package/dist/lib/e2e/constants.d.mts +13 -0
  2. package/dist/lib/e2e/constants.mjs +67 -0
  3. package/dist/lib/e2e/environment.d.mts +1 -1
  4. package/dist/lib/e2e/environment.mjs +29 -6
  5. package/dist/lib/e2e/index.d.mts +1 -0
  6. package/dist/lib/e2e/index.mjs +1 -0
  7. package/dist/lib/e2e/tarball.mjs +25 -0
  8. package/dist/lib/e2e/testHarness.d.mts +33 -3
  9. package/dist/lib/e2e/testHarness.mjs +181 -109
  10. package/dist/runtime/client/client.d.ts +1 -0
  11. package/dist/runtime/client/client.js +2 -0
  12. package/dist/runtime/client/navigation.d.ts +8 -0
  13. package/dist/runtime/client/navigation.js +39 -31
  14. package/dist/runtime/entries/clientSSR.d.ts +1 -0
  15. package/dist/runtime/entries/clientSSR.js +3 -0
  16. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  17. package/dist/runtime/lib/db/createDb.js +4 -0
  18. package/dist/runtime/lib/manifest.d.ts +1 -1
  19. package/dist/runtime/lib/manifest.js +7 -4
  20. package/dist/runtime/lib/realtime/client.js +8 -2
  21. package/dist/runtime/lib/router.d.ts +1 -19
  22. package/dist/runtime/lib/router.test.js +2 -0
  23. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  24. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  25. package/dist/runtime/render/renderToStream.d.ts +1 -1
  26. package/dist/runtime/render/renderToString.d.ts +1 -1
  27. package/dist/runtime/requestInfo/types.d.ts +1 -1
  28. package/dist/runtime/script.d.ts +1 -3
  29. package/dist/runtime/script.js +1 -10
  30. package/dist/runtime/worker.js +25 -0
  31. package/dist/scripts/addon.mjs +3 -0
  32. package/dist/scripts/smoke-test.mjs +4 -9
  33. package/dist/scripts/worker-run.d.mts +1 -1
  34. package/dist/scripts/worker-run.mjs +50 -113
  35. package/dist/vite/buildApp.mjs +2 -0
  36. package/dist/vite/directiveModulesDevPlugin.mjs +1 -1
  37. package/dist/vite/linkerPlugin.mjs +1 -1
  38. package/dist/vite/redwoodPlugin.mjs +0 -4
  39. package/dist/vite/runDirectivesScan.mjs +57 -12
  40. package/package.json +9 -7
  41. package/dist/vite/manifestPlugin.d.mts +0 -4
  42. package/dist/vite/manifestPlugin.mjs +0 -63
  43. /package/dist/runtime/lib/{rwContext.js → types.js} +0 -0
@@ -4,44 +4,12 @@ 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
+ 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
13
  // Environment variable flags for skipping tests
46
14
  const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
47
15
  const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
@@ -52,6 +20,8 @@ let globalDevInstancePromise = null;
52
20
  let globalDeploymentInstancePromise = null;
53
21
  let globalDevInstance = null;
54
22
  let globalDeploymentInstance = null;
23
+ const devInstances = [];
24
+ const deploymentInstances = [];
55
25
  let hooksRegistered = false;
56
26
  /**
57
27
  * Registers global cleanup hooks automatically
@@ -62,11 +32,11 @@ function ensureHooksRegistered() {
62
32
  // Register global afterAll to clean up the playground environment
63
33
  afterAll(async () => {
64
34
  const cleanupPromises = [];
65
- if (globalDevInstance) {
66
- cleanupPromises.push(globalDevInstance.stopDev());
35
+ for (const instance of devInstances) {
36
+ cleanupPromises.push(instance.stopDev());
67
37
  }
68
- if (globalDeploymentInstance) {
69
- cleanupPromises.push(globalDeploymentInstance.cleanup());
38
+ for (const instance of deploymentInstances) {
39
+ cleanupPromises.push(instance.cleanup());
70
40
  }
71
41
  if (globalDevPlaygroundEnv) {
72
42
  cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
@@ -75,8 +45,8 @@ function ensureHooksRegistered() {
75
45
  cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
76
46
  }
77
47
  await Promise.all(cleanupPromises);
78
- globalDevInstance = null;
79
- globalDeploymentInstance = null;
48
+ devInstances.length = 0;
49
+ deploymentInstances.length = 0;
80
50
  globalDevPlaygroundEnv = null;
81
51
  globalDeployPlaygroundEnv = null;
82
52
  });
@@ -114,7 +84,7 @@ function getPlaygroundDirFromImportMeta(importMetaUrl) {
114
84
  * and installs dependencies using a tarball of the SDK.
115
85
  * This ensures that tests run in a clean, isolated environment.
116
86
  */
117
- export function setupPlaygroundEnvironment(options = {}) {
87
+ export function setupPlaygroundEnvironment(options) {
118
88
  const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
119
89
  ensureHooksRegistered();
120
90
  beforeAll(async () => {
@@ -141,7 +111,8 @@ export function setupPlaygroundEnvironment(options = {}) {
141
111
  projectDir: devEnv.targetDir,
142
112
  cleanup: devEnv.cleanup,
143
113
  };
144
- globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
114
+ const devControl = createDevServer();
115
+ globalDevInstancePromise = devControl.start().then((instance) => {
145
116
  globalDevInstance = instance;
146
117
  return instance;
147
118
  });
@@ -150,7 +121,7 @@ export function setupPlaygroundEnvironment(options = {}) {
150
121
  globalDevInstancePromise.catch(() => { });
151
122
  }
152
123
  else {
153
- globalDevInstancePromise = Promise.resolve(null);
124
+ globalDevPlaygroundEnv = null;
154
125
  }
155
126
  if (deploy) {
156
127
  const deployEnv = await setupTarballEnvironment({
@@ -162,7 +133,10 @@ export function setupPlaygroundEnvironment(options = {}) {
162
133
  projectDir: deployEnv.targetDir,
163
134
  cleanup: deployEnv.cleanup,
164
135
  };
165
- globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
136
+ const deployControl = createDeployment();
137
+ globalDeploymentInstancePromise = deployControl
138
+ .start()
139
+ .then((instance) => {
166
140
  globalDeploymentInstance = instance;
167
141
  return instance;
168
142
  });
@@ -170,7 +144,7 @@ export function setupPlaygroundEnvironment(options = {}) {
170
144
  globalDeploymentInstancePromise.catch(() => { });
171
145
  }
172
146
  else {
173
- globalDeploymentInstancePromise = Promise.resolve(null);
147
+ globalDeployPlaygroundEnv = null;
174
148
  }
175
149
  }, SETUP_PLAYGROUND_ENV_TIMEOUT);
176
150
  }
@@ -178,82 +152,106 @@ export function setupPlaygroundEnvironment(options = {}) {
178
152
  * Creates a dev server instance using the shared playground environment.
179
153
  * Automatically registers cleanup to run after the test.
180
154
  */
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");
155
+ export function createDevServer() {
156
+ ensureHooksRegistered();
157
+ if (!globalDevPlaygroundEnv) {
158
+ throw new Error("Dev playground environment not initialized. Enable `dev: true` in setupPlaygroundEnvironment.");
184
159
  }
160
+ const { projectDir } = globalDevPlaygroundEnv;
185
161
  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
- });
162
+ let instance = null;
193
163
  return {
194
- url: devResult.url,
195
- stopDev: devResult.stopDev,
164
+ projectDir,
165
+ start: async () => {
166
+ if (instance)
167
+ return instance;
168
+ if (SKIP_DEV_SERVER_TESTS) {
169
+ throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
170
+ }
171
+ const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
172
+ timeout: DEV_SERVER_TIMEOUT,
173
+ minTries: DEV_SERVER_MIN_TRIES,
174
+ onRetry: (error, tries) => {
175
+ console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
176
+ },
177
+ });
178
+ instance = {
179
+ url: devResult.url,
180
+ projectDir,
181
+ stopDev: devResult.stopDev,
182
+ };
183
+ devInstances.push(instance);
184
+ return instance;
185
+ },
196
186
  };
197
187
  }
198
188
  /**
199
189
  * Creates a deployment instance using the shared playground environment.
200
190
  * Automatically registers cleanup to run after the test.
201
191
  */
202
- export async function createDeployment(projectDir) {
203
- if (SKIP_DEPLOYMENT_TESTS) {
204
- throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
192
+ export function createDeployment() {
193
+ ensureHooksRegistered();
194
+ if (!globalDeployPlaygroundEnv) {
195
+ throw new Error("Deploy playground environment not initialized. Enable `deploy: true` in setupPlaygroundEnvironment.");
205
196
  }
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;
197
+ const { projectDir } = globalDeployPlaygroundEnv;
198
+ let instance = null;
199
+ return {
200
+ projectDir,
201
+ start: async () => {
202
+ if (instance)
203
+ return instance;
204
+ if (SKIP_DEPLOYMENT_TESTS) {
205
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
225
206
  }
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}`);
207
+ const newInstance = await pollValue(async () => {
208
+ const dirName = basename(projectDir);
209
+ const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
210
+ const resourceUniqueKey = match
211
+ ? match[1]
212
+ : Math.random().toString(36).substring(2, 15);
213
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
214
+ await poll(async () => {
215
+ try {
216
+ const response = await fetch(deployResult.url);
217
+ return response.status > 0;
218
+ }
219
+ catch (e) {
220
+ return false;
221
+ }
222
+ }, {
223
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
224
+ });
225
+ const cleanup = async () => {
226
+ const performCleanup = async () => {
227
+ if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
228
+ await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
229
+ }
230
+ await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
231
+ };
232
+ performCleanup().catch((error) => {
233
+ console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
234
+ });
235
+ return Promise.resolve();
236
+ };
237
+ return {
238
+ url: deployResult.url,
239
+ workerName: deployResult.workerName,
240
+ resourceUniqueKey,
241
+ projectDir: projectDir,
242
+ cleanup,
243
+ };
244
+ }, {
245
+ timeout: DEPLOYMENT_TIMEOUT,
246
+ minTries: DEPLOYMENT_MIN_TRIES,
247
+ onRetry: (error, tries) => {
248
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
249
+ },
240
250
  });
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}`);
251
+ deploymentInstances.push(newInstance);
252
+ return newInstance;
255
253
  },
256
- });
254
+ };
257
255
  }
258
256
  /**
259
257
  * Executes a test function with a retry mechanism for specific error codes.
@@ -302,7 +300,7 @@ function createTestRunner(testFn, envType) {
302
300
  return (name, testLogic) => {
303
301
  if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
304
302
  (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
305
- test.skip(name, () => { });
303
+ test.skip(`${name} (${envType})`, () => { });
306
304
  return;
307
305
  }
308
306
  describe.concurrent(name, () => {
@@ -364,12 +362,86 @@ function createTestRunner(testFn, envType) {
364
362
  browser: browser,
365
363
  page: page,
366
364
  url: instance.url,
365
+ projectDir: instance
366
+ .projectDir,
367
367
  });
368
368
  });
369
369
  });
370
370
  });
371
371
  };
372
372
  }
373
+ /**
374
+ * Creates a low-level test runner that provides utilities for creating
375
+ * tests that need to perform setup actions before the server starts.
376
+ */
377
+ function createSDKTestRunner() {
378
+ const internalRunner = (testFn) => {
379
+ return (name, testLogic) => {
380
+ describe.concurrent(name, () => {
381
+ let page;
382
+ let browser;
383
+ beforeAll(async () => {
384
+ const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
385
+ const wsEndpointFile = path.join(tempDir, "wsEndpoint");
386
+ try {
387
+ const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
388
+ browser = await puppeteer.connect({
389
+ browserWSEndpoint: wsEndpoint,
390
+ });
391
+ }
392
+ catch (error) {
393
+ console.warn("Failed to connect to existing browser instance. " +
394
+ "This might happen if you are running a single test file. " +
395
+ "Launching a new browser instance instead.");
396
+ browser = await launchBrowser();
397
+ }
398
+ }, SETUP_WAIT_TIMEOUT);
399
+ afterAll(async () => {
400
+ if (browser) {
401
+ await browser.disconnect();
402
+ }
403
+ });
404
+ beforeEach(async () => {
405
+ if (!globalDevPlaygroundEnv && !globalDeployPlaygroundEnv) {
406
+ throw new Error("Test environment not initialized. Call setupPlaygroundEnvironment() in your test file.");
407
+ }
408
+ page = await browser.newPage();
409
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
410
+ }, SETUP_WAIT_TIMEOUT);
411
+ afterEach(async () => {
412
+ if (page) {
413
+ try {
414
+ await page.close();
415
+ }
416
+ catch (error) {
417
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
418
+ }
419
+ }
420
+ });
421
+ testFn(">", async () => {
422
+ if (!browser) {
423
+ throw new Error("Test environment not ready.");
424
+ }
425
+ await runTestWithRetries(name, async () => {
426
+ await testLogic({
427
+ browser: browser,
428
+ page: page,
429
+ projectDir: globalDevPlaygroundEnv?.projectDir ||
430
+ globalDeployPlaygroundEnv?.projectDir ||
431
+ "",
432
+ });
433
+ });
434
+ });
435
+ });
436
+ };
437
+ };
438
+ const main = internalRunner(test);
439
+ return Object.assign(main, {
440
+ only: internalRunner(test.only),
441
+ skip: test.skip,
442
+ });
443
+ }
444
+ export const testSDK = createSDKTestRunner();
373
445
  /**
374
446
  * High-level test wrapper for dev server tests.
375
447
  * 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,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
+ }
@@ -1,7 +1,13 @@
1
- import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
1
+ // context(justinvdm, 14 Aug 2025): `react-server-dom-webpack` uses this globa ___webpack_require__ global,
2
+ // so we need to import our client entry point (which sets it), before importing
3
+ // prettier-ignore
2
4
  import { initClient } from "../../client/client";
3
- import { packMessage, unpackMessage, } from "./protocol";
5
+ // prettier-ignore
6
+ import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
7
+ // prettier-ignore
4
8
  import { MESSAGE_TYPE } from "./shared";
9
+ // prettier-ignore
10
+ import { packMessage, unpackMessage, } from "./protocol";
5
11
  const DEFAULT_KEY = "default";
6
12
  export const initRealtimeClient = ({ key = DEFAULT_KEY, handleResponse, } = {}) => {
7
13
  const transport = realtimeTransport({ key, handleResponse });
@@ -1,24 +1,6 @@
1
- import type { Kysely } from "kysely";
2
1
  import React from "react";
3
2
  import { RequestInfo } from "../requestInfo/types";
4
- export type DocumentProps<T extends RequestInfo = RequestInfo> = T & {
5
- children: React.ReactNode;
6
- };
7
- export type LayoutProps<T extends RequestInfo = RequestInfo> = {
8
- children?: React.ReactNode;
9
- requestInfo?: T;
10
- };
11
- export type RwContext = {
12
- nonce: string;
13
- Document: React.FC<DocumentProps<any>>;
14
- rscPayload: boolean;
15
- ssr: boolean;
16
- layouts?: React.FC<LayoutProps<any>>[];
17
- databases: Map<string, Kysely<any>>;
18
- scriptsToBeLoaded: Set<string>;
19
- pageRouteResolved: PromiseWithResolvers<void> | undefined;
20
- actionResult?: unknown;
21
- };
3
+ import type { DocumentProps, LayoutProps } from "./types.js";
22
4
  export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<React.JSX.Element | Response | void>;
23
5
  type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) => MaybePromise<Response>;
24
6
  type MaybePromise<T> = T | Promise<T>;
@@ -71,6 +71,8 @@ describe("defineRoutes - Request Handling Behavior", () => {
71
71
  ssr: true,
72
72
  databases: new Map(),
73
73
  scriptsToBeLoaded: new Set(),
74
+ entryScripts: new Set(),
75
+ inlineScripts: new Set(),
74
76
  pageRouteResolved: undefined,
75
77
  },
76
78
  cf: {},
@@ -1,4 +1,5 @@
1
1
  import { type Kysely } from "kysely";
2
+ import React from "react";
2
3
  import { type RequestInfo } from "../requestInfo/types.js";
3
4
  export type RwContext = {
4
5
  nonce: string;
@@ -1,4 +1,4 @@
1
- import { type DocumentProps } from "../lib/router.js";
1
+ import { type DocumentProps } from "../lib/types.js";
2
2
  import { type RequestInfo } from "../requestInfo/types.js";
3
3
  export declare const renderDocumentHtmlStream: ({ rscPayloadStream, Document, requestInfo, shouldSSR, onError, }: {
4
4
  rscPayloadStream: ReadableStream;