rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
210 lines (209 loc) • 9.47 kB
JavaScript
import { setTimeout } from "node:timers/promises";
import { log, RETRIES } from "./constants.mjs";
import { $ } from "../$.mjs";
import { checkUrl, checkServerUp } 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";
}
// Use the provided cwd if available
devProcess = $({
stdio: ["inherit", "pipe", "pipe"], // Pipe stderr again to check both streams
detached: true,
cleanup: false, // Don't auto-kill on exit
cwd: cwd || process.cwd(), // Use provided directory or current directory
env, // Pass the updated environment variables
}) `npm 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 = "";
// Listen for stdout to get the URL
devProcess.stdout?.on("data", (data) => {
const output = data.toString();
console.log(output);
// Try to extract the URL from the server output with a more flexible regex
// Allow for variable amounts of whitespace between "Local:" and the URL
// And handle ANSI color codes by using a more robust pattern
const localMatch = output.match(/Local:.*?(http:\/\/localhost:\d+)/);
if (localMatch && localMatch[1] && !url) {
url = localMatch[1];
log("Found development server URL: %s", url);
}
else if (output.includes("Local:") &&
output.includes("http://localhost:")) {
// Log near-match for debugging
log("Found potential URL pattern but regex didn't match. Content: %s", output);
// Try an alternative, more general pattern that's more resilient to ANSI codes
const altMatch = output.match(/localhost:(\d+)/i);
if (altMatch && altMatch[1] && !url) {
url = `http://localhost:${altMatch[1]}`;
log("Found development server URL with alternative pattern: %s", url);
}
}
});
// Also listen for stderr to check for URL patterns there as well
devProcess.stderr?.on("data", (data) => {
const output = data.toString();
console.error(output); // Output error messages to console
// Check if we already found a URL
if (url)
return;
// Check for localhost URLs in stderr using the same resilient patterns as stdout
const urlMatch = output.match(/localhost:(\d+)/i);
if (urlMatch && urlMatch[1]) {
url = `http://localhost:${urlMatch[1]}`;
log("Found development server URL in stderr: %s", url);
}
});
// 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;
}
// Check if the process is still running
if (devProcess.exitCode !== null) {
log("ERROR: Development server process exited with code %d", devProcess.exitCode);
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");
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, customPath = "/", browserPath, headless = true, bail = false, skipClient = false, realtime = false, skipHmr = false) {
log("Starting dev server test with path: %s", customPath || "/");
console.log("🚀 Testing local development server");
try {
// DRY: check both root and custom path
await checkServerUp(url, customPath, RETRIES, bail);
// Now run the tests with the custom path
const testUrl = url +
(customPath === "/"
? ""
: customPath.startsWith("/")
? customPath
: "/" + customPath);
// Pass the target directory to checkUrl for HMR testing
const targetDir = state.resources.targetDir;
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);
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;
}
}