hype/src/index.tsx

149 lines
4.5 KiB
TypeScript

import { join } from 'path'
import { render as formatHTML } from './html-formatter'
import { type Context, Hono, type Schema, type Env } from 'hono'
import { serveStatic } from 'hono/bun'
import color from 'kleur'
import { transpile } from './utils'
import defaultLayout from './layout'
const SHOW_HTTP_LOG = true
const CSS_RESET = await Bun.file(join(import.meta.dir, '/reset.css')).text()
const PICO_CSS = await Bun.file(join(import.meta.dir, '/pico.css')).text()
export * from './utils'
export type HypeProps = {
pico?: boolean
reset?: boolean
prettyHTML?: boolean
}
export class Hype<
E extends Env = Env,
S extends Schema = {},
BasePath extends string = '/'
> extends Hono<E, S, BasePath> {
props: HypeProps
routesRegistered = false
constructor(props?: HypeProps & ConstructorParameters<typeof Hono<E, S, BasePath>>[0]) {
super(props)
this.props = props ?? {}
}
registerRoutes() {
if (this.routesRegistered) return
this.routesRegistered = true
// static assets in pub/
this.use('/*', serveStatic({ root: './pub' }))
// css lives in src/, close to real code
this.use('/css/*', serveStatic({ root: './src' }))
// console logging
this.use('*', async (c, next) => {
if (!SHOW_HTTP_LOG) return await next()
const start = Date.now()
await next()
const end = Date.now()
const fn = c.res.status < 400 ? color.green : c.res.status < 500 ? color.yellow : color.red
const method = c.req.method === 'GET' ? color.cyan(c.req.method) : color.magenta(c.req.method)
console.log(fn(`${c.res.status}`), `${color.bold(method)} ${c.req.url} (${end - start}ms)`)
})
// exception handler
this.onError((err, c) => {
const isDev = process.env.NODE_ENV !== 'production'
return c.html(
`<!DOCTYPE html>
<html>
<body>
<h1>Error: ${err.message}</h1>
${isDev ? `<pre>${err.stack}</pre>` : '<p>An error occurred</p>'}
</body>
</html>`,
500
)
})
// prettify HTML output
if (this.props.prettyHTML ?? process.env.NODE_ENV !== 'production') {
this.use('*', async (c, next) => {
await next()
if (c.res.headers.get('content-type')?.includes('text/html')) {
const html = await c.res.text()
const formatted = formatHTML(html)
c.res = new Response(formatted, c.res)
}
})
}
// css reset
this.get('/css/reset.css', async c => new Response(CSS_RESET, { headers: { 'Content-Type': 'text/css' } }))
// pico
this.get('/css/pico.css', async c => new Response(PICO_CSS, { headers: { 'Content-Type': 'text/css' } }))
// serve transpiled js
this.on('GET', ['/js/:path{.+}', '/shared/:path{.+}'], async c => {
let path = './src/' + c.req.path.replace('..', '.')
// path must end in .js or .ts
if (!path.endsWith('.js') && !path.endsWith('.ts')) path += '.ts'
const ts = path.replace('.js', '.ts')
if (await Bun.file(ts).exists())
return new Response(await transpile(ts), { headers: { 'Content-Type': 'text/javascript' } })
else if (await Bun.file(ts + 'x').exists())
return new Response(await transpile(ts + 'x'), { headers: { 'Content-Type': 'text/javascript' } })
else if (await Bun.file(path).exists())
return new Response(Bun.file(path), { headers: { 'Content-Type': 'text/javascript' } })
else
return render404(c)
})
// file based routing
this.on('GET', ['/', '/:page'], async c => {
const pageName = (c.req.param('page') ?? 'index').replace('.', '')
if (pageName.startsWith('_')) return render404(c)
console.log(process.env.PWD)
const path = join(process.env.PWD ?? '.', `./src/pages/${pageName}.tsx`)
if (!(await Bun.file(path).exists()))
return render404(c)
const layoutPath = join(process.env.PWD ?? '.', `./src/pages/_layout.tsx`)
let Layout = defaultLayout
if (await Bun.file(layoutPath).exists())
Layout = (await import(layoutPath + `?t=${Date.now()}`)).default
const page = await import(path + `?t=${Date.now()}`)
const innerHTML = typeof page.default === 'function' ? <page.default c={c} req={c.req} /> : page.default
const withLayout = Layout ? <Layout props={this.props}>{innerHTML}</Layout> : innerHTML
return c.html(withLayout)
})
}
get defaults() {
this.registerRoutes()
return {
fetch: this.fetch,
idleTimeout: 255
}
}
}
function render404(c: Context) {
return c.text('File not found', 404)
}