Migration guide
This guide covers everything you need to transition existing codebases from Node.js, Bun, or Deno to esrun.
Unlike Node.js (process, Buffer), Bun (Bun.*), or Deno (Deno.*), esrun does not inject ambient host objects into the global scope. Standard Web APIs (fetch, URL, WebCrypto, Temporal, Streams) are global without imports, while all host-level system access is explicitly imported from the runtime: namespace.
Architectural & Language Differences
Before refactoring specific APIs, keep these structural rules in mind:
Pure ES Modules Only (ESM):
CommonJS (
require(),exports,module.exports) is not supported.All local module imports must include explicit file extensions (
import { config } from "./config.js").Top-level
awaitis natively supported across all modules.
Package Resolution:
Packages installed in
node_modulesvia standard package managers (bun,npm,pnpm) resolve normally (import { Hono } from "hono").Node-specific native C++ addons (N-API) and
node:built-in modules (node:fs,node:child_process,node:os,node:http) are not provided. Use the equivalentruntime:modules.
Capability & Sandboxing Model:
The esrun embeddable engine is deny-by-default. In embedded environments, host capabilities (
FileRead,FileWrite,NetConnect,Env,Run) must be explicitly granted by the Rust host.
Quick Reference Table
| Task / Feature | Node.js | Bun | Deno | esrun Equivalent |
|---|---|---|---|---|
| Process Env | process.env.KEY | Bun.env.KEY | Deno.env.get("KEY") | import { env } from "runtime:process";env.KEY |
| CLI Args | 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 Text File | fs.readFileSync(path, "utf8") | Bun.file(path).text() | Deno.readTextFile(path) | import { file } from "runtime:fs";await file(path).text() |
| Write 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(...) |
| WASI Guest | node:wasi | node:wasi | node:wasi | import { WASI } from "runtime:wasi" |
| YAML / Serialization | js-yaml (npm) | js-yaml (npm) | js-yaml (npm) | import { YAML } from "runtime:serialization" |
Migrating from Node.js
1. Environment & Process Globals
In Node.js, process is globally available. In esrun, import env, args, cwd, and exit explicitly from runtime:process.
// ❌ Node.js const port = process.env.PORT || 8080; const cliArgs = process.argv.slice(2); const currentDir = process.cwd(); process.exit(0); // ✅ esrun import { env, args, cwd, exit } from "runtime:process"; const port = env.PORT || 8080; const cliArgs = args; // Script arguments (excluding binary/script names) const currentDir = cwd(); exit(0);
2. File System Operations (node:fs)
Replace synchronous and callback-based node:fs calls with the Promise-based, Blob-like surface in runtime:fs.
// ❌ Node.js import { readFileSync, writeFileSync, appendFileSync, existsSync } from "node:fs"; if (existsSync("./config.json")) { const raw = readFileSync("./config.json", "utf8"); writeFileSync("./out.txt", "processed"); appendFileSync("./app.log", "done\n"); } // ✅ esrun import { file, write } from "runtime:fs"; const cfg = file("./config.json"); if (await cfg.exists()) { const text = await cfg.text(); // UTF-8 text const json = await cfg.json(); // Parsed JSON const bytes = await cfg.bytes(); // Uint8Array await write("./out.txt", "processed"); await write("./app.log", "done\n", { append: true }); }
3. Path Resolution (__dirname, __filename, node:path)
In ES modules, __dirname and __filename do not exist in global scope. Derive them cleanly using import.meta.url and runtime:path.
// ❌ Node.js (CommonJS) const path = require("path"); const file = path.join(__dirname, "data", "app.db"); // ✅ esrun import { join, dirname, fromFileURL } from "runtime:path"; const __filename = fromFileURL(import.meta.url); const __dirname = dirname(__filename); const dbPath = join(__dirname, "data", "app.db");
4. Subprocesses & Child Processes (node:child_process)
Replace child_process.spawn() or exec() with Command from runtime:system.
// ❌ Node.js import { spawn } from "node:child_process"; const child = spawn("git", ["status"]); // ✅ esrun (Collect output) import { Command } from "runtime:system"; const { code, stdout, stderr } = await new Command("git", { args: ["status"], }).output(); const outputText = new TextDecoder().decode(stdout);
// ✅ esrun (Stream stdin / stdout with Web Streams) import { Command } from "runtime:system"; const child = await new Command("ffmpeg", { args: ["-i", "pipe:0", "-f", "mp3", "pipe:1"], stdin: request.body, // Web ReadableStream / Response body streams directly in }).spawn(); return new Response(child.stdout); // Web ReadableStream streams directly out
5. Binary Data & Buffers (node:buffer)
Replace Node's Buffer with standard Uint8Array, TextEncoder/TextDecoder, and btoa()/atob().
// ❌ Node.js const buf = Buffer.from("Hello World", "utf8"); const b64 = buf.toString("base64"); const back = Buffer.from(b64, "base64").toString("utf8"); // ✅ esrun (Standard Web APIs) const encoder = new TextEncoder(); const decoder = new TextDecoder(); const bytes = encoder.encode("Hello World"); // Uint8Array const b64 = btoa(decoder.decode(bytes)); const back = decoder.decode(encoder.encode(atob(b64)));
6. Cryptography & Hashing (node:crypto)
Replace node:crypto hash streams with standard crypto.subtle or crypto.randomUUID().
// ❌ Node.js import { createHash, randomUUID } from "node:crypto"; const id = randomUUID(); const hash = createHash("sha256").update("data").digest("hex"); // ✅ esrun const id = crypto.randomUUID(); const data = new TextEncoder().encode("data"); const buffer = await crypto.subtle.digest("SHA-256", data); const hashHex = Array.from(new Uint8Array(buffer)) .map((b) => b.toString(16).padStart(2, "0")) .join("");
Migrating from Bun
esrun and Bun both prioritize high performance and modern Web standards. The key migration step is replacing ambient Bun.* globals with explicit runtime: imports.
1. File Handling (Bun.file & Bun.write)
// ❌ Bun const text = await Bun.file("./data.txt").text(); const json = await Bun.file("./data.json").json(); await Bun.write("./out.txt", "data"); // ✅ esrun import { file, write } from "runtime:fs"; const text = await file("./data.txt").text(); const json = await file("./data.json").json(); await write("./out.txt", "data");
2. Subprocesses (Bun.spawn & Bun.$)
// ❌ Bun const proc = Bun.spawn(["git", "rev-parse", "HEAD"]); const text = await new Response(proc.stdout).text(); // ✅ esrun import { Command } from "runtime:system"; const { stdout } = await new Command("git", { args: ["rev-parse", "HEAD"], }).output(); const text = new TextDecoder().decode(stdout).trim();
Migrating from Deno
esrun shares Deno's commitment to Web APIs and capability-gated execution. Migration primarily involves replacing Deno.* namespaces with explicit runtime: modules and switching package resolution to package.json / node_modules.
1. File Operations (Deno.readTextFile, Deno.writeTextFile)
// ❌ Deno const text = await Deno.readTextFile("./data.txt"); await Deno.writeTextFile("./out.txt", "content"); await Deno.remove("./temp.txt"); // ✅ esrun import { file, write, remove } from "runtime:fs"; const text = await file("./data.txt").text(); await write("./out.txt", "content"); await remove("./temp.txt");
2. Process & Environment (Deno.env, Deno.args)
// ❌ Deno const port = Deno.env.get("PORT"); const cliArgs = Deno.args; const currentDir = Deno.cwd(); // ✅ esrun import { env, args, cwd } from "runtime:process"; const port = env.PORT; const cliArgs = args; const currentDir = cwd();
3. Subprocesses (Deno.Command)
// ❌ Deno const command = new Deno.Command("git", { args: ["status"] }); const { code, stdout } = await command.output(); // ✅ esrun import { Command } from "runtime:system"; const { code, stdout } = await new Command("git", { args: ["status"], }).output();
Builtin Serialization Ecosystem
Instead of pulling third-party npm packages for common structured data formats, esrun includes high-performance zero-dependency serializers in runtime:serialization.
import { YAML, MessagePack, TOML, XML, JSONL } from "runtime:serialization"; // 1. YAML const config = YAML.parse(await file("./config.yaml").text()); const yamlOut = YAML.stringify({ 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 & JSONL const xmlData = XML.parse("<app><name>esrun</name></app>"); const jsonlRows = JSONL.parse("{\"id\":1}\n{\"id\":2}\n");
Summary Checklist for Migration
Remove all CommonJS
require()andmodule.exportsin favor of standardimport/export.Ensure all local file import specifiers end with explicit file extensions (
.js).Replace
process.env/process.argvwithimport { env, args } from "runtime:process".Replace
fs.readFileSync/Bun.file/Deno.readTextFilewithimport { file, write } from "runtime:fs".Replace
child_process.spawn/Bun.spawn/Deno.Commandwithimport { Command } from "runtime:system".Convert
Buffermanipulation to Web standardUint8Array,TextEncoder, andTextDecoder.