Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/tools/impl/chrome-for-testing.ts
12925 views
1
/*
2
* chrome-for-testing.ts
3
*
4
* Utilities for downloading binaries from the Chrome for Testing (CfT) API.
5
* https://github.com/GoogleChromeLabs/chrome-for-testing
6
* https://googlechromelabs.github.io/chrome-for-testing/
7
*
8
* Copyright (C) 2026 Posit Software, PBC
9
*/
10
11
import { basename, join } from "../../deno_ral/path.ts";
12
import { existsSync, safeChmodSync, safeRemoveSync, walkSync } from "../../deno_ral/fs.ts";
13
import { debug } from "../../deno_ral/log.ts";
14
import { arch, isWindows, os } from "../../deno_ral/platform.ts";
15
import { unzip } from "../../core/zip.ts";
16
import { InstallContext } from "../types.ts";
17
18
/** CfT platform identifiers matching the Google Chrome for Testing API. */
19
export type CftPlatform =
20
| "linux64"
21
| "mac-arm64"
22
| "mac-x64"
23
| "win32"
24
| "win64";
25
26
/** Platform detection result. */
27
export interface PlatformInfo {
28
platform: CftPlatform;
29
os: string;
30
arch: string;
31
}
32
33
/**
34
* Map os + arch to a CfT platform string.
35
* Throws on unsupported platforms (e.g., linux aarch64 — to be handled by Playwright CDN).
36
*/
37
export function detectCftPlatform(): PlatformInfo {
38
const platformMap: Record<string, CftPlatform> = {
39
"linux-x86_64": "linux64",
40
"darwin-aarch64": "mac-arm64",
41
"darwin-x86_64": "mac-x64",
42
"windows-x86_64": "win64",
43
"windows-x86": "win32",
44
};
45
46
const key = `${os}-${arch}`;
47
const platform = platformMap[key];
48
49
if (!platform) {
50
if (os === "linux" && arch === "aarch64") {
51
throw new Error(
52
"linux-arm64 is not supported by Chrome for Testing. " +
53
"Use 'quarto install chromium' for arm64 support.",
54
);
55
}
56
throw new Error(
57
`Unsupported platform for Chrome for Testing: ${os} ${arch}`,
58
);
59
}
60
61
return { platform, os, arch };
62
}
63
64
/** A single download entry from the CfT API. */
65
export interface CftDownload {
66
platform: CftPlatform;
67
url: string;
68
}
69
70
/** Parsed stable release from the CfT last-known-good-versions API. */
71
export interface CftStableRelease {
72
version: string;
73
downloads: {
74
chrome?: CftDownload[];
75
"chrome-headless-shell"?: CftDownload[];
76
chromedriver?: CftDownload[];
77
};
78
}
79
80
const kCftVersionsUrl =
81
"https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json";
82
83
/**
84
* Fetch the latest stable Chrome version and download URLs from the CfT API.
85
*/
86
export async function fetchLatestCftRelease(): Promise<CftStableRelease> {
87
let response: Response;
88
try {
89
response = await fetch(kCftVersionsUrl);
90
} catch (e) {
91
throw new Error(
92
`Failed to fetch Chrome for Testing API: ${
93
e instanceof Error ? e.message : String(e)
94
}`,
95
);
96
}
97
98
if (!response.ok) {
99
throw new Error(
100
`Chrome for Testing API returned ${response.status}: ${response.statusText}`,
101
);
102
}
103
104
// deno-lint-ignore no-explicit-any
105
let data: any;
106
try {
107
data = await response.json();
108
} catch {
109
throw new Error("Chrome for Testing API returned invalid JSON");
110
}
111
112
const stable = data?.channels?.Stable;
113
if (!stable || !stable.version || !stable.downloads) {
114
throw new Error(
115
"Chrome for Testing API response missing expected 'channels.Stable' structure",
116
);
117
}
118
119
return {
120
version: stable.version,
121
downloads: stable.downloads,
122
};
123
}
124
125
/**
126
* Find a named executable inside an extracted CfT directory.
127
* Handles platform-specific naming (.exe on Windows) and nested directory structures.
128
* Returns absolute path to the executable, or undefined if not found.
129
*/
130
export function findCftExecutable(
131
extractedDir: string,
132
binaryName: string,
133
): string | undefined {
134
const target = isWindows ? `${binaryName}.exe` : binaryName;
135
136
// CfT zips extract to {binaryName}-{platform}/{target}
137
try {
138
const { platform } = detectCftPlatform();
139
const knownPath = join(extractedDir, `${binaryName}-${platform}`, target);
140
if (existsSync(knownPath)) {
141
return knownPath;
142
}
143
} catch (e) {
144
debug(`findCftExecutable: platform detection failed, falling back to walk: ${e}`);
145
}
146
147
// Fallback: bounded walk for unexpected directory structures
148
for (const entry of walkSync(extractedDir, { includeDirs: false, maxDepth: 3 })) {
149
if (basename(entry.path) === target) {
150
return entry.path;
151
}
152
}
153
154
return undefined;
155
}
156
157
/**
158
* Download a CfT zip from URL, extract to targetDir, set executable permissions.
159
* Uses InstallContext.download() for progress reporting with the given label.
160
* When binaryName is provided, sets executable permission only on that binary.
161
* Returns the target directory path.
162
*/
163
export async function downloadAndExtractCft(
164
label: string,
165
url: string,
166
targetDir: string,
167
context: InstallContext,
168
binaryName?: string,
169
): Promise<string> {
170
const tempZipPath = Deno.makeTempFileSync({ suffix: ".zip" });
171
172
try {
173
await context.download(label, url, tempZipPath);
174
await unzip(tempZipPath, targetDir);
175
} finally {
176
safeRemoveSync(tempZipPath);
177
}
178
179
if (binaryName) {
180
const executable = findCftExecutable(targetDir, binaryName);
181
if (executable) {
182
safeChmodSync(executable, 0o755);
183
} else {
184
debug(`downloadAndExtractCft: expected binary '${binaryName}' not found in ${targetDir}`);
185
}
186
}
187
188
return targetDir;
189
}
190
191