// The screenshots without a PDS: what knotless-serve answered for each page, // recorded once in fixtures.json, and played back by a stand-in for it in // front of the pages. The pages are drawn as they are by the real app, from // web/build; only their data is fixed. // // node harness/shots/fixtures.mjs record them again, from the world // `just shots-up` keeps up, or a new // one: `just shots-record` import { existsSync, readFileSync, writeFileSync } from 'node:fs' import { createServer, request } from 'node:http' import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { base, create, pages, port, root, saved, serve, until } from './world.mjs' export const fixtures = join(root, 'harness/shots/fixtures.json') /// What knotless-serve answers itself, rather than handing to the pages. const answered = (path) => /^\/(_data|xrpc)\//.test(path) || /^\/[^/]+\/[^/]+\/raw\//.test(path) /// The headers of an answer worth keeping. const KEPT = ['content-type', 'cache-control', 'location', 'content-security-policy'] /// knotless-serve's place on `port`, in front of the pages: answering from /// `recorded`, or, given `from`, asking knotless-serve there and recording /// what it says. Returns the recording, and how to stop. export async function front({ recorded = {}, from } = {}) { const drawn = pages(port + 1, base) const answer = async (path) => { if (!from) return recorded[path] const res = await fetch(from + path, { redirect: 'manual' }) const body = Buffer.from(await res.arrayBuffer()) const text = body.toString('utf8') const binary = !Buffer.from(text, 'utf8').equals(body) const headers = Object.fromEntries(KEPT.filter((h) => res.headers.has(h)).map((h) => [h, res.headers.get(h)])) return (recorded[path] = { status: res.status, headers, ...(binary ? { base64: body.toString('base64') } : { body: text }) }) } const server = createServer(async (req, res) => { if (answered(req.url)) { const a = await answer(req.url).catch(() => null) if (!a) { res.writeHead(404, { 'content-type': 'application/json' }) res.end(JSON.stringify({ message: `nothing recorded for ${req.url}` })) return } res.writeHead(a.status, a.headers) res.end(a.base64 === undefined ? a.body : Buffer.from(a.base64, 'base64')) return } const headers = { ...req.headers, 'x-forwarded-host': req.headers.host, 'x-forwarded-proto': 'http' } const on = request({ host: '127.0.0.1', port: port + 1, path: req.url, method: req.method, headers }, (up) => { res.writeHead(up.statusCode, up.headers) up.pipe(res) }) on.on('error', (e) => { res.writeHead(502) res.end(`The pages are not being drawn: ${e.message}`) }) req.pipe(on) }) await new Promise((r) => server.listen(port, '127.0.0.1', r)) const kill = () => { server.close() drawn.kill() } try { await until('the pages', async () => (await fetch(`${base}/start`)).ok) } catch (e) { kill() throw e } return { recorded, kill } } /// The recording, if there is one. export const recording = () => (existsSync(fixtures) ? JSON.parse(readFileSync(fixtures, 'utf8')) : null) /// Records every page's answers from `world`, and writes them with the pages. async function record(world) { const at = port + 2 const server = await serve(world, { at, alone: true }) const stand = await front({ from: `http://127.0.0.1:${at}` }) try { for (const path of Object.values(world.views)) { const html = await (await fetch(base + path)).text() // What the browser asks for itself: images, from the raw files. for (const [, src] of html.matchAll(/src="(\/[^"]+\/raw\/[^"]+)"/g)) await fetch(base + src) } } finally { stand.kill() server.kill() } const sorted = Object.fromEntries(Object.entries(stand.recorded).sort(([a], [b]) => (a < b ? -1 : 1))) writeFileSync(fixtures, JSON.stringify({ views: world.views, answers: sorted }, null, 1) + '\n') return Object.keys(sorted).length } if (process.argv[1] === fileURLToPath(import.meta.url)) { const kept = existsSync(saved) ? JSON.parse(readFileSync(saved, 'utf8')) : null const up = kept && (await fetch(`${kept.net.pds}/xrpc/com.atproto.server.describeServer`).then((r) => r.ok, () => false)) const world = up ? kept : await create() // A new world's server is stopped first: the recording starts its own. if (!up) world.server.kill() const n = await record(world) if (!up) world.stop() console.log(`recorded ${n} answers for ${Object.keys(world.views).length} pages in ${fixtures}`) process.exit(0) }