rwsdk 1.0.0-beta.4 → 1.0.0-beta.40

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 (139) hide show
  1. package/dist/lib/constants.d.mts +1 -0
  2. package/dist/lib/constants.mjs +7 -4
  3. package/dist/lib/e2e/browser.mjs +6 -2
  4. package/dist/lib/e2e/constants.d.mts +4 -0
  5. package/dist/lib/e2e/constants.mjs +49 -12
  6. package/dist/lib/e2e/dev.mjs +37 -49
  7. package/dist/lib/e2e/environment.d.mts +2 -0
  8. package/dist/lib/e2e/environment.mjs +201 -64
  9. package/dist/lib/e2e/index.d.mts +1 -0
  10. package/dist/lib/e2e/index.mjs +1 -0
  11. package/dist/lib/e2e/poll.d.mts +1 -1
  12. package/dist/lib/e2e/release.d.mts +1 -0
  13. package/dist/lib/e2e/release.mjs +16 -32
  14. package/dist/lib/e2e/tarball.mjs +2 -34
  15. package/dist/lib/e2e/testHarness.d.mts +34 -3
  16. package/dist/lib/e2e/testHarness.mjs +219 -90
  17. package/dist/lib/e2e/utils.d.mts +1 -0
  18. package/dist/lib/e2e/utils.mjs +15 -0
  19. package/dist/runtime/client/client.d.ts +35 -0
  20. package/dist/runtime/client/client.js +35 -0
  21. package/dist/runtime/client/navigation.d.ts +49 -0
  22. package/dist/runtime/client/navigation.js +80 -31
  23. package/dist/runtime/entries/clientSSR.d.ts +1 -0
  24. package/dist/runtime/entries/clientSSR.js +3 -0
  25. package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
  26. package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
  27. package/dist/runtime/entries/router.d.ts +1 -0
  28. package/dist/runtime/entries/routerClient.d.ts +1 -0
  29. package/dist/runtime/entries/routerClient.js +1 -0
  30. package/dist/runtime/entries/worker.d.ts +2 -0
  31. package/dist/runtime/entries/worker.js +2 -0
  32. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  33. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  34. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  35. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  36. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  37. package/dist/runtime/lib/db/createDb.js +4 -0
  38. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  39. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  40. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  41. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  42. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  43. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  44. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  45. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  46. package/dist/runtime/lib/links.d.ts +21 -7
  47. package/dist/runtime/lib/links.js +82 -24
  48. package/dist/runtime/lib/links.test.js +20 -0
  49. package/dist/runtime/lib/manifest.d.ts +1 -1
  50. package/dist/runtime/lib/manifest.js +7 -4
  51. package/dist/runtime/lib/realtime/client.js +8 -2
  52. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  53. package/dist/runtime/lib/router.d.ts +153 -36
  54. package/dist/runtime/lib/router.js +169 -20
  55. package/dist/runtime/lib/router.test.js +241 -0
  56. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  57. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  58. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  59. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  60. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  61. package/dist/runtime/lib/types.js +1 -0
  62. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  63. package/dist/runtime/render/renderToStream.d.ts +4 -2
  64. package/dist/runtime/render/renderToStream.js +53 -24
  65. package/dist/runtime/render/renderToString.d.ts +3 -6
  66. package/dist/runtime/requestInfo/types.d.ts +4 -1
  67. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  68. package/dist/runtime/requestInfo/utils.js +44 -0
  69. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  70. package/dist/runtime/requestInfo/worker.js +3 -10
  71. package/dist/runtime/script.d.ts +1 -3
  72. package/dist/runtime/script.js +1 -10
  73. package/dist/runtime/state.d.ts +3 -0
  74. package/dist/runtime/state.js +13 -0
  75. package/dist/runtime/worker.d.ts +3 -1
  76. package/dist/runtime/worker.js +32 -0
  77. package/dist/scripts/debug-sync.mjs +18 -20
  78. package/dist/scripts/worker-run.d.mts +1 -1
  79. package/dist/scripts/worker-run.mjs +59 -113
  80. package/dist/use-synced-state/SyncedStateServer.d.mts +21 -0
  81. package/dist/use-synced-state/SyncedStateServer.mjs +128 -0
  82. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  83. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
  84. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  85. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  86. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  87. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  88. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  89. package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
  90. package/dist/use-synced-state/client-core.d.ts +26 -0
  91. package/dist/use-synced-state/client-core.js +39 -0
  92. package/dist/use-synced-state/client.d.ts +3 -0
  93. package/dist/use-synced-state/client.js +4 -0
  94. package/dist/use-synced-state/constants.d.mts +1 -0
  95. package/dist/use-synced-state/constants.mjs +1 -0
  96. package/dist/use-synced-state/useSyncedState.d.ts +20 -0
  97. package/dist/use-synced-state/useSyncedState.js +58 -0
  98. package/dist/use-synced-state/worker.d.mts +13 -0
  99. package/dist/use-synced-state/worker.mjs +69 -0
  100. package/dist/vite/buildApp.mjs +34 -2
  101. package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
  102. package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
  103. package/dist/vite/configPlugin.mjs +9 -14
  104. package/dist/vite/constants.d.mts +1 -0
  105. package/dist/vite/constants.mjs +1 -0
  106. package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
  107. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  108. package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
  109. package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
  110. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  111. package/dist/vite/directivesPlugin.mjs +4 -4
  112. package/dist/vite/envResolvers.d.mts +11 -0
  113. package/dist/vite/envResolvers.mjs +20 -0
  114. package/dist/vite/getViteEsbuild.mjs +2 -1
  115. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  116. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  117. package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
  118. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  119. package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
  120. package/dist/vite/linkerPlugin.d.mts +2 -1
  121. package/dist/vite/linkerPlugin.mjs +11 -3
  122. package/dist/vite/linkerPlugin.test.mjs +15 -0
  123. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  124. package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
  125. package/dist/vite/redwoodPlugin.mjs +8 -10
  126. package/dist/vite/runDirectivesScan.mjs +72 -18
  127. package/dist/vite/ssrBridgePlugin.mjs +132 -40
  128. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  129. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  130. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  131. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  132. package/dist/vite/statePlugin.d.mts +4 -0
  133. package/dist/vite/statePlugin.mjs +62 -0
  134. package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
  135. package/dist/vite/virtualPlugin.mjs +6 -7
  136. package/package.json +27 -10
  137. package/dist/vite/manifestPlugin.d.mts +0 -4
  138. package/dist/vite/manifestPlugin.mjs +0 -63
  139. /package/dist/runtime/lib/{rwContext.js → links.test.d.ts} +0 -0
@@ -5,6 +5,7 @@ export type { Browser, Page } from "puppeteer-core";
5
5
  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, };
6
6
  interface DevServerInstance {
7
7
  url: string;
8
+ projectDir: string;
8
9
  stopDev: () => Promise<void>;
9
10
  }
10
11
  interface DeploymentInstance {
@@ -36,6 +37,11 @@ export interface SetupPlaygroundEnvironmentOptions {
36
37
  * @default true
37
38
  */
38
39
  deploy?: boolean;
40
+ /**
41
+ * Whether to automatically start the dev server.
42
+ * @default true
43
+ */
44
+ autoStartDevServer?: boolean;
39
45
  }
40
46
  /**
41
47
  * A Vitest hook that sets up a playground environment for a test file.
@@ -43,17 +49,29 @@ export interface SetupPlaygroundEnvironmentOptions {
43
49
  * and installs dependencies using a tarball of the SDK.
44
50
  * This ensures that tests run in a clean, isolated environment.
45
51
  */
46
- export declare function setupPlaygroundEnvironment(options?: string | SetupPlaygroundEnvironmentOptions): void;
52
+ export declare function setupPlaygroundEnvironment(options: SetupPlaygroundEnvironmentOptions | string): void;
47
53
  /**
48
54
  * Creates a dev server instance using the shared playground environment.
49
55
  * Automatically registers cleanup to run after the test.
50
56
  */
51
- export declare function createDevServer(projectDir: string): Promise<DevServerInstance>;
57
+ export declare function createDevServer(): {
58
+ projectDir: string;
59
+ start: () => Promise<DevServerInstance>;
60
+ };
52
61
  /**
53
62
  * Creates a deployment instance using the shared playground environment.
54
63
  * Automatically registers cleanup to run after the test.
55
64
  */
56
- export declare function createDeployment(projectDir: string): Promise<DeploymentInstance>;
65
+ export declare function createDeployment(): {
66
+ projectDir: string;
67
+ start: () => Promise<{
68
+ url: string;
69
+ workerName: string;
70
+ resourceUniqueKey: string;
71
+ projectDir: string;
72
+ cleanup: () => Promise<void>;
73
+ }>;
74
+ };
57
75
  /**
58
76
  * Executes a test function with a retry mechanism for specific error codes.
59
77
  * @param name - The name of the test, used for logging.
@@ -63,13 +81,23 @@ export declare function createDeployment(projectDir: string): Promise<Deployment
63
81
  * called automatically on failure.
64
82
  */
65
83
  export declare function runTestWithRetries(name: string, attemptFn: () => Promise<void>): Promise<void>;
84
+ type SDKRunner = (name: string, testLogic: (context: {
85
+ browser: Browser;
86
+ page: Page;
87
+ projectDir: string;
88
+ }) => Promise<void>) => void;
66
89
  declare function createTestRunner(testFn: (typeof test | typeof test.only)["concurrent"], envType: "dev" | "deploy"): (name: string, testLogic: (context: {
67
90
  devServer?: DevServerInstance;
68
91
  deployment?: DeploymentInstance;
69
92
  browser: Browser;
70
93
  page: Page;
71
94
  url: string;
95
+ projectDir: string;
72
96
  }) => Promise<void>) => void;
97
+ export declare const testSDK: SDKRunner & {
98
+ only: SDKRunner;
99
+ skip: typeof test.skip;
100
+ };
73
101
  /**
74
102
  * High-level test wrapper for dev server tests.
75
103
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -83,6 +111,7 @@ export declare namespace testDev {
83
111
  browser: Browser;
84
112
  page: Page;
85
113
  url: string;
114
+ projectDir: string;
86
115
  }) => Promise<void>) => void;
87
116
  }
88
117
  /**
@@ -98,6 +127,7 @@ export declare namespace testDeploy {
98
127
  browser: Browser;
99
128
  page: Page;
100
129
  url: string;
130
+ projectDir: string;
101
131
  }) => Promise<void>) => void;
102
132
  }
103
133
  /**
@@ -110,6 +140,7 @@ export declare function testDevAndDeploy(name: string, testFn: (context: {
110
140
  browser: Browser;
111
141
  page: Page;
112
142
  url: string;
143
+ projectDir: string;
113
144
  }) => Promise<void>): void;
114
145
  export declare namespace testDevAndDeploy {
115
146
  var skip: (name: string, testFn?: any) => void;
@@ -1,7 +1,7 @@
1
1
  import fs from "fs-extra";
2
- import os from "os";
3
2
  import path, { basename, dirname, join as pathJoin } from "path";
4
3
  import puppeteer from "puppeteer-core";
4
+ import { fileURLToPath } from "url";
5
5
  import { afterAll, afterEach, beforeAll, beforeEach, describe, test, } from "vitest";
6
6
  import { launchBrowser } from "./browser.mjs";
7
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";
@@ -9,6 +9,7 @@ import { runDevServer } from "./dev.mjs";
9
9
  import { poll, pollValue } from "./poll.mjs";
10
10
  import { deleteD1Database, deleteWorker, isRelatedToTest, runRelease, } from "./release.mjs";
11
11
  import { setupTarballEnvironment } from "./tarball.mjs";
12
+ import { ensureTmpDir } from "./utils.mjs";
12
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, };
13
14
  // Environment variable flags for skipping tests
14
15
  const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
@@ -20,6 +21,8 @@ let globalDevInstancePromise = null;
20
21
  let globalDeploymentInstancePromise = null;
21
22
  let globalDevInstance = null;
22
23
  let globalDeploymentInstance = null;
24
+ const devInstances = [];
25
+ const deploymentInstances = [];
23
26
  let hooksRegistered = false;
24
27
  /**
25
28
  * Registers global cleanup hooks automatically
@@ -30,21 +33,33 @@ function ensureHooksRegistered() {
30
33
  // Register global afterAll to clean up the playground environment
31
34
  afterAll(async () => {
32
35
  const cleanupPromises = [];
33
- if (globalDevInstance) {
34
- cleanupPromises.push(globalDevInstance.stopDev());
36
+ for (const instance of devInstances) {
37
+ cleanupPromises.push(instance.stopDev().catch((error) => {
38
+ // Suppress all cleanup errors - they don't affect test results
39
+ console.warn(`Suppressing error during dev server cleanup: ${error instanceof Error ? error.message : String(error)}`);
40
+ }));
35
41
  }
36
- if (globalDeploymentInstance) {
37
- cleanupPromises.push(globalDeploymentInstance.cleanup());
42
+ for (const instance of deploymentInstances) {
43
+ cleanupPromises.push(instance.cleanup().catch((error) => {
44
+ // Suppress all cleanup errors - they don't affect test results
45
+ console.warn(`Suppressing error during deployment cleanup: ${error instanceof Error ? error.message : String(error)}`);
46
+ }));
38
47
  }
39
48
  if (globalDevPlaygroundEnv) {
40
- cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
49
+ cleanupPromises.push(globalDevPlaygroundEnv.cleanup().catch((error) => {
50
+ // Suppress all cleanup errors - they don't affect test results
51
+ console.warn(`Suppressing error during dev environment cleanup: ${error instanceof Error ? error.message : String(error)}`);
52
+ }));
41
53
  }
42
54
  if (globalDeployPlaygroundEnv) {
43
- cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
55
+ cleanupPromises.push(globalDeployPlaygroundEnv.cleanup().catch((error) => {
56
+ // Suppress all cleanup errors - they don't affect test results
57
+ console.warn(`Suppressing error during deploy environment cleanup: ${error instanceof Error ? error.message : String(error)}`);
58
+ }));
44
59
  }
45
60
  await Promise.all(cleanupPromises);
46
- globalDevInstance = null;
47
- globalDeploymentInstance = null;
61
+ devInstances.length = 0;
62
+ deploymentInstances.length = 0;
48
63
  globalDevPlaygroundEnv = null;
49
64
  globalDeployPlaygroundEnv = null;
50
65
  });
@@ -62,11 +77,11 @@ function getProjectDirectory() {
62
77
  * Derive the playground directory from import.meta.url by finding the nearest package.json
63
78
  */
64
79
  function getPlaygroundDirFromImportMeta(importMetaUrl) {
65
- const url = new URL(importMetaUrl);
66
- const testFilePath = url.pathname;
80
+ const testFilePath = fileURLToPath(importMetaUrl);
67
81
  let currentDir = dirname(testFilePath);
68
82
  // Walk up the tree from the test file's directory
69
- while (currentDir !== "/") {
83
+ // Stop when the parent directory is the same as the current directory (we've reached the root)
84
+ while (dirname(currentDir) !== currentDir) {
70
85
  // Check if a package.json exists in the current directory
71
86
  if (fs.existsSync(pathJoin(currentDir, "package.json"))) {
72
87
  return currentDir;
@@ -82,8 +97,10 @@ function getPlaygroundDirFromImportMeta(importMetaUrl) {
82
97
  * and installs dependencies using a tarball of the SDK.
83
98
  * This ensures that tests run in a clean, isolated environment.
84
99
  */
85
- export function setupPlaygroundEnvironment(options = {}) {
86
- const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
100
+ export function setupPlaygroundEnvironment(options) {
101
+ const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, autoStartDevServer = true, } = typeof options === "string"
102
+ ? { sourceProjectDir: options, autoStartDevServer: true }
103
+ : options;
87
104
  ensureHooksRegistered();
88
105
  beforeAll(async () => {
89
106
  let projectDir;
@@ -109,18 +126,21 @@ export function setupPlaygroundEnvironment(options = {}) {
109
126
  projectDir: devEnv.targetDir,
110
127
  cleanup: devEnv.cleanup,
111
128
  };
112
- globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
113
- globalDevInstance = instance;
114
- return instance;
115
- });
116
- // Prevent unhandled promise rejections. The error will be handled inside
117
- // the test's beforeEach hook where this promise is awaited.
118
- globalDevInstancePromise.catch(() => { });
129
+ if (autoStartDevServer) {
130
+ const devControl = createDevServer();
131
+ globalDevInstancePromise = devControl.start().then((instance) => {
132
+ globalDevInstance = instance;
133
+ return instance;
134
+ });
135
+ // Prevent unhandled promise rejections. The error will be handled inside
136
+ // the test's beforeEach hook where this promise is awaited.
137
+ globalDevInstancePromise.catch(() => { });
138
+ }
119
139
  }
120
140
  else {
121
- globalDevInstancePromise = Promise.resolve(null);
141
+ globalDevPlaygroundEnv = null;
122
142
  }
123
- if (deploy) {
143
+ if (deploy && !SKIP_DEPLOYMENT_TESTS) {
124
144
  const deployEnv = await setupTarballEnvironment({
125
145
  projectDir,
126
146
  monorepoRoot,
@@ -130,7 +150,10 @@ export function setupPlaygroundEnvironment(options = {}) {
130
150
  projectDir: deployEnv.targetDir,
131
151
  cleanup: deployEnv.cleanup,
132
152
  };
133
- globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
153
+ const deployControl = createDeployment();
154
+ globalDeploymentInstancePromise = deployControl
155
+ .start()
156
+ .then((instance) => {
134
157
  globalDeploymentInstance = instance;
135
158
  return instance;
136
159
  });
@@ -138,7 +161,7 @@ export function setupPlaygroundEnvironment(options = {}) {
138
161
  globalDeploymentInstancePromise.catch(() => { });
139
162
  }
140
163
  else {
141
- globalDeploymentInstancePromise = Promise.resolve(null);
164
+ globalDeployPlaygroundEnv = null;
142
165
  }
143
166
  }, SETUP_PLAYGROUND_ENV_TIMEOUT);
144
167
  }
@@ -146,82 +169,109 @@ export function setupPlaygroundEnvironment(options = {}) {
146
169
  * Creates a dev server instance using the shared playground environment.
147
170
  * Automatically registers cleanup to run after the test.
148
171
  */
149
- export async function createDevServer(projectDir) {
150
- if (SKIP_DEV_SERVER_TESTS) {
151
- throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
172
+ export function createDevServer() {
173
+ ensureHooksRegistered();
174
+ if (!globalDevPlaygroundEnv) {
175
+ throw new Error("Dev playground environment not initialized. Enable `dev: true` in setupPlaygroundEnvironment.");
152
176
  }
177
+ const { projectDir } = globalDevPlaygroundEnv;
153
178
  const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
154
- const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
155
- timeout: DEV_SERVER_TIMEOUT,
156
- minTries: DEV_SERVER_MIN_TRIES,
157
- onRetry: (error, tries) => {
158
- console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
159
- },
160
- });
179
+ let instance = null;
161
180
  return {
162
- url: devResult.url,
163
- stopDev: devResult.stopDev,
181
+ projectDir,
182
+ start: async () => {
183
+ if (instance)
184
+ return instance;
185
+ if (SKIP_DEV_SERVER_TESTS) {
186
+ throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
187
+ }
188
+ const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
189
+ timeout: DEV_SERVER_TIMEOUT,
190
+ minTries: DEV_SERVER_MIN_TRIES,
191
+ onRetry: (error, tries) => {
192
+ console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
193
+ },
194
+ });
195
+ instance = {
196
+ url: devResult.url,
197
+ projectDir,
198
+ stopDev: devResult.stopDev,
199
+ };
200
+ devInstances.push(instance);
201
+ return instance;
202
+ },
164
203
  };
165
204
  }
166
205
  /**
167
206
  * Creates a deployment instance using the shared playground environment.
168
207
  * Automatically registers cleanup to run after the test.
169
208
  */
170
- export async function createDeployment(projectDir) {
171
- if (SKIP_DEPLOYMENT_TESTS) {
172
- throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
209
+ export function createDeployment() {
210
+ ensureHooksRegistered();
211
+ if (!globalDeployPlaygroundEnv) {
212
+ throw new Error("Deploy playground environment not initialized. Enable `deploy: true` in setupPlaygroundEnvironment.");
173
213
  }
174
- return await pollValue(async () => {
175
- // Extract the unique key from the project directory name instead of generating a new one
176
- // The directory name format is: {projectName}-e2e-test-{randomId}
177
- const dirName = basename(projectDir);
178
- const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
179
- const resourceUniqueKey = match
180
- ? match[1]
181
- : Math.random().toString(36).substring(2, 15);
182
- const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
183
- // Poll the URL to ensure it's live before proceeding
184
- await poll(async () => {
185
- try {
186
- const response = await fetch(deployResult.url);
187
- // We consider any response (even 4xx or 5xx) as success,
188
- // as it means the worker is routable.
189
- return response.status > 0;
190
- }
191
- catch (e) {
192
- return false;
214
+ const { projectDir } = globalDeployPlaygroundEnv;
215
+ let instance = null;
216
+ return {
217
+ projectDir,
218
+ start: async () => {
219
+ if (instance)
220
+ return instance;
221
+ if (SKIP_DEPLOYMENT_TESTS) {
222
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
193
223
  }
194
- }, {
195
- timeout: DEPLOYMENT_CHECK_TIMEOUT,
196
- });
197
- const cleanup = async () => {
198
- // Run deployment cleanup in background without blocking
199
- const performCleanup = async () => {
200
- if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
201
- await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
202
- }
203
- await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
204
- };
205
- // Start cleanup in background and return immediately
206
- performCleanup().catch((error) => {
207
- console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
224
+ const newInstance = await pollValue(async () => {
225
+ const dirName = basename(projectDir);
226
+ // Match formats: {projectName}-t-{hash}, {projectName}-test-{hash}, or {projectName}-e2e-test-{hash}
227
+ const match = dirName.match(/-t-([a-f0-9]+)$/) ||
228
+ dirName.match(/-test-([a-f0-9]+)$/) ||
229
+ dirName.match(/-e2e-test-([a-f0-9]+)$/);
230
+ const resourceUniqueKey = match
231
+ ? match[1]
232
+ : Math.random().toString(36).substring(2, 15);
233
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
234
+ await poll(async () => {
235
+ try {
236
+ const response = await fetch(deployResult.url);
237
+ return response.status > 0;
238
+ }
239
+ catch (e) {
240
+ return false;
241
+ }
242
+ }, {
243
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
244
+ });
245
+ const cleanup = async () => {
246
+ const performCleanup = async () => {
247
+ if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
248
+ await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
249
+ }
250
+ await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
251
+ };
252
+ performCleanup().catch((error) => {
253
+ console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
254
+ });
255
+ return Promise.resolve();
256
+ };
257
+ return {
258
+ url: deployResult.url,
259
+ workerName: deployResult.workerName,
260
+ resourceUniqueKey,
261
+ projectDir: projectDir,
262
+ cleanup,
263
+ };
264
+ }, {
265
+ timeout: DEPLOYMENT_TIMEOUT,
266
+ minTries: DEPLOYMENT_MIN_TRIES,
267
+ onRetry: (error, tries) => {
268
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
269
+ },
208
270
  });
209
- return Promise.resolve();
210
- };
211
- return {
212
- url: deployResult.url,
213
- workerName: deployResult.workerName,
214
- resourceUniqueKey,
215
- projectDir: projectDir,
216
- cleanup,
217
- };
218
- }, {
219
- timeout: DEPLOYMENT_TIMEOUT,
220
- minTries: DEPLOYMENT_MIN_TRIES,
221
- onRetry: (error, tries) => {
222
- console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
271
+ deploymentInstances.push(newInstance);
272
+ return newInstance;
223
273
  },
224
- });
274
+ };
225
275
  }
226
276
  /**
227
277
  * Executes a test function with a retry mechanism for specific error codes.
@@ -270,7 +320,7 @@ function createTestRunner(testFn, envType) {
270
320
  return (name, testLogic) => {
271
321
  if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
272
322
  (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
273
- test.skip(name, () => { });
323
+ test.skip(`${name} (${envType})`, () => { });
274
324
  return;
275
325
  }
276
326
  describe.concurrent(name, () => {
@@ -278,7 +328,7 @@ function createTestRunner(testFn, envType) {
278
328
  let instance;
279
329
  let browser;
280
330
  beforeAll(async () => {
281
- const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
331
+ const tempDir = path.join(await ensureTmpDir(), "rwsdk-e2e-tests");
282
332
  const wsEndpointFile = path.join(tempDir, "wsEndpoint");
283
333
  try {
284
334
  const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
@@ -332,12 +382,91 @@ function createTestRunner(testFn, envType) {
332
382
  browser: browser,
333
383
  page: page,
334
384
  url: instance.url,
385
+ projectDir: instance
386
+ .projectDir,
335
387
  });
336
388
  });
337
389
  });
338
390
  });
339
391
  };
340
392
  }
393
+ /**
394
+ * Creates a low-level test runner that provides utilities for creating
395
+ * tests that need to perform setup actions before the server starts.
396
+ */
397
+ function createSDKTestRunner() {
398
+ const internalRunner = (testFn) => {
399
+ return (name, testLogic) => {
400
+ describe.concurrent(name, () => {
401
+ let page;
402
+ let browser;
403
+ beforeAll(async () => {
404
+ const tempDir = path.join(await ensureTmpDir(), "rwsdk-e2e-tests");
405
+ const wsEndpointFile = path.join(tempDir, "wsEndpoint");
406
+ try {
407
+ const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
408
+ browser = await puppeteer.connect({
409
+ browserWSEndpoint: wsEndpoint,
410
+ });
411
+ }
412
+ catch (error) {
413
+ console.warn("Failed to connect to existing browser instance. " +
414
+ "This might happen if you are running a single test file. " +
415
+ "Launching a new browser instance instead.");
416
+ // Check for RWSDK_HEADLESS environment variable (default to true if not set)
417
+ // Set RWSDK_HEADLESS=0 or RWSDK_HEADLESS=false to run in headed mode
418
+ const headless = process.env.RWSDK_HEADLESS === undefined ||
419
+ process.env.RWSDK_HEADLESS === "1" ||
420
+ process.env.RWSDK_HEADLESS === "true";
421
+ browser = await launchBrowser(undefined, headless);
422
+ }
423
+ }, SETUP_WAIT_TIMEOUT);
424
+ afterAll(async () => {
425
+ if (browser) {
426
+ await browser.disconnect();
427
+ }
428
+ });
429
+ beforeEach(async () => {
430
+ if (!globalDevPlaygroundEnv && !globalDeployPlaygroundEnv) {
431
+ throw new Error("Test environment not initialized. Call setupPlaygroundEnvironment() in your test file.");
432
+ }
433
+ page = await browser.newPage();
434
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
435
+ }, SETUP_WAIT_TIMEOUT);
436
+ afterEach(async () => {
437
+ if (page) {
438
+ try {
439
+ await page.close();
440
+ }
441
+ catch (error) {
442
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
443
+ }
444
+ }
445
+ });
446
+ testFn(">", async () => {
447
+ if (!browser) {
448
+ throw new Error("Test environment not ready.");
449
+ }
450
+ await runTestWithRetries(name, async () => {
451
+ await testLogic({
452
+ browser: browser,
453
+ page: page,
454
+ projectDir: globalDevPlaygroundEnv?.projectDir ||
455
+ globalDeployPlaygroundEnv?.projectDir ||
456
+ "",
457
+ });
458
+ });
459
+ });
460
+ });
461
+ };
462
+ };
463
+ const main = internalRunner(test);
464
+ return Object.assign(main, {
465
+ only: internalRunner(test.only),
466
+ skip: test.skip,
467
+ });
468
+ }
469
+ export const testSDK = createSDKTestRunner();
341
470
  /**
342
471
  * High-level test wrapper for dev server tests.
343
472
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -0,0 +1 @@
1
+ export declare function ensureTmpDir(): Promise<string>;
@@ -0,0 +1,15 @@
1
+ import { mkdirp } from "fs-extra";
2
+ import os from "node:os";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ export async function ensureTmpDir() {
6
+ let baseTmpDir = os.tmpdir();
7
+ // context(justinvdm, 2 Nov 2025): Normalize the base temp dir on Windows
8
+ // to prevent short/long path mismatches that break Vite's alias resolution.
9
+ if (process.platform === "win32") {
10
+ baseTmpDir = fs.realpathSync.native(baseTmpDir);
11
+ }
12
+ const tmpDir = path.join(baseTmpDir, "rwsdk-e2e");
13
+ await mkdirp(tmpDir);
14
+ return tmpDir;
15
+ }
@@ -1,8 +1,43 @@
1
1
  import "./setWebpackRequire";
2
2
  export { default as React } from "react";
3
+ export type { Dispatch, MutableRefObject, SetStateAction } from "react";
3
4
  export { ClientOnly } from "./ClientOnly.js";
5
+ export { initClientNavigation, navigate } from "./navigation.js";
4
6
  import type { HydrationOptions, Transport } from "./types";
5
7
  export declare const fetchTransport: Transport;
8
+ /**
9
+ * Initializes the React client and hydrates the RSC payload.
10
+ *
11
+ * This function sets up client-side hydration for React Server Components,
12
+ * making the page interactive. Call this from your client entry point.
13
+ *
14
+ * @param transport - Custom transport for server communication (defaults to fetchTransport)
15
+ * @param hydrateRootOptions - Options passed to React's hydrateRoot
16
+ * @param handleResponse - Custom response handler for navigation errors
17
+ *
18
+ * @example
19
+ * // Basic usage
20
+ * import { initClient } from "rwsdk/client";
21
+ *
22
+ * initClient();
23
+ *
24
+ * @example
25
+ * // With client-side navigation
26
+ * import { initClient, initClientNavigation } from "rwsdk/client";
27
+ *
28
+ * const { handleResponse } = initClientNavigation();
29
+ * initClient({ handleResponse });
30
+ *
31
+ * @example
32
+ * // With custom React hydration options
33
+ * initClient({
34
+ * hydrateRootOptions: {
35
+ * onRecoverableError: (error) => {
36
+ * console.warn("Recoverable error:", error);
37
+ * },
38
+ * },
39
+ * });
40
+ */
6
41
  export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
7
42
  transport?: Transport;
8
43
  hydrateRootOptions?: HydrationOptions;
@@ -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);
@@ -48,6 +50,39 @@ export const fetchTransport = (transportContext) => {
48
50
  };
49
51
  return fetchCallServer;
50
52
  };
53
+ /**
54
+ * Initializes the React client and hydrates the RSC payload.
55
+ *
56
+ * This function sets up client-side hydration for React Server Components,
57
+ * making the page interactive. Call this from your client entry point.
58
+ *
59
+ * @param transport - Custom transport for server communication (defaults to fetchTransport)
60
+ * @param hydrateRootOptions - Options passed to React's hydrateRoot
61
+ * @param handleResponse - Custom response handler for navigation errors
62
+ *
63
+ * @example
64
+ * // Basic usage
65
+ * import { initClient } from "rwsdk/client";
66
+ *
67
+ * initClient();
68
+ *
69
+ * @example
70
+ * // With client-side navigation
71
+ * import { initClient, initClientNavigation } from "rwsdk/client";
72
+ *
73
+ * const { handleResponse } = initClientNavigation();
74
+ * initClient({ handleResponse });
75
+ *
76
+ * @example
77
+ * // With custom React hydration options
78
+ * initClient({
79
+ * hydrateRootOptions: {
80
+ * onRecoverableError: (error) => {
81
+ * console.warn("Recoverable error:", error);
82
+ * },
83
+ * },
84
+ * });
85
+ */
51
86
  export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, } = {}) => {
52
87
  const transportContext = {
53
88
  setRscPayload: () => { },