Coming from Node.js

What happens when you point Cruft at a Node app. Covers what runs unchanged (node: modules, CommonJS and ESM, the web-platform globals), the differences Cruft chooses on purpose (lockfile, exit codes, direct TypeScript), and the module gaps to check before you port.

You have a Node app and a reasonable question: what actually happens if I point cruft at it? This is the map. Cruft is deliberately Node-shaped, it resolves node:* modules, runs CommonJS and ESM, reads package.json, and reports itself as Node 20.10.0 to feature-detection, so most Node code runs unchanged. The rest of this page is what carries over, what differs on purpose, and what to check first.

What works unchanged

  • Running files: cruft app.js, stdin, -e/-p/--check, an interactive REPL. Flags after the entry go to process.argv, like node.
  • Modules: node:fs, bare fs, CJS and ESM mixed, node_modules resolution, import(), top-level await, live ESM bindings.
  • The language: full modern ECMAScript (Cruft runs the full official test262 corpus, tracking a small set of triaged, accepted compatibility exceptions rather than a clean 100% pass), plus Temporal, Intl, WebAssembly.
  • Web platform globals: fetch, URL, Request/Response/Headers, streams, Blob/File/FormData, TextEncoder, structuredClone, crypto/crypto.subtle, EventTarget, timers.
  • process: argv/env/exit/cwd/platform, memoryUsage().rss (real), nextTick, and the lifecycle events (exit/beforeExit/ uncaughtException/unhandledRejection). Both async and synchronous top-level uncaught throws fire your uncaughtException handler.

Differences on purpose

These are the places Cruft deliberately chose to differ.

AreaNodeCruft
run subcommandnode run x runs a file named runrun is a verb, cruft run x
TypeScriptneeds a loader/flag.ts/.mts/.cts run directly (erasure)
Lockfilepackage-lock.jsoncruft-lock.json (not interchangeable)
node_modules layoutper-project copiescontent-addressed store + hard-links (pnpm-style)
Exit codes (own errors)mostly 1sysexits.h for startup/usage (bad flag 64, syntax 65, not-found 66); uncaught/rejection stay 1 like Node
Unknown CLI flagsoften ignoredrejected (exit 64), fail loud
Zero-config isolationnone--sealed-deps, --audit, Compartments

The exit-code difference is the one most likely to bite a script that branches on node's status codes. Branch on non-zero-ness, not specific values (the convention may still converge).

What to check before you port

Cruft is broad but not complete, and depth is stated, not implied. Before relying on a module, check its depth tier in the module reference: focused (substantive), partial (named gaps), or stub (import-time only). The boundaries most likely to affect a real Node app today, stated so you can look them up rather than discover them:

  • Package manager: devDependencies and lifecycle scripts (postinstall) are not processed yet; there is no add/remove/update, edit package.json and re-run cruft install.
  • Weak references (WeakRef/FinalizationRegistry) hold their targets strongly today: a WeakRef never observes its target collected, and finalizers never run. Do not build cache-eviction on them.
  • process.memoryUsage(): rss is real, and heapTotal/heapUsed report a real (coarse — heapTotal == heapUsed) figure; only external/arrayBuffers report 0.
  • A few console and Temporal methods are partial (for example Temporal.Instant's epochSeconds/epochMicroseconds).

Much of what earlier builds could not do now works: fetch follows redirects and honors AbortSignal, the synchronous child_process lanes carry env/input/timeout, node:worker_threads round-trips messages to a file-backed worker (an inline eval:-mode worker is not supported yet), and the TLS client validates the certificate chain, hostname, and expiry. Each module page states its boundaries, and a silent wrong-result is treated as a release blocker, not a footnote.

What Cruft adds

The reasons to consider Cruft beyond "it runs my Node code":

  • Zero-config supply-chain defense: cruft --sealed-deps app.js denies I/O to everything under node_modules while your own code runs normally.
  • Compartments: new Compartment({ globals, timeout_ms }) runs untrusted code in-process with only what you grant.
  • TypeScript with no build step, an embedded SQLite/Postgres stack, a sound language (CruftScript), and a supervised front door (cruft wrap) for migrating node/npm workflows gradually.

You don't have to switch all at once

The lowest-risk way in keeps Node underneath. cruft wrap -- node app.js runs your existing Node under Cruft's supervision (audit log, policy report, optional OS sandbox) with zero compatibility risk, because the engine is still Node. Then cruft run --backend=auto --explain app.js picks the Cruft runtime only where packaged evidence proves it's safe. See the front door.

Start here: run your test suite under cruft --test or cruft app.js, check any failures against the module reference's depth tiers, and reach for --sealed-deps once it's green.