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.

No ambient host globals

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:

Architectural and language differences

Three rules shape the port before any individual API does.

  1. 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-level await works in every module. See Module system.

  2. Packages resolve, node: built-ins do not. Anything installed in node_modules by bun, npm or pnpm resolves normally (import { Hono } from "hono"). Native addons (N-API) and node: modules — node:fs, node:child_process, node:os, node:http — are not provided; the tables below give the runtime: module that replaces each.

  3. Deny by default. A run reaches only what the command line named: --allow-read, --allow-net, --allow-env, --allow-run and 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

TaskNode.jsBunDenoesrun
Environmentprocess.env.KEYBun.env.KEYDeno.env.get("KEY")import { env } from "runtime:process";
env.KEY
CLI argumentsprocess.argv.slice(2)Bun.argvDeno.argsimport { args } from "runtime:process";
args
Working directoryprocess.cwd()process.cwd()Deno.cwd()import { cwd } from "runtime:process";
cwd()
Read a text filefs.readFileSync(path, "utf8")Bun.file(path).text()Deno.readTextFile(path)import { file } from "runtime:fs";
await file(path).text()
Write a filefs.writeFileSync(path, data)Bun.write(path, data)Deno.writeTextFile(path, data)import { write } from "runtime:fs";
await write(path, data)
Module path__filename / __dirnameimport.meta.filenameimport.meta.filenameimport { dirname, fromFileURL } from "runtime:path";
const __filename = fromFileURL(import.meta.url);
Join pathspath.join(...)path.join(...)path.join(...)import { join } from "runtime:path";
join(...)
Subprocesseschild_process.spawn()Bun.spawn()new Deno.Command()import { Command } from "runtime:system";
new Command(...)
UDP datagramsdgram.createSocket("udp4")Bun.udpSocket()Deno.listenDatagram()import { bind } from "runtime:net";
bind({ port })mapping
WASI guestnode:wasinode:wasinode:wasiimport { WASI } from "runtime:wasi"
YAML and other formatsjs-yaml (npm)js-yaml (npm)js-yaml (npm)import { YAML } from "runtime:serialization"
Async contextnew AsyncLocalStorage()new AsyncLocalStorage()new AsyncLocalStorage()import { createContext } from "runtime:context";
createContext({ name })
Tracingasync_hooks, diagnostics_channelad-hocOTEL_DENO=1 --unstable-otelimport { subscribe } from "runtime:diagnostics";
or esrun --otel=<url>
Heap and CPUprocess.memoryUsage()
process.cpuUsage()
process.memoryUsage()Deno.memoryUsage()import { memoryUsage, cpuTime } from "runtime:process";
per agent, not per process
Loop healthmonitorEventLoopDelay()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.

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

WhatWhy, and what to do instead
require(), module.exportsNo 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), FFINo addon ABI and no foreign-function interface. The host extends the runtime through providers, in Rust.
SharedWorker, data:/blob: worker URLsA worker's URL names a file. The dedicated Worker ships.
npm install from the runtimeesrun 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 debuggerAll of it is esdev, the development binary, and none of it is in the one that serves production.

Checklist

Before the first run
  1. Replace require() / module.exports with import / export.

  2. Give every local specifier its file extension (./config.js, not ./config).

  3. process.env, process.argvimport { env, args } from "runtime:process".

  4. fs.readFileSync, Bun.file, Deno.readTextFileimport { file, write } from "runtime:fs".

  5. child_process.spawn, Bun.spawn, Deno.Commandimport { Command } from "runtime:system".

  6. BufferUint8Array, TextEncoder, TextDecoder.

  7. Run it once under esdev --trace-permissions and deploy with the esrun line it prints.

Last updated on
Edit this page