Bun, Hono, Vite, TailwindCSS: An amazing combination has just been revealed!
Tags: webdev, bunjs, javascript, typescript
Published: 2023-09-10 05:54:03

[Bun](https://bun.sh/) is great, yes!, [Lately](https://bun.sh/blog/bun-v1.0) everyone is amazed and I was able to play around with it today, and I found it is pretty awesome to use it alongside with [Hono](https://hono.dev/), [Vite](https://vitejs.dev/) and [TailwindCSS](https://tailwindcss.com/).

Without further ado, here I show you the things I have done.

First, create a project with bun, its way way faster than usual.

```sh
bun create hono my-hono-app
cd my-hono-app
bun install
```

For demo purpose, we'll use some popular libs e.g. [jose](https://github.com/panva/jose).

```sh
bun add jose
```

Change the `tsconfig.json` slightly to be like so:

```json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx",
    "types": [
      "bun-types"
    ],
  }
}
```

Let's put some code making use of `jose`, e.g. create a jwt by ourself, and we place it in `src/core.ts`: 

```ts
import * as jose from "jose";

const secret = "🔥🔥🔥🔥🔥🔥🔥🔥🔥";
const algo = "HS512";

const secretBytes = new TextEncoder().encode(secret);
export const secretB64 = btoa(
  Array.from(secretBytes, (x) => String.fromCodePoint(x)).join("")
);

export const createJWT = async () => {
  return await new jose.SignJWT({
    claims: {
      userId: "ridho",
    },
  })
    .setProtectedHeader({ alg: algo })
    .setAudience("urn:audience")
    .setIssuer("urn:issuer")
    .setSubject("urn:subject")
    .setIssuedAt()
    .setExpirationTime("5m")
    .sign(secretBytes);
};
```

Take note from the above code on how we should create base64 string based on this [MDN](https://developer.mozilla.org/en-US/docs/Glossary/Base64) guide. Later we can validate manually if our jwt and the secretBytes is correct by pasting the token and secret to [jwt.io](https://jwt.io/).

Now lets modify the server entry file in `src/index.ts`.

```ts
import { Hono } from "hono";
import { createJWT, secretB64 } from "./core";

const app = new Hono();

app.get("/", async (c) =>
  c.text(`Secret: ${secretB64} JWT: ${await createJWT()}`)
);

export default app;
```

Yes, its ready. We can now start the development server:

```sh
bun run --hot src/index.ts
```

Great, thats it, very simple and easy! With just very little effort and a few files below, we can serve about 100k request per seconds.

```sh
tree -I node_modules 
.
├── bun.lockb
├── package.json
├── README.md
├── src
│   ├── core.ts
│   └── index.ts
└── tsconfig.json

2 directories, 6 files
```

Ok, lets go further. Instead of simple text response, we want to serve html. Here is how:

```ts
import { html } from "hono/html";

app.get("/hello", (c) =>
  c.html(html`<!DOCTYPE html>
    <html>
      <head>
        <title>Simple demo</title>
      </head>
      <body>
        <h1>Ok</h1>
      </body>
    </html>`)
);
```

I want to write content with jsx you say. Ok, its easy too. First create our jsx file `hello.tsx` as below:

```tsx
export function Hello() {
  return (
    <div>
      <h1>Hello there!</h1>
    </div>
  );
}
```

And use it just like usual, in this simple case, we'll put the jsx into our previous html body:

```tsx
import { html } from "hono/html";
import { jsx } from "hono/jsx";
import { Hello } from "./hello";

app.get("/hello", (c) => {
  const content = jsx(Hello, {});  // <-- HERE IS our JSX content
  return c.html(html`<!DOCTYPE html>
    <html>
      <head>
        <title>Simple demo</title>
      </head>
      <body>
        ${content}
      </body>
    </html>`);
});
```

Yeah, that is awesome, and there is more! We can have the great and famous Vite and all it stuffs, e.g. tailwindcss to works too. 

There is [`@hono/vite-dev-server`](https://github.com/honojs/vite-plugins) that is being actively developed by the author and for sure it will become great when its ready.

During this time, I've explored how to manually integrate Vite with all of our stuff above so far. It will be just brief steps, i believe you could follow along. 

Add the necessary deps:

```sh
bun add --dev vite tailwindcss postcss autoprefixer
```

Init tailwindcss config:

```sh
bunx tailwindcss init --esm --postcss
```

Create `vite.config.ts`:

```ts
import { defineConfig } from "vite";

export default defineConfig({
  build: {
    manifest: true,
    rollupOptions: {
      input: "/src/client.tsx",
    },
  },
});
```

In `src/client.tsx` is where our client code live and managed by vite. For now, it simply contains import to tailwind css entry file.

`tailwind.config.js`:

```js
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.tsx"],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

`postcss.config.js`:

```js
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
```

Add `type=module` to `package.json`:

```json
{
  "type": "module",
  "scripts": {
    "dev": "bun run --hot src/index.ts",
    "vite-dev": "vite",
    "build": "vite build",
    "serve": "NODE_ENV=production bun run src/index.ts"
  },
  "dependencies": {
    "hono": "^3.5.4",
    "jose": "latest"
  },
  "devDependencies": {
    "autoprefixer": "^10.4.15",
    "bun-types": "^0.6.2",
    "postcss": "^8.4.29",
    "tailwindcss": "^3.3.3",
    "vite": "^4.4.9"
  }
}
```

Add or modify `target`, `module` and `moduleResolution` to `tsconfig.json`:

```json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx",
    "types": [
      "bun-types"
    ],
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
  }
}
```

`src/style.css`:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

`src/client.tsx`:

```tsx
import "./style.css"
```

`src/hello.tsx`:

```tsx
export function Hello() {
  return (
    <div class="text-2xl text-red-600 grid h-[100dvh] items-center justify-center text-center">
      <div>
        <h1 class="text-4xl font-bold">Hello there!</h1>
        <p class="font-mono">
          Bun, Hono, Vite, TailwindCSS <br />
          🔥🔥🔥🔥🔥🔥🔥🔥🔥
        </p>
      </div>
    </div>
  );
}
```

`src/index.ts`:

```ts
import { Hono } from "hono";
import { serveStatic } from "hono/bun";
import { html, raw } from "hono/html";
import { jsx } from "hono/jsx";

import { createJWT, secretB64 } from "./core";
import { Hello } from "./hello";

const app = new Hono();

app.use("/assets/*", serveStatic({ root: "./dist" }));

app.get("/", async (c) =>
  c.text(`Secret: ${secretB64} JWT: ${await createJWT()}`)
);

const isProd = process.env.NODE_ENV === "production";

const manifestPath = "../dist/manifest.json";
const cssFile = isProd
  ? (await import(manifestPath)).default["src/client.tsx"]?.css?.at(0)
  : null;

app.get("/hello", (c) => {
  const content = jsx(Hello, {});
  return c.html(html`<!DOCTYPE html>
    <html>
      <head>
        <title>Simple demo</title>
        ${cssFile ? raw(`<link rel="stylesheet" href="${cssFile}">`) : null}
      </head>
      <body class="bg-green-200">
        ${content}
        ${isProd
          ? null
          : raw(`<script
				type="module"
				src="http://localhost:5173/@vite/client"
			></script>
			<script
				type="module"
				src="http://localhost:5173/src/client.tsx"
			></script>`)}
      </body>
    </html>`);
});

export default app;
```

To start development, start bun and vite on separate terminal:

```sh
bun run vite-dev
```

```sh
bun run dev
```

For production, do build and run:

```sh
bun run build
bun run serve
```

Thats it. 

Thank you for reading, have a good weekend!