Deno compatibility

Cruft exposes a `Deno` global that reshapes its Node layer into the Deno runtime API. Most of the namespace works, including the file system, subprocesses, environment, and process info. The permission sandbox is not enforced, and Deno's `npm:`, `jsr:`, and `https:` imports are not supported.

Security: the permission sandbox is not enforced. Deno.permissions always grants and gates nothing, so Deno's --allow-* model provides no isolation on Cruft. Do not use it as a security boundary. For real isolation, run untrusted code in a Compartment, which enforces capabilities at the realm boundary. See Permissions are not enforced below.

Cruft ships a Deno global so code written against the Deno runtime API can run on Cruft's engine. It is on by default and can be turned off with the environment variable CRUFT_DENO_COMPAT=0, after which typeof Deno is "undefined".

The namespace is built on top of Cruft's Node compatibility layer: the same host operations that back node:fs, node:os, and node:http are reshaped into the Deno API. That is the key to reading this page. Anything Deno does that Node also does, the file system, subprocesses, environment, and process information, works well. Anything with no Node backing is either absent or a stub, and the two Deno concepts with no equivalent at all, the permission sandbox and npm:/jsr:/https: imports, do not exist here.

The global

Deno.version and Deno.build report identity:

Deno.version; // { deno: "2.8.0", v8: "14.9.0-cruft", typescript: "6.0.3" }
Deno.build.os; // "darwin"
Deno.build.arch; // "arm64"

Deno.pid, Deno.ppid, Deno.execPath(), Deno.hostname(), Deno.args, and Deno.noColor all report real values.

The file system

The full synchronous and asynchronous file API is implemented:

await Deno.writeTextFile("/tmp/note.txt", "hello");
await Deno.readTextFile("/tmp/note.txt"); // "hello"
Deno.statSync("/tmp/note.txt").size; // 5

readFile and readFileSync return a plain Uint8Array (not a Node Buffer), matching Deno:

Deno.readFileSync("/tmp/note.txt").constructor.name; // "Uint8Array"

Working members: readTextFile, readFile, writeTextFile, writeFile, readDir, stat, lstat, mkdir, remove, rename, realPath, readLink, copyFile, truncate, chmod, chown, link, symlink, utime, makeTempDir, open, and create, each with its *Sync variant. Deno.open returns a Deno.FsFile handle with .write() and .close(), and Deno.SeekMode is { Start: 0, Current: 1, End: 2 }.

Environment

Deno.env is fully working:

Deno.env.set("FOO", "bar");
Deno.env.get("FOO");        // "bar"
Deno.env.has("FOO");        // true
Deno.env.delete("FOO");
Deno.env.toObject().PATH;   // the current PATH

Subprocesses

Deno.Command spawns real processes:

const { code, success, stdout } = await new Deno.Command("echo", {
  args: ["hi", "there"],
}).output();
new TextDecoder().decode(stdout); // "hi there\n"

output, outputSync, and spawn are all present. Deno.exit(code) exits the process, and Deno.kill(pid, signal) signals one (Deno.kill of a missing pid raises ESRCH).

Process and system info

Deno.cwd(), Deno.chdir(), Deno.loadavg(), Deno.networkInterfaces(), Deno.memoryUsage(), Deno.consoleSize(), Deno.uid(), Deno.gid(), Deno.umask(), and Deno.osRelease() all report real values. Deno.inspect(value) formats a value in Deno's style, and Deno.errors carries the standard 23 error classes (NotFound, AlreadyExists, PermissionDenied, and the rest), with Node .code values mapped onto them.

Standard streams

Deno.stdout, Deno.stderr, and Deno.stdin accept byte writes:

Deno.stdout.write(new TextEncoder().encode("direct to stdout\n"));

The Deno resource-id field (Deno.stdout.rid) is undefined; Cruft does not model Deno's resource-id table.

Deno.serve

Deno.serve binds a port and serves requests through a handler:

Deno.serve({ port: 8000, onListen: (a) => console.log("on", a.port) }, (req) =>
  new Response("hello from cruft " + new URL(req.url).pathname),
);

onListen fires with the bound address, and an external client receives the handler's Response. Two caveats: the return value is a plain object, not a Deno HttpServer instance, and a fetch() to the server from its own process does not complete (the event loop does not service both sides of a same-process request); serve to an external client instead.

Permissions are not enforced

Deno.permissions exists, but it is a compatibility shim, not a sandbox. Every query returns granted, revoke reports a denied status but changes nothing, and no file, network, environment, or subprocess operation is ever gated on a permission.

(await Deno.permissions.query({ name: "read", path: "/etc" })).state; // "granted"
await Deno.permissions.revoke({ name: "read" });                      // reports "denied"
Deno.readTextFileSync("/etc/hosts");                                  // still succeeds

Do not rely on Deno.permissions or the --allow-* model for isolation. Cruft's actual isolation primitive is the Compartment, which enforces capabilities at the realm boundary.

Limitations

  • No permission sandbox. Deno.permissions always grants and enforces nothing (above). Use Compartments for real isolation.
  • No npm:, jsr:, or https: imports. These specifiers are not resolved; they fall through to Node-style node_modules lookup and fail. Use Node-style bare, relative, and node: specifiers instead.
  • FFI throws. Deno.dlopen, Deno.UnsafeCallback, Deno.UnsafeFnPointer, and Deno.UnsafePointerView throw "not supported on cruft".
  • No Deno.test, Deno.bench, or Deno.metrics. The test and bench runners and the metrics table are absent. Use cruft --test.
  • Only Deno.serve on the network side. The lower-level Deno.listen, Deno.connect, Deno.serveHttp, Deno.upgradeWebSocket, and Deno.listenTls are not present.
  • Some fields are stubbed. Deno.osUptime() returns a constant 0, Deno.mainModule is not populated, and the resource-id fields on the standard streams are undefined.