122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
/**
|
|
* Detects and validates dev server URLs from process output logs.
|
|
* Handles various formats including full URLs and host:port patterns.
|
|
*/
|
|
|
|
import { getBackendConfig } from "@/lib/config";
|
|
|
|
export interface PreviewUrlInfo {
|
|
url: string;
|
|
port?: number;
|
|
scheme: "http" | "https";
|
|
}
|
|
|
|
/**
|
|
* Detects a preview URL from a line of process output.
|
|
*
|
|
* Rules:
|
|
* - Rejects localhost URLs without a port (e.g., http://localhost)
|
|
* - Accepts full URLs with ports (e.g., http://localhost:3000)
|
|
* - Accepts host:port patterns (e.g., localhost:3000)
|
|
* - Supports localhost, 127.0.0.1, and 0.0.0.0
|
|
*
|
|
* @param line - A line of process output to scan
|
|
* @returns PreviewUrlInfo if a valid URL is found, null otherwise
|
|
*/
|
|
const LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0"]);
|
|
|
|
/** Try to parse a full URL match into a PreviewUrlInfo, returning null if invalid. */
|
|
function tryParseFullUrl(match: string): PreviewUrlInfo | null {
|
|
try {
|
|
const parsed = new URL(match);
|
|
if (LOCALHOST_HOSTS.has(parsed.hostname) && !parsed.port) return null;
|
|
return {
|
|
url: parsed.toString(),
|
|
port: parsed.port ? Number(parsed.port) : undefined,
|
|
scheme: parsed.protocol === "https:" ? "https" : "http",
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Try to extract a URL from host:port pattern matches. */
|
|
function tryParseHostPort(line: string, matches: RegExpMatchArray): PreviewUrlInfo | null {
|
|
const match = matches[matches.length - 1];
|
|
const portMatch = match.match(/:(\d{2,5})$/);
|
|
const port = portMatch ? Number(portMatch[1]) : undefined;
|
|
const scheme = /https/i.test(line) ? "https" : "http";
|
|
return { url: `${scheme}://${match}`, port, scheme };
|
|
}
|
|
|
|
export function detectPreviewUrl(line: string): PreviewUrlInfo | null {
|
|
const fullUrlPattern = /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?[^\s]*/gi;
|
|
const hostPortPattern = /(?:localhost|127\.0\.0\.1|0\.0\.0\.0):(\d{2,5})/gi;
|
|
|
|
const fullUrlMatches = line.match(fullUrlPattern);
|
|
if (fullUrlMatches) {
|
|
for (const match of fullUrlMatches) {
|
|
const result = tryParseFullUrl(match);
|
|
if (result) return result;
|
|
}
|
|
}
|
|
|
|
const hostPortMatches = line.match(hostPortPattern);
|
|
if (hostPortMatches && hostPortMatches.length > 0) {
|
|
return tryParseHostPort(line, hostPortMatches);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Rewrites a detected localhost URL to route through the port proxy for remote executors.
|
|
* For local executors, returns the URL unchanged.
|
|
*
|
|
* @param detectedUrl - The detected localhost URL (e.g., http://localhost:3000/path)
|
|
* @param sessionId - The session ID to include in the proxy path
|
|
* @param isRemote - Whether the session runs on a remote executor
|
|
* @returns The proxy URL (e.g., /port-proxy/{sessionId}/3000/path) or the original URL
|
|
*/
|
|
export function rewritePreviewUrlForProxy(
|
|
detectedUrl: string,
|
|
sessionId: string,
|
|
isRemote: boolean,
|
|
): string | null {
|
|
if (!isRemote) return detectedUrl;
|
|
|
|
try {
|
|
const parsed = new URL(detectedUrl);
|
|
if (!LOCALHOST_HOSTS.has(parsed.hostname)) return null;
|
|
if (!parsed.port) return null;
|
|
const path = parsed.pathname + parsed.search + parsed.hash;
|
|
const backendUrl = getBackendConfig().apiBaseUrl;
|
|
return `${backendUrl}/port-proxy/${sessionId}/${parsed.port}${path}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scans process output for dev server URLs.
|
|
* Returns the last valid URL found.
|
|
*
|
|
* @param output - The full process output to scan
|
|
* @returns The URL string if found, null otherwise
|
|
*/
|
|
export function detectPreviewUrlFromOutput(output: string): string | null {
|
|
if (!output) return null;
|
|
|
|
const lines = output.split("\n");
|
|
let lastValidUrl: string | null = null;
|
|
|
|
for (const line of lines) {
|
|
const urlInfo = detectPreviewUrl(line);
|
|
if (urlInfo) {
|
|
lastValidUrl = urlInfo.url;
|
|
}
|
|
}
|
|
|
|
return lastValidUrl;
|
|
}
|