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.

In scope

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 (no process global, no Buffer, no require), and no npm lifecycle. Code targets Web standards plus the runtime: modules, not the Node.js API.

  • No CommonJS at runtime. Modules are real ES Modules only. There is no require(), no module.exports, and no CJS↔ESM interop; a CommonJS-only package is rejected with a clear error. It is converted by esdev instead — esdev build absorbs CJS→ESM for the deployed artifact, and esdev itself converts on import while developing or testing — so esrun receives ordinary ESM, and a deployed artifact stays the text that was reviewed.

  • No remote module imports. import "https://..." is explicitly rejected. Load data through a runtime:net API instead, keeping code locally verified and safe from supply-chain hijacking.

  • No package installer. esrun resolves an existing node_modules tree but does not install anything. Use your package manager (npm, pnpm, bun) to populate dependencies.

  • No linter or formatter. oxlint and oxfmt are 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 no data:/blob: worker URLs. The dedicated Worker ships (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 & JSXesdev app.ts strips types as files load — erased, never checked
A bundleresdev build — one deployable ES module, and how CommonJS becomes runnable
A test runneresdev test — one process per file
Watch modeesdev --watch — a restart is a SIGTERM, so it drains
A debuggeresdev --inspect — and even there, only in a build that asked for it
Permission discoveryesdev --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.

Last updated on
Edit this page