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