rwsdk 1.0.0-alpha.8 โ†’ 1.0.0-alpha.9

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.
@@ -80,7 +80,24 @@ export async function getBrowserPath(testOptions) {
80
80
  // Add better error handling for the install step
81
81
  try {
82
82
  console.log("Downloading Chrome (this may take a few minutes)...");
83
- await install(installOptions);
83
+ const attempts = 10;
84
+ let installError;
85
+ for (let i = 0; i < attempts; i++) {
86
+ try {
87
+ await install(installOptions);
88
+ installError = null; // Reset error on success
89
+ break; // Exit loop on success
90
+ }
91
+ catch (e) {
92
+ installError = e;
93
+ console.log(`Attempt ${i + 1}/${attempts} failed. Retrying in 1 second...`);
94
+ // Wait for 1 second before retrying
95
+ await new Promise((resolve) => setTimeout(resolve, 1000));
96
+ }
97
+ }
98
+ if (installError) {
99
+ throw installError;
100
+ }
84
101
  console.log("โœ… Chrome installation completed successfully");
85
102
  // Now compute the path for the installed browser
86
103
  const path = computeExecutablePath(installOptions);
@@ -7,7 +7,7 @@ export declare function setupTestEnvironment(options?: SmokeTestOptions): Promis
7
7
  /**
8
8
  * Copy project to a temporary directory with a unique name
9
9
  */
10
- export declare function copyProjectToTempDir(projectDir: string, sync: boolean | undefined, resourceUniqueKey: string, packageManager?: PackageManager): Promise<{
10
+ export declare function copyProjectToTempDir(projectDir: string, resourceUniqueKey: string, packageManager?: PackageManager): Promise<{
11
11
  tempDir: tmp.DirectoryResult;
12
12
  targetDir: string;
13
13
  workerName: string;
@@ -1,15 +1,36 @@
1
1
  import { join } from "path";
2
2
  import debug from "debug";
3
3
  import { pathExists, copy } from "fs-extra";
4
- import * as fs from "fs/promises";
4
+ import * as fs from "node:fs";
5
5
  import tmp from "tmp-promise";
6
6
  import ignore from "ignore";
7
7
  import { relative, basename, resolve } from "path";
8
8
  import { uniqueNamesGenerator, adjectives, animals, } from "unique-names-generator";
9
- import { $ } from "../../lib/$.mjs";
10
- import { debugSync } from "../../scripts/debug-sync.mjs";
11
9
  import { createHash } from "crypto";
10
+ import { $ } from "../../lib/$.mjs";
11
+ import { ROOT_DIR } from "../constants.mjs";
12
+ import path from "node:path";
12
13
  const log = debug("rwsdk:e2e:environment");
14
+ const createSdkTarball = async () => {
15
+ const packResult = await $({ cwd: ROOT_DIR, stdio: "pipe" }) `npm pack`;
16
+ const tarballName = packResult.stdout?.trim();
17
+ const tarballPath = path.join(ROOT_DIR, tarballName);
18
+ log(`๐Ÿ“ฆ Created tarball: ${tarballPath}`);
19
+ const cleanupTarball = async () => {
20
+ if (fs.existsSync(tarballPath)) {
21
+ log(`๐Ÿงน Cleaning up tarball: ${tarballPath}`);
22
+ await fs.promises.rm(tarballPath, { force: true });
23
+ }
24
+ };
25
+ return { tarballPath, cleanupTarball };
26
+ };
27
+ const setTarballDependency = async (targetDir, tarballPath) => {
28
+ const filePath = join(targetDir, "package.json");
29
+ const packageJson = await fs.promises.readFile(filePath, "utf-8");
30
+ const packageJsonContent = JSON.parse(packageJson);
31
+ packageJsonContent.dependencies.rwsdk = `file:${tarballPath}`;
32
+ await fs.promises.writeFile(filePath, JSON.stringify(packageJsonContent, null, 2));
33
+ };
13
34
  /**
14
35
  * Sets up the test environment, preparing any resources needed for testing
15
36
  */
@@ -43,8 +64,7 @@ export async function setupTestEnvironment(options = {}) {
43
64
  // If a project dir is specified, copy it to a temp dir with a unique name
44
65
  if (options.projectDir) {
45
66
  log("Project directory specified: %s", options.projectDir);
46
- const { tempDir, targetDir, workerName } = await copyProjectToTempDir(options.projectDir, options.sync !== false, // default to true if undefined
47
- resourceUniqueKey, // Pass in the existing resourceUniqueKey
67
+ const { tempDir, targetDir, workerName } = await copyProjectToTempDir(options.projectDir, resourceUniqueKey, // Pass in the existing resourceUniqueKey
48
68
  options.packageManager);
49
69
  // Store cleanup function
50
70
  resources.tempDirCleanup = tempDir.cleanup;
@@ -67,118 +87,117 @@ export async function setupTestEnvironment(options = {}) {
67
87
  /**
68
88
  * Copy project to a temporary directory with a unique name
69
89
  */
70
- export async function copyProjectToTempDir(projectDir, sync = true, resourceUniqueKey, packageManager) {
71
- log("Creating temporary directory for project");
72
- // Create a temporary directory
73
- const tempDir = await tmp.dir({ unsafeCleanup: true });
74
- // Create unique project directory name
75
- const originalDirName = basename(projectDir);
76
- const workerName = `${originalDirName}-smoke-test-${resourceUniqueKey}`;
77
- const targetDir = resolve(tempDir.path, workerName);
78
- console.log(`Copying project from ${projectDir} to ${targetDir}`);
79
- // Read project's .gitignore if it exists
80
- let ig = ignore();
81
- const gitignorePath = join(projectDir, ".gitignore");
82
- if (await pathExists(gitignorePath)) {
83
- log("Found .gitignore file at %s", gitignorePath);
84
- const gitignoreContent = await fs.readFile(gitignorePath, "utf-8");
85
- ig = ig.add(gitignoreContent);
86
- }
87
- else {
88
- log("No .gitignore found, using default ignore patterns");
89
- // Add default ignores if no .gitignore exists
90
- ig = ig.add([
91
- "node_modules",
92
- ".git",
93
- "dist",
94
- "build",
95
- ".DS_Store",
96
- "coverage",
97
- ".cache",
98
- ".wrangler",
99
- ".env",
100
- ].join("\n"));
101
- }
102
- // Copy the project directory, respecting .gitignore
103
- log("Starting copy process with ignored patterns");
104
- await copy(projectDir, targetDir, {
105
- filter: (src) => {
106
- // Get path relative to project directory
107
- const relativePath = relative(projectDir, src);
108
- if (!relativePath)
109
- return true; // Include the root directory
110
- // Check against ignore patterns
111
- const result = !ig.ignores(relativePath);
112
- return result;
113
- },
114
- });
115
- log("Project copy completed successfully");
116
- // For yarn, create .yarnrc.yml to disable PnP and use node_modules
117
- if (packageManager === "yarn" || packageManager === "yarn-classic") {
118
- const yarnrcPath = join(targetDir, ".yarnrc.yml");
119
- await fs.writeFile(yarnrcPath, "nodeLinker: node-modules\n");
120
- log("Created .yarnrc.yml to disable PnP for yarn");
121
- }
122
- // Replace workspace:* dependencies with a placeholder before installing
123
- await replaceWorkspaceDependencies(targetDir);
124
- // Install dependencies in the target directory
125
- await installDependencies(targetDir, packageManager);
126
- // Sync SDK to the temp dir if requested
127
- if (sync) {
128
- console.log(`๐Ÿ”„ Syncing SDK to ${targetDir} after installing dependencies...`);
129
- await debugSync({ targetDir });
130
- }
131
- return { tempDir, targetDir, workerName };
132
- }
133
- /**
134
- * Replace workspace:* dependencies with a placeholder version to allow installation
135
- */
136
- async function replaceWorkspaceDependencies(targetDir) {
137
- const packageJsonPath = join(targetDir, "package.json");
90
+ export async function copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager) {
91
+ const { tarballPath, cleanupTarball } = await createSdkTarball();
138
92
  try {
139
- const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
140
- const packageJson = JSON.parse(packageJsonContent);
141
- let modified = false;
142
- // Replace workspace:* dependencies with a placeholder version
143
- if (packageJson.dependencies) {
144
- for (const [name, version] of Object.entries(packageJson.dependencies)) {
145
- if (version === "workspace:*") {
146
- packageJson.dependencies[name] = "0.0.80"; // Use latest published version as placeholder
147
- modified = true;
148
- log(`Replaced workspace dependency ${name} with placeholder version`);
149
- }
150
- }
93
+ log("Creating temporary directory for project");
94
+ // Create a temporary directory
95
+ const tempDir = await tmp.dir({ unsafeCleanup: true });
96
+ // Create unique project directory name
97
+ const originalDirName = basename(projectDir);
98
+ const workerName = `${originalDirName}-test-${resourceUniqueKey}`;
99
+ const targetDir = resolve(tempDir.path, workerName);
100
+ console.log(`Copying project from ${projectDir} to ${targetDir}`);
101
+ // Read project's .gitignore if it exists
102
+ let ig = ignore();
103
+ const gitignorePath = join(projectDir, ".gitignore");
104
+ if (await pathExists(gitignorePath)) {
105
+ log("Found .gitignore file at %s", gitignorePath);
106
+ const gitignoreContent = await fs.promises.readFile(gitignorePath, "utf-8");
107
+ ig = ig.add(gitignoreContent);
151
108
  }
152
- if (packageJson.devDependencies) {
153
- for (const [name, version] of Object.entries(packageJson.devDependencies)) {
154
- if (version === "workspace:*") {
155
- packageJson.devDependencies[name] = "0.0.80"; // Use latest published version as placeholder
156
- modified = true;
157
- log(`Replaced workspace devDependency ${name} with placeholder version`);
158
- }
159
- }
109
+ else {
110
+ log("No .gitignore found, using default ignore patterns");
111
+ // Add default ignores if no .gitignore exists
112
+ ig = ig.add([
113
+ "node_modules",
114
+ ".git",
115
+ "dist",
116
+ "build",
117
+ ".DS_Store",
118
+ "coverage",
119
+ ".cache",
120
+ ".wrangler",
121
+ ".env",
122
+ ].join("\n"));
160
123
  }
161
- if (modified) {
162
- await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
163
- log("Updated package.json with placeholder versions for workspace dependencies");
124
+ // Copy the project directory, respecting .gitignore
125
+ log("Starting copy process with ignored patterns");
126
+ await copy(projectDir, targetDir, {
127
+ filter: (src) => {
128
+ // Get path relative to project directory
129
+ const relativePath = relative(projectDir, src);
130
+ if (!relativePath)
131
+ return true; // Include the root directory
132
+ // Check against ignore patterns
133
+ const result = !ig.ignores(relativePath);
134
+ return result;
135
+ },
136
+ });
137
+ log("Project copy completed successfully");
138
+ // Configure temp project to not use frozen lockfile
139
+ log("โš™๏ธ Configuring temp project to not use frozen lockfile...");
140
+ const npmrcPath = join(targetDir, ".npmrc");
141
+ await fs.promises.writeFile(npmrcPath, "frozen-lockfile=false\n");
142
+ // For yarn, create .yarnrc.yml to disable PnP and allow lockfile changes
143
+ if (packageManager === "yarn") {
144
+ const yarnrcPath = join(targetDir, ".yarnrc.yml");
145
+ const yarnConfig = [
146
+ // todo(justinvdm, 23-09-23): Support yarn pnpm
147
+ "nodeLinker: node-modules",
148
+ "enableImmutableInstalls: false",
149
+ ].join("\n");
150
+ await fs.promises.writeFile(yarnrcPath, yarnConfig);
151
+ log("Created .yarnrc.yml to allow lockfile changes for yarn");
164
152
  }
153
+ await setTarballDependency(targetDir, tarballPath);
154
+ // Install dependencies in the target directory
155
+ await installDependencies(targetDir, packageManager);
156
+ // Return the environment details
157
+ return { tempDir, targetDir, workerName };
165
158
  }
166
- catch (error) {
167
- log("Error replacing workspace dependencies: %O", error);
168
- throw new Error(`Failed to replace workspace dependencies: ${error}`);
159
+ finally {
160
+ await cleanupTarball();
169
161
  }
170
162
  }
171
- /**
172
- * Install project dependencies using pnpm
173
- */
174
163
  async function installDependencies(targetDir, packageManager = "pnpm") {
175
164
  console.log(`๐Ÿ“ฆ Installing project dependencies in ${targetDir} using ${packageManager}...`);
176
165
  try {
166
+ // Clean up any pre-existing node_modules and lockfiles
167
+ log("Cleaning up pre-existing node_modules and lockfiles...");
168
+ await Promise.all([
169
+ fs.promises.rm(join(targetDir, "node_modules"), {
170
+ recursive: true,
171
+ force: true,
172
+ }),
173
+ fs.promises.rm(join(targetDir, "pnpm-lock.yaml"), { force: true }),
174
+ fs.promises.rm(join(targetDir, "yarn.lock"), { force: true }),
175
+ fs.promises.rm(join(targetDir, "package-lock.json"), { force: true }),
176
+ ]);
177
+ log("Cleanup complete.");
178
+ if (packageManager.startsWith("yarn")) {
179
+ log(`Enabling corepack...`);
180
+ await $("corepack", ["enable"], { cwd: targetDir, stdio: "pipe" });
181
+ if (packageManager === "yarn") {
182
+ log(`Preparing yarn@stable with corepack...`);
183
+ await $("corepack", ["prepare", "yarn@stable", "--activate"], {
184
+ cwd: targetDir,
185
+ stdio: "pipe",
186
+ });
187
+ }
188
+ else if (packageManager === "yarn-classic") {
189
+ log(`Preparing yarn@1.22.19 with corepack...`);
190
+ await $("corepack", ["prepare", "yarn@1.22.19", "--activate"], {
191
+ cwd: targetDir,
192
+ stdio: "pipe",
193
+ });
194
+ }
195
+ }
177
196
  const installCommand = {
178
197
  pnpm: ["pnpm", "install"],
179
198
  npm: ["npm", "install"],
180
- yarn: ["yarn", "install", "--immutable"],
181
- "yarn-classic": ["yarn", "install", "--immutable"],
199
+ yarn: ["yarn", "install"],
200
+ "yarn-classic": ["yarn"],
182
201
  }[packageManager];
183
202
  // Run install command in the target directory
184
203
  log(`Running ${installCommand.join(" ")}`);
@@ -186,6 +205,9 @@ async function installDependencies(targetDir, packageManager = "pnpm") {
186
205
  const result = await $(command, args, {
187
206
  cwd: targetDir,
188
207
  stdio: "pipe", // Capture output
208
+ env: {
209
+ YARN_ENABLE_HARDENED_MODE: "0",
210
+ },
189
211
  });
190
212
  console.log("โœ… Dependencies installed successfully");
191
213
  // Log installation details at debug level
@@ -44,7 +44,7 @@ export declare function isRelatedToTest(resourceName: string, resourceUniqueKey:
44
44
  /**
45
45
  * Delete the worker using wrangler
46
46
  */
47
- export declare function deleteWorker(name: string, cwd: string, resourceUniqueKey: string): Promise<void>;
47
+ export declare function deleteWorker(workerName: string, projectDir: string, resourceUniqueKey: string): Promise<void>;
48
48
  /**
49
49
  * List D1 databases using wrangler
50
50
  */
@@ -412,44 +412,53 @@ export function isRelatedToTest(resourceName, resourceUniqueKey) {
412
412
  /**
413
413
  * Delete the worker using wrangler
414
414
  */
415
- export async function deleteWorker(name, cwd, resourceUniqueKey) {
416
- console.log(`Cleaning up: Deleting worker ${name}...`);
417
- // Safety check: if we have a resourceUniqueKey, verify this worker name contains it
418
- if (resourceUniqueKey && !isRelatedToTest(name, resourceUniqueKey)) {
419
- log(`Worker ${name} does not contain unique key ${resourceUniqueKey}, not deleting for safety`);
420
- console.log(`โš ๏ธ Worker ${name} does not seem to be created by this test, skipping deletion for safety`);
415
+ export async function deleteWorker(workerName, projectDir, resourceUniqueKey) {
416
+ console.log(`Cleaning up: Deleting worker ${workerName}...`);
417
+ // We are extra careful here to not delete workers that are not related to
418
+ // the current test run. We check if the worker name contains the resource
419
+ // unique key, and if the project directory also contains the resource unique
420
+ // key.
421
+ if (!isRelatedToTest(workerName, resourceUniqueKey)) {
422
+ console.warn(`โš ๏ธ Worker name "${workerName}" does not contain resource unique key "${resourceUniqueKey}". Skipping delete.`);
421
423
  return;
422
424
  }
425
+ if (!isRelatedToTest(projectDir, resourceUniqueKey)) {
426
+ console.warn(`โš ๏ธ Project dir "${projectDir}" does not contain resource unique key "${resourceUniqueKey}". Skipping delete.`);
427
+ return;
428
+ }
429
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
430
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
431
+ if (!accountId || !apiToken) {
432
+ console.error("โŒ CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN env vars must be set to delete worker");
433
+ return;
434
+ }
435
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${workerName}`;
436
+ console.log(`Running API call: DELETE ${url}`);
423
437
  try {
424
- // Use our $expect utility to handle any confirmation prompts
425
- log("Running wrangler delete command with interactive prompts");
426
- await $expect(`npx wrangler delete ${name}`, [
427
- {
428
- expect: "Are you sure you want to delete",
429
- send: "y\r",
438
+ const response = await fetch(url, {
439
+ method: "DELETE",
440
+ headers: {
441
+ Authorization: `Bearer ${apiToken}`,
442
+ "Content-Type": "application/json",
430
443
  },
431
- ], {
432
- cwd,
433
444
  });
434
- console.log(`โœ… Worker ${name} deleted successfully`);
445
+ if (!response.ok) {
446
+ const errorText = await response.text();
447
+ throw new Error(`Cloudflare API request failed with status ${response.status}: ${errorText}`);
448
+ }
449
+ const responseData = (await response.json());
450
+ if (!responseData.success) {
451
+ throw new Error(`Cloudflare API returned an error: ${JSON.stringify(responseData.errors)}`);
452
+ }
453
+ console.log(`โœ… Successfully deleted worker "${workerName}"`);
435
454
  }
436
455
  catch (error) {
437
- console.error(`Failed to delete worker ${name}: ${error}`);
438
- // Retry with force flag if the first attempt failed
439
- try {
440
- console.log("Retrying with force flag...");
441
- await $expect(`npx wrangler delete ${name} --yes --force`, [
442
- {
443
- expect: "Are you sure you want to delete",
444
- send: "y\r",
445
- },
446
- ], {
447
- cwd,
448
- });
449
- console.log(`โœ… Worker ${name} force deleted successfully`);
456
+ console.error(`โŒ Failed to delete worker "${workerName}"`);
457
+ if (error instanceof Error) {
458
+ console.error(`Error message: ${error.message}`);
450
459
  }
451
- catch (retryError) {
452
- console.error(`Failed to force delete worker ${name}: ${retryError}`);
460
+ else {
461
+ console.error("An unknown error occurred:", error);
453
462
  }
454
463
  }
455
464
  }
@@ -487,6 +496,19 @@ export async function listD1Databases(cwd) {
487
496
  * Delete a D1 database using wrangler
488
497
  */
489
498
  export async function deleteD1Database(name, cwd, resourceUniqueKey) {
499
+ // Check wrangler.jsonc to see if a database is configured
500
+ const wranglerConfigPath = resolve(cwd, "wrangler.jsonc");
501
+ try {
502
+ const configContent = await fs.readFile(wranglerConfigPath, "utf-8");
503
+ const config = parseJsonc(configContent);
504
+ if (!config.d1_databases || config.d1_databases.length === 0) {
505
+ log("No D1 databases configured in wrangler.jsonc, skipping deletion.");
506
+ return;
507
+ }
508
+ }
509
+ catch (error) {
510
+ log(`Could not read or parse wrangler.jsonc at ${wranglerConfigPath}, proceeding with deletion attempt anyway.`, error);
511
+ }
490
512
  console.log(`Cleaning up: Deleting D1 database ${name}...`);
491
513
  try {
492
514
  // First check if the database exists
@@ -5,7 +5,6 @@ interface SetupTarballOptions {
5
5
  interface TarballEnvironment {
6
6
  targetDir: string;
7
7
  cleanup: () => Promise<void>;
8
- tarballPath: string;
9
8
  }
10
9
  /**
11
10
  * Creates a tarball-based test environment similar to the release script approach
@@ -1,8 +1,10 @@
1
1
  import { $ } from "execa";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import os from "node:os";
5
- import crypto from "node:crypto";
4
+ import { copyProjectToTempDir } from "./environment.mjs";
5
+ import { uniqueNamesGenerator, adjectives, animals, } from "unique-names-generator";
6
+ import { createHash } from "crypto";
7
+ import { ROOT_DIR } from "../constants.mjs";
6
8
  const log = (message) => console.log(message);
7
9
  /**
8
10
  * Copies wrangler cache from monorepo to temp directory for deployment tests
@@ -62,128 +64,36 @@ async function copyWranglerCache(targetDir, sdkRoot) {
62
64
  * Creates a tarball-based test environment similar to the release script approach
63
65
  */
64
66
  export async function setupTarballEnvironment({ projectDir, packageManager = "pnpm", }) {
65
- // Generate unique temp directory name
66
- const randomId = crypto.randomBytes(4).toString("hex");
67
- const projectName = path.basename(projectDir);
68
- const tempDirName = `${projectName}-e2e-test-${randomId}`;
69
- const targetDir = path.join(os.tmpdir(), tempDirName);
70
- log(`๐Ÿ“ Creating temp directory: ${targetDir}`);
71
- // Create temp directory
72
- await fs.promises.mkdir(targetDir, { recursive: true });
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}`;
73
81
  try {
74
- // Copy project to temp directory
75
- log(`๐Ÿ“‹ Copying project from ${projectDir} to ${targetDir}`);
76
- await $ `cp -a ${projectDir}/. ${targetDir}/`;
77
- // Configure temp project to not use frozen lockfile
78
- log(`โš™๏ธ Configuring temp project to not use frozen lockfile...`);
79
- const npmrcPath = path.join(targetDir, ".npmrc");
80
- await fs.promises.writeFile(npmrcPath, "frozen-lockfile=false\n");
81
- // Replace workspace:* dependencies with placeholder versions
82
- log(`๐Ÿ”„ Replacing workspace dependencies...`);
83
- const packageJsonPath = path.join(targetDir, "package.json");
84
- const packageJsonContent = await fs.promises.readFile(packageJsonPath, "utf8");
85
- const packageJson = JSON.parse(packageJsonContent);
86
- // Replace workspace:* dependencies with a placeholder version
87
- const replaceWorkspaceDeps = (deps) => {
88
- if (!deps)
89
- return;
90
- for (const [name, version] of Object.entries(deps)) {
91
- if (version === "workspace:*") {
92
- deps[name] = "0.0.80"; // Use a placeholder version that exists on npm
93
- }
94
- }
95
- };
96
- replaceWorkspaceDeps(packageJson.dependencies);
97
- replaceWorkspaceDeps(packageJson.devDependencies);
98
- replaceWorkspaceDeps(packageJson.peerDependencies);
99
- await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
100
- // Find SDK root directory (relative to current working directory)
101
- const currentDir = process.cwd();
102
- const sdkRoot = currentDir.includes("/playground")
103
- ? path.join(currentDir, "../sdk")
104
- : currentDir;
105
- // Pack the SDK
106
- log(`๐Ÿ“ฆ Packing SDK from ${sdkRoot}...`);
107
- const packResult = await $({ cwd: sdkRoot }) `npm pack`;
108
- const tarballName = packResult.stdout.trim();
109
- const tarballPath = path.join(sdkRoot, tarballName);
110
- // Install the tarball in the temp project
111
- log(`๐Ÿ’ฟ Installing tarball ${tarballName} in ${targetDir}...`);
112
- if (packageManager === "pnpm") {
113
- await $({ cwd: targetDir }) `pnpm add ${tarballPath}`;
114
- }
115
- else if (packageManager === "npm") {
116
- await $({ cwd: targetDir }) `npm install ${tarballPath}`;
117
- }
118
- else if (packageManager === "yarn") {
119
- await $({ cwd: targetDir }) `yarn add ${tarballPath}`;
120
- }
121
- else {
122
- throw new Error(`Unsupported package manager: ${packageManager}`);
123
- }
124
- // Verify installation
125
- const sdkPackageJson = JSON.parse(await fs.promises.readFile(path.join(sdkRoot, "package.json"), "utf8"));
126
- const packageName = sdkPackageJson.name;
127
- const installedDistPath = path.join(targetDir, "node_modules", packageName, "dist");
128
- log(`๐Ÿ” Verifying installed package contents...`);
129
- if (!fs.existsSync(installedDistPath)) {
130
- throw new Error(`dist/ directory not found in installed package at ${installedDistPath}`);
131
- }
132
- // Compare checksums like the release script does
133
- const originalDistPath = path.join(sdkRoot, "dist");
134
- if (fs.existsSync(originalDistPath)) {
135
- const getDistChecksum = async (distPath) => {
136
- const findResult = await $({ cwd: distPath }) `find . -type f`;
137
- const sortedFiles = findResult.stdout
138
- .trim()
139
- .split("\n")
140
- .sort()
141
- .join("\n");
142
- return crypto.createHash("md5").update(sortedFiles).digest("hex");
143
- };
144
- const originalChecksum = await getDistChecksum(originalDistPath);
145
- const installedChecksum = await getDistChecksum(installedDistPath);
146
- log(` - Original dist checksum: ${originalChecksum}`);
147
- log(` - Installed dist checksum: ${installedChecksum}`);
148
- if (originalChecksum !== installedChecksum) {
149
- throw new Error("File list in installed dist/ does not match original dist/");
150
- }
151
- log(` โœ… Installed package contents match the local build`);
152
- }
153
- // Copy wrangler cache from monorepo to temp directory for deployment tests
82
+ const { tempDir, targetDir } = await copyProjectToTempDir(projectDir, resourceUniqueKey, packageManager);
83
+ // Copy wrangler cache to improve deployment performance
84
+ const sdkRoot = ROOT_DIR;
154
85
  await copyWranglerCache(targetDir, sdkRoot);
155
- // Cleanup function
156
- const cleanup = async () => {
157
- try {
158
- // Remove tarball
159
- if (fs.existsSync(tarballPath)) {
160
- await fs.promises.unlink(tarballPath);
161
- }
162
- // Remove temp directory
163
- if (fs.existsSync(targetDir)) {
164
- await fs.promises.rm(targetDir, { recursive: true, force: true });
165
- }
166
- }
167
- catch (error) {
168
- console.warn(`Warning: Failed to cleanup temp files: ${error.message}`);
169
- }
170
- };
86
+ log(`โœ… Tarball environment setup complete`);
171
87
  return {
172
88
  targetDir,
173
- cleanup,
174
- tarballPath,
89
+ cleanup: async () => {
90
+ log(`๐Ÿงน Cleaning up tarball environment: ${tempDir.path}`);
91
+ await tempDir.cleanup();
92
+ },
175
93
  };
176
94
  }
177
95
  catch (error) {
178
- // Cleanup on error
179
- try {
180
- if (fs.existsSync(targetDir)) {
181
- await fs.promises.rm(targetDir, { recursive: true, force: true });
182
- }
183
- }
184
- catch (cleanupError) {
185
- console.warn(`Warning: Failed to cleanup after error: ${cleanupError.message}`);
186
- }
96
+ // Cleanup tarball on error
187
97
  throw error;
188
98
  }
189
99
  }
@@ -11,6 +11,7 @@ interface DeploymentInstance {
11
11
  url: string;
12
12
  workerName: string;
13
13
  resourceUniqueKey: string;
14
+ projectDir: string;
14
15
  }
15
16
  /**
16
17
  * Sets up a playground environment for the entire test suite.
@@ -44,6 +45,17 @@ export declare function cleanupDeployment(deployment: DeploymentInstance): Promi
44
45
  * Automatically registers cleanup to run after the test.
45
46
  */
46
47
  export declare function createBrowser(): Promise<Browser>;
48
+ /**
49
+ * Executes a test function with a retry mechanism for specific error codes.
50
+ * @param name - The name of the test, used for logging.
51
+ * @param attemptFn - A function that executes one attempt of the test.
52
+ * It should set up resources, run the test logic, and
53
+ * return a cleanup function. The cleanup function will be
54
+ * called automatically on failure.
55
+ */
56
+ export declare function runTestWithRetries(name: string, attemptFn: () => Promise<{
57
+ cleanup: () => Promise<void>;
58
+ }>): Promise<void>;
47
59
  /**
48
60
  * High-level test wrapper for dev server tests.
49
61
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -56,6 +68,12 @@ export declare function testDev(name: string, testFn: (context: {
56
68
  }) => Promise<void>): void;
57
69
  export declare namespace testDev {
58
70
  var skip: (name: string, testFn?: any) => void;
71
+ var only: (name: string, testFn: (context: {
72
+ devServer: DevServerInstance;
73
+ browser: Browser;
74
+ page: Page;
75
+ url: string;
76
+ }) => Promise<void>) => void;
59
77
  }
60
78
  /**
61
79
  * High-level test wrapper for deployment tests.
@@ -69,6 +87,12 @@ export declare function testDeploy(name: string, testFn: (context: {
69
87
  }) => Promise<void>): void;
70
88
  export declare namespace testDeploy {
71
89
  var skip: (name: string, testFn?: any) => void;
90
+ var only: (name: string, testFn: (context: {
91
+ deployment: DeploymentInstance;
92
+ browser: Browser;
93
+ page: Page;
94
+ url: string;
95
+ }) => Promise<void>) => void;
72
96
  }
73
97
  /**
74
98
  * Unified test function that runs the same test against both dev server and deployment.
@@ -94,5 +118,6 @@ export declare namespace testDevAndDeploy {
94
118
  /**
95
119
  * Utility function for polling/retrying assertions
96
120
  */
97
- export declare function poll(fn: () => Promise<boolean>, timeout?: number, interval?: number): Promise<void>;
121
+ export declare function poll(fn: () => Promise<boolean>, timeout?: number, // 2 minutes
122
+ interval?: number): Promise<void>;
98
123
  export {};