Scope & non-goals
esrun is a secure, standards-based runtime for running server-side JavaScript — and nothing more. Its scope is deliberately narrow. Knowing what it does not do is as important as knowing what it does.
Some of that narrowness is a division of labour, not an absence: TypeScript, bundling, testing, watching and debugging live in a second binary, esdev, which never reaches production. The rest below are durable boundaries for the whole project.
Running standard ES Modules on V8 with a Web-standard API surface, deny-by-default capabilities, a sandboxed module system, and host functionality exposed through runtime: modules — as a server runtime, shipped as the esrun binary.
Non-goals
These are explicit, durable boundaries — not missing features awaiting implementation.
No Node.js compatibility. esrun is not a drop-in for Node.js. There is no
node:builtin namespace, no Node.js globals (noprocessglobal, noBuffer, norequire), and no npm lifecycle. Code targets Web standards plus theruntime:modules, not the Node.js API.No CommonJS at runtime. Modules are real ES Modules only. There is no
require(), nomodule.exports, and no CJS↔ESM interop; a CommonJS-only package is rejected with a clear error. It is converted at build time instead —esdev buildabsorbs CJS→ESM and esrun receives ordinary ESM, so a deployed artifact stays the text that was reviewed.No remote module imports.
import "https://..."is explicitly rejected. Load data through aruntime:netAPI instead, keeping code locally verified and safe from supply-chain hijacking.No package installer. esrun resolves an existing
node_modulestree but does not install anything. Use your package manager (npm, pnpm, bun) to populate dependencies.No linter or formatter.
oxlintandoxfmtare npm binaries that already do this well; a shim around them would add a version matrix for no gain.No FFI or native addons. There is no foreign-function interface and no native addon ABI. The host extends the runtime through injected providers and capability-gated ops, in Rust.
No
SharedWorker, and nodata:/blob:worker URLs. The dedicatedWorkerships (see Workers); the shared one exists to share a worker between documents, and there are none. A worker's URL names a file.
Not in esrun — in esdev
These are development tooling, and the binary that serves production is the one that should have none of it. An inspector port in particular is a total bypass of the capability model: attach and you own the isolate, whatever the flags said.
| TypeScript & JSX | esdev app.ts strips types as files load — erased, never checked |
| A bundler | esdev build — one deployable ES module, and how CommonJS becomes runnable |
| A test runner | esdev test — one process per file |
| Watch mode | esdev --watch — a restart is a SIGTERM, so it drains |
| A debugger | esdev --inspect — and even there, only in a build that asked for it |
| Permission discovery | esdev --trace-permissions — prints the esrun line to deploy with |
esdev shares every line that decides how a run behaves, so a program cannot behave one way there and another in production. It is not a deployment target.
One runtime, not a platform
A server runtime is the whole goal. There is no embedding API on offer: the Rust crates this is built from are not published, and running JavaScript inside somebody else's process is a different product with different goals — it is not part of this project. If you need it, open an issue and say what for; until somebody does, shipping an API nobody asked for would fix its shape before its requirements are known.
Multiple isolates under host control — the "es-vm" idea that used to be listed here as a second layer — went with it.
Workers are the part that stayed, and they ship: a Worker is a real second agent on a real thread, spawned through the same provider seam as every other reach outside the isolate.