Migration guide
What each host API becomes when a codebase moves from Node.js, Bun or Deno to esrun — and, at the end, the parts that have no equivalent because they are out of scope.
Node.js has process and Buffer, Bun has Bun.*, Deno has Deno.*. esrun puts nothing of its own in the global scope: standard Web APIs (fetch, URL, crypto, Temporal, streams) are global because the platform says so, and every host effect is imported from a runtime: module. Whatever a file can reach is visible in its import list.
Choose your starting runtime
The quick reference below gives the one-line replacement. Use the focused guide for the host you are leaving:
Migrating from Node.js — globals, files, paths, subprocesses, buffers, and cryptography.
Migrating from Bun — the
Bun.*namespace, processes, and hashing.Migrating from Deno —
Deno.*APIs and permissions.
Architectural and language differences
Three rules shape the port before any individual API does.
ES modules only. CommonJS —
require(),exports,module.exports— is not supported. A local specifier names a file that exists, extension included (import { config } from "./config.js"); there is no extension guessing. Top-levelawaitworks in every module. See Module system.Packages resolve,
node:built-ins do not. Anything installed innode_modulesbybun,npmorpnpmresolves normally (import { Hono } from "hono"). Native addons (N-API) andnode:modules —node:fs,node:child_process,node:os,node:http— are not provided; the tables below give theruntime:module that replaces each.Deny by default. A run reaches only what the command line named:
--allow-read,--allow-net,--allow-env,--allow-runand the rest are how a service states what it may touch. Code that ran unrestricted elsewhere needs that line written once — see Securing the runtime.
Quick reference
| Task | Node.js | Bun | Deno | esrun |
|---|---|---|---|---|
| Environment | process.env.KEY | Bun.env.KEY | Deno.env.get("KEY") | import { env } from "runtime:process";env.KEY |
| CLI arguments | process.argv.slice(2) | Bun.argv | Deno.args | import { args } from "runtime:process";args |
| Working directory | process.cwd() | process.cwd() | Deno.cwd() | import { cwd } from "runtime:process";cwd() |
| Read a text file | fs.readFileSync(path, "utf8") | Bun.file(path).text() | Deno.readTextFile(path) | import { file } from "runtime:fs";await file(path).text() |
| Write a file | fs.writeFileSync(path, data) | Bun.write(path, data) | Deno.writeTextFile(path, data) | import { write } from "runtime:fs";await write(path, data) |
| Module path | __filename / __dirname | import.meta.filename | import.meta.filename | import { dirname, fromFileURL } from "runtime:path";const __filename = fromFileURL(import.meta.url); |
| Join paths | path.join(...) | path.join(...) | path.join(...) | import { join } from "runtime:path";join(...) |
| Subprocesses | child_process.spawn() | Bun.spawn() | new Deno.Command() | import { Command } from "runtime:system";new Command(...) |
| UDP datagrams | dgram.createSocket("udp4") | Bun.udpSocket() | Deno.listenDatagram() | import { bind } from "runtime:net";bind({ port }) — mapping |
| WASI guest | node:wasi | node:wasi | node:wasi | import { WASI } from "runtime:wasi" |
| YAML and other formats | js-yaml (npm) | js-yaml (npm) | js-yaml (npm) | import { YAML } from "runtime:serialization" |
| Async context | new AsyncLocalStorage() | new AsyncLocalStorage() | new AsyncLocalStorage() | import { createContext } from "runtime:context";createContext({ name }) |
| Tracing | async_hooks, diagnostics_channel | ad-hoc | OTEL_DENO=1 --unstable-otel | import { subscribe } from "runtime:diagnostics";or esrun --otel=<url> |
| Heap and CPU | process.memoryUsage()process.cpuUsage() | process.memoryUsage() | Deno.memoryUsage() | import { memoryUsage, cpuTime } from "runtime:process";per agent, not per process |
| Loop health | monitorEventLoopDelay() | — | — | import { metrics } from "runtime:diagnostics";metrics().loopLagMs |
Provider migration guides
The detailed provider mappings live on separate pages so you can read only the runtime you are moving from.
Serialization without a package
js-yaml, @iarna/toml, fast-xml-parser and msgpackr have no equivalent to install: the formats are in runtime:serialization, and the parsers for all but Protobuf run in Rust.
import { file } from "runtime:fs"; import { YAML, MessagePack, TOML, XML, JSONL } from "runtime:serialization"; // 1. YAML const config = YAML.parse(await file("./config.yaml").text()); const yamlOut = YAML.build({ port: 8080, mode: "production" }); // 2. MessagePack (binary) const encoded = MessagePack.encode({ user: "alex", id: 42 }); const decoded = MessagePack.decode(encoded); // 3. TOML const cargo = TOML.parse(await file("./Cargo.toml").text()); // 4. XML const xmlData = XML.parse("<app><name>esrun</name></app>"); // 5. JSONL is a stream, not a whole-document parse const rows = file("./events.jsonl") .stream() .pipeThrough(new JSONL.DecoderStream()); for await (const row of rows) console.log(row.id);
What does not port
These are non-goals, not gaps waiting to be filled, so a port that depends on one needs a different shape rather than a workaround.
| What | Why, and what to do instead |
|---|---|
require(), module.exports | No CommonJS at run time. esdev build converts a CJS dependency at build time, and esrun receives ordinary ESM. |
import "https://…" | Remote imports are rejected. Fetch data with fetch or runtime:net; code stays local and reviewable. |
| Native addons (N-API), FFI | No addon ABI and no foreign-function interface. The host extends the runtime through providers, in Rust. |
SharedWorker, data:/blob: worker URLs | A worker's URL names a file. The dedicated Worker ships. |
npm install from the runtime | esrun resolves an existing node_modules tree and installs nothing — that stays your package manager's job. |
| TypeScript, a bundler, a test runner, a watcher, a debugger | All of it is esdev, the development binary, and none of it is in the one that serves production. |
Checklist
Replace
require()/module.exportswithimport/export.Give every local specifier its file extension (
./config.js, not./config).process.env,process.argv→import { env, args } from "runtime:process".fs.readFileSync,Bun.file,Deno.readTextFile→import { file, write } from "runtime:fs".child_process.spawn,Bun.spawn,Deno.Command→import { Command } from "runtime:system".Buffer→Uint8Array,TextEncoder,TextDecoder.Run it once under
esdev --trace-permissionsand deploy with theesrunline it prints.