rwsdk 1.0.0-beta.3 → 1.0.0-beta.30

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 (126) 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 +16 -0
  5. package/dist/lib/e2e/constants.mjs +77 -0
  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 +202 -65
  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 +36 -4
  16. package/dist/lib/e2e/testHarness.mjs +216 -128
  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/router.d.ts +1 -0
  26. package/dist/runtime/entries/routerClient.d.ts +1 -0
  27. package/dist/runtime/entries/routerClient.js +1 -0
  28. package/dist/runtime/entries/worker.d.ts +2 -0
  29. package/dist/runtime/entries/worker.js +2 -0
  30. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  31. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  32. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  33. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  34. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  35. package/dist/runtime/lib/db/createDb.js +4 -0
  36. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  37. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  38. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  39. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  40. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  41. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  42. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  43. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  44. package/dist/runtime/lib/links.d.ts +18 -7
  45. package/dist/runtime/lib/links.js +70 -24
  46. package/dist/runtime/lib/links.test.js +20 -0
  47. package/dist/runtime/lib/manifest.d.ts +1 -1
  48. package/dist/runtime/lib/manifest.js +7 -4
  49. package/dist/runtime/lib/realtime/client.js +8 -2
  50. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  51. package/dist/runtime/lib/router.d.ts +147 -33
  52. package/dist/runtime/lib/router.js +169 -20
  53. package/dist/runtime/lib/router.test.js +241 -0
  54. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  55. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  56. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  57. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  58. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  59. package/dist/runtime/lib/types.js +1 -0
  60. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  61. package/dist/runtime/render/renderToStream.d.ts +4 -2
  62. package/dist/runtime/render/renderToStream.js +53 -24
  63. package/dist/runtime/render/renderToString.d.ts +3 -1
  64. package/dist/runtime/requestInfo/types.d.ts +4 -1
  65. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  66. package/dist/runtime/requestInfo/utils.js +44 -0
  67. package/dist/runtime/requestInfo/worker.js +3 -2
  68. package/dist/runtime/script.d.ts +1 -3
  69. package/dist/runtime/script.js +1 -10
  70. package/dist/runtime/state.d.ts +3 -0
  71. package/dist/runtime/state.js +13 -0
  72. package/dist/runtime/worker.d.ts +3 -1
  73. package/dist/runtime/worker.js +26 -0
  74. package/dist/scripts/debug-sync.mjs +18 -20
  75. package/dist/scripts/worker-run.d.mts +1 -1
  76. package/dist/scripts/worker-run.mjs +52 -113
  77. package/dist/use-synced-state/SyncStateServer.d.mts +20 -0
  78. package/dist/use-synced-state/SyncStateServer.mjs +124 -0
  79. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  80. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +109 -0
  81. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  82. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  83. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  84. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  85. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  86. package/dist/use-synced-state/__tests__/worker.test.mjs +69 -0
  87. package/dist/use-synced-state/client.d.ts +28 -0
  88. package/dist/use-synced-state/client.js +39 -0
  89. package/dist/use-synced-state/constants.d.mts +1 -0
  90. package/dist/use-synced-state/constants.mjs +1 -0
  91. package/dist/use-synced-state/useSyncState.d.ts +20 -0
  92. package/dist/use-synced-state/useSyncState.js +58 -0
  93. package/dist/use-synced-state/useSyncedState.d.ts +20 -0
  94. package/dist/use-synced-state/useSyncedState.js +58 -0
  95. package/dist/use-synced-state/worker.d.mts +14 -0
  96. package/dist/use-synced-state/worker.mjs +73 -0
  97. package/dist/vite/buildApp.mjs +34 -2
  98. package/dist/vite/configPlugin.mjs +8 -14
  99. package/dist/vite/constants.d.mts +1 -0
  100. package/dist/vite/constants.mjs +1 -0
  101. package/dist/vite/directiveModulesDevPlugin.mjs +1 -1
  102. package/dist/vite/envResolvers.d.mts +11 -0
  103. package/dist/vite/envResolvers.mjs +20 -0
  104. package/dist/vite/getViteEsbuild.mjs +2 -1
  105. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  106. package/dist/vite/hmrStabilityPlugin.mjs +68 -0
  107. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  108. package/dist/vite/knownDepsResolverPlugin.mjs +1 -12
  109. package/dist/vite/linkerPlugin.d.mts +2 -1
  110. package/dist/vite/linkerPlugin.mjs +11 -3
  111. package/dist/vite/linkerPlugin.test.mjs +15 -0
  112. package/dist/vite/miniflareHMRPlugin.mjs +1 -38
  113. package/dist/vite/moveStaticAssetsPlugin.mjs +14 -4
  114. package/dist/vite/redwoodPlugin.mjs +6 -10
  115. package/dist/vite/runDirectivesScan.mjs +59 -14
  116. package/dist/vite/ssrBridgePlugin.mjs +122 -34
  117. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  118. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  119. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  120. package/dist/vite/staleDepRetryPlugin.mjs +69 -0
  121. package/dist/vite/statePlugin.d.mts +4 -0
  122. package/dist/vite/statePlugin.mjs +62 -0
  123. package/package.json +26 -10
  124. package/dist/vite/manifestPlugin.d.mts +0 -4
  125. package/dist/vite/manifestPlugin.mjs +0 -63
  126. /package/dist/runtime/lib/{rwContext.js → links.test.d.ts} +0 -0
@@ -1,9 +1,11 @@
1
1
  import { type Browser, type Page } from "puppeteer-core";
2
2
  import { test } from "vitest";
3
+ 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";
3
4
  export type { Browser, Page } from "puppeteer-core";
4
- export declare const INSTALL_DEPENDENCIES_RETRIES: number;
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, };
5
6
  interface DevServerInstance {
6
7
  url: string;
8
+ projectDir: string;
7
9
  stopDev: () => Promise<void>;
8
10
  }
9
11
  interface DeploymentInstance {
@@ -35,6 +37,11 @@ export interface SetupPlaygroundEnvironmentOptions {
35
37
  * @default true
36
38
  */
37
39
  deploy?: boolean;
40
+ /**
41
+ * Whether to automatically start the dev server.
42
+ * @default true
43
+ */
44
+ autoStartDevServer?: boolean;
38
45
  }
39
46
  /**
40
47
  * A Vitest hook that sets up a playground environment for a test file.
@@ -42,17 +49,29 @@ export interface SetupPlaygroundEnvironmentOptions {
42
49
  * and installs dependencies using a tarball of the SDK.
43
50
  * This ensures that tests run in a clean, isolated environment.
44
51
  */
45
- export declare function setupPlaygroundEnvironment(options?: string | SetupPlaygroundEnvironmentOptions): void;
52
+ export declare function setupPlaygroundEnvironment(options: SetupPlaygroundEnvironmentOptions | string): void;
46
53
  /**
47
54
  * Creates a dev server instance using the shared playground environment.
48
55
  * Automatically registers cleanup to run after the test.
49
56
  */
50
- export declare function createDevServer(projectDir: string): Promise<DevServerInstance>;
57
+ export declare function createDevServer(): {
58
+ projectDir: string;
59
+ start: () => Promise<DevServerInstance>;
60
+ };
51
61
  /**
52
62
  * Creates a deployment instance using the shared playground environment.
53
63
  * Automatically registers cleanup to run after the test.
54
64
  */
55
- 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
+ };
56
75
  /**
57
76
  * Executes a test function with a retry mechanism for specific error codes.
58
77
  * @param name - The name of the test, used for logging.
@@ -62,13 +81,23 @@ export declare function createDeployment(projectDir: string): Promise<Deployment
62
81
  * called automatically on failure.
63
82
  */
64
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;
65
89
  declare function createTestRunner(testFn: (typeof test | typeof test.only)["concurrent"], envType: "dev" | "deploy"): (name: string, testLogic: (context: {
66
90
  devServer?: DevServerInstance;
67
91
  deployment?: DeploymentInstance;
68
92
  browser: Browser;
69
93
  page: Page;
70
94
  url: string;
95
+ projectDir: string;
71
96
  }) => Promise<void>) => void;
97
+ export declare const testSDK: SDKRunner & {
98
+ only: SDKRunner;
99
+ skip: typeof test.skip;
100
+ };
72
101
  /**
73
102
  * High-level test wrapper for dev server tests.
74
103
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -82,6 +111,7 @@ export declare namespace testDev {
82
111
  browser: Browser;
83
112
  page: Page;
84
113
  url: string;
114
+ projectDir: string;
85
115
  }) => Promise<void>) => void;
86
116
  }
87
117
  /**
@@ -97,6 +127,7 @@ export declare namespace testDeploy {
97
127
  browser: Browser;
98
128
  page: Page;
99
129
  url: string;
130
+ projectDir: string;
100
131
  }) => Promise<void>) => void;
101
132
  }
102
133
  /**
@@ -109,6 +140,7 @@ export declare function testDevAndDeploy(name: string, testFn: (context: {
109
140
  browser: Browser;
110
141
  page: Page;
111
142
  url: string;
143
+ projectDir: string;
112
144
  }) => Promise<void>): void;
113
145
  export declare namespace testDevAndDeploy {
114
146
  var skip: (name: string, testFn?: any) => void;
@@ -1,51 +1,16 @@
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
+ 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;
45
- export const INSTALL_DEPENDENCIES_RETRIES = process.env
46
- .RWSDK_INSTALL_DEPENDENCIES_RETRIES
47
- ? parseInt(process.env.RWSDK_INSTALL_DEPENDENCIES_RETRIES, 10)
48
- : 10;
12
+ import { ensureTmpDir } from "./utils.mjs";
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, };
49
14
  // Environment variable flags for skipping tests
50
15
  const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
51
16
  const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
@@ -56,6 +21,8 @@ let globalDevInstancePromise = null;
56
21
  let globalDeploymentInstancePromise = null;
57
22
  let globalDevInstance = null;
58
23
  let globalDeploymentInstance = null;
24
+ const devInstances = [];
25
+ const deploymentInstances = [];
59
26
  let hooksRegistered = false;
60
27
  /**
61
28
  * Registers global cleanup hooks automatically
@@ -66,21 +33,33 @@ function ensureHooksRegistered() {
66
33
  // Register global afterAll to clean up the playground environment
67
34
  afterAll(async () => {
68
35
  const cleanupPromises = [];
69
- if (globalDevInstance) {
70
- 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
+ }));
71
41
  }
72
- if (globalDeploymentInstance) {
73
- 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
+ }));
74
47
  }
75
48
  if (globalDevPlaygroundEnv) {
76
- 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
+ }));
77
53
  }
78
54
  if (globalDeployPlaygroundEnv) {
79
- 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
+ }));
80
59
  }
81
60
  await Promise.all(cleanupPromises);
82
- globalDevInstance = null;
83
- globalDeploymentInstance = null;
61
+ devInstances.length = 0;
62
+ deploymentInstances.length = 0;
84
63
  globalDevPlaygroundEnv = null;
85
64
  globalDeployPlaygroundEnv = null;
86
65
  });
@@ -98,11 +77,11 @@ function getProjectDirectory() {
98
77
  * Derive the playground directory from import.meta.url by finding the nearest package.json
99
78
  */
100
79
  function getPlaygroundDirFromImportMeta(importMetaUrl) {
101
- const url = new URL(importMetaUrl);
102
- const testFilePath = url.pathname;
80
+ const testFilePath = fileURLToPath(importMetaUrl);
103
81
  let currentDir = dirname(testFilePath);
104
82
  // Walk up the tree from the test file's directory
105
- 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) {
106
85
  // Check if a package.json exists in the current directory
107
86
  if (fs.existsSync(pathJoin(currentDir, "package.json"))) {
108
87
  return currentDir;
@@ -118,8 +97,10 @@ function getPlaygroundDirFromImportMeta(importMetaUrl) {
118
97
  * and installs dependencies using a tarball of the SDK.
119
98
  * This ensures that tests run in a clean, isolated environment.
120
99
  */
121
- export function setupPlaygroundEnvironment(options = {}) {
122
- 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;
123
104
  ensureHooksRegistered();
124
105
  beforeAll(async () => {
125
106
  let projectDir;
@@ -145,18 +126,21 @@ export function setupPlaygroundEnvironment(options = {}) {
145
126
  projectDir: devEnv.targetDir,
146
127
  cleanup: devEnv.cleanup,
147
128
  };
148
- globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
149
- globalDevInstance = instance;
150
- return instance;
151
- });
152
- // Prevent unhandled promise rejections. The error will be handled inside
153
- // the test's beforeEach hook where this promise is awaited.
154
- 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
+ }
155
139
  }
156
140
  else {
157
- globalDevInstancePromise = Promise.resolve(null);
141
+ globalDevPlaygroundEnv = null;
158
142
  }
159
- if (deploy) {
143
+ if (deploy && !SKIP_DEPLOYMENT_TESTS) {
160
144
  const deployEnv = await setupTarballEnvironment({
161
145
  projectDir,
162
146
  monorepoRoot,
@@ -166,7 +150,10 @@ export function setupPlaygroundEnvironment(options = {}) {
166
150
  projectDir: deployEnv.targetDir,
167
151
  cleanup: deployEnv.cleanup,
168
152
  };
169
- globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
153
+ const deployControl = createDeployment();
154
+ globalDeploymentInstancePromise = deployControl
155
+ .start()
156
+ .then((instance) => {
170
157
  globalDeploymentInstance = instance;
171
158
  return instance;
172
159
  });
@@ -174,7 +161,7 @@ export function setupPlaygroundEnvironment(options = {}) {
174
161
  globalDeploymentInstancePromise.catch(() => { });
175
162
  }
176
163
  else {
177
- globalDeploymentInstancePromise = Promise.resolve(null);
164
+ globalDeployPlaygroundEnv = null;
178
165
  }
179
166
  }, SETUP_PLAYGROUND_ENV_TIMEOUT);
180
167
  }
@@ -182,82 +169,109 @@ export function setupPlaygroundEnvironment(options = {}) {
182
169
  * Creates a dev server instance using the shared playground environment.
183
170
  * Automatically registers cleanup to run after the test.
184
171
  */
185
- export async function createDevServer(projectDir) {
186
- if (SKIP_DEV_SERVER_TESTS) {
187
- 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.");
188
176
  }
177
+ const { projectDir } = globalDevPlaygroundEnv;
189
178
  const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
190
- const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
191
- timeout: DEV_SERVER_TIMEOUT,
192
- minTries: DEV_SERVER_MIN_TRIES,
193
- onRetry: (error, tries) => {
194
- console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
195
- },
196
- });
179
+ let instance = null;
197
180
  return {
198
- url: devResult.url,
199
- 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
+ },
200
203
  };
201
204
  }
202
205
  /**
203
206
  * Creates a deployment instance using the shared playground environment.
204
207
  * Automatically registers cleanup to run after the test.
205
208
  */
206
- export async function createDeployment(projectDir) {
207
- if (SKIP_DEPLOYMENT_TESTS) {
208
- 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.");
209
213
  }
210
- return await pollValue(async () => {
211
- // Extract the unique key from the project directory name instead of generating a new one
212
- // The directory name format is: {projectName}-e2e-test-{randomId}
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
- // Poll the URL to ensure it's live before proceeding
220
- await poll(async () => {
221
- try {
222
- const response = await fetch(deployResult.url);
223
- // We consider any response (even 4xx or 5xx) as success,
224
- // as it means the worker is routable.
225
- return response.status > 0;
226
- }
227
- catch (e) {
228
- 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");
229
223
  }
230
- }, {
231
- timeout: DEPLOYMENT_CHECK_TIMEOUT,
232
- });
233
- const cleanup = async () => {
234
- // Run deployment cleanup in background without blocking
235
- const performCleanup = async () => {
236
- if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
237
- await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
238
- }
239
- await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
240
- };
241
- // Start cleanup in background and return immediately
242
- performCleanup().catch((error) => {
243
- 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
+ },
244
270
  });
245
- return Promise.resolve();
246
- };
247
- return {
248
- url: deployResult.url,
249
- workerName: deployResult.workerName,
250
- resourceUniqueKey,
251
- projectDir: projectDir,
252
- cleanup,
253
- };
254
- }, {
255
- timeout: DEPLOYMENT_TIMEOUT,
256
- minTries: DEPLOYMENT_MIN_TRIES,
257
- onRetry: (error, tries) => {
258
- console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
271
+ deploymentInstances.push(newInstance);
272
+ return newInstance;
259
273
  },
260
- });
274
+ };
261
275
  }
262
276
  /**
263
277
  * Executes a test function with a retry mechanism for specific error codes.
@@ -306,7 +320,7 @@ function createTestRunner(testFn, envType) {
306
320
  return (name, testLogic) => {
307
321
  if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
308
322
  (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
309
- test.skip(name, () => { });
323
+ test.skip(`${name} (${envType})`, () => { });
310
324
  return;
311
325
  }
312
326
  describe.concurrent(name, () => {
@@ -314,7 +328,7 @@ function createTestRunner(testFn, envType) {
314
328
  let instance;
315
329
  let browser;
316
330
  beforeAll(async () => {
317
- const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
331
+ const tempDir = path.join(await ensureTmpDir(), "rwsdk-e2e-tests");
318
332
  const wsEndpointFile = path.join(tempDir, "wsEndpoint");
319
333
  try {
320
334
  const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
@@ -368,12 +382,86 @@ function createTestRunner(testFn, envType) {
368
382
  browser: browser,
369
383
  page: page,
370
384
  url: instance.url,
385
+ projectDir: instance
386
+ .projectDir,
371
387
  });
372
388
  });
373
389
  });
374
390
  });
375
391
  };
376
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
+ browser = await launchBrowser();
417
+ }
418
+ }, SETUP_WAIT_TIMEOUT);
419
+ afterAll(async () => {
420
+ if (browser) {
421
+ await browser.disconnect();
422
+ }
423
+ });
424
+ beforeEach(async () => {
425
+ if (!globalDevPlaygroundEnv && !globalDeployPlaygroundEnv) {
426
+ throw new Error("Test environment not initialized. Call setupPlaygroundEnvironment() in your test file.");
427
+ }
428
+ page = await browser.newPage();
429
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
430
+ }, SETUP_WAIT_TIMEOUT);
431
+ afterEach(async () => {
432
+ if (page) {
433
+ try {
434
+ await page.close();
435
+ }
436
+ catch (error) {
437
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
438
+ }
439
+ }
440
+ });
441
+ testFn(">", async () => {
442
+ if (!browser) {
443
+ throw new Error("Test environment not ready.");
444
+ }
445
+ await runTestWithRetries(name, async () => {
446
+ await testLogic({
447
+ browser: browser,
448
+ page: page,
449
+ projectDir: globalDevPlaygroundEnv?.projectDir ||
450
+ globalDeployPlaygroundEnv?.projectDir ||
451
+ "",
452
+ });
453
+ });
454
+ });
455
+ });
456
+ };
457
+ };
458
+ const main = internalRunner(test);
459
+ return Object.assign(main, {
460
+ only: internalRunner(test.only),
461
+ skip: test.skip,
462
+ });
463
+ }
464
+ export const testSDK = createSDKTestRunner();
377
465
  /**
378
466
  * High-level test wrapper for dev server tests.
379
467
  * 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;