import { existsSync } from "node:fs"; import { access, constants } from "node:fs/promises"; import { homedir } from "node:os"; import { resolve as resolvePath, join } from "node:path"; export const DEFAULT_PORT = 9222; const identity = (value) => value; export function parseArgs(argv, options = {}) { const { boolean = [], string = [], number = [], alias = {}, defaults = {}, } = options; const boolSet = new Set(boolean.map(normalizeKey)); const stringSet = new Set(string.map(normalizeKey)); const numberSet = new Set(number.map(normalizeKey)); const aliases = Object.fromEntries( Object.entries(alias).map(([key, target]) => [normalizeKey(key), normalizeKey(target)]), ); const result = { _: [] }; const assignValue = (key, value = true) => { const normalized = normalizeKey(key); const target = aliases[normalized] ?? normalized; result[target] = value; }; for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (token === "--") { result._.push(...argv.slice(index + 1)); break; } if (!token.startsWith("-")) { result._.push(token); continue; } if (token.startsWith("--no-")) { const key = token.slice(5); assignValue(key, false); continue; } const [rawKey, inlineValue] = token.split("=", 2); const key = rawKey.replace(/^--?/, ""); if (boolSet.has(key)) { assignValue(key, inlineValue === undefined ? true : coerceBoolean(inlineValue)); continue; } const expectsString = stringSet.has(key); const expectsNumber = numberSet.has(key); if (inlineValue !== undefined) { assignValue(key, expectsNumber ? Number(inlineValue) : inlineValue); continue; } const nextToken = argv[index + 1]; const hasNext = nextToken !== undefined && !nextToken.startsWith("--"); if ((expectsString || expectsNumber) && hasNext) { index += 1; assignValue(key, expectsNumber ? Number(nextToken) : nextToken); } else if (expectsString || expectsNumber) { assignValue(key, expectsNumber ? NaN : ""); } else { assignValue(key, true); } } for (const [key, value] of Object.entries(defaults)) { if (!(key in result)) { result[key] = typeof value === "function" ? value() : value; } } return result; } export function createLogger({ quiet = false, json = false } = {}) { const format = (args) => args .map((part) => { if (part instanceof Error) { return part.stack ?? part.message; } return typeof part === "object" ? JSON.stringify(part) : String(part); }) .join(" "); const write = (stream, args) => { stream.write(format(args)); stream.write("\n"); }; return { info: (...args) => { if (!quiet && !json) write(process.stderr, args); }, warn: (...args) => { if (!quiet && !json) write(process.stderr, args); }, error: (...args) => { write(process.stderr, args); }, }; } export function printJSON(value) { process.stdout.write(`${JSON.stringify(value)}\n`); } export function fail(message, { code = 1, json = false } = {}) { if (json) { printJSON({ ok: false, error: message }); } else { process.stderr.write(`${message}\n`); } process.exit(code); } export function resolveBrowserConnection(flags = {}) { if (flags.ws || process.env.BROWSER_WS_URL) { const endpoint = flags.ws ?? process.env.BROWSER_WS_URL; return { browserWSEndpoint: endpoint, defaultViewport: null, }; } const host = flags.host ?? process.env.BROWSER_HOST ?? "localhost"; const port = normalizeNumber(flags.port ?? process.env.BROWSER_PORT ?? DEFAULT_PORT, DEFAULT_PORT); return { browserURL: `http://${host}:${port}`, defaultViewport: null, }; } export async function waitFor(fn, { timeout = 10000, interval = 250 } = {}) { const start = Date.now(); let lastError; while (Date.now() - start < timeout) { try { const result = await fn(); if (result) return result; } catch (error) { lastError = error; } await delay(interval); } if (lastError) throw lastError; return null; } export function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } export async function getActivePage(browser, { index = -1 } = {}) { const pages = await browser.pages(); if (pages.length === 0) return null; return index === -1 ? pages.at(-1) : pages[index] ?? null; } export function expandPath(value) { if (!value) return value; if (value.startsWith("~")) { return resolvePath(join(homedir(), value.slice(1))); } return resolvePath(value); } export async function pathExists(path) { try { await access(path, constants.F_OK); return true; } catch { return false; } } export function normalizeNumber(value, fallback) { const number = typeof value === "string" ? Number(value) : value; return Number.isFinite(number) ? number : fallback; } export function findExistingPath(paths) { for (const candidate of paths) { if (!candidate) continue; if (existsSync(candidate)) return candidate; } return null; } function normalizeKey(key) { return key.replace(/^--?/, ""); } function coerceBoolean(value) { if (typeof value === "boolean") return value; const normalized = String(value).toLowerCase(); if (["false", "0", "no", "off"].includes(normalized)) return false; return true; }