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 toprocess.argv, like node. - Modules:
node:fs, barefs, CJS and ESM mixed,node_modulesresolution,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 youruncaughtExceptionhandler.
Differences on purpose
These are the places Cruft deliberately chose to differ.
| Area | Node | Cruft |
|---|---|---|
run subcommand | node run x runs a file named run | run is a verb, cruft run x |
| TypeScript | needs a loader/flag | .ts/.mts/.cts run directly (erasure) |
| Lockfile | package-lock.json | cruft-lock.json (not interchangeable) |
node_modules layout | per-project copies | content-addressed store + hard-links (pnpm-style) |
| Exit codes (own errors) | mostly 1 | sysexits.h for startup/usage (bad flag 64, syntax 65, not-found 66); uncaught/rejection stay 1 like Node |
| Unknown CLI flags | often ignored | rejected (exit 64), fail loud |
| Zero-config isolation | none | --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:
devDependenciesand lifecycle scripts (postinstall) are not processed yet; there is noadd/remove/update, editpackage.jsonand re-runcruft install. - Weak references (
WeakRef/FinalizationRegistry) hold their targets strongly today: aWeakRefnever observes its target collected, and finalizers never run. Do not build cache-eviction on them. process.memoryUsage():rssis real, andheapTotal/heapUsedreport a real (coarse —heapTotal == heapUsed) figure; onlyexternal/arrayBuffersreport0.- A few
consoleandTemporalmethods are partial (for exampleTemporal.Instant'sepochSeconds/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.jsdenies I/O to everything undernode_moduleswhile 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.