DEV Community

Cover image for The same snippet in Bun and in a browser
JSLab
JSLab

Posted on

The same snippet in Bun and in a browser

"Universal JavaScript" sounds simple until your helper touches document, process or fs. Here's a quick way to sort what's portable from what isn't, and a pattern for the bits that aren't.

The portable core

A surprising amount of the platform is the same in Bun and in a browser page. All of this runs unchanged in both:

const url = new URL("https://example.com/search?q=bun&page=2");
url.searchParams.get("page");                 // "2"

const copy = structuredClone(new Map([["a", { n: 1 }]]));
copy.get("a");                                // { n: 1 }

new TextEncoder().encode("héllo").length;     // 6 bytes, 5 characters

new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-1, "day"); // "yesterday"

crypto.randomUUID().length;                   // 36
Enter fullscreen mode Exit fullscreen mode

URL, structuredClone, TextEncoder, Intl, crypto.randomUUID, fetch, setTimeout, AbortController: if your helper sticks to these, it's portable already.

What differs

Run these three lines and the answers depend on where you are:

"document" in globalThis;
"Bun" in globalThis;
globalThis.process?.versions?.bun ?? "not Bun";
Enter fullscreen mode Exit fullscreen mode

In Bun: false, true, and a version string. In a browser page: true, false, "not Bun".

That's feature detection: ask for the thing you need, not for the name of the platform.

The pattern: detect once, at the edge

Keep the environment-specific bit in one small function and keep everything else portable:

function whereAmI(): string {
  if (typeof document !== "undefined") {
    return `a page titled "${document.title || "untitled"}"`;
  }
  const bun = (globalThis as any).process?.versions?.bun;
  return bun ? `Bun ${bun}` : "somewhere else";
}

whereAmI();
Enter fullscreen mode Exit fullscreen mode

typeof document is safe even where document doesn't exist, which is why it's the classic check. The rest of your module never needs to know.

Trying both without two setups

Checking this usually means a Node or Bun script and an HTML page. In JSLab, a JavaScript & TypeScript scratchpad for macOS, each tab picks its runtime from the status bar:

  • Bun (the default): Bun and Node globals and the full Node API surface, no DOM.
  • Browser: a real page, with no Node APIs at all.
  • Browser & Node APIs: a real page plus a Node compatibility layer (fs/promises, child_process, a fetch with no CORS restriction).

Switching the runtime re-runs the tab, so you paste the snippet once, flip the menu, and read the new answers on the same lines.

Here are those three lines in a real JSLab tab, run twice. On top, in Bun: false, true, 1.4.0. Below, after switching the status-bar menu to Browser & Node APIs: true, false, not Bun. Line 7 reads a folder with node:fs/promises, which both of these runtimes allow.

The same JSLab tab shown twice. Top, with Bun selected in the status bar: "document" in globalThis is false, "Bun" in globalThis is true, and the Bun version line is 1.4.0. Bottom, with Browser & Node APIs selected: true, false and not Bun. Line 7, a readdir from node:fs/promises, returns a file count in both.

Two things that only show up in the browser runtimes, and are worth knowing for real browser code too:

  • alert, confirm and prompt can't block there: confirm always returns false and prompt returns null.
  • The plain Browser runtime keeps real CORS, so a fetch behaves the way it would on a website.

Rule of thumb

Write against the web platform first, detect features at the edge, and test in both places before you call it universal.

JSLab launches Nov 15. The runtimes page: https://js-lab.app/docs/runtimes

Top comments (0)