rwsdk 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (158) hide show
  1. package/dist/lib/e2e/browser.d.mts +10 -0
  2. package/dist/lib/e2e/browser.mjs +124 -0
  3. package/dist/lib/e2e/dev.d.mts +8 -0
  4. package/dist/lib/e2e/dev.mjs +242 -0
  5. package/dist/lib/e2e/environment.d.mts +14 -0
  6. package/dist/lib/e2e/environment.mjs +266 -0
  7. package/dist/lib/e2e/index.d.mts +8 -0
  8. package/dist/lib/e2e/index.mjs +8 -0
  9. package/dist/lib/e2e/poll.d.mts +8 -0
  10. package/dist/lib/e2e/poll.mjs +31 -0
  11. package/dist/lib/e2e/release.d.mts +56 -0
  12. package/dist/lib/e2e/release.mjs +559 -0
  13. package/dist/lib/e2e/retry.d.mts +4 -0
  14. package/dist/lib/e2e/retry.mjs +16 -0
  15. package/dist/lib/e2e/setup.d.mts +2 -0
  16. package/dist/lib/e2e/setup.mjs +1 -0
  17. package/dist/lib/e2e/tarball.d.mts +14 -0
  18. package/dist/lib/e2e/tarball.mjs +99 -0
  19. package/dist/lib/e2e/testHarness.d.mts +132 -0
  20. package/dist/lib/e2e/testHarness.mjs +437 -0
  21. package/dist/lib/e2e/types.d.mts +32 -0
  22. package/dist/lib/getShortName.mjs +6 -1
  23. package/dist/lib/getShortName.test.d.mts +1 -0
  24. package/dist/lib/getShortName.test.mjs +25 -0
  25. package/dist/lib/hasPkgScript.d.mts +4 -1
  26. package/dist/lib/hasPkgScript.mjs +9 -6
  27. package/dist/lib/hasPkgScript.test.d.mts +1 -0
  28. package/dist/lib/hasPkgScript.test.mjs +33 -0
  29. package/dist/lib/jsonUtils.mjs +3 -0
  30. package/dist/lib/jsonUtils.test.d.mts +1 -0
  31. package/dist/lib/jsonUtils.test.mjs +90 -0
  32. package/dist/lib/normalizeModulePath.d.mts +5 -0
  33. package/dist/lib/normalizeModulePath.mjs +1 -1
  34. package/dist/lib/normalizeModulePath.test.d.mts +1 -0
  35. package/dist/lib/{normalizeModulePath.test.js → normalizeModulePath.test.mjs} +20 -1
  36. package/dist/lib/smokeTests/browser.mjs +3 -94
  37. package/dist/lib/smokeTests/development.mjs +2 -223
  38. package/dist/lib/smokeTests/environment.d.mts +4 -11
  39. package/dist/lib/smokeTests/environment.mjs +10 -158
  40. package/dist/lib/smokeTests/release.d.mts +2 -49
  41. package/dist/lib/smokeTests/release.mjs +3 -503
  42. package/dist/llms/rules/middleware.d.ts +1 -1
  43. package/dist/llms/rules/middleware.js +4 -4
  44. package/dist/runtime/entries/worker.d.ts +0 -1
  45. package/dist/runtime/entries/worker.js +0 -1
  46. package/dist/runtime/lib/auth/session.d.ts +2 -2
  47. package/dist/runtime/lib/auth/session.js +4 -4
  48. package/dist/runtime/lib/memoizeOnId.test.d.ts +1 -0
  49. package/dist/runtime/lib/memoizeOnId.test.js +49 -0
  50. package/dist/runtime/lib/realtime/protocol.test.d.ts +1 -0
  51. package/dist/runtime/lib/realtime/protocol.test.js +107 -0
  52. package/dist/runtime/lib/realtime/shared.test.d.ts +1 -0
  53. package/dist/runtime/lib/realtime/shared.test.js +18 -0
  54. package/dist/runtime/lib/realtime/validateUpgradeRequest.test.d.ts +1 -0
  55. package/dist/runtime/lib/realtime/validateUpgradeRequest.test.js +66 -0
  56. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  57. package/dist/runtime/lib/router.js +40 -22
  58. package/dist/runtime/lib/router.test.js +590 -2
  59. package/dist/runtime/lib/rwContext.d.ts +22 -0
  60. package/dist/runtime/lib/rwContext.js +1 -0
  61. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +18 -0
  62. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +143 -0
  63. package/dist/runtime/lib/turnstile/verifyTurnstileToken.d.ts +2 -1
  64. package/dist/runtime/lib/turnstile/verifyTurnstileToken.js +6 -6
  65. package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.d.ts +1 -0
  66. package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.js +49 -0
  67. package/dist/runtime/register/worker.d.ts +1 -1
  68. package/dist/runtime/register/worker.js +33 -21
  69. package/dist/runtime/render/assembleDocument.d.ts +6 -0
  70. package/dist/runtime/render/assembleDocument.js +22 -0
  71. package/dist/runtime/render/createThenableFromReadableStream.d.ts +1 -0
  72. package/dist/runtime/render/createThenableFromReadableStream.js +9 -0
  73. package/dist/runtime/render/normalizeActionResult.d.ts +1 -0
  74. package/dist/runtime/render/normalizeActionResult.js +43 -0
  75. package/dist/runtime/render/preloads.d.ts +2 -2
  76. package/dist/runtime/render/preloads.js +2 -3
  77. package/dist/runtime/render/{renderRscThenableToHtmlStream.d.ts → renderDocumentHtmlStream.d.ts} +3 -3
  78. package/dist/runtime/render/renderDocumentHtmlStream.js +39 -0
  79. package/dist/runtime/render/renderHtmlStream.d.ts +7 -0
  80. package/dist/runtime/render/renderHtmlStream.js +31 -0
  81. package/dist/runtime/render/renderToRscStream.d.ts +5 -3
  82. package/dist/runtime/render/renderToRscStream.js +12 -41
  83. package/dist/runtime/render/renderToStream.d.ts +2 -1
  84. package/dist/runtime/render/renderToStream.js +15 -8
  85. package/dist/runtime/render/stylesheets.d.ts +2 -2
  86. package/dist/runtime/render/stylesheets.js +2 -3
  87. package/dist/runtime/requestInfo/types.d.ts +0 -2
  88. package/dist/runtime/requestInfo/worker.js +1 -9
  89. package/dist/runtime/ssrBridge.d.ts +2 -1
  90. package/dist/runtime/ssrBridge.js +2 -1
  91. package/dist/runtime/worker.d.ts +1 -0
  92. package/dist/runtime/worker.js +11 -14
  93. package/dist/scripts/debug-sync.mjs +102 -133
  94. package/dist/vite/buildApp.d.mts +2 -1
  95. package/dist/vite/buildApp.mjs +9 -5
  96. package/dist/vite/checkIsUsingPrisma.d.mts +4 -0
  97. package/dist/vite/checkIsUsingPrisma.mjs +2 -2
  98. package/dist/vite/checkIsUsingPrisma.test.d.mts +1 -0
  99. package/dist/vite/checkIsUsingPrisma.test.mjs +30 -0
  100. package/dist/vite/configPlugin.mjs +54 -14
  101. package/dist/vite/createDirectiveLookupPlugin.d.mts +9 -0
  102. package/dist/vite/createDirectiveLookupPlugin.mjs +33 -29
  103. package/dist/vite/createDirectiveLookupPlugin.test.d.mts +1 -0
  104. package/dist/vite/createDirectiveLookupPlugin.test.mjs +40 -0
  105. package/dist/vite/directiveModulesDevPlugin.d.mts +4 -1
  106. package/dist/vite/directiveModulesDevPlugin.mjs +6 -5
  107. package/dist/vite/directiveModulesDevPlugin.test.d.mts +1 -0
  108. package/dist/vite/directiveModulesDevPlugin.test.mjs +59 -0
  109. package/dist/vite/directivesPlugin.d.mts +1 -0
  110. package/dist/vite/directivesPlugin.mjs +1 -1
  111. package/dist/vite/directivesPlugin.test.d.mts +1 -0
  112. package/dist/vite/directivesPlugin.test.mjs +24 -0
  113. package/dist/vite/ensureAliasArray.test.d.mts +1 -0
  114. package/dist/vite/ensureAliasArray.test.mjs +71 -0
  115. package/dist/vite/findSpecifiers.mjs +2 -1
  116. package/dist/vite/findSpecifiers.test.d.mts +1 -0
  117. package/dist/vite/findSpecifiers.test.mjs +202 -0
  118. package/dist/vite/findSsrSpecifiers.test.d.mts +1 -0
  119. package/dist/vite/findSsrSpecifiers.test.mjs +99 -0
  120. package/dist/vite/hasDirective.d.mts +6 -3
  121. package/dist/vite/hasDirective.mjs +43 -27
  122. package/dist/vite/hasDirective.test.d.mts +1 -0
  123. package/dist/vite/hasDirective.test.mjs +107 -0
  124. package/dist/vite/isJsFile.test.d.mts +1 -0
  125. package/dist/vite/isJsFile.test.mjs +38 -0
  126. package/dist/vite/{reactConditionsResolverPlugin.d.mts → knownDepsResolverPlugin.d.mts} +2 -2
  127. package/dist/vite/{reactConditionsResolverPlugin.mjs → knownDepsResolverPlugin.mjs} +28 -23
  128. package/dist/vite/linkerPlugin.d.mts +8 -0
  129. package/dist/vite/linkerPlugin.mjs +30 -22
  130. package/dist/vite/linkerPlugin.test.d.mts +1 -0
  131. package/dist/vite/linkerPlugin.test.mjs +41 -0
  132. package/dist/vite/miniflareHMRPlugin.d.mts +5 -0
  133. package/dist/vite/miniflareHMRPlugin.mjs +2 -2
  134. package/dist/vite/miniflareHMRPlugin.test.d.mts +1 -0
  135. package/dist/vite/miniflareHMRPlugin.test.mjs +42 -0
  136. package/dist/vite/redwoodPlugin.d.mts +9 -0
  137. package/dist/vite/redwoodPlugin.mjs +29 -5
  138. package/dist/vite/redwoodPlugin.test.d.mts +1 -0
  139. package/dist/vite/redwoodPlugin.test.mjs +34 -0
  140. package/dist/vite/resolveForcedPaths.d.mts +4 -0
  141. package/dist/vite/resolveForcedPaths.mjs +9 -0
  142. package/dist/vite/runDirectivesScan.d.mts +22 -1
  143. package/dist/vite/runDirectivesScan.mjs +105 -58
  144. package/dist/vite/runDirectivesScan.test.d.mts +1 -0
  145. package/dist/vite/runDirectivesScan.test.mjs +73 -0
  146. package/dist/vite/ssrBridgePlugin.mjs +8 -1
  147. package/dist/vite/transformClientComponents.mjs +6 -4
  148. package/dist/vite/transformClientComponents.test.mjs +116 -58
  149. package/dist/vite/transformServerFunctions.d.mts +1 -1
  150. package/dist/vite/transformServerFunctions.mjs +1 -1
  151. package/dist/vite/transformServerFunctions.test.mjs +3 -3
  152. package/package.json +56 -47
  153. package/dist/runtime/imports/resolveSSRValue.d.ts +0 -1
  154. package/dist/runtime/imports/resolveSSRValue.js +0 -8
  155. package/dist/runtime/render/renderRscThenableToHtmlStream.js +0 -54
  156. package/dist/runtime/render/transformRscToHtmlStream.d.ts +0 -8
  157. package/dist/runtime/render/transformRscToHtmlStream.js +0 -19
  158. /package/dist/lib/{normalizeModulePath.test.d.ts → e2e/types.mjs} +0 -0
@@ -0,0 +1,437 @@
1
+ import { test, beforeAll, afterAll, afterEach, describe, beforeEach, } from "vitest";
2
+ import { basename, join as pathJoin, dirname } from "path";
3
+ import { setupTarballEnvironment } from "./tarball.mjs";
4
+ import { runDevServer } from "./dev.mjs";
5
+ import { runRelease, deleteWorker, deleteD1Database, isRelatedToTest, } from "./release.mjs";
6
+ import { launchBrowser } from "./browser.mjs";
7
+ import puppeteer from "puppeteer-core";
8
+ import fs from "fs-extra";
9
+ import os from "os";
10
+ import path from "path";
11
+ import { poll, pollValue } from "./poll.mjs";
12
+ const SETUP_PLAYGROUND_ENV_TIMEOUT = process.env
13
+ .RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT
14
+ ? parseInt(process.env.RWSDK_SETUP_PLAYGROUND_ENV_TIMEOUT, 10)
15
+ : 15 * 60 * 1000;
16
+ const DEPLOYMENT_TIMEOUT = process.env.RWSDK_DEPLOYMENT_TIMEOUT
17
+ ? parseInt(process.env.RWSDK_DEPLOYMENT_TIMEOUT, 10)
18
+ : 5 * 60 * 1000;
19
+ const DEPLOYMENT_MIN_TRIES = process.env.RWSDK_DEPLOYMENT_MIN_TRIES
20
+ ? parseInt(process.env.RWSDK_DEPLOYMENT_MIN_TRIES, 10)
21
+ : 5;
22
+ const DEPLOYMENT_CHECK_TIMEOUT = process.env.RWSDK_DEPLOYMENT_CHECK_TIMEOUT
23
+ ? parseInt(process.env.RWSDK_DEPLOYMENT_CHECK_TIMEOUT, 10)
24
+ : 5 * 60 * 1000;
25
+ const PUPPETEER_TIMEOUT = process.env.RWSDK_PUPPETEER_TIMEOUT
26
+ ? parseInt(process.env.RWSDK_PUPPETEER_TIMEOUT, 10)
27
+ : 60 * 1000 * 2;
28
+ const HYDRATION_TIMEOUT = process.env.RWSDK_HYDRATION_TIMEOUT
29
+ ? parseInt(process.env.RWSDK_HYDRATION_TIMEOUT, 10)
30
+ : 5000;
31
+ const DEV_SERVER_TIMEOUT = process.env.RWSDK_DEV_SERVER_TIMEOUT
32
+ ? parseInt(process.env.RWSDK_DEV_SERVER_TIMEOUT, 10)
33
+ : 5 * 60 * 1000;
34
+ const DEV_SERVER_MIN_TRIES = process.env.RWSDK_DEV_SERVER_MIN_TRIES
35
+ ? parseInt(process.env.RWSDK_DEV_SERVER_MIN_TRIES, 10)
36
+ : 5;
37
+ const SETUP_WAIT_TIMEOUT = process.env.RWSDK_SETUP_WAIT_TIMEOUT
38
+ ? parseInt(process.env.RWSDK_SETUP_WAIT_TIMEOUT, 10)
39
+ : 10 * 60 * 1000;
40
+ // Environment variable flags for skipping tests
41
+ const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
42
+ const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
43
+ // Global test environment state
44
+ let globalDevPlaygroundEnv = null;
45
+ let globalDeployPlaygroundEnv = null;
46
+ let globalDevInstancePromise = null;
47
+ let globalDeploymentInstancePromise = null;
48
+ let globalDevInstance = null;
49
+ let globalDeploymentInstance = null;
50
+ let hooksRegistered = false;
51
+ /**
52
+ * Registers global cleanup hooks automatically
53
+ */
54
+ function ensureHooksRegistered() {
55
+ if (hooksRegistered)
56
+ return;
57
+ // Register global afterAll to clean up the playground environment
58
+ afterAll(async () => {
59
+ const cleanupPromises = [];
60
+ if (globalDevInstance) {
61
+ cleanupPromises.push(globalDevInstance.stopDev());
62
+ }
63
+ if (globalDeploymentInstance) {
64
+ cleanupPromises.push(globalDeploymentInstance.cleanup());
65
+ }
66
+ if (globalDevPlaygroundEnv) {
67
+ cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
68
+ }
69
+ if (globalDeployPlaygroundEnv) {
70
+ cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
71
+ }
72
+ await Promise.all(cleanupPromises);
73
+ globalDevInstance = null;
74
+ globalDeploymentInstance = null;
75
+ globalDevPlaygroundEnv = null;
76
+ globalDeployPlaygroundEnv = null;
77
+ });
78
+ hooksRegistered = true;
79
+ }
80
+ /**
81
+ * Get the project directory for the current test by looking at the call stack
82
+ */
83
+ function getProjectDirectory() {
84
+ // For now, let's hardcode this to '../playground/hello-world' since we only have one project
85
+ // TODO: Make this more dynamic when we have multiple playground projects
86
+ return "../playground/hello-world";
87
+ }
88
+ /**
89
+ * Derive the playground directory from import.meta.url by finding the nearest package.json
90
+ */
91
+ function getPlaygroundDirFromImportMeta(importMetaUrl) {
92
+ const url = new URL(importMetaUrl);
93
+ const testFilePath = url.pathname;
94
+ let currentDir = dirname(testFilePath);
95
+ // Walk up the tree from the test file's directory
96
+ while (currentDir !== "/") {
97
+ // Check if a package.json exists in the current directory
98
+ if (fs.existsSync(pathJoin(currentDir, "package.json"))) {
99
+ return currentDir;
100
+ }
101
+ currentDir = dirname(currentDir);
102
+ }
103
+ throw new Error(`Could not determine playground directory from import.meta.url: ${importMetaUrl}. ` +
104
+ `Failed to find a package.json in any parent directory.`);
105
+ }
106
+ /**
107
+ * A Vitest hook that sets up a playground environment for a test file.
108
+ * It creates a temporary directory, copies the playground project into it,
109
+ * and installs dependencies using a tarball of the SDK.
110
+ * This ensures that tests run in a clean, isolated environment.
111
+ */
112
+ export function setupPlaygroundEnvironment(options = {}) {
113
+ const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
114
+ ensureHooksRegistered();
115
+ beforeAll(async () => {
116
+ let projectDir;
117
+ if (!sourceProjectDir) {
118
+ projectDir = getProjectDirectory();
119
+ }
120
+ else if (sourceProjectDir.startsWith("file://")) {
121
+ // This is import.meta.url, derive the playground directory
122
+ projectDir = getPlaygroundDirFromImportMeta(sourceProjectDir);
123
+ }
124
+ else {
125
+ // This is an explicit path
126
+ projectDir = sourceProjectDir;
127
+ }
128
+ console.log(`Setting up playground environment from ${projectDir}...`);
129
+ if (dev) {
130
+ const devEnv = await setupTarballEnvironment({
131
+ projectDir,
132
+ monorepoRoot,
133
+ packageManager: process.env.PACKAGE_MANAGER || "pnpm",
134
+ });
135
+ globalDevPlaygroundEnv = {
136
+ projectDir: devEnv.targetDir,
137
+ cleanup: devEnv.cleanup,
138
+ };
139
+ globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
140
+ globalDevInstance = instance;
141
+ return instance;
142
+ });
143
+ // Prevent unhandled promise rejections. The error will be handled inside
144
+ // the test's beforeEach hook where this promise is awaited.
145
+ globalDevInstancePromise.catch(() => { });
146
+ }
147
+ else {
148
+ globalDevInstancePromise = Promise.resolve(null);
149
+ }
150
+ if (deploy) {
151
+ const deployEnv = await setupTarballEnvironment({
152
+ projectDir,
153
+ monorepoRoot,
154
+ packageManager: process.env.PACKAGE_MANAGER || "pnpm",
155
+ });
156
+ globalDeployPlaygroundEnv = {
157
+ projectDir: deployEnv.targetDir,
158
+ cleanup: deployEnv.cleanup,
159
+ };
160
+ globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
161
+ globalDeploymentInstance = instance;
162
+ return instance;
163
+ });
164
+ // Prevent unhandled promise rejections
165
+ globalDeploymentInstancePromise.catch(() => { });
166
+ }
167
+ else {
168
+ globalDeploymentInstancePromise = Promise.resolve(null);
169
+ }
170
+ }, SETUP_PLAYGROUND_ENV_TIMEOUT);
171
+ }
172
+ /**
173
+ * Creates a dev server instance using the shared playground environment.
174
+ * Automatically registers cleanup to run after the test.
175
+ */
176
+ export async function createDevServer(projectDir) {
177
+ if (SKIP_DEV_SERVER_TESTS) {
178
+ throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
179
+ }
180
+ const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
181
+ const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
182
+ timeout: DEV_SERVER_TIMEOUT,
183
+ minTries: DEV_SERVER_MIN_TRIES,
184
+ onRetry: (error, tries) => {
185
+ console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
186
+ },
187
+ });
188
+ return {
189
+ url: devResult.url,
190
+ stopDev: devResult.stopDev,
191
+ };
192
+ }
193
+ /**
194
+ * Creates a deployment instance using the shared playground environment.
195
+ * Automatically registers cleanup to run after the test.
196
+ */
197
+ export async function createDeployment(projectDir) {
198
+ if (SKIP_DEPLOYMENT_TESTS) {
199
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
200
+ }
201
+ return await pollValue(async () => {
202
+ // Extract the unique key from the project directory name instead of generating a new one
203
+ // The directory name format is: {projectName}-e2e-test-{randomId}
204
+ const dirName = basename(projectDir);
205
+ const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
206
+ const resourceUniqueKey = match
207
+ ? match[1]
208
+ : Math.random().toString(36).substring(2, 15);
209
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
210
+ // Poll the URL to ensure it's live before proceeding
211
+ await poll(async () => {
212
+ try {
213
+ const response = await fetch(deployResult.url);
214
+ // We consider any response (even 4xx or 5xx) as success,
215
+ // as it means the worker is routable.
216
+ return response.status > 0;
217
+ }
218
+ catch (e) {
219
+ return false;
220
+ }
221
+ }, {
222
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
223
+ });
224
+ const cleanup = async () => {
225
+ // Run deployment cleanup in background without blocking
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
+ // Start cleanup in background and return immediately
233
+ performCleanup().catch((error) => {
234
+ console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
235
+ });
236
+ return Promise.resolve();
237
+ };
238
+ return {
239
+ url: deployResult.url,
240
+ workerName: deployResult.workerName,
241
+ resourceUniqueKey,
242
+ projectDir: projectDir,
243
+ cleanup,
244
+ };
245
+ }, {
246
+ timeout: DEPLOYMENT_TIMEOUT,
247
+ minTries: DEPLOYMENT_MIN_TRIES,
248
+ onRetry: (error, tries) => {
249
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
250
+ },
251
+ });
252
+ }
253
+ /**
254
+ * Executes a test function with a retry mechanism for specific error codes.
255
+ * @param name - The name of the test, used for logging.
256
+ * @param attemptFn - A function that executes one attempt of the test.
257
+ * It should set up resources, run the test logic, and
258
+ * return a cleanup function. The cleanup function will be
259
+ * called automatically on failure.
260
+ */
261
+ export async function runTestWithRetries(name, attemptFn) {
262
+ const MAX_RETRIES_PER_CODE = 6;
263
+ const retryCounts = {};
264
+ let attempt = 0;
265
+ while (true) {
266
+ attempt++;
267
+ try {
268
+ await attemptFn();
269
+ if (attempt > 1) {
270
+ console.log(`[runTestWithRetries] Test "${name}" succeeded on attempt ${attempt}.`);
271
+ }
272
+ return; // Success
273
+ }
274
+ catch (e) {
275
+ const errorCode = e?.code;
276
+ if (typeof errorCode === "string" && errorCode) {
277
+ const count = (retryCounts[errorCode] || 0) + 1;
278
+ retryCounts[errorCode] = count;
279
+ if (count <= MAX_RETRIES_PER_CODE) {
280
+ console.log(`[runTestWithRetries] Attempt ${attempt} for "${name}" failed with code ${errorCode}. Retrying (failure ${count}/${MAX_RETRIES_PER_CODE} for this code)...`);
281
+ await new Promise((resolve) => setTimeout(resolve, 1000));
282
+ continue; // Next attempt
283
+ }
284
+ else {
285
+ console.error(`[runTestWithRetries] Test "${name}" failed with code ${errorCode} after ${MAX_RETRIES_PER_CODE} retries for this code.`);
286
+ throw e; // Give up
287
+ }
288
+ }
289
+ else {
290
+ console.error(`[runTestWithRetries] Test "${name}" failed on attempt ${attempt} with a non-retryable error:`, e);
291
+ throw e;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ function createTestRunner(testFn, envType) {
297
+ return (name, testLogic) => {
298
+ if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
299
+ (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
300
+ test.skip(name, () => { });
301
+ return;
302
+ }
303
+ describe.concurrent(name, () => {
304
+ let page;
305
+ let instance;
306
+ let browser;
307
+ beforeAll(async () => {
308
+ const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
309
+ const wsEndpointFile = path.join(tempDir, "wsEndpoint");
310
+ try {
311
+ const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
312
+ browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
313
+ }
314
+ catch (error) {
315
+ console.warn("Failed to connect to existing browser instance. " +
316
+ "This might happen if you are running a single test file. " +
317
+ "Launching a new browser instance instead.");
318
+ browser = await launchBrowser();
319
+ }
320
+ }, SETUP_WAIT_TIMEOUT);
321
+ afterAll(async () => {
322
+ if (browser) {
323
+ await browser.disconnect();
324
+ }
325
+ });
326
+ beforeEach(async () => {
327
+ const instancePromise = envType === "dev"
328
+ ? globalDevInstancePromise
329
+ : globalDeploymentInstancePromise;
330
+ if (!instancePromise) {
331
+ throw new Error("Test environment promises not initialized. Call setupPlaygroundEnvironment() in your test file.");
332
+ }
333
+ [instance] = await Promise.all([instancePromise]);
334
+ if (!instance) {
335
+ throw new Error(`No ${envType} instance found. Make sure to enable it in setupPlaygroundEnvironment.`);
336
+ }
337
+ page = await browser.newPage();
338
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
339
+ }, SETUP_WAIT_TIMEOUT);
340
+ afterEach(async () => {
341
+ if (page) {
342
+ try {
343
+ await page.close();
344
+ }
345
+ catch (error) {
346
+ // Suppress errors during page close, as the browser might already be disconnecting
347
+ // due to the test suite finishing.
348
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
349
+ }
350
+ }
351
+ });
352
+ testFn(">", async () => {
353
+ if (!instance || !browser) {
354
+ throw new Error("Test environment not ready.");
355
+ }
356
+ await runTestWithRetries(name, async () => {
357
+ await testLogic({
358
+ [envType === "dev" ? "devServer" : "deployment"]: instance,
359
+ browser: browser,
360
+ page: page,
361
+ url: instance.url,
362
+ });
363
+ });
364
+ });
365
+ });
366
+ };
367
+ }
368
+ /**
369
+ * High-level test wrapper for dev server tests.
370
+ * Automatically skips if RWSDK_SKIP_DEV=1
371
+ */
372
+ export function testDev(...args) {
373
+ return createTestRunner(test.concurrent, "dev")(...args);
374
+ }
375
+ testDev.skip = (name, testFn) => {
376
+ test.skip(name, testFn || (() => { }));
377
+ };
378
+ testDev.only = createTestRunner(test.only, "dev");
379
+ /**
380
+ * High-level test wrapper for deployment tests.
381
+ * Automatically skips if RWSDK_SKIP_DEPLOY=1
382
+ */
383
+ export function testDeploy(...args) {
384
+ return createTestRunner(test.concurrent, "deploy")(...args);
385
+ }
386
+ testDeploy.skip = (name, testFn) => {
387
+ test.skip(name, testFn || (() => { }));
388
+ };
389
+ testDeploy.only = createTestRunner(test.only, "deploy");
390
+ /**
391
+ * Unified test function that runs the same test against both dev server and deployment.
392
+ * Automatically skips based on environment variables.
393
+ */
394
+ export function testDevAndDeploy(name, testFn) {
395
+ testDev(`${name} (dev)`, testFn);
396
+ testDeploy(`${name} (deployment)`, testFn);
397
+ }
398
+ /**
399
+ * Skip version of testDevAndDeploy
400
+ */
401
+ testDevAndDeploy.skip = (name, testFn) => {
402
+ test.skip(name, testFn || (() => { }));
403
+ };
404
+ testDevAndDeploy.only = (name, testFn) => {
405
+ testDev.only(`${name} (dev)`, testFn);
406
+ testDeploy.only(`${name} (deployment)`, testFn);
407
+ };
408
+ /**
409
+ * Waits for the page to be fully loaded and hydrated.
410
+ * This should be used before any user interaction is simulated.
411
+ */
412
+ export async function waitForHydration(page) {
413
+ // 1. Wait for the document to be fully loaded.
414
+ await page.waitForFunction('document.readyState === "complete"');
415
+ // 2. Wait a short, fixed amount of time for client-side hydration to finish.
416
+ // This is a pragmatic approach to ensure React has mounted.
417
+ await new Promise((resolve) => setTimeout(resolve, HYDRATION_TIMEOUT));
418
+ }
419
+ export function trackPageErrors(page) {
420
+ const consoleErrors = [];
421
+ const failedRequests = [];
422
+ page.on("requestfailed", (request) => {
423
+ failedRequests.push(`${request.url()} | ${request.failure()?.errorText}`);
424
+ });
425
+ page.on("console", (msg) => {
426
+ if (msg.type() === "error") {
427
+ consoleErrors.push(msg.text());
428
+ }
429
+ });
430
+ return {
431
+ get: () => ({
432
+ // context(justinvdm, 25 Sep 2025): Filter out irrelevant 404s (e.g. favicon)
433
+ consoleErrors: consoleErrors.filter((e) => !e.includes("404")),
434
+ failedRequests,
435
+ }),
436
+ };
437
+ }
@@ -0,0 +1,32 @@
1
+ export type PackageManager = "pnpm" | "npm" | "yarn" | "yarn-classic";
2
+ /**
3
+ * Options for smoke tests
4
+ */
5
+ export interface SmokeTestOptions {
6
+ projectDir?: string;
7
+ artifactDir?: string;
8
+ headless?: boolean;
9
+ keep?: boolean;
10
+ sync?: boolean;
11
+ bail?: boolean;
12
+ skipDev?: boolean;
13
+ skipRelease?: boolean;
14
+ skipClient?: boolean;
15
+ packageManager?: PackageManager;
16
+ realtime?: boolean;
17
+ skipHmr?: boolean;
18
+ skipStyleTests?: boolean;
19
+ tarballPath?: string;
20
+ }
21
+ /**
22
+ * Resources created during a test run that need to be cleaned up
23
+ */
24
+ export interface TestResources {
25
+ tempDirCleanup?: () => Promise<void>;
26
+ workerName?: string;
27
+ targetDir?: string;
28
+ originalCwd: string;
29
+ workerCreatedDuringTest: boolean;
30
+ stopDev?: () => Promise<void>;
31
+ resourceUniqueKey: string;
32
+ }
@@ -1,2 +1,7 @@
1
1
  import { relative } from "node:path";
2
- export const getShortName = (file, root) => file.startsWith(root) ? relative(root, file) : file;
2
+ import path from "node:path";
3
+ export const getShortName = (file, root) => file === root
4
+ ? ""
5
+ : file.startsWith(root + path.sep)
6
+ ? relative(root, file)
7
+ : file;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getShortName } from "./getShortName.mjs";
3
+ import path from "node:path";
4
+ describe("getShortName", () => {
5
+ it("should return the relative path if the file is inside the root", () => {
6
+ const root = path.join("/Users", "test", "project");
7
+ const file = path.join(root, "src", "index.ts");
8
+ expect(getShortName(file, root)).toBe(path.join("src", "index.ts"));
9
+ });
10
+ it("should return the original path if the file is outside the root", () => {
11
+ const root = path.join("/Users", "test", "project");
12
+ const file = path.join("/Users", "test", "another", "project", "src", "index.ts");
13
+ expect(getShortName(file, root)).toBe(file);
14
+ });
15
+ it("should return an empty string if the paths are identical", () => {
16
+ const root = path.join("/Users", "test", "project");
17
+ const file = path.join("/Users", "test", "project");
18
+ expect(getShortName(file, root)).toBe("");
19
+ });
20
+ it("should handle paths that are substrings of each other correctly", () => {
21
+ const root = path.join("/Users", "test", "project");
22
+ const file = path.join("/Users", "test", "project-longer", "src", "index.ts");
23
+ expect(getShortName(file, root)).toBe(file);
24
+ });
25
+ });
@@ -1 +1,4 @@
1
- export declare const hasPkgScript: (projectRootDir: string, script: string) => Promise<any>;
1
+ import { readFile as fsReadFile } from "fs/promises";
2
+ export declare let _pkgCache: Record<string, any> | undefined;
3
+ export declare const hasPkgScript: (projectRootDir: string, script: string, readFile?: typeof fsReadFile) => Promise<any>;
4
+ export declare const _resetPkgCache: () => void;
@@ -1,9 +1,12 @@
1
- import { readFile } from "fs/promises";
1
+ import { readFile as fsReadFile } from "fs/promises";
2
2
  import { resolve } from "path";
3
- let pkg;
4
- export const hasPkgScript = async (projectRootDir, script) => {
5
- if (!pkg) {
6
- pkg = JSON.parse(await readFile(resolve(projectRootDir, "package.json"), "utf-8"));
3
+ export let _pkgCache;
4
+ export const hasPkgScript = async (projectRootDir, script, readFile = fsReadFile) => {
5
+ if (!_pkgCache) {
6
+ _pkgCache = JSON.parse((await readFile(resolve(projectRootDir, "package.json"))).toString());
7
7
  }
8
- return pkg.scripts?.[script];
8
+ return _pkgCache?.scripts?.[script];
9
+ };
10
+ export const _resetPkgCache = () => {
11
+ _pkgCache = undefined;
9
12
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { hasPkgScript, _resetPkgCache } from "./hasPkgScript.mjs";
3
+ // Manually reset the cache before each test
4
+ beforeEach(() => {
5
+ _resetPkgCache();
6
+ });
7
+ describe("hasPkgScript", () => {
8
+ it("should return the script if it exists", async () => {
9
+ const fakeReadFile = async () => JSON.stringify({ scripts: { "dev:init": "command" } });
10
+ const result = await hasPkgScript("/test", "dev:init", fakeReadFile);
11
+ expect(result).toBe("command");
12
+ });
13
+ it("should return undefined if the script does not exist", async () => {
14
+ const fakeReadFile = async () => JSON.stringify({ scripts: { test: "command" } });
15
+ const result = await hasPkgScript("/test", "dev:init", fakeReadFile);
16
+ expect(result).toBeUndefined();
17
+ });
18
+ it("should return undefined if scripts block does not exist", async () => {
19
+ const fakeReadFile = async () => JSON.stringify({});
20
+ const result = await hasPkgScript("/test", "dev:init", fakeReadFile);
21
+ expect(result).toBeUndefined();
22
+ });
23
+ it("should cache the package.json read", async () => {
24
+ let readCount = 0;
25
+ const fakeReadFile = async () => {
26
+ readCount++;
27
+ return JSON.stringify({ scripts: { "dev:init": "command" } });
28
+ };
29
+ await hasPkgScript("/test", "dev:init", fakeReadFile);
30
+ await hasPkgScript("/test", "dev:init", fakeReadFile);
31
+ expect(readCount).toBe(1);
32
+ });
33
+ });
@@ -158,6 +158,9 @@ export function parseJson(input, defaultValue, findUuid = false) {
158
158
  }
159
159
  }
160
160
  }
161
+ if (lastJson !== null) {
162
+ return lastJson;
163
+ }
161
164
  return defaultValue;
162
165
  }
163
166
  catch (error) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { extractLastJson, extractAllJson, parseJson } from "./jsonUtils.mjs";
3
+ describe("jsonUtils", () => {
4
+ describe("extractLastJson", () => {
5
+ it("should extract the last JSON object from a string", () => {
6
+ const output = 'some text {"a":1} some other text {"b":2, "c": {"d": 3}} end';
7
+ expect(extractLastJson(output)).toEqual({ b: 2, c: { d: 3 } });
8
+ });
9
+ it("should extract the last JSON array from a string", () => {
10
+ const output = "start [1, 2] middle [3, 4, [5]] end";
11
+ expect(extractLastJson(output)).toEqual([3, 4, [5]]);
12
+ });
13
+ it("should return the object if the string is just JSON", () => {
14
+ const output = '{"a":1}';
15
+ expect(extractLastJson(output)).toEqual({ a: 1 });
16
+ });
17
+ it("should handle nested structures correctly", () => {
18
+ const output = '{"a":{"b":{"c":"d"}}}';
19
+ expect(extractLastJson(output)).toEqual({ a: { b: { c: "d" } } });
20
+ });
21
+ it("should return null if no valid JSON is found", () => {
22
+ const output = "this is just some text without json";
23
+ expect(extractLastJson(output)).toBeNull();
24
+ });
25
+ it("should return null for malformed JSON", () => {
26
+ const output = '{"a":1, "b":}';
27
+ expect(extractLastJson(output)).toBeNull();
28
+ });
29
+ it("should handle undefined and empty string input", () => {
30
+ expect(extractLastJson(undefined)).toBeNull();
31
+ expect(extractLastJson("")).toBeNull();
32
+ });
33
+ });
34
+ describe("extractAllJson", () => {
35
+ it("should extract all JSON objects from a string", () => {
36
+ const output = '{"a":1} some text {"b":2} and then {"c":3, "d": [4]}';
37
+ expect(extractAllJson(output)).toEqual([
38
+ { a: 1 },
39
+ { b: 2 },
40
+ { c: 3, d: [4] },
41
+ ]);
42
+ });
43
+ it("should extract all JSON arrays from a string", () => {
44
+ const output = "[1,2] then [3,4]";
45
+ expect(extractAllJson(output)).toEqual([
46
+ [1, 2],
47
+ [3, 4],
48
+ ]);
49
+ });
50
+ it("should handle a mix of objects and arrays", () => {
51
+ const output = '{"a":1} [2,3] {"b":4}';
52
+ expect(extractAllJson(output)).toEqual([{ a: 1 }, [2, 3], { b: 4 }]);
53
+ });
54
+ it("should return an empty array if no JSON is found", () => {
55
+ const output = "no json here";
56
+ expect(extractAllJson(output)).toEqual([]);
57
+ });
58
+ it("should ignore malformed JSON", () => {
59
+ const output = '{"a":1} {"b":2,} [3,4]';
60
+ expect(extractAllJson(output)).toEqual([{ a: 1 }, [3, 4]]);
61
+ });
62
+ });
63
+ describe("parseJson", () => {
64
+ it("should parse the last JSON object by default", () => {
65
+ const output = '{"a":1} {"b":2}';
66
+ expect(parseJson(output, {})).toEqual({ b: 2 });
67
+ });
68
+ it("should return the default value if no JSON is found", () => {
69
+ const output = "no json";
70
+ expect(parseJson(output, { default: true })).toEqual({ default: true });
71
+ });
72
+ it("should find an object with a uuid property when requested", () => {
73
+ const output = '{"a":1} {"uuid":"123-abc", "data": "yes"} {"c":3}';
74
+ expect(parseJson(output, {}, true)).toEqual({
75
+ uuid: "123-abc",
76
+ data: "yes",
77
+ });
78
+ });
79
+ it("should return the last object if findUuid is true but no object with uuid is found", () => {
80
+ const output = '{"a":1} {"b":2}';
81
+ expect(parseJson(output, {}, true)).toEqual({ b: 2 });
82
+ });
83
+ it("should return the default value if findUuid is true and no JSON is found", () => {
84
+ const output = "no json";
85
+ expect(parseJson(output, { default: true }, true)).toEqual({
86
+ default: true,
87
+ });
88
+ });
89
+ });
90
+ });