import { readFileSync } from "node:fs"; import { homedir, platform, tmpdir } from "node:os"; import { basename, join, resolve, sep, win32 } from "node:path"; import { findExecutableFile, findExecutable as findExecutableOnPath, } from "./src/pi-ext-executable.ts"; export type Json = | null | boolean | number | string | Json[] | { [key: string]: Json }; export type Frame = { id: number; result?: Json; error?: { code: number; message: string; data?: Json }; transportError?: boolean; [key: string]: Json | undefined; }; export function profileName(name: string): string { if ( !/^[a-z0-9][a-z0-9_-]{0,47}$/.test(name) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(name) ) throw new Error( "profile name must be 1-48 lowercase ASCII letters, digits, _ or -, starting with letter/digit; reserved device names are invalid", ); return name; } export function dataRoot( os = platform(), env = process.env, home = homedir(), ): string { if (os === "win32") return win32.join( env.LOCALAPPDATA || win32.join(home, "AppData", "Local"), "chrome-cdp", "profiles", ); if (os === "darwin") return join( home, "Library", "Application Support", "chrome-cdp", "profiles", ); return join( env.XDG_DATA_HOME || join(home, ".local", "share"), "chrome-cdp", "profiles", ); } export function downloads( os = platform(), env = process.env, home = homedir(), ): string { if (os === "win32") return win32.join(env.USERPROFILE || home, "Downloads"); if (os !== "darwin") { try { const file = readFileSync( join(env.XDG_CONFIG_HOME || join(home, ".config"), "user-dirs.dirs"), "utf8", ); const found = file.match(/^XDG_DOWNLOAD_DIR="([^"\n]+)"/m)?.[1]; if (found) return found.replace(/^\$HOME(?=\/|$)/, home); } catch { /* default */ } } return join(home, "Downloads"); } export function artifactPath(root: string, filename: string): string { if ( basename(filename) !== filename || filename === "." || filename === ".." || /[\\/\0]/.test(filename) ) throw new Error( "artifact filename must be a basename inside configured artifact directory", ); const dest = resolve(root, filename); if (!dest.startsWith(resolve(root) + sep)) throw new Error("artifact path escapes root"); return dest; } export function ref(value: Json, results: Map): Json { if (typeof value === "string") { if (value.startsWith("\\{{")) return value.slice(1); const match = /^\{\{([a-zA-Z][\w-]*)(\.\w+(?:\.\w+)*)\}\}$/.exec(value); if (!match) return value; let node: unknown = results.get(match[1]); if (!node) throw new Error(`unknown result label: ${match[1]}`); for (const key of match[2].slice(1).split(".")) { if (node === null || typeof node !== "object" || !(key in node)) throw new Error(`missing reference: ${value}`); node = (node as Record)[key]; } return node as Json; } if (Array.isArray(value)) return value.map((x) => ref(x, results)); if (value && typeof value === "object") return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, ref(v, results)]), ); return value; } export function executableCandidates(os = platform()): string[] { if (os === "win32") return ["chrome-for-testing.exe", "chrome.exe", "chromium.exe"]; if (os === "darwin") return [ "chrome-for-testing", "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", `${homedir()}/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, "/Applications/Chromium.app/Contents/MacOS/Chromium", ]; return [ "chrome-for-testing", "google-chrome-stable", "google-chrome", "chromium", "chromium-browser", ]; } export function findExecutable( explicit?: string, os = platform(), env = process.env, ): string { const candidates = explicit ? [explicit] : executableCandidates(os); if (!explicit && os === "win32") { for (const root of [ env.PROGRAMFILES, env["PROGRAMFILES(X86)"], env.LOCALAPPDATA, ]) { if (root) candidates.push( win32.join(root, "Google", "Chrome", "Application", "chrome.exe"), ); } } for (const candidate of candidates) { let file: string | undefined; if (candidate.includes("/") || candidate.includes("\\")) file = findExecutableFile(candidate, { platform: os }); else { file = findExecutableOnPath([candidate], { path: env.PATH ?? "", platform: os, }); if (!file && !env.PATH) file = findExecutableFile(candidate, { platform: os }); } if (file) return resolve(file); } throw new Error( `Chrome not found; set chrome-cdp.executablePath (checked: ${candidates.join(", ")})`, ); } export function binaryKey(file: string): string { const name = basename(file).toLowerCase(); return name.includes("testing") ? "chrome-for-testing" : name.includes("chromium") ? "chromium" : "google-chrome-stable"; } export function validateFlags(flags: string[]): void { const forbidden = /^--(?:user-data-dir|remote-debugging-(?:port|address|pipe)|headless|window-size|disable-web-security|no-sandbox|download-default-directory|profile-directory|incognito|guest)(?:=|$)/; for (const flag of flags) if (forbidden.test(flag)) throw new Error(`conflicting Chrome flag: ${flag}`); } export const TMP_PREFIX = join(tmpdir(), "pi-chrome-cdp-");