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