DEV Community

Cover image for Live reloading HTML with Bun
aabccd021
aabccd021

Posted on

1

Live reloading HTML with Bun

TL;DR

This articles shows how I made bun-html-live-reload.
The code will refresh the browser everytime the server is hot-reloaded, by sending Server Sent Events (SSE).

Introduction

Building server-rendered website using Bun's builtin HTTP server is pretty easy.

// server.ts
Bun.serve({
  fetch: () => {
    return new Response("<h1>Hello, World!</h1>", {
      headers: { "Content-Type": "text/html" },
    });
  },
});
Enter fullscreen mode Exit fullscreen mode

You can run this code with hot reload enabled by running bun --hot server.tscommand. And now everytime you change the html content, Bun will automatically reload the server.

Problem with this approach is that, while the server is reloaded, the browser is not.
So you don't see the changes in the browser, and you have to manually refresh the page yourself.

Instead, we want to live reload our HTML like this.

In this article, we will build a simple live reload mechanism for HTML content,
so everytime you change the server.ts file, or any other files that imported by it,
the browser will automatically refresh the page, and you will see the changes immediately.

The end result will look like this from user's perspective:

// server.ts
import { withHtmlLiveReload } from "bun-html-live-reload";

Bun.serve({
  fetch: withHtmlLiveReload(async (req) => {
    return new Response("<h1>Hello, World!</h1>", {
      headers: { "Content-Type": "text/html" },
    });
  }),
});
Enter fullscreen mode Exit fullscreen mode

The idea (Server Sent Events)

The idea is very simple. Everytime the server is reloaded, we will tell the browser to refresh the page.
We can either use WebSockets or Server Sent Events (SSE) to achieve this.

In this article we will use SSE for these reasons:

  • The message is one way (server to browser), not bidirectional.
  • No need to upgrade the connection like WebSocket.

Sending SSE message is pretty simple.
We will need to:

  • Prepare endpoint to send the message.
  • Inject SSE listener script to every HTML response.

Understanding Bun's hot reload

Before sending the reload message, we need to understand how Bun's hot reload works.
Let's run following code with bun --hot server.ts:

// server.ts
let message = "hello";
console.log(message);
Enter fullscreen mode Exit fullscreen mode

As expected, the server will print hello to the console.

Without toucing the terminal, if we edit the message and save the file.

// server.ts
let message = "hello world";
console.log(message);
Enter fullscreen mode Exit fullscreen mode

The server will print hello world to the console.

So basically everytime we change the file, Bun will re-run all the top level code in the file.

In the next section, we will need to persist a variable between reloads.
To do that, we can use globalThis.

globalThis.counter = globalThis.counter ?? 0;
globalThis.counter += 1;

console.log("Counter: ", counter);
Enter fullscreen mode Exit fullscreen mode

Everytime we change our code, the counter will be increased by 1.

For type safetiness in typescript, we can define the type of the variable in a global scope.

declare global {
  var counter: number | undefined;
}

globalThis.counter = globalThis.counter ?? 0;
globalThis.counter += 1;

console.log("Counter: ", counter);
Enter fullscreen mode Exit fullscreen mode

Listening to SSE event

To make browser listen to SSE event, we will use EventSource API.
The /__bun_live_reload endpoint will be used as the source of the event.
Then we will use location.reload() to refresh the browser everytime the event is received.

new EventSource("/__bun_live_reload").onmessage = () => {
  location.reload();
};
Enter fullscreen mode Exit fullscreen mode

We will inject this script to every HTML response on the later section.

// TODO: Inject this script to every HTML response
const liveReloadScript = `
<script>
  new EventSource("/__bun_live_reload").onmessage = () => {
    location.reload();
  };
</script>
`;
Enter fullscreen mode Exit fullscreen mode

Injecting the script

To inject the script to every HTML response, we will create a wrapper for fetch function.

// bun-html-live-reload.ts
type Fetch = (req: Request) => Promise<Response>;
export function withHtmlLiveReload(handler: Fetch): Fetch {
  return async (req) => {
    const response = await handler(req);
    // TODO: Inject the script to every HTML response
    return response;
  };
}
Enter fullscreen mode Exit fullscreen mode

For simplicity of this tutorial, we will simply append the client's live reload script to the end of the HTML content.

export function withHtmlLiveReload(handler: Fetch): Fetch {
  return async (req) => {
    const response = await handler(req);
    const htmlText = await response.text();
    const newHtmlText = htmlText + liveReloadScript;
    return new Response(newHtmlText, { headers: response.headers });
  };
}
Enter fullscreen mode Exit fullscreen mode

Sending the SSE message

To send the SSE message, we will need to create a new endpoint /__bun_live_reload, and return ReadableStream object as the response.

Don't forget to add text/event-stream as the Content-Type header.

export function withHtmlLiveReload(handler: Fetch): Fetch {
  return async (req) => {
    const response = await handler(req);

    if (new URL(req.url).pathname === "/__bun_live_reload") {
      const stream = new ReadableStream({
        start(client) {},
      });
      return new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
        },
      });
    }

    // ...
    return response;
  };
}
Enter fullscreen mode Exit fullscreen mode

Then we can save the client object as a global variable.

After that, on the next reload, we can send the message to the client by calling enqueue on the top level.

// withHtmlLiveReload
const stream = new ReadableStream({
  start(client) {
    globalThis.client = client;
  },
});
// withHtmlLiveReload

// This will send the message to the client
globalThis.client?.enqueue("data:\n\n");
Enter fullscreen mode Exit fullscreen mode

The message string data:\n\n is the minimum message that required to trigger the event. You can add more data here if you want to send it to the client.

Also you might want to add type definition for this

declare global {
  var client: ReadableStreamDefaultController | undefined;
}
Enter fullscreen mode Exit fullscreen mode

Putting it all together

Here is the full code of bun-html-live-reload.ts:

// bun-html-live-reload.ts
declare global {
  var client: ReadableStreamDefaultController | undefined;
}

type Fetch = (req: Request) => Promise<Response>;

globalThis.client?.enqueue("data:\n\n");

const liveReloadScript = `
  <script>
    new EventSource("/__bun_live_reload").onmessage = () => {
      location.reload();
    };
  </script>
`;

export function withHtmlLiveReload(handler: Fetch): Fetch {
  return async (req) => {

    if (new URL(req.url).pathname === "/__bun_live_reload") {
      const stream = new ReadableStream({
        start(client) {
          globalThis.client = client;
        },
      });
      return new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
        },
      });
    }

    const response = await handler(req);
    const htmlText = await response.text();
    const newHtmlText = htmlText + liveReloadScript;
    return new Response(newHtmlText, { headers: response.headers });
  };
}
Enter fullscreen mode Exit fullscreen mode

Hot sauce if you're wrong - web dev trivia for staff engineers

Hot sauce if you're wrong ยท web dev trivia for staff engineers (Chris vs Jeremy, Leet Heat S1.E4)

  • Shipping Fast: Test your knowledge of deployment strategies and techniques
  • Authentication: Prove you know your OAuth from your JWT
  • CSS: Demonstrate your styling expertise under pressure
  • Acronyms: Decode the alphabet soup of web development
  • Accessibility: Show your commitment to building for everyone

Contestants must answer rapid-fire questions across the full stack of modern web development. Get it right, earn points. Get it wrong? The spice level goes up!

Watch Video ๐ŸŒถ๏ธ๐Ÿ”ฅ

Top comments (0)

Image of DataStax

AI Agents Made Easy with Langflow

Connect models, vector stores, memory and other AI building blocks with the click of a button to build and deploy AI-powered agents.

Get started for free

๐Ÿ‘‹ Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someoneโ€™s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay