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>`) | |
} |