UNPKG

rwsdk

Version:

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

195 lines (194 loc) 9.79 kB
import { join } from "path"; import * as fs from "fs/promises"; import { log } from "./constants.mjs"; import { getSmokeTestFunctionsTemplate } from "./templates/smokeTestFunctions.template"; import { getSmokeTestTemplate } from "./templates/SmokeTest.template"; import { getSmokeTestClientTemplate } from "./templates/SmokeTestClient.template"; import MagicString from "magic-string"; import { parse as parseJsonc } from "jsonc-parser"; /** * Creates the smoke test components in the target project directory */ export async function createSmokeTestComponents(targetDir, skipClient = false) { console.log("Creating smoke test components in project..."); // Create directories if they don't exist const componentsDir = join(targetDir, "src", "app", "components"); log("Creating components directory: %s", componentsDir); await fs.mkdir(componentsDir, { recursive: true }); // Create __smokeTestFunctions.ts const smokeTestFunctionsPath = join(componentsDir, "__smokeTestFunctions.ts"); log("Creating __smokeTestFunctions.ts at: %s", smokeTestFunctionsPath); const smokeTestFunctionsContent = getSmokeTestFunctionsTemplate(); // Create SmokeTest.tsx with conditional client component import const smokeTestPath = join(componentsDir, "__SmokeTest.tsx"); log("Creating __SmokeTest.tsx at: %s", smokeTestPath); const smokeTestContent = getSmokeTestTemplate(skipClient); // Write the server files log("Writing SmokeTestFunctions file"); await fs.writeFile(smokeTestFunctionsPath, smokeTestFunctionsContent); log("Writing SmokeTest component file"); await fs.writeFile(smokeTestPath, smokeTestContent); // Only create client component if not skipping client-side tests if (!skipClient) { // Create SmokeTestClient.tsx const smokeTestClientPath = join(componentsDir, "__SmokeTestClient.tsx"); log("Creating __SmokeTestClient.tsx at: %s", smokeTestClientPath); const smokeTestClientContent = getSmokeTestClientTemplate(); log("Writing SmokeTestClient component file"); await fs.writeFile(smokeTestClientPath, smokeTestClientContent); log("Created client-side smoke test component"); } else { log("Skipping client-side smoke test component creation"); } // Modify worker.tsx and wrangler.jsonc for realtime support await modifyAppForRealtime(targetDir); log("Smoke test components created successfully"); console.log("Created smoke test components:"); console.log(`- ${smokeTestFunctionsPath}`); console.log(`- ${smokeTestPath}`); if (!skipClient) { console.log(`- ${join(componentsDir, "__SmokeTestClient.tsx")}`); } else { console.log("- Client component skipped (--skip-client was specified)"); } } /** * Modifies the worker.tsx and wrangler.jsonc files to add realtime support */ export async function modifyAppForRealtime(targetDir) { log("Modifying worker.tsx and wrangler.jsonc for realtime support"); // Modify worker.tsx const workerPath = join(targetDir, "src", "worker.tsx"); if (await fs .access(workerPath) .then(() => true) .catch(() => false)) { log("Found worker.tsx, checking for realtime code"); const workerContent = await fs.readFile(workerPath, "utf-8"); // Check if the realtime export line already exists const hasRealtimeExport = workerContent.includes('export { RealtimeDurableObject } from "rwsdk/realtime/durableObject"'); const hasRealtimeRoute = workerContent.includes("realtimeRoute("); if (!hasRealtimeExport || !hasRealtimeRoute) { log("Need to modify worker.tsx for realtime support"); const s = new MagicString(workerContent); // Add the export line if it doesn't exist if (!hasRealtimeExport) { const importRegex = /import.*?from.*?;\n/g; let lastImportMatch; let lastImportPosition = 0; // Find the position after the last import statement while ((lastImportMatch = importRegex.exec(workerContent)) !== null) { lastImportPosition = lastImportMatch.index + lastImportMatch[0].length; } if (lastImportPosition > 0) { s.appendRight(lastImportPosition, 'export { RealtimeDurableObject } from "rwsdk/realtime/durableObject";\n'); log("Added RealtimeDurableObject export"); } } // Add the realtimeRoute line if it doesn't exist if (!hasRealtimeRoute) { const defineAppMatch = workerContent.match(/export default defineApp\(\[/); if (defineAppMatch && defineAppMatch.index !== undefined) { const insertPosition = defineAppMatch.index + defineAppMatch[0].length; s.appendRight(insertPosition, "\n realtimeRoute(() => env.REALTIME_DURABLE_OBJECT),"); log("Added realtimeRoute to defineApp"); } } // Import realtimeRoute if it's not already imported if (!workerContent.includes("realtimeRoute")) { // Find the router import to append to it const routerImportMatch = workerContent.match(/import \{(.*?)\} from "rwsdk\/router";/); if (routerImportMatch) { const importList = routerImportMatch[1]; if (!importList.includes("realtimeRoute")) { s.replace(routerImportMatch[0], routerImportMatch[0].replace(/import \{(.*?)\} from "rwsdk\/router";/, (match, imports) => `import { ${imports}, realtimeRoute } from "rwsdk/router";`)); log("Added realtimeRoute to router imports"); } } } // Write the modified file await fs.writeFile(workerPath, s.toString(), "utf-8"); log("Successfully modified worker.tsx"); } else { log("worker.tsx already has realtime support, no changes needed"); } } else { log("worker.tsx not found, skipping modification"); } // Modify wrangler.jsonc const wranglerPath = join(targetDir, "wrangler.jsonc"); if (await fs .access(wranglerPath) .then(() => true) .catch(() => false)) { log("Found wrangler.jsonc, checking for realtime durable objects"); const wranglerContent = await fs.readFile(wranglerPath, "utf-8"); const wranglerConfig = parseJsonc(wranglerContent); let modified = false; // Check if REALTIME_DURABLE_OBJECT already exists in durable_objects bindings const hasDurableObjectBinding = wranglerConfig.durable_objects?.bindings?.some((binding) => binding.name === "REALTIME_DURABLE_OBJECT"); // Check if RealtimeDurableObject is already in migrations const hasMigration = wranglerConfig.migrations?.some((migration) => migration.new_sqlite_classes?.includes("RealtimeDurableObject")); if (!hasDurableObjectBinding || !hasMigration) { log("Need to modify wrangler.jsonc for realtime support"); // Create a deep copy of the config to make modifications const newConfig = JSON.parse(JSON.stringify(wranglerConfig)); // Add durable objects binding if needed if (!hasDurableObjectBinding) { if (!newConfig.durable_objects) { newConfig.durable_objects = {}; } if (!newConfig.durable_objects.bindings) { newConfig.durable_objects.bindings = []; } newConfig.durable_objects.bindings.push({ name: "REALTIME_DURABLE_OBJECT", class_name: "RealtimeDurableObject", }); modified = true; log("Added REALTIME_DURABLE_OBJECT to durable_objects bindings"); } // Add migration if needed if (!hasMigration) { if (!newConfig.migrations) { newConfig.migrations = [ { tag: "v1", new_sqlite_classes: ["RealtimeDurableObject"], }, ]; modified = true; log("Added new migrations with RealtimeDurableObject"); } else if (newConfig.migrations.length > 0) { // Add RealtimeDurableObject to the first migration's sqlite classes const firstMigration = newConfig.migrations[0]; if (!firstMigration.new_sqlite_classes) { firstMigration.new_sqlite_classes = ["RealtimeDurableObject"]; } else if (!firstMigration.new_sqlite_classes.includes("RealtimeDurableObject")) { firstMigration.new_sqlite_classes.push("RealtimeDurableObject"); } modified = true; log("Added RealtimeDurableObject to existing migration"); } } if (modified) { // Write the modified config back to the file await fs.writeFile(wranglerPath, JSON.stringify(newConfig, null, 2), "utf-8"); log("Successfully modified wrangler.jsonc"); } } else { log("wrangler.jsonc already has realtime support, no changes needed"); } } else { log("wrangler.jsonc not found, skipping modification"); } }