A custom module loader and global shim for Node to make it compatible with the browser.
The goal is to make code that works in browsers first, but can also run anywhere that Node runs.
**NOTE: The module has migrated from @rangermauve/webrun
to just webrun
# Install the CLI
npm install -g webrun
# Run it without installing globally
npx webrun "https://rangermauve.hashbase.io/example.js"
# Load a module from the web and log to the console
webrun "https://rangermauve.hashbase.io/example.js"
# Run a local file
webrun ./example.js
Then in your JS:
// Load code from an HTTPS server
import example from "https://rangermauve.hashbase.io/esm.js";
// Load from the dat network
// Requires the `webrun-plugin-dat` module to be installed
import datExample from "dat://rangermauve.hashbase.io/esm.js";
// Load from the IPFS network. Might not always be online.
// Requires the `webrun-plugin-ipfs` module to be installed
import ipfsExample from "ipfs://QmTWdgJtp3fXaszsomragX8dPXsqWe5c8uQETy6NkFJ7xA";
example();
datExample();
ipfsExample();
You can opt-into dat and IPFS support by installing the webrun-plugin-dat or webrun-plugin-ipfs modules.
You can start a REPL using:
webrun
Then you can load modules using the new dynamic import syntax.
This will return a promise that contains all the exported properties.
If you want to load the default export you can use something like the following:
let {default: example} = await import("https://rangermauve.hashbase.io/esm.js")
example()
You can enable the require
global by adding the --allow-require
flag. This is disabled by default to encourage use of import
and to limit what scripts can do. This behaves differently from the usual require in that it's a global and always requires relative to the current working directory. Instead of adding allow-require, though, you should use webrunify to build your CommonJS dependencies into a single ESM-compatible bundle.
You can opt-into input from STDIN and output to STDOUT using self.onmessage
and self.postMessage
.
These are the same APIs that exist for iframes and WebWorkers which means that your worker code can potentially run in webrun and vice-versa.
// Get text from STDIN, uppercase it, send it to STDOUT
self.onmessage = (text) => self.postMessage((text+"").toUpperCase())
We want to be as close to the web as possible, so instead of adding a non-standard global for CLI arguments, we pass them in as query string params.
You can access the URL of the current module using the new import.meta.url syntax.
For example, given the file example.js
:
console.log(`My URL is: ${import.meta.url}`)
Running this:
webrun example.js --foo bar --fizz buzz
Will result in
My URL is file://whatever/the/path/us/example.js?foo=bar&fizz=buzz
You can access these arguemnts using the following
const url = new URL(import.meta.url)
const foo = url.searchParams.get('foo')
const fizz = url.searchParams.get('fizz')
Here's a list of the APIs that are supported, or are going to be supported eventually. Feel free to open an issue if you have ideas about other APIs that can be added.
- console.*
- timers (setInterval, setTimeout, etc)
- atob and btoa
- fetch
- websocket
- localStorage (Persists to
./.webrun/localstorage
) - sessionStorage (same as localStorage, clears itself after exiting the parent process)
- crypto.randomBytes() using node's crypto module.
- TextEncoder / TextDecoder
- WindowEventHandlers
- self.close()
- self.postMessage / self.onmessage
- EventTarget
- URL
- WHATWG Streams
- EventSource
- WebRTC
- Keypress and Mousemove events
- SubtleCrypto (or with this)
- Cache Storage
- self.navigator
- self.location
- indexDB (Doesn't persist, removed)
- libdweb APIs
- Web Bluetooth
- WebMIDI APi
- Delete the
.webrun
folder in the current directory. This will clear the cache - If that doesn't work, raise an issue.
The new experimental-modules feature in Node.js is great, but it currently only works with file:
URLs.
This means that modules made for the web are totally incompatible with Node. As a result, there's now four environments to code against: The legacy web with script tags, the web with ESM, Legacy CommonJS modules, and ESM in Node.js.
Luckily, node's vm builtin module now has support for custom ESM loaders. This means that we can now create a context that's separated from Node's globals and use anything we want for loading the contents of modules.
That's exactly how this works. It intercepts calls to https://
imports, downloads the content to the ./.webrun/web-cache
folder, and loads it with the VM module.
Some browser APIs have been added to the global scope so hopefully a lot of modules made for browsers should work here, too. Feel free to open an issue to add your favorite missing browser API.
In addition to loading content from https://
URLs, this loader also supports dat://
URLs. This way you can download code right from the peer to peer web!
You can still load Node modules by using require
, but this should only be done for APIs that you absolutely can't get on the web because otherwise your code won't be portable to the web.
PRs for additional protocols are welcome! All you need is an async function that takes a URL, and returns the file content string.
- Able to load from the filesystem
- Able to load HTTPS URLs
- Able to load Dat URLs
- Able to load using
reqire
(behind a flag) - Browser APIs
- Dat protocol support
- Load from Dat URLs
- DatArchive global
- Experimental Beaker APIs (does it make sense?)
- DatPeers
- Library
- IPFS
- Load from IPFS URLs
- Load from IPNS URLs
- ipfs global
- CLI arguments: Add them to searchParams for the URL being loaded