This is a simple example on how to stream HTML with Elysia in Bun with TailwindCSS (through twind).
This uses the latest @twind/core API.
Please suggest any speed improvements in the comments 🙏
This is a simple example on how to stream HTML with Elysia in Bun with TailwindCSS (through twind).
This uses the latest @twind/core API.
Please suggest any speed improvements in the comments 🙏
| import { Elysia } from "elysia" | |
| import { html } from "@elysiajs/html" | |
| import { renderToStream, Suspense } from "@kitajs/html/suspense" | |
| import { getStyles, tw, tx, withTwind } from "./tw" | |
| import { Readable } from "stream" | |
| const renderAsyncEl = (rid: number) => { | |
| return ( | |
| <> | |
| <Suspense | |
| fallback={<h3 class="text-green-500">loading joke...</h3>} | |
| rid={rid} | |
| catch={e => ( | |
| <h3 class="text-red-500" safe> | |
| {(e as Error).message} | |
| </h3> | |
| )} | |
| > | |
| <FetchJoke /> | |
| </Suspense> | |
| <Suspense | |
| fallback={<h3 class="text-green-500">loading quote...</h3>} | |
| rid={rid} | |
| catch={e => ( | |
| <h3 class="text-red-500" safe> | |
| {(e as Error).message} | |
| </h3> | |
| )} | |
| > | |
| <FetchQuote /> | |
| </Suspense> | |
| </> | |
| ) | |
| } | |
| const FetchJoke = async () => { | |
| const res = await fetch( | |
| "https://v2.jokeapi.dev/joke/Programming?type=single&amount=5" | |
| ) | |
| const data = await res.json() | |
| if (!res.ok || !Array.isArray(data.jokes) || data.jokes.length !== 5) { | |
| throw new Error("Failed to fetch joke") | |
| } | |
| return ( | |
| <ol class="bg-yellow-500 px-8 py-4 list-number"> | |
| {data.jokes.map(({ joke }: { joke: string }) => <li safe>{joke}</li>) as "safe"} | |
| </ol> | |
| ) | |
| } | |
| const FetchQuote = async () => { | |
| const res = await fetch("https://zenquotes.io/api/random") | |
| const data = await res.json() | |
| if (!res.ok || !Array.isArray(data) || data.length !== 1) { | |
| throw new Error("Failed to fetch quote") | |
| } | |
| return ( | |
| <blockquote class="text-2xl p-4 text-center bg-blue-500 text-white mt-4"> | |
| <p safe>{data[0].q}</p> | |
| <footer class="text-right text-sm"> | |
| <cite safe>{data[0].a}</cite> | |
| </footer> | |
| </blockquote> | |
| ) | |
| } | |
| const app = new Elysia() | |
| .use(html()) | |
| .get("/", () => { | |
| tw.clear() | |
| const html = <h1 class="text-center text-5xl font-bold my-4">Hello, world!</h1> | |
| return withTwind(` | |
| <!DOCTYPE html> | |
| <html> | |
| <head><title>Synchronous HTML!</title></head> | |
| <body>${html}</body> | |
| </html> | |
| `) | |
| }) | |
| .get("/async", async () => { | |
| const html = Readable.toWeb( | |
| renderToStream(renderAsyncEl) // Renders asynchronous JSX to a stream.readable | |
| ) // Converts stream.readable to web ReadableStream. Readable.toWeb is experimental | |
| let recvFirstChunk = false | |
| const st = html.pipeThrough( | |
| new TransformStream({ | |
| start() { }, | |
| transform(chunk, controller) { | |
| if (!recvFirstChunk) { | |
| tw.clear() | |
| const html = new TextDecoder("utf-8").decode(chunk) | |
| const shell = `<!DOCTYPE html><html><head><title>Asynchronous HTML!</title></head><body>${html}</body></html>` | |
| controller.enqueue(new TextEncoder("utf-8").encode(withTwind(shell))) | |
| recvFirstChunk = true | |
| } else { | |
| // Do not send HTML shell, it is already present. | |
| let { html, css } = getStyles(new TextDecoder("utf-8").decode(chunk)) | |
| // Only oppend CSS if it exists | |
| if (!css.trim()) { | |
| controller.enqueue(new TextEncoder("utf-8").encode(html)) | |
| } else { | |
| // Use javascript to append the styles to the head | |
| css = JSON.stringify(css) | |
| const hash = Bun.hash(chunk) | |
| html += `<script id="ssr__sas-${hash}">var s=document.createElement('style');s.innerText=${css};document.head.appendChild(s);document.querySelector("#ssr__sas-${hash}").remove()</script>` | |
| controller.enqueue(new TextEncoder("utf-8").encode(html)) | |
| } | |
| } | |
| } | |
| }) | |
| ) | |
| return new Response(st, { | |
| headers: { "Content-Type": "text/html; charset=utf-8" } | |
| }) | |
| }) | |
| .listen(3000) | |
| console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`) |
| import { | |
| defineConfig, | |
| extract, | |
| injectGlobal as injectGlobal$, | |
| twind, | |
| tx as tx$, | |
| virtual | |
| } from "@twind/core" | |
| import pAutoprefix from "@twind/preset-autoprefix" | |
| import pTailwind from "@twind/preset-tailwind" | |
| import pExt from "@twind/preset-ext" | |
| import pContainerQueries from "@twind/preset-container-queries" | |
| export const config = defineConfig({ | |
| presets: [pAutoprefix(), pTailwind(), pExt(), pContainerQueries()] | |
| }) | |
| export const sheet = virtual() | |
| export const tw = twind(config, sheet) | |
| export const tx = tx$.bind(tw) | |
| export const injectGlobal = injectGlobal$.bind(tw) | |
| export function getStyles(html: string) { | |
| return extract(html, tw) | |
| } | |
| export function withTwind(html: string) { | |
| const { html: newHTML, css } = getStyles(html) | |
| return newHTML.replace("</head>", `<style>${css}</style></head>`) | |
| } |