Files
2025-11-30 09:05:55 +08:00

218 lines
5.9 KiB
JavaScript
Executable File

import { existsSync } from "node:fs";
import { access, constants } from "node:fs/promises";
import { homedir } from "node:os";
import { resolve as resolvePath, join } from "node:path";
export const DEFAULT_PORT = 9222;
const identity = (value) => value;
export function parseArgs(argv, options = {}) {
const {
boolean = [],
string = [],
number = [],
alias = {},
defaults = {},
} = options;
const boolSet = new Set(boolean.map(normalizeKey));
const stringSet = new Set(string.map(normalizeKey));
const numberSet = new Set(number.map(normalizeKey));
const aliases = Object.fromEntries(
Object.entries(alias).map(([key, target]) => [normalizeKey(key), normalizeKey(target)]),
);
const result = { _: [] };
const assignValue = (key, value = true) => {
const normalized = normalizeKey(key);
const target = aliases[normalized] ?? normalized;
result[target] = value;
};
for (let index = 0; index < argv.length; index++) {
const token = argv[index];
if (token === "--") {
result._.push(...argv.slice(index + 1));
break;
}
if (!token.startsWith("-")) {
result._.push(token);
continue;
}
if (token.startsWith("--no-")) {
const key = token.slice(5);
assignValue(key, false);
continue;
}
const [rawKey, inlineValue] = token.split("=", 2);
const key = rawKey.replace(/^--?/, "");
if (boolSet.has(key)) {
assignValue(key, inlineValue === undefined ? true : coerceBoolean(inlineValue));
continue;
}
const expectsString = stringSet.has(key);
const expectsNumber = numberSet.has(key);
if (inlineValue !== undefined) {
assignValue(key, expectsNumber ? Number(inlineValue) : inlineValue);
continue;
}
const nextToken = argv[index + 1];
const hasNext = nextToken !== undefined && !nextToken.startsWith("--");
if ((expectsString || expectsNumber) && hasNext) {
index += 1;
assignValue(key, expectsNumber ? Number(nextToken) : nextToken);
} else if (expectsString || expectsNumber) {
assignValue(key, expectsNumber ? NaN : "");
} else {
assignValue(key, true);
}
}
for (const [key, value] of Object.entries(defaults)) {
if (!(key in result)) {
result[key] = typeof value === "function" ? value() : value;
}
}
return result;
}
export function createLogger({ quiet = false, json = false } = {}) {
const format = (args) =>
args
.map((part) => {
if (part instanceof Error) {
return part.stack ?? part.message;
}
return typeof part === "object" ? JSON.stringify(part) : String(part);
})
.join(" ");
const write = (stream, args) => {
stream.write(format(args));
stream.write("\n");
};
return {
info: (...args) => {
if (!quiet && !json) write(process.stderr, args);
},
warn: (...args) => {
if (!quiet && !json) write(process.stderr, args);
},
error: (...args) => {
write(process.stderr, args);
},
};
}
export function printJSON(value) {
process.stdout.write(`${JSON.stringify(value)}\n`);
}
export function fail(message, { code = 1, json = false } = {}) {
if (json) {
printJSON({ ok: false, error: message });
} else {
process.stderr.write(`${message}\n`);
}
process.exit(code);
}
export function resolveBrowserConnection(flags = {}) {
if (flags.ws || process.env.BROWSER_WS_URL) {
const endpoint = flags.ws ?? process.env.BROWSER_WS_URL;
return {
browserWSEndpoint: endpoint,
defaultViewport: null,
};
}
const host = flags.host ?? process.env.BROWSER_HOST ?? "localhost";
const port = normalizeNumber(flags.port ?? process.env.BROWSER_PORT ?? DEFAULT_PORT, DEFAULT_PORT);
return {
browserURL: `http://${host}:${port}`,
defaultViewport: null,
};
}
export async function waitFor(fn, { timeout = 10000, interval = 250 } = {}) {
const start = Date.now();
let lastError;
while (Date.now() - start < timeout) {
try {
const result = await fn();
if (result) return result;
} catch (error) {
lastError = error;
}
await delay(interval);
}
if (lastError) throw lastError;
return null;
}
export function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getActivePage(browser, { index = -1 } = {}) {
const pages = await browser.pages();
if (pages.length === 0) return null;
return index === -1 ? pages.at(-1) : pages[index] ?? null;
}
export function expandPath(value) {
if (!value) return value;
if (value.startsWith("~")) {
return resolvePath(join(homedir(), value.slice(1)));
}
return resolvePath(value);
}
export async function pathExists(path) {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}
export function normalizeNumber(value, fallback) {
const number = typeof value === "string" ? Number(value) : value;
return Number.isFinite(number) ? number : fallback;
}
export function findExistingPath(paths) {
for (const candidate of paths) {
if (!candidate) continue;
if (existsSync(candidate)) return candidate;
}
return null;
}
function normalizeKey(key) {
return key.replace(/^--?/, "");
}
function coerceBoolean(value) {
if (typeof value === "boolean") return value;
const normalized = String(value).toLowerCase();
if (["false", "0", "no", "off"].includes(normalized)) return false;
return true;
}