Migrating from Node.js
1. Environment and process globals
In Node.js process is ambient. In esrun, env, args, cwd and exit are imported from runtime:process, and reading any of them needs --allow-env.
// ❌ 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. Filesystem operations (node:fs)
Synchronous and callback node:fs calls become the promise-based, Blob-like surface in runtime:fs. Every path is resolved inside the project root jail, and reads and writes are two separate grants.
// ❌ 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)
__dirname and __filename are CommonJS, and import.meta here carries url and resolve only. Derive them from import.meta.url with 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 (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 and buffers (node:buffer)
Buffer becomes Uint8Array, with TextEncoder/TextDecoder for text and btoa()/atob() for base64.
// ❌ 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 and hashing (node:crypto)
createHash, createHmac and timingSafeEqual become runtime:hashing; keys, signatures and ciphers become crypto.subtle; randomUUID and randomBytes become crypto.
// ❌ Node.js import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto"; const id = randomUUID(); const digest = createHash("sha256").update("data").digest("hex"); const mac = createHmac("sha256", secret).update(body).digest("hex"); // ✅ esrun import { hash, Hasher, hmac, timingSafeEqual } from "runtime:hashing"; const id = crypto.randomUUID(); const digest = hash("sha256", "data", "hex"); const mac = hmac("sha256", secret, body, "hex"); // Chunked, for anything too large to hold at once. const h = new Hasher("sha256"); for await (const chunk of stream) h.update(chunk); h.digest("hex");
| Node | esrun |
|---|---|
createHash(alg) | hash(alg, data, "hex") or new Hasher(alg) |
createHmac(alg, key) | hmac(alg, key, data, "hex") |
timingSafeEqual(a, b) | timingSafeEqual(a, b) |
scrypt, pbkdf2 | password.hash() / crypto.subtle.deriveBits |
bcrypt, argon2 (npm) | password.hash() / password.verify() |
randomUUID, randomBytes | crypto.randomUUID(), crypto.getRandomValues() |
createSign, createCipheriv, generateKeyPair | crypto.subtle |
// Passwords: no npm package, and the parameters travel with the hash. import { password } from "runtime:hashing"; const stored = await password.hash(input); // argon2id if (await password.verify(input, user.hash) && password.needsRehash(user.hash)) { user.hash = await password.hash(input); }