159 lines
4.3 KiB
JavaScript
Executable File
159 lines
4.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
import { mkdtemp } from "node:fs/promises";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { randomUUID } from "node:crypto";
|
||
import puppeteer from "puppeteer-core";
|
||
|
||
import {
|
||
DEFAULT_PORT,
|
||
createLogger,
|
||
fail,
|
||
getActivePage,
|
||
parseArgs,
|
||
printJSON,
|
||
resolveBrowserConnection,
|
||
normalizeNumber,
|
||
} from "./config.js";
|
||
|
||
const args = parseArgs(process.argv.slice(2), {
|
||
boolean: ["json", "quiet"],
|
||
string: ["element", "format", "ws", "host", "out"],
|
||
number: ["port", "timeout", "quality"],
|
||
alias: {
|
||
j: "json",
|
||
q: "quiet",
|
||
},
|
||
defaults: {
|
||
port: DEFAULT_PORT,
|
||
timeout: 30000,
|
||
format: "png",
|
||
},
|
||
});
|
||
|
||
const jsonOutput = Boolean(args.json);
|
||
const logger = createLogger({ quiet: Boolean(args.quiet), json: jsonOutput });
|
||
|
||
const timeout = normalizeNumber(args.timeout, 30000);
|
||
const port = normalizeNumber(args.port, DEFAULT_PORT);
|
||
|
||
const format = normalizeFormat(args.format);
|
||
if (!format) {
|
||
fail("Invalid --format. Use png or jpeg.", { json: jsonOutput });
|
||
}
|
||
|
||
const quality = determineQuality(args.quality, format);
|
||
|
||
const connectionOptions = resolveBrowserConnection({ port, host: args.host, ws: args.ws });
|
||
|
||
let browser;
|
||
try {
|
||
browser = await puppeteer.connect({
|
||
...connectionOptions,
|
||
timeout,
|
||
});
|
||
} catch (error) {
|
||
fail(`Failed to connect to browser: ${error.message}`, { json: jsonOutput });
|
||
}
|
||
|
||
const outputFile = args.out ? args.out : await allocateTempFile(format);
|
||
|
||
try {
|
||
const page = await getActivePage(browser, { index: -1 });
|
||
if (!page) {
|
||
fail("No active page found. Navigate first.", { json: jsonOutput });
|
||
}
|
||
|
||
let dimensions;
|
||
let buffer;
|
||
|
||
if (args.element) {
|
||
const handle = await page.$(args.element);
|
||
if (!handle) {
|
||
fail(`Element not found: ${args.element}`, { json: jsonOutput });
|
||
}
|
||
const box = await handle.boundingBox();
|
||
if (!box) {
|
||
fail(`Element not visible: ${args.element}`, { json: jsonOutput });
|
||
}
|
||
buffer = await handle.screenshot({
|
||
path: outputFile,
|
||
type: format,
|
||
quality,
|
||
});
|
||
dimensions = {
|
||
width: Math.round(box.width),
|
||
height: Math.round(box.height),
|
||
};
|
||
} else {
|
||
const metrics = await page.evaluate(() => {
|
||
const width = Math.max(
|
||
document.documentElement.scrollWidth,
|
||
document.body?.scrollWidth ?? 0,
|
||
window.innerWidth,
|
||
);
|
||
const height = Math.max(
|
||
document.documentElement.scrollHeight,
|
||
document.body?.scrollHeight ?? 0,
|
||
window.innerHeight,
|
||
);
|
||
return {
|
||
width: Math.round(width),
|
||
height: Math.round(height),
|
||
};
|
||
});
|
||
|
||
buffer = await page.screenshot({
|
||
path: outputFile,
|
||
type: format,
|
||
quality,
|
||
fullPage: true,
|
||
});
|
||
|
||
dimensions = metrics;
|
||
}
|
||
|
||
if (!buffer) {
|
||
fail("Screenshot failed.", { json: jsonOutput });
|
||
}
|
||
|
||
const result = {
|
||
ok: true,
|
||
path: outputFile,
|
||
format,
|
||
width: dimensions?.width ?? null,
|
||
height: dimensions?.height ?? null,
|
||
element: Boolean(args.element),
|
||
};
|
||
|
||
if (jsonOutput) {
|
||
printJSON(result);
|
||
} else {
|
||
logger.info(`📸 Screenshot saved (${result.width ?? "?"}×${result.height ?? "?"})`);
|
||
process.stdout.write(`${outputFile}\n`);
|
||
}
|
||
} catch (error) {
|
||
fail(`Screenshot failed: ${error.message}`, { json: jsonOutput });
|
||
} finally {
|
||
if (browser) await browser.disconnect();
|
||
}
|
||
|
||
function normalizeFormat(value) {
|
||
if (!value) return "png";
|
||
const normalized = String(value).toLowerCase();
|
||
if (["png", "jpeg"].includes(normalized)) return normalized;
|
||
return null;
|
||
}
|
||
|
||
function determineQuality(value, currentFormat) {
|
||
if (currentFormat !== "jpeg") return undefined;
|
||
if (value === undefined) return 80;
|
||
const numeric = normalizeNumber(value, 80);
|
||
return Math.min(100, Math.max(1, numeric));
|
||
}
|
||
|
||
async function allocateTempFile(currentFormat) {
|
||
const directory = await mkdtemp(join(tmpdir(), "browser-tools-"));
|
||
return join(directory, `screenshot-${randomUUID()}.${currentFormat}`);
|
||
} |