Coming from Bun

What happens when you run Bun code under Cruft. The Bun global is small: Bun.serve works and bun:sqlite carries over, but Bun.file, Bun.write, and Bun.$ are absent and need the node: / cruft: modules. Includes a rewrite table and the first porting steps.

The headline: Cruft is Node-shaped. The Bun global is small: Bun.serve works (backed by node:http), but Bun.file, Bun.write, and Bun.$ are not present. If your code leans on the wider Bun.* surface, it will not run unchanged. What Cruft shares with Bun is the sensibility (single fast binary, TypeScript with no build step, batteries included) and a few specific APIs, not the whole Bun.* namespace.

What carries over

  • Bun.serve({ fetch }): works, backed by Cruft's node:http server. The returned handle is a Node http.Server, so read the port from server.address().port and stop it with server.close() (not server.stop()), and the websocket/tls/unix options are not wired. See Bun compatibility.
  • bun:sqlite: the one Bun module Cruft implements directly. Bun's Database/Statement shape (new Database, .query().get()/.all(), .prepare().run() returning { changes, lastInsertRowid }) works as a Bun-compatible adapter over Cruft's own SQLite engine. See the SQL stack.
  • TypeScript and JSX run directly: like Bun, a .ts/.tsx file just runs, no build step. (Cruft uses Node's --strip-types erasure model, so it does not transform enum/decorators, it refuses them; see TypeScript support.)
  • The Web-platform surface Bun leans on (fetch, URL, streams, Blob/File, crypto.subtle, structuredClone) is all present.
  • The single-binary, fast-start ergonomics and a built-in package manager, test runner, and REPL.

What does not carry over

BunCruft equivalent
Bun.file(path)node:fs / cruft:fs (no Bun.file lazy-blob object)
Bun.writenode:fs writes
Bun.$ (shell)node:child_process (execSync/spawn)
bun install / bun.lockbcruft install / cruft-lock.json (text, not binary)
bun:testnode:test via cruft --test (test runner)
bun:ffinot present (native addons are node:* N-API instead)
other Bun.* (version, hash, password, gzipSync, Transpiler, Glob, …)absent, use the node:* / cruft:* surfaces

So the port is mechanical where an equivalent exists (mostly: reach for the node:* or cruft:* surface) and a rewrite where it's a Bun.*-only feature.

Where Cruft goes further than Bun

The isolation story is Cruft's distinctive ground, and Bun has no equivalent:

  • Compartments: run untrusted code in a fresh realm with only granted globals and an uncatchable timeout.
  • --sealed-deps: deny I/O to node_modules with zero config, a supply-chain defense.
  • cruft agent: a fail-closed, budgeted, audited sandbox for LLM-authored code.
  • CruftScript: a sound typed language (.fts) with runtime-validated boundaries, not just erased types.

Note on version reporting

Cruft reports process.versions.node = "26.3.0" (with a V8 version string) so that Node-feature-detecting packages work. That is a compatibility adapter. It does not claim to be Node or to embed V8; the engine is entirely Cruft's own. Bun does the analogous thing.

Practical first steps

  1. Grep your code for Bun. and bun: to find the call sites.
  2. Bun.serve and bun:sqlite stay as they are.
  3. Swap the rest: Bun.file/Bun.writenode:fs, Bun.$node:child_process, bun:testnode:test.
  4. Run cruft install, then your suite under cruft --test, and check any module failures against the module reference depth tiers.