Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/tools/impl/chrome-headless-shell.ts
12925 views
1
/*
2
* chrome-headless-shell.ts
3
*
4
* InstallableTool implementation for Chrome Headless Shell via Chrome for Testing (CfT).
5
* Provides quarto install/uninstall chrome-headless-shell functionality.
6
*
7
* Copyright (C) 2026 Posit Software, PBC
8
*/
9
10
import { join } from "../../deno_ral/path.ts";
11
import { existsSync, safeMoveSync, safeRemoveSync } from "../../deno_ral/fs.ts";
12
import { quartoDataDir } from "../../core/appdirs.ts";
13
import {
14
InstallableTool,
15
InstallContext,
16
PackageInfo,
17
RemotePackageInfo,
18
} from "../types.ts";
19
import {
20
detectCftPlatform,
21
downloadAndExtractCft,
22
fetchLatestCftRelease,
23
findCftExecutable,
24
} from "./chrome-for-testing.ts";
25
26
const kVersionFileName = "version";
27
28
// -- Version helpers --
29
30
/** Return the chrome-headless-shell install directory under quartoDataDir. */
31
export function chromeHeadlessShellInstallDir(): string {
32
return quartoDataDir("chrome-headless-shell");
33
}
34
35
/**
36
* Find the chrome-headless-shell executable in the install directory.
37
* Returns the absolute path if installed, undefined otherwise.
38
*/
39
export function chromeHeadlessShellExecutablePath(): string | undefined {
40
const dir = chromeHeadlessShellInstallDir();
41
if (!existsSync(dir)) {
42
return undefined;
43
}
44
return findCftExecutable(dir, "chrome-headless-shell");
45
}
46
47
/** Record the installed version as a plain text file. */
48
export function noteInstalledVersion(dir: string, version: string): void {
49
Deno.writeTextFileSync(join(dir, kVersionFileName), version);
50
}
51
52
/** Read the installed version. Returns undefined if not present. */
53
export function readInstalledVersion(dir: string): string | undefined {
54
const path = join(dir, kVersionFileName);
55
if (!existsSync(path)) {
56
return undefined;
57
}
58
const text = Deno.readTextFileSync(path).trim();
59
return text || undefined;
60
}
61
62
/** Check if chrome-headless-shell is installed in the given directory. */
63
export function isInstalled(dir: string): boolean {
64
return existsSync(join(dir, kVersionFileName)) &&
65
findCftExecutable(dir, "chrome-headless-shell") !== undefined;
66
}
67
68
// -- InstallableTool methods --
69
70
async function installed(): Promise<boolean> {
71
return isInstalled(chromeHeadlessShellInstallDir());
72
}
73
74
function installDirIfInstalled(): Promise<string | undefined> {
75
const dir = chromeHeadlessShellInstallDir();
76
if (isInstalled(dir)) {
77
return Promise.resolve(dir);
78
}
79
return Promise.resolve(undefined);
80
}
81
82
async function installedVersion(): Promise<string | undefined> {
83
return readInstalledVersion(chromeHeadlessShellInstallDir());
84
}
85
86
async function latestRelease(): Promise<RemotePackageInfo> {
87
const release = await fetchLatestCftRelease();
88
const { platform } = detectCftPlatform();
89
90
const downloads = release.downloads["chrome-headless-shell"];
91
if (!downloads) {
92
throw new Error("Chrome for Testing API has no chrome-headless-shell downloads");
93
}
94
95
const dl = downloads.find((d) => d.platform === platform);
96
if (!dl) {
97
throw new Error(
98
`No chrome-headless-shell download for platform ${platform}`,
99
);
100
}
101
102
return {
103
url: dl.url,
104
version: release.version,
105
assets: [{ name: "chrome-headless-shell", url: dl.url }],
106
};
107
}
108
109
async function preparePackage(ctx: InstallContext): Promise<PackageInfo> {
110
const release = await latestRelease();
111
const workingDir = Deno.makeTempDirSync({ prefix: "quarto-chrome-hs-" });
112
113
try {
114
await downloadAndExtractCft(
115
"Chrome Headless Shell",
116
release.url,
117
workingDir,
118
ctx,
119
"chrome-headless-shell",
120
);
121
} catch (e) {
122
safeRemoveSync(workingDir, { recursive: true });
123
throw e;
124
}
125
126
return {
127
filePath: workingDir,
128
version: release.version,
129
};
130
}
131
132
async function install(pkg: PackageInfo, _ctx: InstallContext): Promise<void> {
133
const installDir = chromeHeadlessShellInstallDir();
134
135
// Clear existing contents
136
if (existsSync(installDir)) {
137
for (const entry of Deno.readDirSync(installDir)) {
138
safeRemoveSync(join(installDir, entry.name), { recursive: true });
139
}
140
}
141
142
// Move extracted contents into install directory
143
for (const entry of Deno.readDirSync(pkg.filePath)) {
144
safeMoveSync(join(pkg.filePath, entry.name), join(installDir, entry.name));
145
}
146
147
noteInstalledVersion(installDir, pkg.version);
148
}
149
150
async function afterInstall(_ctx: InstallContext): Promise<boolean> {
151
return false;
152
}
153
154
async function uninstall(ctx: InstallContext): Promise<void> {
155
await ctx.withSpinner(
156
{ message: "Removing Chrome Headless Shell..." },
157
async () => {
158
safeRemoveSync(chromeHeadlessShellInstallDir(), { recursive: true });
159
},
160
);
161
}
162
163
// -- Exported tool definition --
164
165
export const chromeHeadlessShellInstallable: InstallableTool = {
166
name: "Chrome Headless Shell",
167
prereqs: [],
168
installed,
169
installDir: installDirIfInstalled,
170
installedVersion,
171
latestRelease,
172
preparePackage,
173
install,
174
afterInstall,
175
uninstall,
176
};
177
178