UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

165 lines (164 loc) 6.95 kB
import { join } from "path"; import { pathExists, copy } from "fs-extra"; import * as fs from "fs/promises"; import tmp from "tmp-promise"; import ignore from "ignore"; import { relative, basename, resolve } from "path"; import { uniqueNamesGenerator, adjectives, animals, } from "unique-names-generator"; import { $ } from "../../lib/$.mjs"; import { log } from "./constants.mjs"; import { debugSync } from "../../scripts/debug-sync.mjs"; import { createSmokeTestComponents } from "./codeUpdates.mjs"; import { createHash } from "crypto"; /** * Sets up the test environment, preparing any resources needed for testing */ export async function setupTestEnvironment(options = {}) { log("Setting up test environment with options: %O", options); // Generate a resource unique key for this test run right at the start const uniqueNameSuffix = uniqueNamesGenerator({ dictionaries: [adjectives, animals], separator: "-", length: 2, style: "lowerCase", }); // Create a short unique hash based on the timestamp const hash = createHash("md5") .update(Date.now().toString()) .digest("hex") .substring(0, 8); // Create a resource unique key even if we're not copying a project const resourceUniqueKey = `${uniqueNameSuffix}-${hash}`; const resources = { tempDirCleanup: undefined, workerName: undefined, originalCwd: process.cwd(), targetDir: undefined, workerCreatedDuringTest: false, stopDev: undefined, resourceUniqueKey, // Set at initialization }; log("Current working directory: %s", resources.originalCwd); try { // If a project dir is specified, copy it to a temp dir with a unique name if (options.projectDir) { log("Project directory specified: %s", options.projectDir); const { tempDir, targetDir, workerName } = await copyProjectToTempDir(options.projectDir, options.sync !== false, // default to true if undefined resourceUniqueKey, // Pass in the existing resourceUniqueKey options.packageManager); // Store cleanup function resources.tempDirCleanup = tempDir.cleanup; resources.workerName = workerName; resources.targetDir = targetDir; log("Target directory: %s", targetDir); // Create the smoke test components in the user's project log("Creating smoke test components"); await createSmokeTestComponents(targetDir, options.skipClient); } else { log("No project directory specified, using current directory"); // When no project dir is specified, we'll use the current directory resources.targetDir = resources.originalCwd; } return resources; } catch (error) { log("Error during test environment setup: %O", error); throw error; } } /** * Copy project to a temporary directory with a unique name */ export async function copyProjectToTempDir(projectDir, sync = true, resourceUniqueKey, packageManager) { log("Creating temporary directory for project"); // Create a temporary directory const tempDir = await tmp.dir({ unsafeCleanup: true }); // Create unique project directory name const originalDirName = basename(projectDir); const workerName = `${originalDirName}-smoke-test-${resourceUniqueKey}`; const targetDir = resolve(tempDir.path, workerName); console.log(`Copying project from ${projectDir} to ${targetDir}`); // Read project's .gitignore if it exists let ig = ignore(); const gitignorePath = join(projectDir, ".gitignore"); if (await pathExists(gitignorePath)) { log("Found .gitignore file at %s", gitignorePath); const gitignoreContent = await fs.readFile(gitignorePath, "utf-8"); ig = ig.add(gitignoreContent); } else { log("No .gitignore found, using default ignore patterns"); // Add default ignores if no .gitignore exists ig = ig.add([ "node_modules", ".git", "dist", "build", ".DS_Store", "coverage", ".cache", ".wrangler", ".env", ].join("\n")); } // Copy the project directory, respecting .gitignore log("Starting copy process with ignored patterns"); await copy(projectDir, targetDir, { filter: (src) => { // Get path relative to project directory const relativePath = relative(projectDir, src); if (!relativePath) return true; // Include the root directory // Check against ignore patterns const result = !ig.ignores(relativePath); return result; }, }); log("Project copy completed successfully"); // For yarn, create .yarnrc.yml to disable PnP and use node_modules if (packageManager === "yarn" || packageManager === "yarn-classic") { const yarnrcPath = join(targetDir, ".yarnrc.yml"); await fs.writeFile(yarnrcPath, "nodeLinker: node-modules\n"); log("Created .yarnrc.yml to disable PnP for yarn"); } // Install dependencies in the target directory await installDependencies(targetDir, packageManager); // Sync SDK to the temp dir if requested if (sync) { console.log(`🔄 Syncing SDK to ${targetDir} after installing dependencies...`); await debugSync({ targetDir }); } return { tempDir, targetDir, workerName }; } /** * Install project dependencies using pnpm */ async function installDependencies(targetDir, packageManager = "pnpm") { console.log(`📦 Installing project dependencies in ${targetDir} using ${packageManager}...`); try { const installCommand = { pnpm: ["pnpm", "install"], npm: ["npm", "install"], yarn: ["yarn", "install", "--immutable"], "yarn-classic": ["yarn", "install", "--immutable"], }[packageManager]; // Run install command in the target directory log(`Running ${installCommand.join(" ")}`); const [command, ...args] = installCommand; const result = await $(command, args, { cwd: targetDir, stdio: "pipe", // Capture output }); console.log("✅ Dependencies installed successfully"); // Log installation details at debug level if (result.stdout) { log(`${packageManager} install output: %s`, result.stdout); } } catch (error) { log("ERROR: Failed to install dependencies: %O", error); console.error(`❌ Failed to install dependencies: ${error instanceof Error ? error.message : String(error)}`); throw new Error(`Failed to install project dependencies. Please ensure the project can be installed with ${packageManager}.`); } }