131 lines · 4.4 KB · TypeScript
RawDownload
import { execFileSync } from "node:child_process";import { existsSync } from "node:fs";import os from "node:os";import path from "node:path";import { chromium, type BrowserContext } from "playwright-core";import { getGlobalSingleton } from "../instances"; const LAUNCH_TIMEOUT_MS = 15_000; const browserDataDirectory = (): string => path.join(os.tmpdir(), "local-studio-browser-profile"); const resolveOnPath = (binary: string): string | null => {  try {    const resolved = execFileSync("which", [binary], { encoding: "utf8" }).trim();    return resolved && existsSync(resolved) ? resolved : null;  } catch {    return null;  }}; const platformBrowserCandidates = (): string[] => {  if (process.platform === "darwin") {    return [      "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",      "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",      "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",      "/Applications/Chromium.app/Contents/MacOS/Chromium",      "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",      "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",      "/Applications/Arc.app/Contents/MacOS/Arc",      "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",    ];  }  if (process.platform === "win32") {    const roots = [      process.env["PROGRAMFILES"],      process.env["PROGRAMFILES(X86)"],      process.env["LOCALAPPDATA"],    ].filter((value): value is string => Boolean(value));    const suffixes = [      "Google\\Chrome\\Application\\chrome.exe",      "Google\\Chrome Beta\\Application\\chrome.exe",      "Chromium\\Application\\chrome.exe",      "BraveSoftware\\Brave-Browser\\Application\\brave.exe",      "Microsoft\\Edge\\Application\\msedge.exe",      "Vivaldi\\Application\\vivaldi.exe",    ];    return roots.flatMap((root) => suffixes.map((suffix) => path.join(root, suffix)));  }  return [    "chromium-browser",    "chromium",    "google-chrome-stable",    "google-chrome",    "brave-browser",    "microsoft-edge",    "microsoft-edge-stable",    "vivaldi-stable",  ]    .map(resolveOnPath)    .filter((value): value is string => Boolean(value));}; export const findBrowserBinary = (): string | null => {  const override = process.env["LOCAL_STUDIO_CHROME_PATH"]?.trim();  if (override) return existsSync(override) ? override : null;  const bundled = chromium.executablePath();  if (bundled && existsSync(bundled)) return bundled;  return platformBrowserCandidates().find((candidate) => existsSync(candidate)) ?? null;}; class PlaywrightManager {  private context: BrowserContext | null = null;  private launching: Promise<BrowserContext> | null = null;   isAvailable(): boolean {    return findBrowserBinary() !== null;  }   async ensure(): Promise<BrowserContext> {    if (this.context) return this.context;    if (this.launching) return this.launching;    const executablePath = findBrowserBinary();    if (!executablePath) {      throw new Error("Browser unavailable: no Chromium found — set LOCAL_STUDIO_CHROME_PATH");    }    const launch = (userDataDir: string): Promise<BrowserContext> =>      chromium.launchPersistentContext(userDataDir, {        executablePath,        headless: true,        viewport: { width: 1280, height: 800 },        timeout: LAUNCH_TIMEOUT_MS,        args: ["--no-first-run", "--no-default-browser-check", "--disable-dev-shm-usage"],      });    const dataDirectory = browserDataDirectory();    this.launching = launch(dataDirectory)      .catch((error: unknown) => {        if (!String(error).includes("ProcessSingleton")) throw error;        return launch(`${dataDirectory}-${process.pid}`);      })      .then((context) => {        this.context = context;        context.once("close", () => {          if (this.context === context) this.context = null;        });        return context;      })      .finally(() => {        this.launching = null;      });    return this.launching;  }   stop(): void {    const context = this.context;    this.context = null;    if (context) void context.close().catch(() => undefined);  }} export const playwrightManager = getGlobalSingleton(  "playwrightManager",  () => new PlaywrightManager(),); getGlobalSingleton("playwrightExitHook", () => {  if (typeof process !== "undefined") {    process.on("exit", () => playwrightManager.stop());  }  return true;});