DEV Community

Cover image for One editor, three runtimes: Bun, Browser, and the one in between
JSLab
JSLab

Posted on

One editor, three runtimes: Bun, Browser, and the one in between

Here are five lines of JavaScript:

"document" in globalThis;
"Bun" in globalThis;
globalThis.process?.versions?.bun ?? "not Bun";

import { readdir } from "node:fs/promises";
(await readdir(".")).length;
Enter fullscreen mode Exit fullscreen mode

What do they print?

It depends on where they run. Run them in Bun and there's no document, there is a Bun, and readdir counts your files. Run them in a browser and document is there, Bun isn't, and that node:fs import doesn't make it past the build.

Most of us switch between those worlds all day. This post is about turning that into one switch per tab, in JSLab. It's our own tool, so discount the praise accordingly. The snippet above comes straight from the runtime docs.

Three runtimes, one menu

Each JSLab tab runs its code in one of three runtimes. You pick one from the runtime menu in the status bar, from Actions → Runtime, or from the command palette.

Runtime What your code gets Web View
Bun (default) Bun and Node globals and the full Node API surface, with no DOM No
Browser & Node APIs A real page, with the DOM and web APIs, plus a Node compatibility layer Yes
Browser A real page only, with no Node APIs at all Yes

Your code is bundled before it runs, so importing an installed package works in all three.

Bun: the default

New tabs open in Bun. Your code runs in a separate Bun process, using the Bun that ships inside JSLab. You get fs, path, child_process, http and the rest, and no DOM.

This is where scripts, data wrangling and algorithm sketches go. Because it's a separate process, an infinite loop or a crash doesn't take the editor down with it.

Browser: a real page, nothing else

The Browser runtime is a real WebKit page with no Node at all: a blank HTML page with your bundled code running in it.

document.body.innerHTML = "<p>Plain browser page</p>";
navigator.userAgent.includes("AppleWebKit");
Enter fullscreen mode Exit fullscreen mode

fetch is the page's own, and CORS applies exactly as it would on a website. That's the point: if you want to know what a site would actually see, this is the honest runtime. Import a Node module and the build fails with a code frame pointing at the import.

Browser & Node APIs: the one in between

The surprising one. It's a real page with document, canvas and requestAnimationFrame, plus a Node compatibility layer bridged out to JSLab:

import { createHash } from "node:crypto";
import os from "node:os";

document.body.innerHTML = `<h1>Hello from ${os.platform()}</h1>`;
createHash("sha256").update("JSLab").digest("hex");
document.querySelector("h1");
Enter fullscreen mode Exit fullscreen mode

A page that renders a heading and hashes a string with Node's crypto, in one file, with no server.

You get fs/promises and the callback-style fs functions, child_process.exec, execFile and spawn, plus os, path, crypto, buffer, events and friends. And fetch goes through JSLab, so it reaches any URL without CORS getting in the way:

const response = await fetch("https://api.github.com/zen");
response.status;
await response.text();
Enter fullscreen mode Exit fullscreen mode

It's not all of Node. Synchronous fs, http, net, tls, dgram, worker_threads and vm aren't available. Call one and the message offers to switch the tab to Bun. For a synchronous fs call, it also names the async alternative.

The Web View

Both browser runtimes render into a Web View tile beside the console. Bun has no page, so it has no Web View. Toggle it with ⌥⌘W.

Every run reloads the page, so the DOM starts clean each time. Nothing from the last run hangs around. The page starts with a single <div id="root">.

Now the fun part: React. You can mount a component the usual way, with createRoot, and it renders in the Web View like it would on any page:

A React Counter component in a JSLab Browser tab with TSX, mounted by hand with createRoot from react-dom/client. The Web View tile under the output pane shows the rendered component: Ticks: 24.

But you don't have to. Default-export a component and JSLab renders it for you:

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>Hello from React</h1>
      <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

No createRoot, no index.html, no dev server. Auto Run fires a moment after you stop typing, so you change the JSX and the component rerenders in the tile.

JSLab works out the framework from your imports: React, Vue, Solid or Angular. The Framework menu in the status bar shows what it found, and you can override it. You need the framework's renderer package installed (react-dom for React); if it's missing, the console tells you which one. The quickest start is the + button in the tab bar. Pick a framework and JSLab sets the tab's language, runtime and framework, installs the packages and opens starter code.

It isn't only components. Canvas, Three.js and Web Audio run in the Web View as they would in a browser.

The rule of thumb

You can use this even if you never touch JSLab. Choose a runtime by asking what your code needs to reach:

  • Only data and the file system? Use a Node-style runtime. No DOM means no accidental browser assumptions.
  • Only a page? Use a plain browser with real CORS, so what works is what will ship.
  • A page and a file or an API? That's the in-between case, the one we usually solve with a dev server, a proxy and a script.

Try it

JSLab is a JavaScript and TypeScript scratchpad for Apple Silicon Macs on macOS 14 or later, with a 14-day free trial. It launches on November 15. Until then, read Choosing a runtime and The Web View. Then guess what those five lines print in each runtime, before you can check.

Top comments (0)