From c49e3a9687d03cbf97a4fb66e03a3d69a888b48b Mon Sep 17 00:00:00 2001 From: aedes Date: Wed, 22 Apr 2026 02:31:45 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20playwright-based=20mirror=20=E2=80=94?= =?UTF-8?q?=20renders=20page=20then=20snapshots=20final=20DOM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/mirror.js | 151 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 scripts/mirror.js diff --git a/scripts/mirror.js b/scripts/mirror.js new file mode 100644 index 0000000..1778c60 --- /dev/null +++ b/scripts/mirror.js @@ -0,0 +1,151 @@ +// Usage: node scripts/mirror.js [output-dir] +// Renders the target page in headless Chromium with the cookie, waits for +// networkidle, captures final DOM + every fetched resource, rewrites URLs to +// point to the local mirror, strips +`; + +function urlToSafePath(u) { + const parsed = new URL(u); + let p = parsed.pathname; + if (parsed.search) { + const safe = parsed.search.slice(1).replace(/[^a-zA-Z0-9._-]/g, '_'); + p += '_q_' + safe; + } + return p.replace(/^\/+/, ''); +} + +async function main() { + const [targetUrl, cookieValue, outDir] = process.argv.slice(2); + if (!targetUrl || !cookieValue) { + console.error('Usage: node mirror.js [out=/app/public/q]'); + process.exit(1); + } + const OUT = outDir || '/app/public/q'; + const MIRROR = path.join(OUT, 'mirror'); + + const urlObj = new URL(targetUrl); + const ORIGIN = urlObj.origin; + const HOST = urlObj.hostname; + + console.log(`[mirror] launching chromium → ${targetUrl}`); + const browser = await chromium.launch({ headless: true }); + const ctx = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + viewport: { width: 1440, height: 900 }, + }); + await ctx.addCookies([{ + name: 'MoodleSession', + value: cookieValue, + domain: HOST, + path: '/', + secure: true, + sameSite: 'None', + }]); + + const resources = new Map(); + const page = await ctx.newPage(); + + page.on('response', async (resp) => { + const rurl = resp.url(); + if (!rurl.startsWith('http')) return; + try { + const buf = await resp.body(); + if (buf && buf.length > 0) resources.set(rurl, buf); + } catch (e) { /* redirect/204/etc */ } + }); + + await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 90000 }); + console.log(`[mirror] networkidle reached. extra settle…`); + await page.waitForTimeout(4000); + + // Re-inline all CSS into