Path: blob/main/src/tools/impl/chrome-headless-shell.ts
12925 views
/*1* chrome-headless-shell.ts2*3* InstallableTool implementation for Chrome Headless Shell via Chrome for Testing (CfT).4* Provides quarto install/uninstall chrome-headless-shell functionality.5*6* Copyright (C) 2026 Posit Software, PBC7*/89import { join } from "../../deno_ral/path.ts";10import { existsSync, safeMoveSync, safeRemoveSync } from "../../deno_ral/fs.ts";11import { quartoDataDir } from "../../core/appdirs.ts";12import {13InstallableTool,14InstallContext,15PackageInfo,16RemotePackageInfo,17} from "../types.ts";18import {19detectCftPlatform,20downloadAndExtractCft,21fetchLatestCftRelease,22findCftExecutable,23} from "./chrome-for-testing.ts";2425const kVersionFileName = "version";2627// -- Version helpers --2829/** Return the chrome-headless-shell install directory under quartoDataDir. */30export function chromeHeadlessShellInstallDir(): string {31return quartoDataDir("chrome-headless-shell");32}3334/**35* Find the chrome-headless-shell executable in the install directory.36* Returns the absolute path if installed, undefined otherwise.37*/38export function chromeHeadlessShellExecutablePath(): string | undefined {39const dir = chromeHeadlessShellInstallDir();40if (!existsSync(dir)) {41return undefined;42}43return findCftExecutable(dir, "chrome-headless-shell");44}4546/** Record the installed version as a plain text file. */47export function noteInstalledVersion(dir: string, version: string): void {48Deno.writeTextFileSync(join(dir, kVersionFileName), version);49}5051/** Read the installed version. Returns undefined if not present. */52export function readInstalledVersion(dir: string): string | undefined {53const path = join(dir, kVersionFileName);54if (!existsSync(path)) {55return undefined;56}57const text = Deno.readTextFileSync(path).trim();58return text || undefined;59}6061/** Check if chrome-headless-shell is installed in the given directory. */62export function isInstalled(dir: string): boolean {63return existsSync(join(dir, kVersionFileName)) &&64findCftExecutable(dir, "chrome-headless-shell") !== undefined;65}6667// -- InstallableTool methods --6869async function installed(): Promise<boolean> {70return isInstalled(chromeHeadlessShellInstallDir());71}7273function installDirIfInstalled(): Promise<string | undefined> {74const dir = chromeHeadlessShellInstallDir();75if (isInstalled(dir)) {76return Promise.resolve(dir);77}78return Promise.resolve(undefined);79}8081async function installedVersion(): Promise<string | undefined> {82return readInstalledVersion(chromeHeadlessShellInstallDir());83}8485async function latestRelease(): Promise<RemotePackageInfo> {86const release = await fetchLatestCftRelease();87const { platform } = detectCftPlatform();8889const downloads = release.downloads["chrome-headless-shell"];90if (!downloads) {91throw new Error("Chrome for Testing API has no chrome-headless-shell downloads");92}9394const dl = downloads.find((d) => d.platform === platform);95if (!dl) {96throw new Error(97`No chrome-headless-shell download for platform ${platform}`,98);99}100101return {102url: dl.url,103version: release.version,104assets: [{ name: "chrome-headless-shell", url: dl.url }],105};106}107108async function preparePackage(ctx: InstallContext): Promise<PackageInfo> {109const release = await latestRelease();110const workingDir = Deno.makeTempDirSync({ prefix: "quarto-chrome-hs-" });111112try {113await downloadAndExtractCft(114"Chrome Headless Shell",115release.url,116workingDir,117ctx,118"chrome-headless-shell",119);120} catch (e) {121safeRemoveSync(workingDir, { recursive: true });122throw e;123}124125return {126filePath: workingDir,127version: release.version,128};129}130131async function install(pkg: PackageInfo, _ctx: InstallContext): Promise<void> {132const installDir = chromeHeadlessShellInstallDir();133134// Clear existing contents135if (existsSync(installDir)) {136for (const entry of Deno.readDirSync(installDir)) {137safeRemoveSync(join(installDir, entry.name), { recursive: true });138}139}140141// Move extracted contents into install directory142for (const entry of Deno.readDirSync(pkg.filePath)) {143safeMoveSync(join(pkg.filePath, entry.name), join(installDir, entry.name));144}145146noteInstalledVersion(installDir, pkg.version);147}148149async function afterInstall(_ctx: InstallContext): Promise<boolean> {150return false;151}152153async function uninstall(ctx: InstallContext): Promise<void> {154await ctx.withSpinner(155{ message: "Removing Chrome Headless Shell..." },156async () => {157safeRemoveSync(chromeHeadlessShellInstallDir(), { recursive: true });158},159);160}161162// -- Exported tool definition --163164export const chromeHeadlessShellInstallable: InstallableTool = {165name: "Chrome Headless Shell",166prereqs: [],167installed,168installDir: installDirIfInstalled,169installedVersion,170latestRelease,171preparePackage,172install,173afterInstall,174uninstall,175};176177178