core/factories/browser.factory.ts
core/factories/browser.factory.ts is a file in Bane's Lab Build Scripts. 88 lines of code and 15 definitions.
import type { BrowserHandle, BrowserWindow } from "#types/browser.types";
import { type ChildProcess, spawn } from "node:child_process";
import {
EXIT_WAIT_MS,
PROFILE_PREFIX,
PROFILE_REMOVE_RETRIES,
PROFILE_RETRY_DELAY_MS,
} from "#configuration/constants/browser.constants";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { browserStartFailed } from "#configuration/strings/browser.strings";
import { join } from "node:path";
import { once } from "node:events";
import process from "node:process";
import { terminateTree } from "#core/adapters/server.adapter";
import { wait } from "#core/timers/base.timer";
export const freshProfile = function freshProfile(root: string): string {
mkdirSync(root, { recursive: true });
return mkdtempSync(join(root, PROFILE_PREFIX));
};
const WINDOWS = "win32";
const softwareFlags: readonly string[] = ["--headless=new", "--use-gl=angle", "--use-angle=swiftshader"];
const hardwareFlags: readonly string[] = ["--enable-features=Vulkan"];
export const browserArguments = function browserArguments(window: BrowserWindow): string[] {
return [
...(window.software ? softwareFlags : hardwareFlags),
"--remote-debugging-port=0",
`--user-data-dir=${window.profileDir}`,
`--window-size=${String(window.width)},${String(window.height)}`,
"--hide-scrollbars",
"--mute-audio",
"--no-first-run",
"--no-default-browser-check",
"--disable-extensions",
"--ignore-certificate-errors",
"--allow-insecure-localhost",
"about:blank",
];
};
const isRunning = function isRunning(child: ChildProcess): boolean {
return child.pid !== undefined && child.exitCode === null && child.signalCode === null;
};
const stopped = async function stopped(child: ChildProcess): Promise<void> {
if (!isRunning(child) || child.pid === undefined) {
return;
}
const exit = once(child, "exit");
terminateTree(child.pid);
await Promise.race([exit, wait(EXIT_WAIT_MS)]);
};
const asError = function asError(reason: unknown): Error {
return reason instanceof Error ? reason : new Error(String(reason));
};
export const settleBrowser = async function settleBrowser<T>(
handle: BrowserHandle,
work: (handle: BrowserHandle) => Promise<T>,
): Promise<T> {
const [outcome] = await Promise.allSettled([work(handle)]);
const [closing] = await Promise.allSettled([handle.close()]);
if (outcome.status === "fulfilled") {
if (closing.status === "rejected") {
throw asError(closing.reason);
}
return outcome.value;
}
const primary = asError(outcome.reason);
if (closing.status === "rejected") {
throw new AggregateError([primary, asError(closing.reason)], primary.message);
}
throw primary;
};
export const launchBrowser = function launchBrowser(binary: string, window: BrowserWindow): BrowserHandle {
const child = spawn(binary, browserArguments(window), { detached: process.platform !== WINDOWS, stdio: "ignore" });
child.on("error", (cause: Error) => {
process.stderr.write(browserStartFailed(binary, cause.message));
});
return {
close: async (): Promise<void> => {
await stopped(child);
rmSync(window.profileDir, {
force: true,
maxRetries: PROFILE_REMOVE_RETRIES,
recursive: true,
retryDelay: PROFILE_RETRY_DELAY_MS,
});
},
exitCode: (): number | null => child.exitCode,
};
};