repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/chrome-cdp/lib.ts
Rawimport { 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-");