Monorepo for Aesthetic.Computer aesthetic.computer
at main 96 lines 3.9 kB view raw
1#!/usr/bin/env node 2import puppeteer from "puppeteer-core"; 3import http from "http"; 4 5const CDP_HOST = "host.docker.internal"; 6const CDP_PORT = 9222; 7 8function httpGet(url, headers = {}) { 9 return new Promise((resolve, reject) => { 10 const u = new URL(url); 11 const req = http.request({ 12 hostname: u.hostname, 13 port: u.port, 14 path: u.pathname, 15 method: "GET", 16 headers: { Host: "localhost", ...headers } 17 }, (res) => { 18 let data = ""; 19 res.on("data", chunk => data += chunk); 20 res.on("end", () => resolve(data)); 21 }); 22 req.on("error", reject); 23 req.end(); 24 }); 25} 26 27async function getWSEndpoint() { 28 const data = await httpGet(`http://${CDP_HOST}:${CDP_PORT}/json/version`); 29 const json = JSON.parse(data); 30 return json.webSocketDebuggerUrl.replace("ws://localhost", `ws://${CDP_HOST}:${CDP_PORT}`); 31} 32 33export async function getBrowser() { 34 const wsEndpoint = await getWSEndpoint(); 35 return puppeteer.connect({ 36 browserWSEndpoint: wsEndpoint, 37 defaultViewport: null, 38 }); 39} 40 41export async function listPages() { 42 const browser = await getBrowser(); 43 const pages = await browser.pages(); 44 const info = await Promise.all(pages.map(async (p, i) => ({ 45 index: i, 46 url: p.url(), 47 title: await p.title(), 48 }))); 49 await browser.disconnect(); 50 return info; 51} 52 53export async function getPage(pattern) { 54 const browser = await getBrowser(); 55 const pages = await browser.pages(); 56 let page = typeof pattern === "number" ? pages[pattern] : pages.find(p => p.url().includes(pattern)); 57 if (!page) { await browser.disconnect(); throw new Error("No page: " + pattern); } 58 return { browser, page }; 59} 60 61export class PageController { 62 constructor(page, browser) { this.page = page; this.browser = browser; } 63 static async connect(pattern) { 64 const { browser, page } = await getPage(pattern); 65 return new PageController(page, browser); 66 } 67 async goto(url, o = {}) { await this.page.goto(url, { waitUntil: "networkidle2", ...o }); return this; } 68 async click(s, o = {}) { await this.page.waitForSelector(s, { timeout: 10000, ...o }); await this.page.click(s); return this; } 69 async type(s, text, o = {}) { await this.page.waitForSelector(s, { timeout: 10000 }); await this.page.click(s); await this.page.type(s, text, o); return this; } 70 async fill(s, v) { await this.page.waitForSelector(s, { timeout: 10000 }); await this.page.$eval(s, (el, val) => { el.value = val; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); }, v); return this; } 71 async getContent() { return this.page.evaluate(() => document.body.innerText); } 72 async getHtml() { return this.page.content(); } 73 async wait(ms) { await new Promise(r => setTimeout(r, ms)); return this; } 74 async waitNav(o = {}) { await this.page.waitForNavigation({ waitUntil: "networkidle2", ...o }); return this; } 75 async waitFor(s, o = {}) { await this.page.waitForSelector(s, { timeout: 10000, ...o }); return this; } 76 async screenshot(path) { await this.page.screenshot({ path, fullPage: true }); return this; } 77 async eval(fn, ...args) { return this.page.evaluate(fn, ...args); } 78 url() { return this.page.url(); } 79 async title() { return this.page.title(); } 80 async close() { await this.browser.disconnect(); } 81} 82 83if (import.meta.url === `file://${process.argv[1]}`) { 84 const [cmd, arg] = process.argv.slice(2); 85 if (cmd === "list") { 86 const pages = await listPages(); 87 console.log("Tabs:"); 88 pages.forEach(p => console.log(" [" + p.index + "] " + p.title.slice(0,40) + " - " + p.url.slice(0,60))); 89 } else if (cmd === "content") { 90 const ctrl = await PageController.connect(isNaN(arg) ? arg : parseInt(arg) || 0); 91 console.log(await ctrl.getContent()); 92 await ctrl.close(); 93 } else { 94 console.log("Usage: node browser.mjs list | content [pattern]"); 95 } 96}