RangerMauve / webrun

Run Web-first ESM modules outside of the browser
https://webrun-presentation.hashbase.io/
MIT License
76 stars 4 forks source link

WebRun

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

Usage:

# 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.

STDIO

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())

CLI arguments

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')

Web API support

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.

API

Plugins

Help it's not working!

How it works:

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.

Roadmap: