Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/chrome-cdp/lib.ts

Raw
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<string, Frame>): 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<string, unknown>)[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-");