Running Node.js in the browser

Adam Zieliński Avatar

Posted by

on

When I’ve shared the new PHP Playground beta (with a terminal and a file explorer), Jon Surrell asked me what was still missing before we can create Gutenberg blocks. The answer was Node.js. On Thursday, it seemed nearly impossible. On Wednesday, I had a working prototype.

It can already run npm, webpack, and more. With more work, it could support @wordpress/scripts and unlock building WordPress blocks in the browser!

The prototype

You’ll find the source code with setup instructions in the adamziel/node-js-in-the-browser GitHub repo. Sadly, I don’t have a demo link to share but hey, here’s a screenshot:

Screenshot of a terminal interface displaying Node.js commands for installing and running a demo project called 'demo-cowsay'.

You can build a simple tsx file with WebPack using these commands:

npm install --verbose
node ./node_modules/.bin/webpack build
ls dist
cat dist/bundle.js

Why is Node.js needed to build WordPress blocks?

Because the WordPress documentation trains developers to write JSX code use the @wordpress/scripts npm package to transform that JSX to plain .js code, a bunch of .css files, and a .php manifest. Under the hood, WordPress scripts use webpack, babel, and a bunch of custom transforms to replace import { Paragraph } from "@wordpress/blocks" with window.wp.blocks.Paragraph.

I’m not compiling Node.js to WASM!

That would be a huge project. You’d need to build the entire v8 JavaScript engine, all its dependencies, their dependencies, etc. Then libuv. Then you’d need to make it work with browser’s asynchronous filesystem. Then, …. You get the idea. It’s a lot and I don’t think that’s worth it. Besides, native Node.js installation is ~80MB on my mac, which a static WASM build would likely double or more. And there would be a speed penalty for running JS VM inside a JS VM.

Instead, I’m eval()-ing Node.js JavaScript in the browser

Node.js programs are just JavaScript. Every browser already ships a good JavaScript runtime. We can take advantage of that!

What separates JavaScript written for Node.js from JavaScript written for the browser? The standard library. We can call require("fs").readFileSync() in Node.js, but not in the browser. I used to think that fs and other standard modules are written in C, but I’ve just learned they’re written in JavaScript!

There’s a catch, though. The JavaScript standard library uses a layer of lower level C bindings to interact with your hardware. For example, import { rmSync } from "node:fs" delegates some work to internalBinding('fs').rmdir();. Luckily, those internal C bindings have a much smaller surface than the JS stdlib. You can polyfill them, initialize all the expected global variables, load the Node.js stdlib in the browser, and now you’re able to run webpack and other npm packages.

And that’s exactly how the node-js-in-the-browser demo works.

Initially I’ve tried browserify and a few other projects reimplementing Node standard library for web browsers. They’re tempting because they seem mature and they ship much less code. Unfortunately, npm and other packages rely on highly nuanced behaviors of the Node.js standard library that those polyfills lacked.

It’s promising, but there’s still a long way to go!

The proof of concept is lean and does just enough to run npm, webpack, and some other popular packages. At least a few features required for running @wordpress/scripts are missing:

So while these early results are encouraging and I hope to continue that work in the future, it needs more effort before it’s a drop-in replacement. I’m setting this work aside for now to help finalize XDebug, PHPMyAdmin support, and a few other WordPress Playground–related projects.

Leave a Reply

Discover more from Adam's Perspective

Subscribe now to keep reading and get access to the full archive.

Continue reading