Skip to content

Commit 88877d9

Browse files
authored
feat(ux): report back to user on install in process (#33)
1 parent 2e902b7 commit 88877d9

7 files changed

Lines changed: 524 additions & 75 deletions

File tree

backend/bun/src/index.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,91 @@ import type {
1111
NodeHTMLToImageBuffer,
1212
} from "./types";
1313

14+
import { checkPuppeteerInstalled, installPuppeteer } from "./utils/puppeteer";
15+
import path from "node:path";
16+
import os from "node:os";
17+
18+
/**
19+
* Handles the health check command
20+
* Returns JSON with information about whether Puppeteer is installed
21+
*/
22+
const handleHealth = async () => {
23+
const cacheDir = path.join(os.homedir(), ".cache", "puppeteer");
24+
const { isInstalled, executablePath } = checkPuppeteerInstalled(cacheDir);
25+
26+
writeJSONToStdout({
27+
success: true,
28+
debug: false,
29+
data: {
30+
isInstalled,
31+
executablePath,
32+
},
33+
});
34+
};
35+
36+
/**
37+
* Handles the install command
38+
* Installs Puppeteer and sends progress updates as JSON (one line per update)
39+
*/
40+
const handleInstall = async () => {
41+
const cacheDir = path.join(os.homedir(), ".cache", "puppeteer");
42+
43+
const progressCallback = (progress: {
44+
status: string;
45+
message: string;
46+
progress?: number;
47+
}) => {
48+
// Send progress update as JSON on a single line
49+
// Write directly - process.stdout.write is non-blocking by default
50+
const progressJson = JSON.stringify({
51+
success: true,
52+
debug: false,
53+
data: {
54+
type: JSONRequestType.Install,
55+
status: progress.status,
56+
message: progress.message,
57+
progress: progress.progress,
58+
},
59+
});
60+
process.stdout.write(progressJson + "\n");
61+
};
62+
63+
const executablePath = await installPuppeteer(cacheDir, progressCallback);
64+
65+
if (executablePath) {
66+
writeJSONToStdout({
67+
success: true,
68+
debug: false,
69+
data: {
70+
type: JSONRequestType.Install,
71+
status: "completed",
72+
message: "Browser installation completed successfully",
73+
executablePath,
74+
},
75+
});
76+
} else {
77+
writeJSONToStdout({
78+
success: false,
79+
error: "Failed to install browser",
80+
});
81+
}
82+
};
83+
1484
const main = async () => {
85+
// Check for command line arguments
86+
const args = process.argv.slice(2);
87+
if (args.length > 0) {
88+
const command = args[0];
89+
if (command === "health") {
90+
await handleHealth();
91+
return;
92+
} else if (command === "install") {
93+
await handleInstall();
94+
return;
95+
}
96+
}
97+
98+
// Default behavior: read from stdin
1599
const jsonPayload = await getJSONFromStdin();
16100

17101
if ("error" in jsonPayload) {

backend/bun/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const enum JSONRequestType {
77
CodeImageGeneration = "image",
88
CodeHTMLGeneration = "html",
99
CodeRTFGeneration = "rtf",
10+
Health = "health",
11+
Install = "install",
1012
}
1113

1214
interface FontSettingsFont {

backend/bun/src/utils/puppeteer.ts

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ import {
1414
} from "@puppeteer/browsers";
1515
import { writeJSONToStdout } from "./stdin";
1616

17+
/**
18+
* Progress callback type for install operations
19+
*/
20+
export type InstallProgressCallback = (progress: {
21+
status: string;
22+
message: string;
23+
progress?: number;
24+
}) => void;
25+
1726
/**
1827
* Gets the executable path for the installed Chrome browser.
1928
* @param cacheDir - The cache directory where browsers are stored
@@ -31,7 +40,6 @@ const getInstalledBrowserPath = (cacheDir: string): string | null => {
3140
const browserPlatform =
3241
platformMap[currentPlatform] || BrowserPlatform.LINUX;
3342

34-
// Try to get the latest installed browser
3543
const cache = new Cache(cacheDir);
3644
const installedBrowsers = cache.getInstalledBrowsers();
3745
const chromeBrowser = installedBrowsers.find(
@@ -57,14 +65,9 @@ const getInstalledBrowserPath = (cacheDir: string): string | null => {
5765
* Sets up Puppeteer cache directory and ensures browser is installed.
5866
* This is necessary when running in a bundled binary where the default
5967
* cache path might not be accessible.
60-
* Uses platform-specific default cache directories:
61-
* - Windows: %LOCALAPPDATA%\puppeteer
62-
* - macOS: ~/Library/Caches/puppeteer
63-
* - Linux: ~/.cache/puppeteer
6468
* @returns The executable path of the installed browser, or null if not found
6569
*/
6670
export const setupPuppeteer = async (): Promise<string | null> => {
67-
// Use platform-specific default cache directory
6871
const cacheDir = path.join(os.homedir(), ".cache", "puppeteer");
6972

7073
// Ensure the cache directory exists
@@ -138,3 +141,149 @@ export const setupPuppeteer = async (): Promise<string | null> => {
138141

139142
return executablePath;
140143
};
144+
145+
/**
146+
* Checks if Puppeteer browser is installed
147+
* @param cacheDir - The cache directory where browsers are stored
148+
* @returns Object with isInstalled boolean and executablePath if installed
149+
*/
150+
export const checkPuppeteerInstalled = (
151+
cacheDir: string,
152+
): { isInstalled: boolean; executablePath: string | null } => {
153+
try {
154+
const executablePath = getInstalledBrowserPath(cacheDir);
155+
if (executablePath && existsSync(executablePath)) {
156+
return { isInstalled: true, executablePath };
157+
}
158+
159+
try {
160+
const puppeteerPath = puppeteer.executablePath();
161+
if (puppeteerPath && existsSync(puppeteerPath)) {
162+
return { isInstalled: true, executablePath: puppeteerPath };
163+
}
164+
} catch (/* eslint-disable-line */ error) {
165+
// Browser is not installed
166+
}
167+
168+
return { isInstalled: false, executablePath: null };
169+
} catch (/* eslint-disable-line */ error) {
170+
return { isInstalled: false, executablePath: null };
171+
}
172+
};
173+
174+
/**
175+
* Installs Puppeteer browser with progress updates
176+
* @param cacheDir - The cache directory where browsers are stored
177+
* @param progressCallback - Callback function to report progress
178+
* @returns The executable path of the installed browser, or null if installation failed
179+
*/
180+
export const installPuppeteer = async (
181+
cacheDir: string,
182+
progressCallback?: InstallProgressCallback,
183+
): Promise<string | null> => {
184+
try {
185+
await mkdir(cacheDir, { recursive: true });
186+
187+
const platformMap: Record<string, BrowserPlatform> = {
188+
win32: BrowserPlatform.WIN32,
189+
darwin: BrowserPlatform.MAC,
190+
linux: BrowserPlatform.LINUX,
191+
};
192+
193+
const currentPlatform = process.platform;
194+
const browserPlatform =
195+
platformMap[currentPlatform] || BrowserPlatform.LINUX;
196+
197+
if (progressCallback) {
198+
progressCallback({
199+
status: "resolving",
200+
message: "Resolving requirements ...",
201+
});
202+
}
203+
204+
const buildId = await resolveBuildId(
205+
Browser.CHROME,
206+
browserPlatform,
207+
BrowserTag.LATEST,
208+
);
209+
210+
if (progressCallback) {
211+
progressCallback({
212+
status: "installing",
213+
message: `Installing requirements ...`,
214+
progress: 0,
215+
});
216+
}
217+
218+
const installStartTime = Date.now();
219+
let periodicUpdateInterval: NodeJS.Timeout | null = null;
220+
let isInstalling = true;
221+
222+
if (progressCallback) {
223+
periodicUpdateInterval = setInterval(() => {
224+
if (!isInstalling || !progressCallback) {
225+
return;
226+
}
227+
228+
const elapsedSeconds = Math.floor(
229+
(Date.now() - installStartTime) / 1000,
230+
);
231+
232+
progressCallback({
233+
status: "installing",
234+
message: `Still installing ... (${elapsedSeconds}s elapsed)`,
235+
progress: undefined,
236+
});
237+
}, 5000);
238+
}
239+
240+
try {
241+
await install({
242+
browser: Browser.CHROME,
243+
buildId: buildId,
244+
cacheDir: cacheDir,
245+
});
246+
247+
isInstalling = false;
248+
if (periodicUpdateInterval) {
249+
clearInterval(periodicUpdateInterval);
250+
periodicUpdateInterval = null;
251+
}
252+
} catch (installErr) {
253+
isInstalling = false;
254+
if (periodicUpdateInterval) {
255+
clearInterval(periodicUpdateInterval);
256+
periodicUpdateInterval = null;
257+
}
258+
throw installErr;
259+
}
260+
261+
if (progressCallback) {
262+
progressCallback({
263+
status: "completed",
264+
message: "Requirements installed successfully.",
265+
progress: 100,
266+
});
267+
}
268+
269+
const executablePath = computeExecutablePath({
270+
cacheDir: cacheDir,
271+
browser: Browser.CHROME,
272+
platform: browserPlatform,
273+
buildId: buildId,
274+
});
275+
276+
return executablePath;
277+
} catch (installError) {
278+
if (progressCallback) {
279+
progressCallback({
280+
status: "error",
281+
message:
282+
installError instanceof Error
283+
? installError.message
284+
: String(installError),
285+
});
286+
}
287+
return null;
288+
}
289+
};

0 commit comments

Comments
 (0)