rwsdk 1.0.0-alpha.6 → 1.0.0-alpha.8

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 (56) hide show
  1. package/dist/lib/e2e/browser.d.mts +10 -0
  2. package/dist/lib/e2e/browser.mjs +107 -0
  3. package/dist/lib/e2e/dev.d.mts +8 -0
  4. package/dist/lib/e2e/dev.mjs +232 -0
  5. package/dist/lib/e2e/environment.d.mts +14 -0
  6. package/dist/lib/e2e/environment.mjs +201 -0
  7. package/dist/lib/e2e/index.d.mts +7 -0
  8. package/dist/lib/e2e/index.mjs +7 -0
  9. package/dist/lib/e2e/release.d.mts +56 -0
  10. package/dist/lib/e2e/release.mjs +537 -0
  11. package/dist/lib/e2e/tarball.d.mts +14 -0
  12. package/dist/lib/e2e/tarball.mjs +189 -0
  13. package/dist/lib/e2e/testHarness.d.mts +98 -0
  14. package/dist/lib/e2e/testHarness.mjs +393 -0
  15. package/dist/lib/e2e/types.d.mts +31 -0
  16. package/dist/lib/e2e/types.mjs +1 -0
  17. package/dist/lib/smokeTests/browser.mjs +3 -94
  18. package/dist/lib/smokeTests/development.mjs +2 -223
  19. package/dist/lib/smokeTests/environment.d.mts +4 -11
  20. package/dist/lib/smokeTests/environment.mjs +10 -158
  21. package/dist/lib/smokeTests/release.d.mts +2 -49
  22. package/dist/lib/smokeTests/release.mjs +3 -503
  23. package/dist/runtime/lib/injectHtmlAtMarker.d.ts +11 -0
  24. package/dist/runtime/lib/injectHtmlAtMarker.js +90 -0
  25. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  26. package/dist/runtime/lib/router.js +8 -2
  27. package/dist/runtime/lib/router.test.js +85 -1
  28. package/dist/runtime/lib/rwContext.d.ts +22 -0
  29. package/dist/runtime/lib/rwContext.js +1 -0
  30. package/dist/runtime/render/assembleDocument.d.ts +6 -0
  31. package/dist/runtime/render/assembleDocument.js +22 -0
  32. package/dist/runtime/render/createThenableFromReadableStream.d.ts +1 -0
  33. package/dist/runtime/render/createThenableFromReadableStream.js +9 -0
  34. package/dist/runtime/render/normalizeActionResult.d.ts +1 -0
  35. package/dist/runtime/render/normalizeActionResult.js +43 -0
  36. package/dist/runtime/render/preloads.d.ts +2 -2
  37. package/dist/runtime/render/preloads.js +2 -3
  38. package/dist/runtime/render/{renderRscThenableToHtmlStream.d.ts → renderDocumentHtmlStream.d.ts} +3 -3
  39. package/dist/runtime/render/renderDocumentHtmlStream.js +39 -0
  40. package/dist/runtime/render/renderHtmlStream.d.ts +7 -0
  41. package/dist/runtime/render/renderHtmlStream.js +31 -0
  42. package/dist/runtime/render/renderToRscStream.d.ts +2 -3
  43. package/dist/runtime/render/renderToRscStream.js +2 -41
  44. package/dist/runtime/render/renderToStream.d.ts +2 -1
  45. package/dist/runtime/render/renderToStream.js +15 -8
  46. package/dist/runtime/render/stylesheets.d.ts +2 -2
  47. package/dist/runtime/render/stylesheets.js +2 -3
  48. package/dist/runtime/ssrBridge.d.ts +2 -1
  49. package/dist/runtime/ssrBridge.js +2 -1
  50. package/dist/runtime/worker.d.ts +1 -0
  51. package/dist/runtime/worker.js +11 -6
  52. package/dist/vite/configPlugin.mjs +2 -2
  53. package/package.json +8 -4
  54. package/dist/runtime/render/renderRscThenableToHtmlStream.js +0 -54
  55. package/dist/runtime/render/transformRscToHtmlStream.d.ts +0 -8
  56. package/dist/runtime/render/transformRscToHtmlStream.js +0 -19
@@ -0,0 +1,537 @@
1
+ import { join, basename, dirname, resolve } from "path";
2
+ import { setTimeout } from "node:timers/promises";
3
+ import debug from "debug";
4
+ import { $ } from "../../lib/$.mjs";
5
+ import { execaCommand } from "execa";
6
+ import { existsSync, readFileSync } from "fs";
7
+ import { pathExists } from "fs-extra";
8
+ import { parse as parseJsonc } from "jsonc-parser";
9
+ import * as fs from "fs/promises";
10
+ import { extractLastJson, parseJson } from "../../lib/jsonUtils.mjs";
11
+ const log = debug("rwsdk:e2e:release");
12
+ /**
13
+ * Find wrangler cache by searching up the directory tree for node_modules/.cache/wrangler
14
+ */
15
+ function findWranglerCache(startDir) {
16
+ let currentDir = resolve(startDir);
17
+ const root = resolve("/");
18
+ while (currentDir !== root) {
19
+ const cacheDir = join(currentDir, "node_modules/.cache/wrangler");
20
+ const accountCachePath = join(cacheDir, "wrangler-account.json");
21
+ if (existsSync(accountCachePath)) {
22
+ log("Found wrangler cache at: %s", accountCachePath);
23
+ return accountCachePath;
24
+ }
25
+ // Move up one directory
26
+ const parentDir = dirname(currentDir);
27
+ if (parentDir === currentDir) {
28
+ // Reached filesystem root
29
+ break;
30
+ }
31
+ currentDir = parentDir;
32
+ }
33
+ log("No wrangler cache found in directory tree starting from: %s", startDir);
34
+ return null;
35
+ }
36
+ /**
37
+ * A mini expect-like utility for handling interactive CLI prompts and verifying output
38
+ * @param command The command to execute
39
+ * @param expectations Array of {expect, send} objects for interactive responses and verification
40
+ * @param options Additional options for command execution including working directory and environment
41
+ * @returns Promise that resolves when the command completes
42
+ */
43
+ export async function $expect(command, expectations, options = {
44
+ reject: true,
45
+ }) {
46
+ return new Promise((resolve, reject) => {
47
+ log("$expect starting with command: %s", command);
48
+ log("Working directory: %s", options.cwd ?? process.cwd());
49
+ log("Expected patterns: %O", expectations.map((e) => e.expect.toString()));
50
+ console.log(`Running command: ${command}`);
51
+ // Spawn the process with pipes for interaction
52
+ const childProcess = execaCommand(command, {
53
+ cwd: options.cwd ?? process.cwd(),
54
+ stdio: "pipe",
55
+ reject: false, // Never reject so we can handle the error ourselves
56
+ env: options.env ?? process.env,
57
+ });
58
+ log("Process spawned with PID: %s", childProcess.pid);
59
+ let stdout = "";
60
+ let stderr = "";
61
+ let buffer = "";
62
+ let lastMatchIndex = 0; // Track the index where the last match occurred
63
+ // Track patterns that have been matched
64
+ const matchHistory = new Map();
65
+ // Track current expectation index to process them in order
66
+ let currentExpectationIndex = 0;
67
+ // Initialize match count for each pattern
68
+ expectations.forEach(({ expect: expectPattern }) => {
69
+ matchHistory.set(expectPattern, 0);
70
+ log("Initialized pattern match count for: %s", expectPattern.toString());
71
+ });
72
+ // Collect stdout
73
+ childProcess.stdout?.on("data", (data) => {
74
+ const chunk = data.toString();
75
+ stdout += chunk;
76
+ buffer += chunk;
77
+ // Print to console
78
+ process.stdout.write(chunk);
79
+ // Only process expectations that haven't been fully matched yet
80
+ // and in the order they were provided
81
+ while (currentExpectationIndex < expectations.length) {
82
+ const { expect: expectPattern, send } = expectations[currentExpectationIndex];
83
+ const pattern = expectPattern instanceof RegExp
84
+ ? expectPattern
85
+ : new RegExp(expectPattern, "m");
86
+ // Only search in the unmatched portion of the buffer
87
+ const searchBuffer = buffer.substring(lastMatchIndex);
88
+ log("Testing pattern: %s against buffer from position %d (%d chars)", pattern.toString(), lastMatchIndex, searchBuffer.length);
89
+ // Enhanced debugging: show actual search buffer content
90
+ log("Search buffer content for debugging: %O", searchBuffer);
91
+ const match = searchBuffer.match(pattern);
92
+ if (match) {
93
+ // Found a match
94
+ const patternStr = expectPattern.toString();
95
+ const matchCount = matchHistory.get(expectPattern) || 0;
96
+ // Update the lastMatchIndex to point after this match
97
+ // Calculate the absolute position in the full buffer
98
+ const matchStartPosition = lastMatchIndex + match.index;
99
+ const matchEndPosition = matchStartPosition + match[0].length;
100
+ lastMatchIndex = matchEndPosition;
101
+ log(`Pattern matched: "${patternStr}" (occurrence #${matchCount + 1}) at position ${matchStartPosition}-${matchEndPosition}`);
102
+ // Only send a response if one is specified
103
+ if (send) {
104
+ log(`Sending response: "${send.replace(/\r/g, "\\r")}" to stdin`);
105
+ childProcess.stdin?.write(send);
106
+ }
107
+ else {
108
+ log(`Pattern "${patternStr}" matched (verification only)`);
109
+ }
110
+ // Increment the match count for this pattern
111
+ matchHistory.set(expectPattern, matchCount + 1);
112
+ log("Updated match count for %s: %d", patternStr, matchCount + 1);
113
+ // Move to the next expectation
114
+ currentExpectationIndex++;
115
+ // If we've processed all expectations but need to wait for stdin response,
116
+ // delay closing stdin until the next data event
117
+ if (currentExpectationIndex >= expectations.length && send) {
118
+ log("All patterns matched, closing stdin after last response");
119
+ childProcess.stdin?.end();
120
+ }
121
+ break; // Exit the while loop to process next chunk
122
+ }
123
+ else {
124
+ log("Pattern not matched. Attempting to diagnose the mismatch:");
125
+ // Try to find the closest substring that might partially match
126
+ const patternString = pattern.toString();
127
+ const patternCore = patternString.substring(1, patternString.lastIndexOf("/") > 0
128
+ ? patternString.lastIndexOf("/")
129
+ : patternString.length);
130
+ // Try partial matches to diagnose the issue
131
+ for (let i = 3; i < patternCore.length; i++) {
132
+ const partialPattern = patternCore.substring(0, i);
133
+ const partialRegex = new RegExp(partialPattern, "m");
134
+ const matches = partialRegex.test(searchBuffer);
135
+ log(" Partial pattern '%s': %s", partialPattern, matches ? "matched" : "not matched");
136
+ // Once we find where the matching starts to fail, stop
137
+ if (!matches)
138
+ break;
139
+ }
140
+ // Break the while loop as this pattern doesn't match yet
141
+ break;
142
+ }
143
+ }
144
+ // If all expectations have been matched, we can close stdin if not already closed
145
+ if (currentExpectationIndex >= expectations.length &&
146
+ childProcess.stdin?.writable) {
147
+ log("All patterns matched, ensuring stdin is closed");
148
+ childProcess.stdin.end();
149
+ }
150
+ });
151
+ // Collect stderr if needed
152
+ if (childProcess.stderr) {
153
+ childProcess.stderr.on("data", (data) => {
154
+ const chunk = data.toString();
155
+ stderr += chunk;
156
+ // Also write stderr to console
157
+ process.stderr.write(chunk);
158
+ });
159
+ }
160
+ // Handle process completion
161
+ childProcess.on("close", (code) => {
162
+ log("Process closed with code: %s", code);
163
+ // Log the number of matches for each pattern
164
+ log("Pattern match summary:");
165
+ for (const [pattern, count] of matchHistory.entries()) {
166
+ log(` - "${pattern.toString()}": ${count} matches`);
167
+ }
168
+ // Check if any required patterns were not matched
169
+ const unmatchedPatterns = Array.from(matchHistory.entries())
170
+ .filter(([_, count]) => count === 0)
171
+ .map(([pattern, _]) => pattern.toString());
172
+ if (unmatchedPatterns.length > 0) {
173
+ log("WARNING: Some expected patterns were not matched: %O", unmatchedPatterns);
174
+ }
175
+ log("$expect completed. Total stdout: %d bytes, stderr: %d bytes", stdout.length, stderr.length);
176
+ resolve({ stdout, stderr, code });
177
+ });
178
+ childProcess.on("error", (err) => {
179
+ log("Process error: %O", err);
180
+ if (options.reject) {
181
+ reject(new Error(`Failed to execute command: ${err.message}`));
182
+ }
183
+ else {
184
+ resolve({ stdout, stderr, code: null });
185
+ }
186
+ });
187
+ });
188
+ }
189
+ /**
190
+ * Ensures Cloudflare account ID is set in environment
191
+ * First checks wrangler cache, then environment variables, and finally guides the user
192
+ */
193
+ export async function ensureCloudflareAccountId(cwd, projectDir) {
194
+ // Skip if already set
195
+ if (process.env.CLOUDFLARE_ACCOUNT_ID) {
196
+ log("CLOUDFLARE_ACCOUNT_ID is already set: %s", process.env.CLOUDFLARE_ACCOUNT_ID);
197
+ console.log(`Using existing CLOUDFLARE_ACCOUNT_ID: ${process.env.CLOUDFLARE_ACCOUNT_ID}`);
198
+ return;
199
+ }
200
+ console.log("CLOUDFLARE_ACCOUNT_ID not set, checking wrangler cache...");
201
+ try {
202
+ // Search up the directory tree for wrangler cache (supports monorepo setups)
203
+ projectDir = projectDir || cwd || process.cwd();
204
+ log("Looking for wrangler cache starting from: %s", projectDir);
205
+ const accountCachePath = findWranglerCache(projectDir);
206
+ if (accountCachePath) {
207
+ try {
208
+ const accountCache = JSON.parse(readFileSync(accountCachePath, "utf8"));
209
+ if (accountCache.account?.id) {
210
+ const accountId = accountCache.account.id;
211
+ process.env.CLOUDFLARE_ACCOUNT_ID = accountId;
212
+ log("Found CLOUDFLARE_ACCOUNT_ID in wrangler cache: %s", accountId);
213
+ console.log(`✅ Setting CLOUDFLARE_ACCOUNT_ID to ${accountId} (from wrangler cache)`);
214
+ console.log(` Cache location: ${accountCachePath}`);
215
+ return;
216
+ }
217
+ }
218
+ catch (parseError) {
219
+ log("Failed to parse wrangler account cache: %O", parseError);
220
+ // Continue to other methods if cache parsing fails
221
+ }
222
+ }
223
+ else {
224
+ console.log(`⚠️ No wrangler account cache found in directory tree starting from: ${projectDir}`);
225
+ }
226
+ // If we get here, we couldn't find the account ID in the cache
227
+ // Give clear guidance to the user
228
+ console.log("⚠️ Could not find Cloudflare account ID");
229
+ console.log("Please either:");
230
+ console.log(" 1. Run 'npx wrangler login' to authenticate with Cloudflare");
231
+ console.log(" 2. Set CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN environment variables");
232
+ // Try wrangler whoami as a final attempt
233
+ console.log("\nAttempting to get account info from wrangler...");
234
+ const result = await $({
235
+ cwd: projectDir,
236
+ stdio: "pipe",
237
+ }) `npx wrangler whoami`;
238
+ // First try regex pattern matching on the text output
239
+ if (result.stdout) {
240
+ const accountIdMatch = result.stdout.match(/Account ID: ([a-f0-9]{32})/);
241
+ if (accountIdMatch && accountIdMatch[1]) {
242
+ const accountId = accountIdMatch[1];
243
+ process.env.CLOUDFLARE_ACCOUNT_ID = accountId;
244
+ log("Extracted CLOUDFLARE_ACCOUNT_ID from whoami text: %s", accountId);
245
+ console.log(`✅ Setting CLOUDFLARE_ACCOUNT_ID to ${accountId} (from wrangler whoami)`);
246
+ return;
247
+ }
248
+ }
249
+ // Fallback: try to extract any JSON that might be in the output
250
+ const accountInfo = extractLastJson(result.stdout);
251
+ if (accountInfo && accountInfo.account && accountInfo.account.id) {
252
+ const accountId = accountInfo.account.id;
253
+ process.env.CLOUDFLARE_ACCOUNT_ID = accountId;
254
+ log("Extracted CLOUDFLARE_ACCOUNT_ID from whoami JSON: %s", accountId);
255
+ console.log(`✅ Setting CLOUDFLARE_ACCOUNT_ID to ${accountId} (from wrangler whoami)`);
256
+ return;
257
+ }
258
+ // If we get here, we've exhausted all options
259
+ throw new Error("Could not find Cloudflare account ID. Please login with 'npx wrangler login' or set CLOUDFLARE_ACCOUNT_ID manually.");
260
+ }
261
+ catch (error) {
262
+ log("Error during account ID detection: %O", error);
263
+ throw error;
264
+ }
265
+ }
266
+ /**
267
+ * Run the release command to deploy to Cloudflare
268
+ */
269
+ export async function runRelease(cwd, projectDir, resourceUniqueKey) {
270
+ log("Running release command");
271
+ console.log("\n🚀 Deploying worker to Cloudflare...");
272
+ try {
273
+ // Make sure we have an account ID
274
+ await ensureCloudflareAccountId(cwd, projectDir);
275
+ // Extract worker name from directory name to ensure consistency
276
+ const dirName = cwd ? basename(cwd) : "unknown-worker";
277
+ // Ensure resource unique key is included in worker name for tracking
278
+ if (resourceUniqueKey && !dirName.includes(resourceUniqueKey)) {
279
+ log(`Worker name doesn't contain our unique key, this is unexpected: ${dirName}, key: ${resourceUniqueKey}`);
280
+ console.log(`⚠️ Worker name doesn't contain our unique key. This might cause cleanup issues.`);
281
+ }
282
+ // Ensure the worker name in wrangler.jsonc matches our unique name
283
+ if (cwd) {
284
+ try {
285
+ const wranglerPath = join(cwd, "wrangler.jsonc");
286
+ if (await pathExists(wranglerPath)) {
287
+ log("Updating wrangler.jsonc to use our unique worker name: %s", dirName);
288
+ // Read the wrangler config - handle both jsonc and json formats
289
+ const wranglerContent = await fs.readFile(wranglerPath, "utf-8");
290
+ // Use parseJsonc which handles comments and is more tolerant
291
+ let wranglerConfig;
292
+ try {
293
+ wranglerConfig = parseJsonc(wranglerContent);
294
+ }
295
+ catch (parseError) {
296
+ // Fallback to standard JSON if jsonc parsing fails
297
+ log("JSONC parsing failed, trying standard JSON: %O", parseError);
298
+ wranglerConfig = JSON.parse(wranglerContent);
299
+ }
300
+ // Update the name
301
+ if (wranglerConfig.name !== dirName) {
302
+ wranglerConfig.name = dirName;
303
+ await fs.writeFile(wranglerPath, JSON.stringify(wranglerConfig, null, 2));
304
+ log("Updated wrangler.jsonc with unique worker name: %s", dirName);
305
+ }
306
+ }
307
+ }
308
+ catch (error) {
309
+ log("Error updating wrangler.jsonc: %O", error);
310
+ console.error(`Warning: Could not update wrangler.jsonc: ${error}`);
311
+ }
312
+ }
313
+ // Run release command with our interactive $expect utility and retry logic
314
+ log("Running release command with interactive prompts and retries");
315
+ const MAX_RETRIES = 3;
316
+ let lastError = null;
317
+ let result = null;
318
+ for (let i = 0; i < MAX_RETRIES; i++) {
319
+ try {
320
+ console.log(`\n🚀 Deploying worker to Cloudflare (Attempt ${i + 1}/${MAX_RETRIES})...`);
321
+ result = await $expect("npm run release", [
322
+ {
323
+ // Make the pattern more flexible to account for potential whitespace differences
324
+ expect: /Do you want to proceed with deployment\?\s*\(y\/N\)/i,
325
+ send: "y\r",
326
+ },
327
+ ], {
328
+ reject: false, // Add reject: false to prevent uncaught promise rejections
329
+ env: {
330
+ NODE_ENV: "production",
331
+ RWSDK_RENAME_WORKER: "1",
332
+ RWSDK_RENAME_DB: "1",
333
+ ...process.env,
334
+ },
335
+ cwd,
336
+ });
337
+ // Check exit code to ensure command succeeded
338
+ if (result.code === 0) {
339
+ log(`Release command succeeded on attempt ${i + 1}`);
340
+ lastError = null; // Clear last error on success
341
+ break; // Exit the loop on success
342
+ }
343
+ else {
344
+ throw new Error(`Release command failed with exit code ${result.code}`);
345
+ }
346
+ }
347
+ catch (error) {
348
+ lastError = error;
349
+ log(`Attempt ${i + 1} failed: ${lastError.message}`);
350
+ if (i < MAX_RETRIES - 1) {
351
+ console.log(` Waiting 5 seconds before retrying...`);
352
+ await setTimeout(5000);
353
+ }
354
+ }
355
+ }
356
+ if (lastError || !result) {
357
+ log("ERROR: Release command failed after all retries.");
358
+ throw lastError || new Error("Release command failed after all retries.");
359
+ }
360
+ // Check exit code to ensure command succeeded
361
+ if (result.code !== 0) {
362
+ // Add more contextual information about the error
363
+ let errorMessage = `Release command failed with exit code ${result.code}`;
364
+ // Add stderr output to the error message if available
365
+ if (result.stderr && result.stderr.trim().length > 0) {
366
+ // Extract the most relevant part of the error message
367
+ const errorLines = result.stderr
368
+ .split("\n")
369
+ .filter((line) => line.includes("ERROR") ||
370
+ line.includes("error:") ||
371
+ line.includes("failed"))
372
+ .slice(0, 3) // Take just the first few error lines
373
+ .join("\n");
374
+ if (errorLines) {
375
+ errorMessage += `\nError details: ${errorLines}`;
376
+ }
377
+ }
378
+ log("ERROR: %s", errorMessage);
379
+ throw new Error(errorMessage);
380
+ }
381
+ const stdout = result.stdout;
382
+ // Extract deployment URL from output
383
+ log("Extracting deployment URL from output");
384
+ const urlMatch = stdout.match(/https:\/\/([a-zA-Z0-9-]+)\.redwoodjs\.workers\.dev/);
385
+ if (!urlMatch || !urlMatch[0]) {
386
+ log("ERROR: Could not extract deployment URL from release output");
387
+ // Log more details about the output for debugging
388
+ log("Release command stdout: %s", stdout);
389
+ if (result.stderr) {
390
+ log("Release command stderr: %s", result.stderr);
391
+ }
392
+ throw new Error("Could not extract deployment URL from release output");
393
+ }
394
+ const url = urlMatch[0];
395
+ const workerName = urlMatch[1];
396
+ log("Successfully deployed to %s (worker: %s)", url, workerName);
397
+ console.log(`✅ Successfully deployed to ${url}`);
398
+ return { url, workerName };
399
+ }
400
+ catch (error) {
401
+ log("ERROR: Failed to run release command: %O", error);
402
+ throw error;
403
+ }
404
+ }
405
+ /**
406
+ * Check if a resource name includes a specific resource unique key
407
+ * This is used to identify resources created during our tests
408
+ */
409
+ export function isRelatedToTest(resourceName, resourceUniqueKey) {
410
+ return resourceName.includes(resourceUniqueKey);
411
+ }
412
+ /**
413
+ * Delete the worker using wrangler
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`);
421
+ return;
422
+ }
423
+ 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",
430
+ },
431
+ ], {
432
+ cwd,
433
+ });
434
+ console.log(`✅ Worker ${name} deleted successfully`);
435
+ }
436
+ 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`);
450
+ }
451
+ catch (retryError) {
452
+ console.error(`Failed to force delete worker ${name}: ${retryError}`);
453
+ }
454
+ }
455
+ }
456
+ /**
457
+ * List D1 databases using wrangler
458
+ */
459
+ export async function listD1Databases(cwd) {
460
+ log("Listing D1 databases");
461
+ try {
462
+ const result = await $({
463
+ cwd,
464
+ stdio: "pipe",
465
+ }) `npx wrangler d1 list --json`;
466
+ // Parse the JSON output to extract the last valid JSON
467
+ const data = parseJson(result.stdout, []);
468
+ if (Array.isArray(data)) {
469
+ log("Found %d D1 databases in parsed array", data.length);
470
+ return data;
471
+ }
472
+ else if (data.databases && Array.isArray(data.databases)) {
473
+ log("Found %d D1 databases in 'databases' property", data.databases.length);
474
+ return data.databases;
475
+ }
476
+ // If nothing worked, return an empty array
477
+ log("Could not parse JSON from output, returning empty array");
478
+ return [];
479
+ }
480
+ catch (error) {
481
+ log("Error listing D1 databases: %O", error);
482
+ console.error(`Failed to list D1 databases: ${error}`);
483
+ return [];
484
+ }
485
+ }
486
+ /**
487
+ * Delete a D1 database using wrangler
488
+ */
489
+ export async function deleteD1Database(name, cwd, resourceUniqueKey) {
490
+ console.log(`Cleaning up: Deleting D1 database ${name}...`);
491
+ try {
492
+ // First check if the database exists
493
+ const databases = await listD1Databases(cwd);
494
+ const exists = databases.some((db) => db.name === name);
495
+ if (!exists) {
496
+ log(`D1 database ${name} not found, skipping deletion`);
497
+ console.log(`⚠️ D1 database ${name} not found, skipping deletion`);
498
+ return;
499
+ }
500
+ // Extra safety check: if we have a resourceUniqueKey, verify this database is related to our test
501
+ if (resourceUniqueKey && !isRelatedToTest(name, resourceUniqueKey)) {
502
+ log(`D1 database ${name} does not contain unique key ${resourceUniqueKey}, not deleting for safety`);
503
+ console.log(`⚠️ D1 database ${name} does not seem to be created by this test, skipping deletion for safety`);
504
+ return;
505
+ }
506
+ // Use our $expect utility to handle any confirmation prompts
507
+ log("Running wrangler d1 delete command with interactive prompts");
508
+ await $expect(`npx wrangler d1 delete ${name}`, [
509
+ {
510
+ expect: "Are you sure you want to delete",
511
+ send: "y\r",
512
+ },
513
+ ], {
514
+ cwd,
515
+ });
516
+ console.log(`✅ D1 database ${name} deleted successfully`);
517
+ }
518
+ catch (error) {
519
+ console.error(`Failed to delete D1 database ${name}: ${error}`);
520
+ // Retry with force flag if the first attempt failed
521
+ try {
522
+ console.log("Retrying with force flag...");
523
+ await $expect(`npx wrangler d1 delete ${name} --yes --force`, [
524
+ {
525
+ expect: "Are you sure you want to delete",
526
+ send: "y\r",
527
+ },
528
+ ], {
529
+ cwd,
530
+ });
531
+ console.log(`✅ D1 database ${name} force deleted successfully`);
532
+ }
533
+ catch (retryError) {
534
+ console.error(`Failed to force delete D1 database ${name}: ${retryError}`);
535
+ }
536
+ }
537
+ }
@@ -0,0 +1,14 @@
1
+ interface SetupTarballOptions {
2
+ projectDir: string;
3
+ packageManager?: "pnpm" | "npm" | "yarn";
4
+ }
5
+ interface TarballEnvironment {
6
+ targetDir: string;
7
+ cleanup: () => Promise<void>;
8
+ tarballPath: string;
9
+ }
10
+ /**
11
+ * Creates a tarball-based test environment similar to the release script approach
12
+ */
13
+ export declare function setupTarballEnvironment({ projectDir, packageManager, }: SetupTarballOptions): Promise<TarballEnvironment>;
14
+ export {};