Coming from Deno

What happens when you run Deno code under Cruft. Cruft ships a Deno global, so most of the Deno runtime API (the file system, env, Deno.Command, Deno.serve) runs. What does not carry over is Deno's module system (npm:, jsr:, https: imports) and its permission sandbox, which is not enforced. Includes a rewrite table and the first porting steps.

The headline: Cruft is Node-shaped, and it ships a Deno global. Unlike a Bun port, a lot of Deno runtime code runs unchanged, because Cruft reshapes its node:* layer into the Deno.* API. The two things that do not carry over are the parts that make Deno Deno: its module system (npm:, jsr:, and https: imports) and its permission sandbox, which exists on Cruft only as a compatibility stub and enforces nothing. The Deno global is on by default and can be turned off with CRUFT_DENO_COMPAT=0.

Deno.permissions is not a sandbox on Cruft — it enforces nothing.

Cruft ships Deno.permissions as a compatibility stub only. Every query returns granted, request always resolves granted, revoke does nothing, and no file, network, or subprocess call is ever gated on a permission. The --allow-read, --allow-net, --allow-write, and related flags do not exist and are not honored.

If you relied on Deno's permission model to contain untrusted code, that boundary is gone the moment the code runs under Cruft. Do not port a security assumption across — port to a Compartment, which is enforced at the realm boundary with an uncatchable timeout, and hand each one only the capabilities it needs.

What carries over

  • The Deno.* runtime API, most of it: the file system (readTextFile, readFile, writeTextFile, readDir, stat, mkdir, remove, open, and the rest, sync and async), Deno.env (get/set/has/delete/toObject), Deno.Command (real subprocesses), Deno.serve, Deno.stdout/stderr, and process info (pid, cwd(), execPath(), hostname(), loadavg(), memoryUsage()). Byte APIs return a plain Uint8Array, as in Deno. See Deno compatibility for the full surface.
  • TypeScript runs directly: a .ts/.tsx file just runs, no build step. (Cruft uses Node's --strip-types erasure model, so it refuses enum and value namespace rather than transforming them; see TypeScript support.)
  • The Web-platform surface Deno leans on (fetch, URL, streams, Blob/File, crypto.subtle, structuredClone) is all present.
  • The single-binary, fast-start ergonomics, plus a built-in package manager, test runner, and REPL.

What does not carry over

DenoCruft equivalent
import x from "npm:pkg"a bare import x from "pkg" plus cruft install (no npm: scheme)
import x from "jsr:@scope/pkg"not resolved; the JSR registry is not wired
import x from "https://…/mod.ts"not resolved; there are no remote-URL imports
--allow-read / --allow-net (the sandbox)not enforced; use Compartments for real isolation
Deno.test / deno testnode:test via cruft --test
Deno.dlopen and the FFInot present (they throw); native code is node:* N-API
deno.json, import mapsnot read; resolution is package.json + node_modules

Deno's module resolver is the real porting work. Cruft resolves Node-style: bare specifiers from node_modules, relative paths, and node: builtins. So a Deno project that imports from npm:, jsr:, or https: URLs needs those turned into ordinary dependencies (cruft install), while its Deno.* runtime calls mostly stay as they are.

Permissions are not a sandbox here

This is the difference that matters for security, so it is worth stating plainly: Deno.permissions on Cruft always grants and enforces nothing. Every query returns granted, revoke changes nothing, and no file, network, or subprocess operation is ever gated on a permission. Deno's --allow-* model does not translate.

Do not rely on it. Cruft's actual isolation primitive is the Compartment: a fresh realm that reaches only the capabilities you hand it, with an uncatchable timeout. If you came to Deno for the permission sandbox, that is the surface to port to, and it is stronger, because it is enforced at the realm boundary rather than by a flag.

The compatibility bet

Deno and Cruft both build on their own foundations and take security seriously, but they draw the lines differently:

  • Deno ships its own runtime API and module system first, runs a permission sandbox, and is built on V8.
  • Cruft targets the Node API surface as its compatibility contract, is built on its own engine (no V8), reshapes that Node layer into a Deno.* global for portability, and puts real isolation in Compartments rather than --allow-*.

Practically: your Deno.readTextFile, Deno.env, Deno.Command, and Deno.serve code runs; your npm:/jsr:/https: imports become installed dependencies; and your permission model becomes a Compartment.

Note on version reporting

Deno.version reports { deno: "2.8.0", v8: "14.9.0-cruft", typescript: "6.0.3" } so that version-detecting code is satisfied. That is a compatibility persona: the engine is entirely Cruft's own, not V8, and the -cruft suffix says so.

Practical first steps

  1. Grep your imports for npm:, jsr:, and https://, those are the rewrite list. Turn each into an ordinary dependency and cruft install.
  2. Leave your Deno.* runtime calls, most work; check anything unusual against Deno compatibility.
  3. Replace --allow-* reasoning with a Compartment; do not treat Deno.permissions as a boundary.
  4. Swap Deno.test for node:test and run your suite under cruft --test.