91 lines · 2.9 KB · TypeScript
RawDownload
/** * Pure URL helpers shared between the preview server, desktop main process, * and web renderer. Centralising these guarantees the four call sites agree * on what counts as "loopback" and how to normalise a free-form URL string. */ const TAB_ID_PREFIX = "tab_";let nextPreviewTabSequence = 0; /** * Generate a fresh preview tab id. Lives in shared (not contracts) because * the contracts package is schema-only — runtime helpers belong here. */export function newPreviewTabId(): string {  nextPreviewTabSequence += 1;  return `${TAB_ID_PREFIX}${nextPreviewTabSequence.toString(36)}`;} const LOOPBACK_HOSTS: ReadonlySet<string> = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]); /** Internal — used by `lsof` parsing where the host string is wire-formatted. */export const LSOF_LOCAL_HOST_TOKENS: ReadonlySet<string> = new Set([  ...LOOPBACK_HOSTS,  "*",  "[::]",  "[::1]",]); const LOOPBACK_PREFIX_PATTERN = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\])(?::|\/|$)/i; export function isLoopbackHost(host: string): boolean {  if (LOOPBACK_HOSTS.has(host)) return true;  if (host === "[::1]") return true;  return false;} /** True when a raw URL string looks like a loopback dev URL we can preview. */export function isPreviewableUrl(rawUrl: string): boolean {  try {    const parsed = new URL(rawUrl);    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;    return isLoopbackHost(parsed.hostname);  } catch {    return false;  }} export class PreviewUrlNormalizationError extends Error {  readonly rawUrl: string;  readonly detail: string;  constructor(rawUrl: string, detail: string) {    super(`Invalid preview URL: ${rawUrl} (${detail})`);    this.name = "PreviewUrlNormalizationError";    this.rawUrl = rawUrl;    this.detail = detail;  }} /** * Normalise a free-form URL string into a fully-qualified `http(s)://` URL. * * - Bare loopback hosts (`localhost`, `localhost:5173`) become `http://...`. * - Bare public hosts (`example.com`) become `https://...`. * - Already-qualified URLs are validated and returned as `URL.href`. * * Throws `PreviewUrlNormalizationError` for empty, unparseable, or * unsupported-protocol inputs. */export function normalizePreviewUrl(rawUrl: string): string {  const trimmed = rawUrl.trim();  if (trimmed.length === 0) {    throw new PreviewUrlNormalizationError(rawUrl, "empty");  }  const useHttp = LOOPBACK_PREFIX_PATTERN.test(trimmed);  const candidate = trimmed.includes("://")    ? trimmed    : `${useHttp ? "http" : "https"}://${trimmed}`;  let parsed: URL;  try {    parsed = new URL(candidate);  } catch (cause) {    throw new PreviewUrlNormalizationError(      rawUrl,      cause instanceof Error ? cause.message : "unparseable",    );  }  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {    throw new PreviewUrlNormalizationError(rawUrl, `unsupported protocol ${parsed.protocol}`);  }  return parsed.href;}