a post-component library for building user-interfaces on the web.
at main 116 lines 3.3 kB view raw
1import { serve } from '@hono/node-server' 2import { serveStatic } from '@hono/node-server/serve-static' 3import { transformSync } from 'amaro' 4import { Hono } from 'hono' 5import * as path from 'node:path' 6import { fileURLToPath } from 'node:url' 7import * as puppeteer from 'puppeteer' 8import type { Runtime } from './main.ts' 9 10export interface BrowserRuntimeOptions { 11 collect_coverage?: boolean 12} 13 14export async function create_browser_runtime(options: BrowserRuntimeOptions = {}): Promise<Runtime> { 15 const collect_coverage = options.collect_coverage ?? true 16 17 const browser = await puppeteer.launch({ 18 // headless: false, 19 // devtools: true, 20 }) 21 22 const app = new Hono() 23 24 app.get('/@runner', c => { 25 const pkg = (specifier: string) => '/' + path.relative('.', fileURLToPath(import.meta.resolve(specifier))) 26 27 return c.html(` 28 <!doctype html> 29 <link rel="icon" href="data:" /> 30 <script type="importmap">${JSON.stringify({ 31 imports: { 32 dhtml: '/dist/index.js', 33 'dhtml/client': '/dist/client.js', 34 'dhtml/server': '/dist/server.js', 35 birpc: pkg('birpc'), 36 devalue: pkg('devalue'), 37 mitata: pkg('mitata'), 38 }, 39 })}</script> 40 <script type="module" src="/scripts/test/runtime.ts"></script> 41 `) 42 }) 43 44 app.use(async (c, next) => { 45 await next() 46 if (c.res.ok && c.req.path.endsWith('.ts')) { 47 const { code } = transformSync(await c.res.text(), { mode: 'strip-only' }) 48 c.res = c.body(code) 49 c.res.headers.set('content-type', 'text/javascript') 50 c.res.headers.delete('content-length') 51 } 52 }) 53 app.use(serveStatic({ root: './' })) 54 app.use(async (c, next) => { 55 await next() 56 c.header('Cross-Origin-Opener-Policy', 'same-origin') 57 c.header('Cross-Origin-Embedder-Policy', 'require-corp') 58 c.header('Cross-Origin-Resource-Policy', 'same-origin') 59 }) 60 61 const server = serve({ 62 fetch: app.fetch, 63 port: 0, 64 }) 65 66 let addr = server.address()! 67 if (typeof addr !== 'string') { 68 addr = addr.family === 'IPv6' ? `[${addr.address}]:${addr.port}` : `${addr.address}:${addr.port}` 69 } 70 71 const [page] = await browser.pages() 72 page.on('console', async msg => { 73 const args = await Promise.all(msg.args().map(arg => arg.jsonValue())) 74 const type = msg.type() 75 switch (type) { 76 case 'startGroup': 77 console.group(...args) 78 break 79 case 'startGroupCollapsed': 80 console.groupCollapsed(...args) 81 break 82 case 'endGroup': 83 console.groupEnd() 84 break 85 case 'verbose': 86 console.log(...args) 87 break 88 default: 89 const fn = console[type] 90 // @ts-expect-error 91 fn(...args) 92 } 93 }) 94 const { port1, port2 } = new MessageChannel() 95 await page.exposeFunction('__postMessage', (data: any) => port1.postMessage(data)) 96 97 if (collect_coverage) await page.coverage.startJSCoverage({ includeRawScriptCoverage: true }) 98 await page.goto(`http://${addr}/@runner`) 99 100 const onmessage = await page.waitForFunction(() => window.__onmessage) 101 port1.onmessage = e => onmessage.evaluate((fn, data) => fn(data), e.data) 102 103 return { 104 port: port2, 105 async coverage() { 106 if (!collect_coverage) return [] 107 const coverage = await page.coverage.stopJSCoverage() 108 return coverage.map(c => c.rawScriptCoverage!) 109 }, 110 async [Symbol.asyncDispose]() { 111 port1.close() 112 server.close() 113 await browser.close() 114 }, 115 } 116}