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

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 (214) hide show
  1. package/dist/lib/constants.mjs +1 -2
  2. package/dist/lib/e2e/browser.d.mts +10 -0
  3. package/dist/lib/e2e/browser.mjs +123 -0
  4. package/dist/lib/e2e/dev.d.mts +8 -0
  5. package/dist/lib/e2e/dev.mjs +242 -0
  6. package/dist/lib/e2e/environment.d.mts +10 -0
  7. package/dist/lib/e2e/environment.mjs +210 -0
  8. package/dist/lib/e2e/index.d.mts +8 -0
  9. package/dist/lib/e2e/index.mjs +8 -0
  10. package/dist/lib/e2e/poll.d.mts +8 -0
  11. package/dist/lib/e2e/poll.mjs +31 -0
  12. package/dist/lib/e2e/release.d.mts +56 -0
  13. package/dist/lib/e2e/release.mjs +559 -0
  14. package/dist/lib/e2e/retry.d.mts +4 -0
  15. package/dist/lib/e2e/retry.mjs +16 -0
  16. package/dist/lib/e2e/setup.d.mts +2 -0
  17. package/dist/lib/e2e/setup.mjs +1 -0
  18. package/dist/lib/e2e/tarball.d.mts +14 -0
  19. package/dist/lib/e2e/tarball.mjs +99 -0
  20. package/dist/lib/e2e/testHarness.d.mts +132 -0
  21. package/dist/lib/e2e/testHarness.mjs +436 -0
  22. package/dist/lib/e2e/types.d.mts +32 -0
  23. package/dist/lib/getShortName.mjs +6 -2
  24. package/dist/lib/getShortName.test.d.mts +1 -0
  25. package/dist/lib/getShortName.test.mjs +25 -0
  26. package/dist/lib/getSrcPaths.js +2 -2
  27. package/dist/lib/hasPkgScript.d.mts +4 -1
  28. package/dist/lib/hasPkgScript.mjs +9 -6
  29. package/dist/lib/hasPkgScript.test.d.mts +1 -0
  30. package/dist/lib/hasPkgScript.test.mjs +33 -0
  31. package/dist/lib/jsonUtils.mjs +3 -0
  32. package/dist/lib/jsonUtils.test.d.mts +1 -0
  33. package/dist/lib/jsonUtils.test.mjs +90 -0
  34. package/dist/lib/normalizeModulePath.d.mts +5 -0
  35. package/dist/lib/normalizeModulePath.mjs +1 -1
  36. package/dist/lib/normalizeModulePath.test.d.mts +1 -0
  37. package/dist/lib/{normalizeModulePath.test.js → normalizeModulePath.test.mjs} +21 -2
  38. package/dist/lib/setupEnvFiles.mjs +2 -2
  39. package/dist/lib/smokeTests/artifacts.mjs +2 -2
  40. package/dist/lib/smokeTests/browser.d.mts +1 -1
  41. package/dist/lib/smokeTests/browser.mjs +8 -100
  42. package/dist/lib/smokeTests/cleanup.mjs +6 -9
  43. package/dist/lib/smokeTests/codeUpdates.mjs +5 -5
  44. package/dist/lib/smokeTests/development.mjs +3 -224
  45. package/dist/lib/smokeTests/environment.d.mts +3 -11
  46. package/dist/lib/smokeTests/environment.mjs +17 -151
  47. package/dist/lib/smokeTests/release.d.mts +2 -49
  48. package/dist/lib/smokeTests/release.mjs +4 -504
  49. package/dist/lib/smokeTests/reporting.mjs +2 -2
  50. package/dist/lib/smokeTests/runSmokeTests.mjs +4 -4
  51. package/dist/lib/smokeTests/utils.mjs +3 -3
  52. package/dist/lib/testUtils/stubEnvVars.mjs +1 -1
  53. package/dist/llms/rules/middleware.d.ts +1 -1
  54. package/dist/llms/rules/middleware.js +4 -4
  55. package/dist/runtime/client/client.d.ts +2 -2
  56. package/dist/runtime/client/client.js +2 -2
  57. package/dist/runtime/client/navigation.test.js +1 -1
  58. package/dist/runtime/client/types.d.ts +1 -1
  59. package/dist/runtime/entries/client.d.ts +2 -2
  60. package/dist/runtime/entries/client.js +2 -2
  61. package/dist/runtime/entries/router.d.ts +1 -1
  62. package/dist/runtime/entries/router.js +1 -1
  63. package/dist/runtime/entries/worker.d.ts +5 -6
  64. package/dist/runtime/entries/worker.js +5 -6
  65. package/dist/runtime/imports/worker.js +1 -1
  66. package/dist/runtime/lib/auth/session.d.ts +2 -2
  67. package/dist/runtime/lib/auth/session.js +5 -5
  68. package/dist/runtime/lib/db/DOWorkerDialect.d.ts +1 -1
  69. package/dist/runtime/lib/db/DOWorkerDialect.js +1 -1
  70. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  71. package/dist/runtime/lib/db/index.d.ts +2 -2
  72. package/dist/runtime/lib/db/index.js +2 -2
  73. package/dist/runtime/lib/db/migrations.d.ts +1 -1
  74. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +3 -3
  75. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +1 -1
  76. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +2 -2
  77. package/dist/runtime/lib/db/typeInference/builders/createView.d.ts +1 -1
  78. package/dist/runtime/lib/db/typeInference/builders/dropTable.d.ts +1 -1
  79. package/dist/runtime/lib/db/typeInference/builders/dropView.d.ts +1 -1
  80. package/dist/runtime/lib/db/typeInference/builders/schema.d.ts +3 -3
  81. package/dist/runtime/lib/db/typeInference/database.d.ts +2 -2
  82. package/dist/runtime/lib/memoizeOnId.test.d.ts +1 -0
  83. package/dist/runtime/lib/memoizeOnId.test.js +49 -0
  84. package/dist/runtime/lib/realtime/client.js +2 -2
  85. package/dist/runtime/lib/realtime/durableObject.js +1 -1
  86. package/dist/runtime/lib/realtime/protocol.test.d.ts +1 -0
  87. package/dist/runtime/lib/realtime/protocol.test.js +107 -0
  88. package/dist/runtime/lib/realtime/shared.test.d.ts +1 -0
  89. package/dist/runtime/lib/realtime/shared.test.js +18 -0
  90. package/dist/runtime/lib/realtime/validateUpgradeRequest.test.d.ts +1 -0
  91. package/dist/runtime/lib/realtime/validateUpgradeRequest.test.js +66 -0
  92. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  93. package/dist/runtime/lib/realtime/worker.js +2 -2
  94. package/dist/runtime/lib/router.d.ts +1 -1
  95. package/dist/runtime/lib/router.js +40 -22
  96. package/dist/runtime/lib/router.test.js +591 -3
  97. package/dist/runtime/lib/rwContext.d.ts +22 -0
  98. package/dist/runtime/lib/rwContext.js +1 -0
  99. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +18 -0
  100. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +143 -0
  101. package/dist/runtime/lib/turnstile/useTurnstile.js +1 -1
  102. package/dist/runtime/lib/turnstile/verifyTurnstileToken.d.ts +2 -1
  103. package/dist/runtime/lib/turnstile/verifyTurnstileToken.js +6 -6
  104. package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.d.ts +1 -0
  105. package/dist/runtime/lib/turnstile/verifyTurnstileToken.test.js +49 -0
  106. package/dist/runtime/register/worker.d.ts +1 -1
  107. package/dist/runtime/register/worker.js +34 -22
  108. package/dist/runtime/render/assembleDocument.d.ts +6 -0
  109. package/dist/runtime/render/assembleDocument.js +22 -0
  110. package/dist/runtime/render/createThenableFromReadableStream.d.ts +1 -0
  111. package/dist/runtime/render/createThenableFromReadableStream.js +9 -0
  112. package/dist/runtime/render/normalizeActionResult.d.ts +1 -0
  113. package/dist/runtime/render/normalizeActionResult.js +43 -0
  114. package/dist/runtime/render/preloads.d.ts +3 -3
  115. package/dist/runtime/render/preloads.js +2 -3
  116. package/dist/runtime/render/{renderRscThenableToHtmlStream.d.ts → renderDocumentHtmlStream.d.ts} +3 -3
  117. package/dist/runtime/render/renderDocumentHtmlStream.js +39 -0
  118. package/dist/runtime/render/renderHtmlStream.d.ts +7 -0
  119. package/dist/runtime/render/renderHtmlStream.js +31 -0
  120. package/dist/runtime/render/renderToRscStream.d.ts +5 -3
  121. package/dist/runtime/render/renderToRscStream.js +12 -41
  122. package/dist/runtime/render/renderToStream.d.ts +3 -2
  123. package/dist/runtime/render/renderToStream.js +17 -10
  124. package/dist/runtime/render/stylesheets.d.ts +2 -2
  125. package/dist/runtime/render/stylesheets.js +2 -3
  126. package/dist/runtime/requestInfo/types.d.ts +0 -2
  127. package/dist/runtime/requestInfo/worker.d.ts +1 -1
  128. package/dist/runtime/requestInfo/worker.js +1 -9
  129. package/dist/runtime/script.js +1 -1
  130. package/dist/runtime/ssrBridge.d.ts +3 -2
  131. package/dist/runtime/ssrBridge.js +3 -2
  132. package/dist/runtime/worker.d.ts +2 -1
  133. package/dist/runtime/worker.js +13 -16
  134. package/dist/scripts/addon.d.mts +1 -0
  135. package/dist/scripts/addon.mjs +75 -0
  136. package/dist/scripts/debug-sync.mjs +106 -137
  137. package/dist/scripts/ensure-deploy-env.mjs +6 -6
  138. package/dist/scripts/migrate-new.mjs +3 -4
  139. package/dist/scripts/smoke-test.mjs +2 -2
  140. package/dist/scripts/worker-run.mjs +7 -9
  141. package/dist/vite/buildApp.d.mts +2 -1
  142. package/dist/vite/buildApp.mjs +10 -6
  143. package/dist/vite/checkIsUsingPrisma.d.mts +4 -0
  144. package/dist/vite/checkIsUsingPrisma.mjs +2 -2
  145. package/dist/vite/checkIsUsingPrisma.test.d.mts +1 -0
  146. package/dist/vite/checkIsUsingPrisma.test.mjs +30 -0
  147. package/dist/vite/configPlugin.mjs +55 -15
  148. package/dist/vite/createDirectiveLookupPlugin.d.mts +9 -0
  149. package/dist/vite/createDirectiveLookupPlugin.mjs +34 -30
  150. package/dist/vite/createDirectiveLookupPlugin.test.d.mts +1 -0
  151. package/dist/vite/createDirectiveLookupPlugin.test.mjs +40 -0
  152. package/dist/vite/createViteAwareResolver.d.mts +1 -2
  153. package/dist/vite/createViteAwareResolver.mjs +1 -1
  154. package/dist/vite/directiveModulesDevPlugin.d.mts +4 -1
  155. package/dist/vite/directiveModulesDevPlugin.mjs +9 -8
  156. package/dist/vite/directiveModulesDevPlugin.test.d.mts +1 -0
  157. package/dist/vite/directiveModulesDevPlugin.test.mjs +59 -0
  158. package/dist/vite/directivesPlugin.d.mts +1 -0
  159. package/dist/vite/directivesPlugin.mjs +4 -4
  160. package/dist/vite/directivesPlugin.test.d.mts +1 -0
  161. package/dist/vite/directivesPlugin.test.mjs +24 -0
  162. package/dist/vite/ensureAliasArray.test.d.mts +1 -0
  163. package/dist/vite/ensureAliasArray.test.mjs +71 -0
  164. package/dist/vite/findSpecifiers.mjs +3 -2
  165. package/dist/vite/findSpecifiers.test.d.mts +1 -0
  166. package/dist/vite/findSpecifiers.test.mjs +202 -0
  167. package/dist/vite/findSsrSpecifiers.mjs +1 -1
  168. package/dist/vite/findSsrSpecifiers.test.d.mts +1 -0
  169. package/dist/vite/findSsrSpecifiers.test.mjs +99 -0
  170. package/dist/vite/getViteEsbuild.mjs +1 -1
  171. package/dist/vite/hasDirective.d.mts +6 -3
  172. package/dist/vite/hasDirective.mjs +43 -27
  173. package/dist/vite/hasDirective.test.d.mts +1 -0
  174. package/dist/vite/hasDirective.test.mjs +107 -0
  175. package/dist/vite/index.d.mts +1 -1
  176. package/dist/vite/invalidateCacheIfPrismaClientChanged.mjs +2 -2
  177. package/dist/vite/isJsFile.test.d.mts +1 -0
  178. package/dist/vite/isJsFile.test.mjs +38 -0
  179. package/dist/vite/{reactConditionsResolverPlugin.d.mts → knownDepsResolverPlugin.d.mts} +3 -3
  180. package/dist/vite/{reactConditionsResolverPlugin.mjs → knownDepsResolverPlugin.mjs} +29 -24
  181. package/dist/vite/linkerPlugin.d.mts +8 -0
  182. package/dist/vite/linkerPlugin.mjs +32 -24
  183. package/dist/vite/linkerPlugin.test.d.mts +1 -0
  184. package/dist/vite/linkerPlugin.test.mjs +41 -0
  185. package/dist/vite/miniflareHMRPlugin.d.mts +5 -0
  186. package/dist/vite/miniflareHMRPlugin.mjs +7 -7
  187. package/dist/vite/miniflareHMRPlugin.test.d.mts +1 -0
  188. package/dist/vite/miniflareHMRPlugin.test.mjs +42 -0
  189. package/dist/vite/prismaPlugin.mjs +1 -1
  190. package/dist/vite/redwoodPlugin.d.mts +9 -0
  191. package/dist/vite/redwoodPlugin.mjs +44 -20
  192. package/dist/vite/redwoodPlugin.test.d.mts +1 -0
  193. package/dist/vite/redwoodPlugin.test.mjs +34 -0
  194. package/dist/vite/resolveForcedPaths.d.mts +4 -0
  195. package/dist/vite/resolveForcedPaths.mjs +9 -0
  196. package/dist/vite/runDirectivesScan.d.mts +22 -1
  197. package/dist/vite/runDirectivesScan.mjs +109 -61
  198. package/dist/vite/runDirectivesScan.test.d.mts +1 -0
  199. package/dist/vite/runDirectivesScan.test.mjs +73 -0
  200. package/dist/vite/ssrBridgePlugin.mjs +10 -3
  201. package/dist/vite/transformClientComponents.mjs +8 -6
  202. package/dist/vite/transformClientComponents.test.mjs +117 -59
  203. package/dist/vite/transformJsxScriptTagsPlugin.mjs +1 -1
  204. package/dist/vite/transformJsxScriptTagsPlugin.test.mjs +2 -2
  205. package/dist/vite/transformServerFunctions.d.mts +1 -1
  206. package/dist/vite/transformServerFunctions.mjs +5 -5
  207. package/dist/vite/transformServerFunctions.test.mjs +3 -3
  208. package/package.json +61 -47
  209. package/dist/runtime/imports/resolveSSRValue.d.ts +0 -1
  210. package/dist/runtime/imports/resolveSSRValue.js +0 -8
  211. package/dist/runtime/render/renderRscThenableToHtmlStream.js +0 -54
  212. package/dist/runtime/render/transformRscToHtmlStream.d.ts +0 -8
  213. package/dist/runtime/render/transformRscToHtmlStream.js +0 -19
  214. /package/dist/lib/{normalizeModulePath.test.d.ts → e2e/types.mjs} +0 -0
@@ -0,0 +1,14 @@
1
+ interface SetupTarballOptions {
2
+ projectDir: string;
3
+ monorepoRoot?: string;
4
+ packageManager?: "pnpm" | "npm" | "yarn" | "yarn-classic";
5
+ }
6
+ interface TarballEnvironment {
7
+ targetDir: string;
8
+ cleanup: () => Promise<void>;
9
+ }
10
+ /**
11
+ * Creates a tarball-based test environment similar to the release script approach
12
+ */
13
+ export declare function setupTarballEnvironment({ projectDir, monorepoRoot, packageManager, }: SetupTarballOptions): Promise<TarballEnvironment>;
14
+ export {};
@@ -0,0 +1,99 @@
1
+ import { createHash } from "crypto";
2
+ import { $ } from "execa";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { adjectives, animals, uniqueNamesGenerator, } from "unique-names-generator";
6
+ import { ROOT_DIR } from "../constants.mjs";
7
+ import { copyProjectToTempDir } from "./environment.mjs";
8
+ const log = (message) => console.log(message);
9
+ /**
10
+ * Copies wrangler cache from monorepo to temp directory for deployment tests
11
+ */
12
+ async function copyWranglerCache(targetDir, sdkRoot) {
13
+ try {
14
+ // Find the monorepo root by starting from the SDK root directory
15
+ // and walking up to find the monorepo root
16
+ let currentDir = path.resolve(sdkRoot);
17
+ let monorepoRoot = null;
18
+ // Walk up the directory tree to find the monorepo root
19
+ while (currentDir !== path.dirname(currentDir)) {
20
+ const nodeModulesPath = path.join(currentDir, "node_modules");
21
+ const packageJsonPath = path.join(currentDir, "package.json");
22
+ if (fs.existsSync(nodeModulesPath) && fs.existsSync(packageJsonPath)) {
23
+ try {
24
+ const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf8"));
25
+ // Check if this looks like our monorepo root
26
+ if (packageJson.name === "rw-sdk-monorepo" ||
27
+ packageJson.private === true) {
28
+ monorepoRoot = currentDir;
29
+ break;
30
+ }
31
+ }
32
+ catch {
33
+ // Continue searching if we can't read the package.json
34
+ }
35
+ }
36
+ currentDir = path.dirname(currentDir);
37
+ }
38
+ if (!monorepoRoot) {
39
+ log(` ⚠️ Could not find monorepo root, skipping wrangler cache copy`);
40
+ return;
41
+ }
42
+ const sourceCachePath = path.join(monorepoRoot, "node_modules/.cache/wrangler");
43
+ const targetCachePath = path.join(targetDir, "node_modules/.cache/wrangler");
44
+ if (fs.existsSync(sourceCachePath)) {
45
+ log(` 🔐 Copying wrangler cache from monorepo to temp directory...`);
46
+ // Ensure the target cache directory exists
47
+ await fs.promises.mkdir(path.dirname(targetCachePath), {
48
+ recursive: true,
49
+ });
50
+ // Copy the entire wrangler cache directory
51
+ await $ `cp -r ${sourceCachePath} ${path.dirname(targetCachePath)}`;
52
+ log(` ✅ Wrangler cache copied successfully`);
53
+ }
54
+ else {
55
+ log(` ⚠️ No wrangler cache found in monorepo, deployment tests may require authentication`);
56
+ }
57
+ }
58
+ catch (error) {
59
+ log(` ⚠️ Failed to copy wrangler cache: ${error.message}`);
60
+ // Don't throw - this is not a fatal error, deployment tests will just need manual auth
61
+ }
62
+ }
63
+ /**
64
+ * Creates a tarball-based test environment similar to the release script approach
65
+ */
66
+ export async function setupTarballEnvironment({ projectDir, monorepoRoot, packageManager = "pnpm", }) {
67
+ log(`🚀 Setting up tarball environment for ${projectDir}`);
68
+ // Generate a resource unique key for this test run
69
+ const uniqueNameSuffix = uniqueNamesGenerator({
70
+ dictionaries: [adjectives, animals],
71
+ separator: "-",
72
+ length: 2,
73
+ style: "lowerCase",
74
+ });
75
+ // Create a short unique hash based on the timestamp
76
+ const hash = createHash("md5")
77
+ .update(Date.now().toString())
78
+ .digest("hex")
79
+ .substring(0, 8);
80
+ const resourceUniqueKey = `${uniqueNameSuffix}-${hash}`;
81
+ try {
82
+ const { tempDir, targetDir } = await copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager, monorepoRoot);
83
+ // Copy wrangler cache to improve deployment performance
84
+ const sdkRoot = ROOT_DIR;
85
+ await copyWranglerCache(targetDir, sdkRoot);
86
+ log(`✅ Tarball environment setup complete`);
87
+ return {
88
+ targetDir,
89
+ cleanup: async () => {
90
+ log(`🧹 Cleaning up tarball environment: ${tempDir.path}`);
91
+ await tempDir.cleanup();
92
+ },
93
+ };
94
+ }
95
+ catch (error) {
96
+ // Cleanup tarball on error
97
+ throw error;
98
+ }
99
+ }
@@ -0,0 +1,132 @@
1
+ import { type Browser, type Page } from "puppeteer-core";
2
+ import { test } from "vitest";
3
+ export type { Browser, Page } from "puppeteer-core";
4
+ interface DevServerInstance {
5
+ url: string;
6
+ stopDev: () => Promise<void>;
7
+ }
8
+ interface DeploymentInstance {
9
+ url: string;
10
+ workerName: string;
11
+ resourceUniqueKey: string;
12
+ projectDir: string;
13
+ cleanup: () => Promise<void>;
14
+ }
15
+ export interface SetupPlaygroundEnvironmentOptions {
16
+ /**
17
+ * The directory of the playground project to set up.
18
+ * Can be an absolute path, or a `import.meta.url` `file://` string.
19
+ * If not provided, it will be inferred from the test file's path.
20
+ */
21
+ sourceProjectDir?: string;
22
+ /**
23
+ * The root directory of the monorepo, if the project is part of one.
24
+ * This is used to correctly set up the test environment for monorepo projects.
25
+ */
26
+ monorepoRoot?: string;
27
+ /**
28
+ * Whether to provision a dev server for the test suite.
29
+ * @default true
30
+ */
31
+ dev?: boolean;
32
+ /**
33
+ * Whether to provision a deployment for the test suite.
34
+ * @default true
35
+ */
36
+ deploy?: boolean;
37
+ }
38
+ /**
39
+ * A Vitest hook that sets up a playground environment for a test file.
40
+ * It creates a temporary directory, copies the playground project into it,
41
+ * and installs dependencies using a tarball of the SDK.
42
+ * This ensures that tests run in a clean, isolated environment.
43
+ */
44
+ export declare function setupPlaygroundEnvironment(options?: string | SetupPlaygroundEnvironmentOptions): void;
45
+ /**
46
+ * Creates a dev server instance using the shared playground environment.
47
+ * Automatically registers cleanup to run after the test.
48
+ */
49
+ export declare function createDevServer(projectDir: string): Promise<DevServerInstance>;
50
+ /**
51
+ * Creates a deployment instance using the shared playground environment.
52
+ * Automatically registers cleanup to run after the test.
53
+ */
54
+ export declare function createDeployment(projectDir: string): Promise<DeploymentInstance>;
55
+ /**
56
+ * Executes a test function with a retry mechanism for specific error codes.
57
+ * @param name - The name of the test, used for logging.
58
+ * @param attemptFn - A function that executes one attempt of the test.
59
+ * It should set up resources, run the test logic, and
60
+ * return a cleanup function. The cleanup function will be
61
+ * called automatically on failure.
62
+ */
63
+ export declare function runTestWithRetries(name: string, attemptFn: () => Promise<void>): Promise<void>;
64
+ declare function createTestRunner(testFn: (typeof test | typeof test.only)["concurrent"], envType: "dev" | "deploy"): (name: string, testLogic: (context: {
65
+ devServer?: DevServerInstance;
66
+ deployment?: DeploymentInstance;
67
+ browser: Browser;
68
+ page: Page;
69
+ url: string;
70
+ }) => Promise<void>) => void;
71
+ /**
72
+ * High-level test wrapper for dev server tests.
73
+ * Automatically skips if RWSDK_SKIP_DEV=1
74
+ */
75
+ export declare function testDev(...args: Parameters<ReturnType<typeof createTestRunner>>): void;
76
+ export declare namespace testDev {
77
+ var skip: (name: string, testFn?: any) => void;
78
+ var only: (name: string, testLogic: (context: {
79
+ devServer?: DevServerInstance;
80
+ deployment?: DeploymentInstance;
81
+ browser: Browser;
82
+ page: Page;
83
+ url: string;
84
+ }) => Promise<void>) => void;
85
+ }
86
+ /**
87
+ * High-level test wrapper for deployment tests.
88
+ * Automatically skips if RWSDK_SKIP_DEPLOY=1
89
+ */
90
+ export declare function testDeploy(...args: Parameters<ReturnType<typeof createTestRunner>>): void;
91
+ export declare namespace testDeploy {
92
+ var skip: (name: string, testFn?: any) => void;
93
+ var only: (name: string, testLogic: (context: {
94
+ devServer?: DevServerInstance;
95
+ deployment?: DeploymentInstance;
96
+ browser: Browser;
97
+ page: Page;
98
+ url: string;
99
+ }) => Promise<void>) => void;
100
+ }
101
+ /**
102
+ * Unified test function that runs the same test against both dev server and deployment.
103
+ * Automatically skips based on environment variables.
104
+ */
105
+ export declare function testDevAndDeploy(name: string, testFn: (context: {
106
+ devServer?: DevServerInstance;
107
+ deployment?: DeploymentInstance;
108
+ browser: Browser;
109
+ page: Page;
110
+ url: string;
111
+ }) => Promise<void>): void;
112
+ export declare namespace testDevAndDeploy {
113
+ var skip: (name: string, testFn?: any) => void;
114
+ var only: (name: string, testFn: (context: {
115
+ devServer?: DevServerInstance;
116
+ deployment?: DeploymentInstance;
117
+ browser: Browser;
118
+ page: Page;
119
+ url: string;
120
+ }) => Promise<void>) => void;
121
+ }
122
+ /**
123
+ * Waits for the page to be fully loaded and hydrated.
124
+ * This should be used before any user interaction is simulated.
125
+ */
126
+ export declare function waitForHydration(page: Page): Promise<void>;
127
+ export declare function trackPageErrors(page: Page): {
128
+ get: () => {
129
+ consoleErrors: string[];
130
+ failedRequests: string[];
131
+ };
132
+ };
@@ -0,0 +1,436 @@
1
+ import fs from "fs-extra";
2
+ import os from "os";
3
+ import path, { basename, dirname, join as pathJoin } from "path";
4
+ import puppeteer from "puppeteer-core";
5
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, test, } from "vitest";
6
+ import { launchBrowser } from "./browser.mjs";
7
+ import { runDevServer } from "./dev.mjs";
8
+ import { poll, pollValue } from "./poll.mjs";
9
+ import { deleteD1Database, deleteWorker, isRelatedToTest, runRelease, } from "./release.mjs";
10
+ 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
+ // Environment variable flags for skipping tests
40
+ const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
41
+ const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
42
+ // Global test environment state
43
+ let globalDevPlaygroundEnv = null;
44
+ let globalDeployPlaygroundEnv = null;
45
+ let globalDevInstancePromise = null;
46
+ let globalDeploymentInstancePromise = null;
47
+ let globalDevInstance = null;
48
+ let globalDeploymentInstance = null;
49
+ let hooksRegistered = false;
50
+ /**
51
+ * Registers global cleanup hooks automatically
52
+ */
53
+ function ensureHooksRegistered() {
54
+ if (hooksRegistered)
55
+ return;
56
+ // Register global afterAll to clean up the playground environment
57
+ afterAll(async () => {
58
+ const cleanupPromises = [];
59
+ if (globalDevInstance) {
60
+ cleanupPromises.push(globalDevInstance.stopDev());
61
+ }
62
+ if (globalDeploymentInstance) {
63
+ cleanupPromises.push(globalDeploymentInstance.cleanup());
64
+ }
65
+ if (globalDevPlaygroundEnv) {
66
+ cleanupPromises.push(globalDevPlaygroundEnv.cleanup());
67
+ }
68
+ if (globalDeployPlaygroundEnv) {
69
+ cleanupPromises.push(globalDeployPlaygroundEnv.cleanup());
70
+ }
71
+ await Promise.all(cleanupPromises);
72
+ globalDevInstance = null;
73
+ globalDeploymentInstance = null;
74
+ globalDevPlaygroundEnv = null;
75
+ globalDeployPlaygroundEnv = null;
76
+ });
77
+ hooksRegistered = true;
78
+ }
79
+ /**
80
+ * Get the project directory for the current test by looking at the call stack
81
+ */
82
+ function getProjectDirectory() {
83
+ // For now, let's hardcode this to '../playground/hello-world' since we only have one project
84
+ // TODO: Make this more dynamic when we have multiple playground projects
85
+ return "../playground/hello-world";
86
+ }
87
+ /**
88
+ * Derive the playground directory from import.meta.url by finding the nearest package.json
89
+ */
90
+ function getPlaygroundDirFromImportMeta(importMetaUrl) {
91
+ const url = new URL(importMetaUrl);
92
+ const testFilePath = url.pathname;
93
+ let currentDir = dirname(testFilePath);
94
+ // Walk up the tree from the test file's directory
95
+ while (currentDir !== "/") {
96
+ // Check if a package.json exists in the current directory
97
+ if (fs.existsSync(pathJoin(currentDir, "package.json"))) {
98
+ return currentDir;
99
+ }
100
+ currentDir = dirname(currentDir);
101
+ }
102
+ throw new Error(`Could not determine playground directory from import.meta.url: ${importMetaUrl}. ` +
103
+ `Failed to find a package.json in any parent directory.`);
104
+ }
105
+ /**
106
+ * A Vitest hook that sets up a playground environment for a test file.
107
+ * It creates a temporary directory, copies the playground project into it,
108
+ * and installs dependencies using a tarball of the SDK.
109
+ * This ensures that tests run in a clean, isolated environment.
110
+ */
111
+ export function setupPlaygroundEnvironment(options = {}) {
112
+ const { sourceProjectDir, monorepoRoot, dev = true, deploy = true, } = typeof options === "string" ? { sourceProjectDir: options } : options;
113
+ ensureHooksRegistered();
114
+ beforeAll(async () => {
115
+ let projectDir;
116
+ if (!sourceProjectDir) {
117
+ projectDir = getProjectDirectory();
118
+ }
119
+ else if (sourceProjectDir.startsWith("file://")) {
120
+ // This is import.meta.url, derive the playground directory
121
+ projectDir = getPlaygroundDirFromImportMeta(sourceProjectDir);
122
+ }
123
+ else {
124
+ // This is an explicit path
125
+ projectDir = sourceProjectDir;
126
+ }
127
+ console.log(`Setting up playground environment from ${projectDir}...`);
128
+ if (dev) {
129
+ const devEnv = await setupTarballEnvironment({
130
+ projectDir,
131
+ monorepoRoot,
132
+ packageManager: process.env.PACKAGE_MANAGER || "pnpm",
133
+ });
134
+ globalDevPlaygroundEnv = {
135
+ projectDir: devEnv.targetDir,
136
+ cleanup: devEnv.cleanup,
137
+ };
138
+ globalDevInstancePromise = createDevServer(devEnv.targetDir).then((instance) => {
139
+ globalDevInstance = instance;
140
+ return instance;
141
+ });
142
+ // Prevent unhandled promise rejections. The error will be handled inside
143
+ // the test's beforeEach hook where this promise is awaited.
144
+ globalDevInstancePromise.catch(() => { });
145
+ }
146
+ else {
147
+ globalDevInstancePromise = Promise.resolve(null);
148
+ }
149
+ if (deploy) {
150
+ const deployEnv = await setupTarballEnvironment({
151
+ projectDir,
152
+ monorepoRoot,
153
+ packageManager: process.env.PACKAGE_MANAGER || "pnpm",
154
+ });
155
+ globalDeployPlaygroundEnv = {
156
+ projectDir: deployEnv.targetDir,
157
+ cleanup: deployEnv.cleanup,
158
+ };
159
+ globalDeploymentInstancePromise = createDeployment(deployEnv.targetDir).then((instance) => {
160
+ globalDeploymentInstance = instance;
161
+ return instance;
162
+ });
163
+ // Prevent unhandled promise rejections
164
+ globalDeploymentInstancePromise.catch(() => { });
165
+ }
166
+ else {
167
+ globalDeploymentInstancePromise = Promise.resolve(null);
168
+ }
169
+ }, SETUP_PLAYGROUND_ENV_TIMEOUT);
170
+ }
171
+ /**
172
+ * Creates a dev server instance using the shared playground environment.
173
+ * Automatically registers cleanup to run after the test.
174
+ */
175
+ export async function createDevServer(projectDir) {
176
+ if (SKIP_DEV_SERVER_TESTS) {
177
+ throw new Error("Dev server tests are skipped via RWSDK_SKIP_DEV=1");
178
+ }
179
+ const packageManager = process.env.PACKAGE_MANAGER || "pnpm";
180
+ const devResult = await pollValue(() => runDevServer(packageManager, projectDir), {
181
+ timeout: DEV_SERVER_TIMEOUT,
182
+ minTries: DEV_SERVER_MIN_TRIES,
183
+ onRetry: (error, tries) => {
184
+ console.log(`Retrying dev server creation (attempt ${tries})... Error: ${error.message}`);
185
+ },
186
+ });
187
+ return {
188
+ url: devResult.url,
189
+ stopDev: devResult.stopDev,
190
+ };
191
+ }
192
+ /**
193
+ * Creates a deployment instance using the shared playground environment.
194
+ * Automatically registers cleanup to run after the test.
195
+ */
196
+ export async function createDeployment(projectDir) {
197
+ if (SKIP_DEPLOYMENT_TESTS) {
198
+ throw new Error("Deployment tests are skipped via RWSDK_SKIP_DEPLOY=1");
199
+ }
200
+ return await pollValue(async () => {
201
+ // Extract the unique key from the project directory name instead of generating a new one
202
+ // The directory name format is: {projectName}-e2e-test-{randomId}
203
+ const dirName = basename(projectDir);
204
+ const match = dirName.match(/-e2e-test-([a-f0-9]+)$/);
205
+ const resourceUniqueKey = match
206
+ ? match[1]
207
+ : Math.random().toString(36).substring(2, 15);
208
+ const deployResult = await runRelease(projectDir, projectDir, resourceUniqueKey);
209
+ // Poll the URL to ensure it's live before proceeding
210
+ await poll(async () => {
211
+ try {
212
+ const response = await fetch(deployResult.url);
213
+ // We consider any response (even 4xx or 5xx) as success,
214
+ // as it means the worker is routable.
215
+ return response.status > 0;
216
+ }
217
+ catch (e) {
218
+ return false;
219
+ }
220
+ }, {
221
+ timeout: DEPLOYMENT_CHECK_TIMEOUT,
222
+ });
223
+ const cleanup = async () => {
224
+ // Run deployment cleanup in background without blocking
225
+ const performCleanup = async () => {
226
+ if (isRelatedToTest(deployResult.workerName, resourceUniqueKey)) {
227
+ await deleteWorker(deployResult.workerName, projectDir, resourceUniqueKey);
228
+ }
229
+ await deleteD1Database(resourceUniqueKey, projectDir, resourceUniqueKey);
230
+ };
231
+ // Start cleanup in background and return immediately
232
+ performCleanup().catch((error) => {
233
+ console.warn(`Warning: Background deployment cleanup failed: ${error.message}`);
234
+ });
235
+ return Promise.resolve();
236
+ };
237
+ return {
238
+ url: deployResult.url,
239
+ workerName: deployResult.workerName,
240
+ resourceUniqueKey,
241
+ projectDir: projectDir,
242
+ cleanup,
243
+ };
244
+ }, {
245
+ timeout: DEPLOYMENT_TIMEOUT,
246
+ minTries: DEPLOYMENT_MIN_TRIES,
247
+ onRetry: (error, tries) => {
248
+ console.log(`Retrying deployment creation (attempt ${tries})... Error: ${error.message}`);
249
+ },
250
+ });
251
+ }
252
+ /**
253
+ * Executes a test function with a retry mechanism for specific error codes.
254
+ * @param name - The name of the test, used for logging.
255
+ * @param attemptFn - A function that executes one attempt of the test.
256
+ * It should set up resources, run the test logic, and
257
+ * return a cleanup function. The cleanup function will be
258
+ * called automatically on failure.
259
+ */
260
+ export async function runTestWithRetries(name, attemptFn) {
261
+ const MAX_RETRIES_PER_CODE = 6;
262
+ const retryCounts = {};
263
+ let attempt = 0;
264
+ while (true) {
265
+ attempt++;
266
+ try {
267
+ await attemptFn();
268
+ if (attempt > 1) {
269
+ console.log(`[runTestWithRetries] Test "${name}" succeeded on attempt ${attempt}.`);
270
+ }
271
+ return; // Success
272
+ }
273
+ catch (e) {
274
+ const errorCode = e?.code;
275
+ if (typeof errorCode === "string" && errorCode) {
276
+ const count = (retryCounts[errorCode] || 0) + 1;
277
+ retryCounts[errorCode] = count;
278
+ if (count <= MAX_RETRIES_PER_CODE) {
279
+ console.log(`[runTestWithRetries] Attempt ${attempt} for "${name}" failed with code ${errorCode}. Retrying (failure ${count}/${MAX_RETRIES_PER_CODE} for this code)...`);
280
+ await new Promise((resolve) => setTimeout(resolve, 1000));
281
+ continue; // Next attempt
282
+ }
283
+ else {
284
+ console.error(`[runTestWithRetries] Test "${name}" failed with code ${errorCode} after ${MAX_RETRIES_PER_CODE} retries for this code.`);
285
+ throw e; // Give up
286
+ }
287
+ }
288
+ else {
289
+ console.error(`[runTestWithRetries] Test "${name}" failed on attempt ${attempt} with a non-retryable error:`, e);
290
+ throw e;
291
+ }
292
+ }
293
+ }
294
+ }
295
+ function createTestRunner(testFn, envType) {
296
+ return (name, testLogic) => {
297
+ if ((envType === "dev" && SKIP_DEV_SERVER_TESTS) ||
298
+ (envType === "deploy" && SKIP_DEPLOYMENT_TESTS)) {
299
+ test.skip(name, () => { });
300
+ return;
301
+ }
302
+ describe.concurrent(name, () => {
303
+ let page;
304
+ let instance;
305
+ let browser;
306
+ beforeAll(async () => {
307
+ const tempDir = path.join(os.tmpdir(), "rwsdk-e2e-tests");
308
+ const wsEndpointFile = path.join(tempDir, "wsEndpoint");
309
+ try {
310
+ const wsEndpoint = await fs.readFile(wsEndpointFile, "utf-8");
311
+ browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
312
+ }
313
+ catch (error) {
314
+ console.warn("Failed to connect to existing browser instance. " +
315
+ "This might happen if you are running a single test file. " +
316
+ "Launching a new browser instance instead.");
317
+ browser = await launchBrowser();
318
+ }
319
+ }, SETUP_WAIT_TIMEOUT);
320
+ afterAll(async () => {
321
+ if (browser) {
322
+ await browser.disconnect();
323
+ }
324
+ });
325
+ beforeEach(async () => {
326
+ const instancePromise = envType === "dev"
327
+ ? globalDevInstancePromise
328
+ : globalDeploymentInstancePromise;
329
+ if (!instancePromise) {
330
+ throw new Error("Test environment promises not initialized. Call setupPlaygroundEnvironment() in your test file.");
331
+ }
332
+ [instance] = await Promise.all([instancePromise]);
333
+ if (!instance) {
334
+ throw new Error(`No ${envType} instance found. Make sure to enable it in setupPlaygroundEnvironment.`);
335
+ }
336
+ page = await browser.newPage();
337
+ page.setDefaultTimeout(PUPPETEER_TIMEOUT);
338
+ }, SETUP_WAIT_TIMEOUT);
339
+ afterEach(async () => {
340
+ if (page) {
341
+ try {
342
+ await page.close();
343
+ }
344
+ catch (error) {
345
+ // Suppress errors during page close, as the browser might already be disconnecting
346
+ // due to the test suite finishing.
347
+ console.warn(`Suppressing error during page.close() in test "${name}":`, error);
348
+ }
349
+ }
350
+ });
351
+ testFn(">", async () => {
352
+ if (!instance || !browser) {
353
+ throw new Error("Test environment not ready.");
354
+ }
355
+ await runTestWithRetries(name, async () => {
356
+ await testLogic({
357
+ [envType === "dev" ? "devServer" : "deployment"]: instance,
358
+ browser: browser,
359
+ page: page,
360
+ url: instance.url,
361
+ });
362
+ });
363
+ });
364
+ });
365
+ };
366
+ }
367
+ /**
368
+ * High-level test wrapper for dev server tests.
369
+ * Automatically skips if RWSDK_SKIP_DEV=1
370
+ */
371
+ export function testDev(...args) {
372
+ return createTestRunner(test.concurrent, "dev")(...args);
373
+ }
374
+ testDev.skip = (name, testFn) => {
375
+ test.skip(name, testFn || (() => { }));
376
+ };
377
+ testDev.only = createTestRunner(test.only, "dev");
378
+ /**
379
+ * High-level test wrapper for deployment tests.
380
+ * Automatically skips if RWSDK_SKIP_DEPLOY=1
381
+ */
382
+ export function testDeploy(...args) {
383
+ return createTestRunner(test.concurrent, "deploy")(...args);
384
+ }
385
+ testDeploy.skip = (name, testFn) => {
386
+ test.skip(name, testFn || (() => { }));
387
+ };
388
+ testDeploy.only = createTestRunner(test.only, "deploy");
389
+ /**
390
+ * Unified test function that runs the same test against both dev server and deployment.
391
+ * Automatically skips based on environment variables.
392
+ */
393
+ export function testDevAndDeploy(name, testFn) {
394
+ testDev(`${name} (dev)`, testFn);
395
+ testDeploy(`${name} (deployment)`, testFn);
396
+ }
397
+ /**
398
+ * Skip version of testDevAndDeploy
399
+ */
400
+ testDevAndDeploy.skip = (name, testFn) => {
401
+ test.skip(name, testFn || (() => { }));
402
+ };
403
+ testDevAndDeploy.only = (name, testFn) => {
404
+ testDev.only(`${name} (dev)`, testFn);
405
+ testDeploy.only(`${name} (deployment)`, testFn);
406
+ };
407
+ /**
408
+ * Waits for the page to be fully loaded and hydrated.
409
+ * This should be used before any user interaction is simulated.
410
+ */
411
+ export async function waitForHydration(page) {
412
+ // 1. Wait for the document to be fully loaded.
413
+ await page.waitForFunction('document.readyState === "complete"');
414
+ // 2. Wait a short, fixed amount of time for client-side hydration to finish.
415
+ // This is a pragmatic approach to ensure React has mounted.
416
+ await new Promise((resolve) => setTimeout(resolve, HYDRATION_TIMEOUT));
417
+ }
418
+ export function trackPageErrors(page) {
419
+ const consoleErrors = [];
420
+ const failedRequests = [];
421
+ page.on("requestfailed", (request) => {
422
+ failedRequests.push(`${request.url()} | ${request.failure()?.errorText}`);
423
+ });
424
+ page.on("console", (msg) => {
425
+ if (msg.type() === "error") {
426
+ consoleErrors.push(msg.text());
427
+ }
428
+ });
429
+ return {
430
+ get: () => ({
431
+ // context(justinvdm, 25 Sep 2025): Filter out irrelevant 404s (e.g. favicon)
432
+ consoleErrors: consoleErrors.filter((e) => !e.includes("404")),
433
+ failedRequests,
434
+ }),
435
+ };
436
+ }