experimental new frontend() style

This commit is contained in:
Chris Wanstrath 2026-01-27 20:10:40 -08:00
parent 7b9cade936
commit b9b4e205c9
3 changed files with 44 additions and 10 deletions

View File

@ -1,11 +1,11 @@
import { frontend as fe } from 'hype' import { fe } from 'hype'
fe(function test() { const test = fe(() => {
alert('ding dong') alert('ding dong')
}) })
export default () => ( export default () => (
<section> <section>
<a href="#" onclick="test()">test</a> <a href="#" onclick={test}>test</a>
</section> </section>
) )

View File

@ -1,14 +1,48 @@
import { AsyncLocalStorage } from 'async_hooks' import { AsyncLocalStorage } from 'async_hooks'
export const fnStorage = new AsyncLocalStorage<{ fns: string[] }>() export const fnStorage = new AsyncLocalStorage<{
fns: Map<string, string>
counter: number
}>()
// Designate a function in a .tsx file as frontend export function fe<T extends Record<string, unknown>>(
export function frontend(code: Function) { fn: (args?: T) => void,
args?: T
): string {
const store = fnStorage.getStore() const store = fnStorage.getStore()
store?.fns.push(code.toString()) if (!store) {
// Fallback to IIFE if outside request context
return args
? `(${fn.toString()})(${JSON.stringify(args)})`
: `(${fn.toString()})()`
}
const fnStr = fn.toString()
// Dedupe by function body
for (const [name, body] of store.fns)
if (body === fnStr)
return args ? `${name}(${JSON.stringify(args)})` : `${name}()`
const name = `frontendFn${store.counter++}`
store.fns.set(name, fnStr)
return args ? `${name}(${JSON.stringify(args)})` : `${name}()`
} }
export function feFunctions(): string[] { export function feFunctions(): string[] {
const store = fnStorage.getStore() const store = fnStorage.getStore()
return store?.fns ?? [] if (!store?.fns.size) return []
return [...store.fns.entries()].map(([name, body]) => {
// Handle arrow functions vs regular functions
if (body.startsWith('(') || body.startsWith('async ('))
return `const ${name} = ${body};`
// Named function - rename it
return body.replace(/^(async\s+)?function\s*\w*/, `$1function ${name}`)
})
} }
// Keep for backwards compat
export const frontend = fe

View File

@ -13,7 +13,7 @@ const CSS_RESET = await Bun.file(join(import.meta.dir, '/css/reset.css')).text()
const PICO_CSS = await Bun.file(join(import.meta.dir, '/css/pico.css')).text() const PICO_CSS = await Bun.file(join(import.meta.dir, '/css/pico.css')).text()
export * from './utils' export * from './utils'
export { frontend } from './frontend' export { frontend, frontend as fe } from './frontend'
export type { Context } from 'hono' export type { Context } from 'hono'
const pageCache = new Map() const pageCache = new Map()
@ -94,7 +94,7 @@ export class Hype<
// serve frontend js // serve frontend js
this.use('*', async (c, next) => { this.use('*', async (c, next) => {
await fnStorage.run({ fns: [] }, async () => { await fnStorage.run({ fns: new Map(), counter: 0 }, async () => {
await next() await next()
const contentType = c.res.headers.get('content-type') const contentType = c.res.headers.get('content-type')