Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:05:55 +08:00
commit 89c3c1ab83
18 changed files with 1955 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
#!/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}`);
}