| import { serve } from '@hono/node-server' | |
| import { serveStatic } from '@hono/node-server/serve-static' | |
| import { Hono } from 'hono' | |
| import { logger } from 'hono/logger' | |
| import { prettyJSON } from 'hono/pretty-json' | |
| import os from 'node:os' | |
| import { format } from 'node:util' | |
| import { isMainThread, parentPort, Worker } from 'node:worker_threads' | |
| import playwright from 'playwright-extra' | |
| import prettyBytes from 'pretty-bytes' | |
| import prettyMs from 'pretty-ms' | |
| import pluginStealth from 'puppeteer-extra-plugin-stealth' | |
| const TIMEOUT_MS = 6e4 | |
| const MAX_CODE_LENGTH = 6e4 | |
| if (!isMainThread) { | |
| const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor | |
| parentPort.on('message', async (code) => { | |
| try { | |
| const fn = new AsyncFunction( | |
| 'playwright', | |
| 'pluginStealth', | |
| 'console', | |
| code | |
| ) | |
| parentPort.postMessage({ | |
| result: await fn(playwright, pluginStealth, console) | |
| }) | |
| } catch (e) { | |
| parentPort.postMessage({ error: format(e) }) | |
| } | |
| }) | |
| } else { | |
| const isNumber = (v) => typeof v === 'number' && !isNaN(v) | |
| const transformObj = (obj, cb) => | |
| JSON.parse( | |
| JSON.stringify(obj, (key, val) => | |
| cb(key, val) ? prettyBytes(val) : val | |
| ).replace(/_(\w)/g, (_, g) => g.toUpperCase()) | |
| ) | |
| const getServerStats = () => { | |
| const stats = {} | |
| stats.uptime = prettyMs(process.uptime() * 1e3) | |
| stats.osUptime = prettyMs(os.uptime() * 1e3) | |
| const report = process.report?.getReport?.() | |
| if (report) | |
| Object.assign(stats, { | |
| header: report.header, | |
| javascriptHeap: transformObj( | |
| report.javascriptHeap, | |
| (k, v) => !/(ContextCount|Garbage)/i.test(k) && isNumber(v) | |
| ), | |
| resourceUsage: transformObj( | |
| report.resourceUsage, | |
| (k, v) => | |
| !/(Percent|IO|reads|write)/i.test(k) && isNumber(v) | |
| ), | |
| uvthreadResourceUsage: transformObj( | |
| report.uvthreadResourceUsage, | |
| (k, v) => isNumber(v) | |
| ) | |
| }) | |
| stats.memoryUsage = transformObj(process.memoryUsage(), (k, v) => | |
| isNumber(v) | |
| ) | |
| return stats | |
| } | |
| const runBrowserScript = (code) => | |
| new Promise((resolve, reject) => { | |
| const worker = new Worker(new URL(import.meta.url), { | |
| resourceLimits: { maxOldGenerationSizeMb: 512 } | |
| }) | |
| const timer = setTimeout(() => { | |
| worker.terminate() | |
| reject(new Error('Execution timeout')) | |
| }, TIMEOUT_MS) | |
| const cleanup = () => { | |
| clearTimeout(timer) | |
| worker.terminate() | |
| } | |
| worker.postMessage(code) | |
| worker.on('message', ({ result, error }) => { | |
| cleanup() | |
| error ? reject(new Error(error)) : resolve(format(result)) | |
| }) | |
| worker.on('error', (e) => { | |
| cleanup() | |
| reject(e) | |
| }) | |
| worker.on('exit', (exitCode) => { | |
| clearTimeout(timer) | |
| exitCode !== 0 && | |
| reject(new Error(`Worker exited with code ${exitCode}`)) | |
| }) | |
| }) | |
| const app = new Hono() | |
| app.use(logger()) | |
| app.use(prettyJSON({ force: true })) | |
| app.use( | |
| '/file/*', | |
| serveStatic({ | |
| root: os.tmpdir(), | |
| rewriteRequestPath: (path) => path.replace(/^\/file/, '') | |
| }) | |
| ) | |
| app.get('/', (c) => c.json(getServerStats())) | |
| app.post('/run', async (c) => { | |
| const body = await c.req.json().catch(() => ({})) | |
| const { code } = body | |
| if (!code) return c.json({ error: 'Code is required' }, 400) | |
| if (typeof code !== 'string') | |
| return c.json({ error: 'Code must be a string' }, 400) | |
| if (code.length > MAX_CODE_LENGTH) | |
| return c.json({ error: 'Code too long' }, 400) | |
| try { | |
| return c.json({ result: await runBrowserScript(code) }) | |
| } catch (e) { | |
| return c.json({ error: format(e) }, 500) | |
| } | |
| }) | |
| const port = process.env.SPACE_ID ? 7860 : +(process.env.PORT || 3000) | |
| serve({ fetch: app.fetch, port }, console.log) | |
| } |