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'snode:httpserver. The returned handle is a Nodehttp.Server, so read the port fromserver.address().portand stop it withserver.close()(notserver.stop()), and thewebsocket/tls/unixoptions are not wired. See Bun compatibility.bun:sqlite: the one Bun module Cruft implements directly. Bun'sDatabase/Statementshape (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/.tsxfile just runs, no build step. (Cruft uses Node's--strip-typeserasure model, so it does not transformenum/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
| Bun | Cruft equivalent |
|---|---|
Bun.file(path) | node:fs / cruft:fs (no Bun.file lazy-blob object) |
Bun.write | node:fs writes |
Bun.$ (shell) | node:child_process (execSync/spawn) |
bun install / bun.lockb | cruft install / cruft-lock.json (text, not binary) |
bun:test | node:test via cruft --test (test runner) |
bun:ffi | not 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 tonode_moduleswith 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
- Grep your code for
Bun.andbun:to find the call sites. Bun.serveandbun:sqlitestay as they are.- Swap the rest:
Bun.file/Bun.write→node:fs,Bun.$→node:child_process,bun:test→node:test. - Run
cruft install, then your suite undercruft --test, and check any module failures against the module reference depth tiers.