rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
271 lines (270 loc) • 13 kB
JavaScript
import { setTimeout } from "node:timers/promises";
import { log, RETRIES } from "./constants.mjs";
import { $ } from "../$.mjs";
import { checkUrl, checkServerUp, launchBrowser } from "./browser.mjs";
import { fail } from "./utils.mjs";
import { state } from "./state.mjs";
/**
* Run the local development server and return the URL
*/
export async function runDevServer(cwd) {
console.log("🚀 Starting development server...");
// Function to stop the dev server - defined early so we can use it in error handling
let devProcess = null;
let isErrorExpected = false;
const stopDev = async () => {
isErrorExpected = true;
if (!devProcess) {
log("No dev process to stop");
return;
}
console.log("Stopping development server...");
try {
// Send a regular termination signal first
devProcess.kill();
// Wait for the process to terminate with a timeout
const terminationTimeout = 5000; // 5 seconds timeout
const terminationPromise = Promise.race([
// Wait for natural process termination
(async () => {
try {
await devProcess;
log("Dev server process was terminated normally");
return true;
}
catch (e) {
// Expected error when the process is killed
log("Dev server process was terminated");
return true;
}
})(),
// Or timeout
(async () => {
await setTimeout(terminationTimeout);
return false;
})(),
]);
// Check if process terminated within timeout
const terminated = await terminationPromise;
// If not terminated within timeout, force kill
if (!terminated) {
log("Dev server process did not terminate within timeout, force killing with SIGKILL");
console.log("⚠️ Development server not responding after 5 seconds timeout, force killing...");
// Try to kill with SIGKILL if the process still has a pid
if (devProcess.pid) {
try {
// Use process.kill with SIGKILL for a stronger termination
process.kill(devProcess.pid, "SIGKILL");
log("Sent SIGKILL to process %d", devProcess.pid);
}
catch (killError) {
log("Error sending SIGKILL to process: %O", killError);
// Non-fatal, as the process might already be gone
}
}
}
}
catch (e) {
// Process might already have exited
log("Could not kill dev server process: %O", e);
}
console.log("Development server stopped");
};
try {
// Check if we're in CI mode
const inCIMode = process.env.CI === "true" || process.env.CI === "1";
// Start dev server with stdout pipe to capture URL
// Create environment variables object
const env = { ...process.env };
// Disable colors when running in CI mode to make URL parsing more reliable
if (inCIMode) {
log("Running in CI mode, disabling colors for dev server output");
env.NO_COLOR = "1";
env.FORCE_COLOR = "0";
}
// Map package manager names to actual commands
const getPackageManagerCommand = (pm) => {
switch (pm) {
case "yarn-classic":
return "yarn";
default:
return pm;
}
};
const pm = getPackageManagerCommand(state.options.packageManager || "npm");
// Use the provided cwd if available
devProcess = $({
all: true,
detached: false, // Keep attached so we can access streams
cleanup: false, // Don't auto-kill on exit
cwd: cwd || process.cwd(), // Use provided directory or current directory
env, // Pass the updated environment variables
stdio: 'pipe', // Ensure streams are piped
}) `${pm} run dev`;
devProcess.catch((error) => {
if (!isErrorExpected) {
// Use fail() directly here to properly handle errors from the dev process
fail(error, 1, "Development Server Process");
}
});
log("Development server process spawned in directory: %s", cwd || process.cwd());
// Store chunks to parse the URL
let url = "";
let allOutput = "";
// Listen for all output to get the URL
const handleOutput = (data, source) => {
const output = data.toString();
console.log(output);
allOutput += output; // Accumulate all output
log("Received output from %s: %s", source, output.replace(/\n/g, "\\n"));
if (!url) {
// Multiple patterns to catch different package manager outputs
const patterns = [
// Standard Vite output: "Local: http://localhost:5173/"
/Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
// Alternative Vite output: "➜ Local: http://localhost:5173/"
/[➜→]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
// Unicode-safe arrow pattern
/[\u27A1\u2192\u279C]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
// Direct URL pattern: "http://localhost:5173"
/(https?:\/\/localhost:\d+)/i,
// Port-only pattern: "localhost:5173"
/localhost:(\d+)/i,
// Server ready messages
/server.*ready.*localhost:(\d+)/i,
/dev server.*localhost:(\d+)/i,
];
for (const pattern of patterns) {
const match = output.match(pattern);
log("Testing pattern %s against output: %s", pattern.source, output.replace(/\n/g, "\\n"));
if (match) {
log("Pattern matched: %s, groups: %o", pattern.source, match);
if (match[1] && match[1].startsWith("http")) {
url = match[1];
log("Found development server URL with pattern %s: %s", pattern.source, url);
break;
}
else if (match[1] && /^\d+$/.test(match[1])) {
url = `http://localhost:${match[1]}`;
log("Found development server URL with port pattern %s: %s", pattern.source, url);
break;
}
}
}
// Log potential matches for debugging
if (!url &&
(output.includes("localhost") ||
output.includes("Local") ||
output.includes("server"))) {
log("Potential URL pattern found but not matched: %s", output.trim());
}
}
};
// Listen to all possible output streams
log("Setting up stream listeners. Available streams: all=%s, stdout=%s, stderr=%s", !!devProcess.all, !!devProcess.stdout, !!devProcess.stderr);
devProcess.all?.on("data", (data) => handleOutput(data, "all"));
devProcess.stdout?.on("data", (data) => handleOutput(data, "stdout"));
devProcess.stderr?.on("data", (data) => handleOutput(data, "stderr"));
// Also try listening to the raw process output
if (devProcess.child) {
log("Setting up child process stream listeners");
devProcess.child.stdout?.on("data", (data) => handleOutput(data, "child.stdout"));
devProcess.child.stderr?.on("data", (data) => handleOutput(data, "child.stderr"));
}
// Wait for URL with timeout
const waitForUrl = async () => {
const start = Date.now();
const timeout = 60000; // 60 seconds
while (Date.now() - start < timeout) {
if (url) {
return url;
}
// Fallback: check accumulated output if stream listeners aren't working
if (!url && allOutput) {
log("Checking accumulated output for URL patterns: %s", allOutput.replace(/\n/g, "\\n"));
const patterns = [
/Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
/[➜→]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
/[\u27A1\u2192\u279C]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
/(https?:\/\/localhost:\d+)/i,
/localhost:(\d+)/i,
];
for (const pattern of patterns) {
const match = allOutput.match(pattern);
if (match) {
if (match[1] && match[1].startsWith("http")) {
url = match[1];
log("Found URL in accumulated output with pattern %s: %s", pattern.source, url);
return url;
}
else if (match[1] && /^\d+$/.test(match[1])) {
url = `http://localhost:${match[1]}`;
log("Found URL in accumulated output with port pattern %s: %s", pattern.source, url);
return url;
}
}
}
}
// Check if the process is still running
if (devProcess.exitCode !== null) {
log("ERROR: Development server process exited with code %d. Final output: %s", devProcess.exitCode, allOutput);
throw new Error(`Development server process exited with code ${devProcess.exitCode}`);
}
await setTimeout(500); // Check every 500ms
}
log("ERROR: Timed out waiting for dev server URL. Final accumulated output: %s", allOutput);
throw new Error("Timed out waiting for dev server URL");
};
// Wait for the URL
const serverUrl = await waitForUrl();
console.log(`✅ Development server started at ${serverUrl}`);
return { url: serverUrl, stopDev };
}
catch (error) {
// Make sure to try to stop the server on error
log("Error during dev server startup: %O", error);
await stopDev().catch((e) => {
log("Failed to stop dev server during error handling: %O", e);
});
throw error;
}
}
/**
* Runs tests against the development server
*/
export async function runDevTest(url, artifactDir, browserPath, headless = true, bail = false, skipClient = false, realtime = false, skipHmr = false, skipStyleTests = false) {
log("Starting dev server test");
console.log("🚀 Testing local development server");
const browser = await launchBrowser(browserPath, headless);
const page = await browser.newPage();
try {
const testUrl = new URL("/__smoke_test", url).toString();
// DRY: check both root and custom path
await checkServerUp(url, "/", RETRIES, bail);
// Pass the target directory to checkUrl for HMR testing
const targetDir = state.resources.targetDir;
await page.goto(testUrl, { waitUntil: "networkidle0" });
await checkUrl(testUrl, artifactDir, browserPath, headless, bail, skipClient, "Development", // Add environment context parameter
realtime, // Add realtime parameter
targetDir, // Add target directory for HMR testing
skipHmr, // Add skip HMR option
skipStyleTests);
log("Development server test completed successfully");
}
catch (error) {
// Add more context about the specific part that failed
if (error instanceof Error && error.message.includes("Server at")) {
state.failures.push({
step: "Development - Server Availability",
error: error.message,
details: error.stack,
});
}
log("Error during development server testing: %O", error);
// Make sure we throw the error so it's properly handled upstream
throw error;
}
finally {
await browser.close();
}
}