Skip to main content

9 posts tagged with "Runtime"

The Node-compatible runtime that executes inside the tab.

View All Tags

Bun runs in the tab and there is no Bun in it, so the interesting part is what refuses

· 11 min read

Bun is a single native binary written in Zig around JavaScriptCore. There is no wasm32 build of it, there is not going to be one soon, and a browser tab cannot execute a Mach-O or ELF file regardless.

So bun index.ts in a page is not Bun. It is Bun's API, implemented on top of the Node runtime these posts keep describing, and the honest version of that sentence is the whole subject here. A compatibility shim is only useful if you can tell, from inside it, which parts are real. Most of this post is about the parts that are not, and how they say so.

Node can require() an ES module now, and it refuses two things. We could not refuse either

· 15 min read

Recent Node versions will let you require() an ES module. It is a genuinely hard thing to have shipped, and it comes with two documented refusals: ERR_REQUIRE_ASYNC_MODULE if anything in the required graph uses top-level await, and ERR_REQUIRE_CYCLE_MODULE if the graph has a cycle that crosses the CommonJS boundary.

Node can refuse, because import() is always sitting there as an escape hatch. Tell the user to await it and the problem is theirs.

In a browser worker there is no escape hatch. require() is synchronous all the way down because the filesystem under it is, and a project's entry point is required by a loader that cannot return a promise to anyone. Every import becomes CommonJS at load time or the program does not start. Refusing was not on the menu.

A step debugger with no inspector to talk to, and the second SharedArrayBuffer that makes it pause

· 12 min read

A breakpoint is not a feature you write. It is a favour the engine does you. When you set one in VS Code, nothing in your program changes: V8's inspector holds the isolate, walks the real call stack, and hands back scopes it already had. Every step debugger you have used is a thin client in front of that.

A Web Worker has no inspector. There is no --inspect port to open, no inspector binding to require, no way to ask the engine to stop. So the first question here was not how to build a debug UI. It was where a pause could possibly come from.

Flask, Django and FastAPI answering real requests, with no socket underneath

· 15 min read

Every Python web framework bottoms out in the same two lines, whatever it calls them:

sock.bind((host, port))
sock.listen(backlog)

A browser tab does not have that. There is no TCP stack in a page, no file descriptor to bind, and no amount of WebAssembly changes it. So the interesting question is not whether you can run Flask's Python in a browser, because you can. It is what happens when someone types flask run.

Next.js 16 renders React Server Components in a browser tab, and the AsyncLocalStorage trap

· 8 min read

For most of this project's life, our notes said Next.js was out of reach. The reasoning looked solid: Next compiles with SWC, SWC is native Rust, there is no native code in a browser tab, therefore no Next.js. A hard wall, filed away.

That verdict was wrong, and it was wrong in the most ordinary way: we had decided something was impossible and then stopped rechecking it.

next dev --webpack now boots inside a browser tab, compiles an App Router page, renders React Server Components, and answers GET / → 200 with real HTML. No server. The kernel, the filesystem, the process model, the dev server and the React render all live in one tab.

Getting there was mostly unremarkable engineering, plus one problem that has no correct solution.

Real npm, yarn and pnpm in the browser, and how each one broke the runtime

· 8 min read

Once you are running Node's real lib/ in a tab, the interesting question stops being "can we reimplement npm?" and becomes "why would we?". The package managers are just Node programs. If the runtime underneath them is honest, the real CLIs should run unmodified.

They do. npm, yarn, pnpm and corepack in Vivari are the actual published releases, vendored and executed as-is. But "should run" and "runs" are separated by every Node API each tool happens to touch, and the four of them touch almost disjoint sets. Each one, in turn, walked into a different corner of the runtime and found the wall.

Running Node's real lib/ in a browser tab

· 6 min read

There are two ways to give a browser a Node-compatible runtime, and for a long time we were confidently building the wrong one.

Path A is the obvious one: hand-write the core modules. Implement fs on top of your virtual filesystem, implement path as string manipulation, implement events as a small emitter, and keep going. It feels productive immediately. path takes an afternoon. events takes a morning. fs takes a week and mostly works.

Then you reach stream, and progress stops.

The one browser API that makes a Node runtime possible

· 8 min read

Every browser-based Node runtime runs into the same wall on day one, and it is not the filesystem, the module resolver, or the process model. It is one line of code:

const config = fs.readFileSync("/app/package.json", "utf8");

That call has to return bytes. Not a promise, not a callback: bytes, on the next line. And reading those bytes means asking something else for them, which in a browser means waiting. Browsers are built on exactly one promise to the user: nothing blocks. So the very first thing Node requires is the one thing the platform refuses to do.

There is precisely one exception, and this post is about building on top of it.