denoland/deno
github.com/denoland/deno ↗ · 3997 files · 37.1 MB · scan capped
Deno [![Twitter badge][]][Twitter link] [![Bluesky badge][]][Bluesky link] [![Discord badge][]][Discord link] [![YouTube badge][]][YouTube link] Deno (/ˈdiːnoʊ/, pronounced dee-no) is a JavaScript, TypeScript, and WebAssembly runtime with secure defaults and a
Overview
This is the Deno runtime monorepo — a Rust workspace (~80 crates) that builds a JavaScript/TypeScript/WebAssembly runtime on V8 and Tokio, plus the `deno` CLI that wraps it with a package manager, type checker, formatter, linter, test runner, LSP server, and single-file-binary compiler. The distinctive shape is three layers: `libs/` holds vendored foundational crates (notably `deno_core` v0.412.0 at `libs/core`, the op2 proc-macro at `libs/ops`, and a hand-rolled zero-allocation flag parser at `libs/cli_parser` that replaced clap with `const` command tables), `ext/` holds 32 self-contained extension crates that expose native ops to JS, and `runtime/`+`cli/` assemble them into the shipped binary. Everything user code touches passes through a capability permission system (`runtime/permissions/lib.rs`, ~11.7k lines) that gates read/write/net/env/sys/run/ffi/import, and the CLI is built around a lazily-memoised dependency-injection container (`CliFactory`) so that `deno fmt` never pays for the npm resolver. It targets JS/TS application developers and, increasingly, Node.js users — a large fraction of `ext/node` exists purely for npm/Node compatibility.
System map
The binary entry is a nine-line `cli/main.rs` that delegates to `deno::main()` in `cli/lib.rs`, which parses flags, initialises V8, and dispatches a `DenoSubcommand` to a handler under `cli/tools/`. Shared services are constructed lazily through `CliFactory`, a container of `Deferred<T>` once-cells covering caches, resolvers, the file fetcher, npm installer, type checker, and permission container. The JS runtime is assembled in `runtime/worker.rs::common_extensions`, which registers ~35 extension crates from `ext/` in a strict, snapshot-sensitive order; a V8 startup snapshot is baked at build time by `cli/snapshot/build.rs`. Storage is the DENO_DIR: SQLite caches (`cli/cache/caches.rs`) for dep analysis, type checking, code cache, fast-check, and lint/fmt incremental state, plus an HTTP cache and npm package cache. Builds run through `cargo`/`./x`, CI is generated TypeScript (`.github/workflows/ci.ts` → `ci.generated.yml`), and the deployment shape is a single statically-linked executable distributed per-platform.
- `cli/main.rs` is deliberately a 9-line shim calling `deno::main()`; the real entrypoint is `pub fn main()` in `cli/lib.rs`, which sets a panic hook, raises the fd limit, installs the aws-lc-rs rustls crypto provider, and runs everything inside `create_and_run_current_thread_with_maybe_metrics`.
- Subcommand dispatch is a single `match flags.subcommand` in `run_subcommand` (cli/lib.rs:124-556) mapping each `DenoSubcommand` variant to a `tools::*` handler wrapped in `spawn_subcommand`, which boxes the future locally specifically to avoid blowing the stack on Windows debug builds.
- `DenoSubcommand` is defined in the vendored parser crate at `libs/cli_parser/src/flags.rs:786` with 47 variants (Add, Audit, Bundle, Compile, Deploy, Desktop, Jupyter, Lsp, Pack, Publish, X, …); the CLI re-exports it via `pub use deno_cli_parser::flags::*` in `cli/args/flags.rs`.
- Flag parsing is a hand-written zero-cost parser, not clap: `libs/cli_parser/src/defs.rs` declares the whole command tree as `const` `ArgDef`/`CommandDef` tables living in `.rodata`, `parse.rs` walks argv against them, and `convert.rs` turns the `ParseResult` into `Flags`; `help.rs` and `completions.rs` render from the same tables.
- `CliFactory` is the dependency-injection root: `CliFactoryServices` holds ~25 `Deferred<T>` (once-cell) slots — `caches`, `file_fetcher`, `npm_installer_factory`, `resolver_factory`, `type_checker`, `root_permissions_container`, `module_graph_builder` — so a subcommand only constructs the services it actually touches.
- The JS runtime's extension set is one ordered `vec![]` in `runtime/worker.rs::common_extensions` (line 1199) listing deno_telemetry, deno_webidl, deno_web, deno_webgpu, deno_image, deno_canvas, deno_fetch, deno_cache, deno_websocket, deno_webstorage, deno_crypto, deno_ffi, deno_net, deno_tls, deno_kv, deno_cron, deno_napi, deno_http, deno_io, deno_fs, deno_os, deno_process, deno_node_crypto, deno_node_sqlite, deno_node, plus crate-local ops; a comment warns ordering must stay in sync with `web_worker.rs`, `snapshot_info.rs`, and `snapshot.rs`.
- `deno_core` is vendored into this repo as `libs/core` (package name `deno_core`, version 0.412.0) rather than consumed from crates.io, alongside `libs/serde_v8`, `libs/ops` (the `#[op2]` proc macro), and `libs/deno_v8`; `libs/dcore/src/main.rs` is a standalone dev harness binary for that core.
- Persistent state lives in DENO_DIR as SQLite `CacheDB`s created by `cli/cache/caches.rs`: `fmt_incremental_cache_db`, `lint_incremental_cache_db`, `dep_analysis_db`, `fast_check_db`, `node_analysis_db`, `type_checking_cache_db`, and `code_cache_db`; `CliFactory::caches()` pre-warms the relevant ones based on the subcommand.
- A V8 startup snapshot is produced at build time: `cli/snapshot/build.rs` calls `deno_runtime::snapshot::create_runtime_snapshot` writing `CLI_SNAPSHOT.bin` plus `EXTENSION_RESIDUAL_SOURCES.rs`, and `cli/build.rs` compresses ~60 TypeScript `lib.*.d.ts` declaration files into the binary.
- `deno compile` produces a self-contained executable by appending a virtual filesystem and metadata to a base binary: `cli/standalone/binary.rs` defines `DenoCompileBinaryWriter::write_bin` and frames the payload with `MAGIC_BYTES`, using `VfsBuilder`/`VirtualDirectory` from `deno_lib::standalone::virtual_fs`.
- There are two runtime binaries: the full `deno` CLI (`cli/`) and `denort` (`cli/rt_desktop`, v2.7.4) — a trimmed runtime used as the base for compiled output, with its own `binary.rs`, `file_system.rs`, and `code_cache.rs`.
- The developer entrypoint is `./x`, a shebang script running `tools/x.ts` under Deno; it exposes a `Record<string, Command>` registry with `setup`, `build`, `check`, `test-unit`, `test-node`, `test-compat`, `test-spec`, `test-core`, `test-napi`, `fmt`, `lint`, `lint-js`, `verify`, each documented with LLM-oriented help text.
Feature catalog
The CLI surface is unusually broad for a runtime: beyond `run`/`eval`/`repl`/`serve`, it ships a full npm/JSR package manager (`add`, `remove`, `install`, `outdated`, `why`, `audit`, `link`, `pack`, `publish`), a TypeScript type checker, dprint-backed formatter, deno_lint-backed linter with a JS plugin API, a test runner with leak sanitizers and coverage, a benchmarking harness, a documentation generator, a Jupyter kernel, an LSP server, a single-binary compiler, and a Deno Deploy client that bootstraps itself from JSR at runtime. Extensions in `ext/` expose the user-visible Web and Node APIs: fetch, WebSocket, WebGPU, Web Crypto, KV, Cron, FFI, N-API, and a 119-file Node polyfill tree. `deno serve` adds multi-process HTTP serving with `--parallel`, and a tunnel client can expose a local process through Deno's cluster endpoint.
- Package management is consolidated in `cli/tools/pm`, reached from `run_subcommand` for `Add`, `Audit`, `ApproveScripts`, `Remove`, `Link`, `Unlink`, and `Why`; `deno cache` is implemented as a thin alias onto `tools::installer::install_from_entrypoints`.
- `deno serve` runs the module as an HTTP server with configurable parallelism: `tools::serve::serve` resolves the main module, optionally opens a browser via `open::that_detached`, and fans out to `parallelism_count(serve_flags.parallel)` workers, each labelled `[serve-worker-nn]` in console output by `99_main.js`.
- A module with `export default { fetch }` is auto-detected at bootstrap: `core.addMainModuleHandler` in `bootstrapMainRuntime` calls `registerDeclarativeServer`, warns when run under `deno run`, and errors when run under `deno serve` without it.
- `deno deploy` does not embed the deploy client: it fetches `@deno/deploy/meta.json` from JSR at runtime, picks the latest version, and executes `jsr:@deno/deploy@<version>` in a worker with `PermissionsContainer::allow_all` plus a custom `ops::deploy::deno_deploy` extension.
- The tunnel feature connects to `tunnel.global.prod.deno-cluster.net:443` (overridable via `DENO_DEPLOY_TUNNEL_ENDPOINT`), authenticates with `Authentication::App { token, org, app }`, prints 'You are connected to https://…' on `Event::Routed`, and injects the tunnel's returned metadata as process env vars.
- `deno test` supports coverage by creating the coverage dir and exporting `DENO_COVERAGE_DIR` (canonicalised) so spawned subprocesses share it; `--coverage-raw-data-only` plus `--clean` control whether prior contents are wiped.
- The test runner ships resource/op/timer leak sanitizers: `cli/tools/test/sanitizers.rs` diffs `RuntimeActivityStats` across a test, spins the event loop up to `MAX_SANITIZER_LOOP_SPINS` (16), and tracks `LeakKey::{AsyncOp,Resource,Timer,Interval}` so leaks escaping a sanitizer-ignoring test aren't misattributed to later tests.
- A Jupyter kernel is built in: `DenoSubcommand::Jupyter` routes to `tools::jupyter`, and the kernel-side JS (`40_jupyter.js`, `jupyter_kernel.js`) is lazy-loaded into the worker by `CliMainWorkerFactory::create_custom_worker` when `needs_test_modules` is set.
- The LSP is a tower-lsp server started by `cli::lsp::start()` over stdio, with ~30 modules including `ts_server.rs` (the largest, driving TypeScript), `diagnostics.rs`, `completions.rs`, `registries.rs` for import completions, and `parent_process_checker.rs` to exit when the editor dies.
- `deno publish` supports GitHub Actions OIDC: when `auth::is_gha()` and `gha_oidc_token()` are present and `DISABLE_JSR_PROVENANCE` is unset, it exchanges the Actions ID token for a `githuboidc <token>` authorization and attaches provenance.
- Node compatibility is the largest single extension: `ext/node` (~6 MB of source) declares `deno_core::extension!(deno_node, …)` with ops for blocklists, buffers, DNS, module hooks, and a `polyfills/` directory of 119 entries covering node:fs, node:http, node:stream, node:child_process, node:cluster, node:repl and more.
- Most node polyfills are registered as `lazy_loaded_esm` rather than eager `esm` so that non-Node `deno run` programs skip roughly 2 MB of polyfill deserialization; `node:module`, `node:process`, `node:stream`, `node:net`, `node:tty` and `node:repl` are each explicitly mapped.
Data flow
Input boundaries are argv (parsed into `Flags` by the const-table parser), environment variables and `.env` files, config files (`deno.json`/`package.json`) discovered through the workspace factory, and remote sources fetched over HTTP from JSR and npm registries. `CliFactory` owns the service graph; `ModuleGraphBuilder` builds a `deno_graph::ModuleGraph` from roots, `TypeChecker` optionally validates it against a TSC-backed cache, and `CliEmitter` transpiles TypeScript to JavaScript. At runtime, `CliModuleLoader::load` serves module source out of the prepared graph (or delegates to JS load hooks, or to an eszip), and V8 executes it. Every side-effecting op re-enters Rust through `PermissionsContainer`, which is the single state owner for capability decisions. Output boundaries are stdout/stderr (via `deno_print`/`DrawThread`), the DENO_DIR SQLite and HTTP caches, the lockfile, OpenTelemetry exporters, and for `compile`, a written executable.
- argv → `Flags` happens in `resolve_flags_and_init`, which special-cases the `COMPLETE` env var for shell completions, distinguishes `--version` from `-V` before parsing, and only then calls `flags_from_vec_with_initial_cwd`.
- `.env` files are loaded into the process environment before any service construction: `load_env_variables_from_env_files(&cwd, files, flags.log_level)` runs right after flag parsing, and `WatchEnvTracker::snapshot()` captures pre-load state when `--watch` is active.
- Unstable feature configuration is merged from three sources in order: CLI flags, `flags.unstable_config.fill_with_env(&sys)`, and the `DENO_COMPAT` env var which force-enables node compat; `DENO_CONDITIONS` similarly seeds `flags.node_conditions`.
- Module loading is two-phase: `ModuleLoadPreparer::prepare_module_load` builds the graph with npm resolution (`build_graph_roots_with_npm_resolution`), validates roots, runs `TypeChecker::check` only when `type_check_mode().is_true()` and the graph wasn't already checked, then writes the lockfile *after* type checking because checking can discover `@types/node`.
- `CliModuleLoader::load` has three source paths in priority order: an eszip loader when `maybe_eszip_loader` is set, the JS module-hook bridge (`hook_registry.push_load`) for async non-CJS loads, and otherwise the prepared graph; it records every specifier into `loaded_files` for watch-mode invalidation.
- The load-hook bridge deliberately skips CJS modules because `Module._load` already runs `executeLoadHookChain` synchronously — going through both would invoke user hooks twice.
- Remote source fetching flows through `CliFileFetcher`, which takes a `FetchPermissionsOptionRef` per call; `deploy` explicitly passes `AllowAll` plus `CacheSetting::RespectHeaders` so jsr.io's `cache-control: max-age=60` is honoured instead of pinning the first cached copy forever.
- npm resolution mode determines the read-permission checker: `create_module_loader_factory` picks `NpmRegistryReadPermissionCheckerMode::Byonm`, `::Local(node_modules_dir)`, or `::Global(npm_cache_root)` before constructing `CliModuleLoaderFactory`.
- The root permission set is derived once from config+flags: `CliFactory::root_permissions_container` builds `Permissions::from_options(desc_parser, cli_options.permissions_options()?)` and wraps it in a `PermissionsContainer` that all workers clone from.
- Worker construction pre-installs npm packages: `create_custom_worker` collects `NpmPackageReqReference`s from the main module plus preload/require modules and calls `npm_installer.add_package_reqs` with `PackageCaching::Only` under the Lazy strategy or `PackageCaching::All` otherwise.
- For an `npm:` main module, the specifier is rewritten to a resolved bin entrypoint via `resolve_npm_binary_entrypoint`, and the lockfile is flushed with `write_if_changed()` so the next run can reuse the resolution.
- Bootstrap data crosses the Rust→JS boundary as a positional array: `bootstrapMainRuntime(runtimeOptions)` destructures indices 0–19 (denoVersion, location, unstableFeatures, hasNodeModulesDir, argv0, nodeDebug, mode, servePort, serveHost, otelConfig, standalone, nodeClusterUniqueId, …) and then removes the bootstrap object from the global scope.
Behavior traces
Three flows dominate. Startup: `main()` → panic hook/logging/fd-limit → optional control-socket handshake → `resolve_flags_and_init` → `init_v8` → `run_subcommand`. Primary action (`deno run`): `tools::run::run_script` → `CliFactory` service warm-up → main-module resolution → npm install → `create_cli_main_worker_factory_with_roots` → `create_main_worker_with_unconfigured_runtime` → `CliMainWorker::run`. Persistence (`deno compile`): `compile_inner` → module graph creation → `DenoCompileBinaryWriter::write_bin` → VFS build → `MAGIC_BYTES`-framed binary written to disk. A fourth path worth knowing is the pre-warmed-runtime handshake on Unix, where `DENO_UNSTABLE_CONTROL_SOCK` lets a parent hand this process an already-initialised `UnconfiguredRuntime` and a replacement argv.
- Startup order in `main()` is: `boot_phase("main start")` → `setup_panic_hook()` → `init_logging(None, None)` → `raise_fd_limit()` / `ensure_stdio_open()` → set prompt callbacks to `DrawThread::hide`/`show` → `maybe_setup_permission_broker()` → install aws-lc-rs → `maybe_rewrite_node_arg0(args)`.
- `boot_phase(label)` is a built-in startup profiler: gated on `DENO_STARTUP_PHASES`, it prints `[boot] <label> <elapsed>` to stderr at ~10 checkpoints including 'after init_v8', 'after cli_options', 'after worker_factory', and 'after worker.run (exit)'.
- On Unix, `wait_for_start` reads `DENO_UNSTABLE_CONTROL_SOCK`, immediately removes the env var, and may return `(UnconfiguredRuntime, Vec<OsString> args, PathBuf cwd)` — a pre-initialised runtime that causes `main()` to skip `init_v8` entirely.
- V8 flags are resolved in `init_v8`: `DenoSubcommand::Lsp` gets `--stack-size=1024 --max-old-space-size=3072` (matching VS Code), everything else gets `get_default_v8_flags()`; a `--single-threaded` flag in either argv or `V8_FLAGS` env switches to `v8::Platform::new_single_threaded`.
- The tunnel is connected before everything else because it sets env vars and OTEL: `flags.tunnel` (or `DENO_CONNECTED`) triggers `initialize_tunnel` prior to unstable-config resolution, and the subcommand is explicitly excluded when it is `Deploy`.
- `initialize_tunnel` resolves credentials through a four-way fallback — (DENO_DEPLOY_TOKEN + ORG + APP) → deploy config app + env token → deploy config + keyring token → interactive `auth_tunnel` — and on `Error::Unauthorized` deletes the stored credential and retries once after re-auth.
- `auth_tunnel` re-executes the current binary (`env::current_exe()`) as `deno deploy tunnel-login --out <tempfile>`, waits for it, and parses `AuthTunnelOutput` JSON from the temp file; a non-zero exit calls `deno_runtime::exit(1)`.
- `run_script` order is: `check_permission_before_script` → watch-mode branch → `CliFactory::from_flags` → `cli_options` → `deno_dir` → `http_client_provider` → `workspace_resolver` → `node_resolver` → background upgrade check → `resolve_main_module_with_resolver` → `maybe_npm_install` → `try_run_npm_bin_executable` → worker factory → `worker.run()`.
- Before loading any JS, `run_script` checks whether an `npm:` main module resolves to a *native* binary: `resolve_npm_native_bin` resolves the package folder, finds the bin via `tools::x::find_bin_value`, and re-checks file magic with `node_resolver::is_binary` because `read_bin_value` classifies any shebang-less file as `BinValue::Executable`.
- `check_permission_before_script` warns (but does not fail) when permission flags appear *after* the script argument — a common user error that would otherwise silently pass them to the script as argv.
- Test/bench/jupyter/lint runtimes get extra JS injected at worker-creation time: `create_custom_worker` calls `lazy_load_es_module_with_code` for `40_test_common.js`, `40_test_snapshot.js`, `40_test.js`, `40_bench.js`, `40_jupyter.js`, `jupyter_kernel.js`, `40_lint_selector.js`, `40_lint.js` when `needs_test_modules` is set (driven by `DenoSubcommand::needs_test()`).
- Under `--watch`, `run_with_watch` drives `util::file_watcher::watch_recv` with `WatcherRestartMode::Automatic`, rebuilding a `CliFactory::from_flags_for_watcher` per iteration; `--watch` is rejected outright for npm packages whose bin is a native executable.
Testing signals
Testing is Rust-driven but mostly JS/JSONC-authored. Five separate cargo test crates each use `file_test_runner` with `harness = false` and a custom `main()`: `specs_tests` (CLI integration via `__test__.jsonc`), `unit_tests` (`tests/unit/*_test.ts`), `unit_node_tests`, `node_compat_tests`, and `integration_tests` (28 Rust files). Web Platform Tests run from a git submodule under `tests/wpt`, and results are uploaded to wpt.fyi and S3 from CI. Each runner computes a content hash of its inputs (`check_ci_hash`) and skips entirely when nothing changed. Coverage gaps are structural rather than accidental: the spec suite exercises the CLI as a black box, so internal Rust units like `CliFactory` wiring, the tunnel client, and `standalone/binary.rs` framing have little direct unit coverage.
- Spec tests are the primary integration suite: `tests/specs/mod.rs::main` collects one test per directory containing `__test__.jsonc` via `TestPerDirectoryCollectionStrategy`, wrapped in a `FileTestMapperStrategy` that expands the manifest's `tests` map into individual cases.
- There are 58 top-level spec categories under `tests/specs/` (add, bundle, cert, check, compile, coverage, cpu_prof, deploy, jsr, jupyter, lockfile, npm, permission, publish, serve, task, watch, workspaces, x, …), each containing many per-directory manifests.
- The spec runner explicitly blocks agent misuse: if `CLAUDE_CODE_ENTRYPOINT` is set and no filter argument was passed, `main()` panics with 'Running the entire spec test suite from Claude Code is not allowed. Please provide a test filter'.
- Every runner short-circuits on unchanged inputs via `test_util::hash::check_ci_hash`, which hashes `tests/specs`, `tests/util`, `tests/testdata`, `tests/registry`, the `deno` binary, `test_server`, and `denort`; a `CiHashStatus::Skip` returns immediately.
- Unit tests are TS files run through cargo: `unit_tests` collects `tests/unit/.*_test\.ts$` with `TestPerFileCollectionStrategy`; `CLAUDE.md` warns they must not be run directly with `./target/debug/deno test` because they depend on the cargo harness setup.
- Flakiness is first-class: the spec runner threads a shared `FlakyTestTracker` through `run_maybe_flaky_test`, and the `__test__.jsonc` schema has a `flaky: boolean` field plus an `if` field for platform gating (`mac`, `linux`, `windows`, `unix`, `notMacIntel`).
- Spec tests run against a live local HTTP server: `let _http_guard = test_util::http_server();` is held for the duration of the run, and `tests/util/server/` builds the `test_server` binary with `servers/`, `https.rs`, and `npm.rs` fixtures.
- Rust-side integration tests live in `tests/integration/` as 28 files (`lsp_tests.rs`, `npm_tests.rs`, `publish_tests.rs`, `watcher_tests.rs`, `upgrade_tests.rs`, `compile_tests.rs`, `sqlite_extension_tests.rs`, …) in the `integration_tests` crate, with an `upgrade` cargo feature gating upgrade tests.
- Node.js compatibility is tested against the real Node test suite pulled in as a submodule: CI clones `./tests/node_compat/runner/suite` only for the `node_compat` test crate, and the results are summarised by `tests/node_compat/report.rs` and `slack.ts`.
- WPT runs on non-tag builds: CI clones `tests/wpt/suite`, writes the hosts file, runs `./tests/wpt/wpt.ts run --all --quiet`, and on release uploads `wpt.json`/`wptreport.json.gz` to `s3://dl-deno-land/wpt/` and to wpt.fyi via `tools/upload_wptfyi.js`.
- Core crate tests run under cargo-nextest, not plain cargo test: `./x test-core` and CI both run `cargo nextest run` across `deno_core`, `deno_v8`, `build-your-own-js-snapshot`, `dcore`, `deno_ops`, `deno_ops_compile_test_runner`, `serde_v8`, `deno_core_testing`, plus a separate `cargo test --doc` pass.
- CI asserts tests do not mutate the working tree: after running a test crate it checks git status and fails with '❌ Git working directory is dirty. Ensure `cargo test` is not modifying git tracked files.'
Risk map
The blast radius concentrates in three places: the permission system (an 11.7k-line file that every capability check funnels through), the extension ordering list in `runtime/worker.rs` (silently snapshot-coupled across four files), and the many `unsafe { std::env::set_var(...) }` calls in startup that are only sound because they run before any thread is spawned. Security-sensitive paths include FFI `dlopen`, subprocess spawn with LD_PRELOAD-style env filtering, the permission broker's IPC socket, the Deno Deploy keyring token, the runtime-fetched deploy CLI executed with `allow_all`, and the `node` shim that mutates the process `PATH`. Several performance-motivated lazy-loading decisions (lazy node polyfills, hoisted `registerDeclarativeServer` guard) are fragile in the sense that they encode non-obvious invariants in comments rather than tests.
- `deno deploy` executes code fetched from JSR at runtime with `PermissionsContainer::allow_all` — the version is chosen by `resolve_deploy_cli_version` from `@deno/deploy/meta.json`, so a compromised or unexpected JSR publish would run fully privileged on the user's machine.
- Subprocess spawn blocks a real privilege-escalation vector: `check_run_permission` refuses to spawn with any `LD_*`/`DYLD_*` env var set unless `--allow-run` is granted for all, because those allow arbitrary code execution via LD_PRELOAD.
- `op_ffi_load` gates on `check_ffi_partial_with_path` and then calls `Library::open` on the (possibly denort-remapped) path — once loaded, the dynamic library runs with full process privileges outside the permission model.
- The permission prompter defends against terminal spoofing by escaping bidi and zero-width Unicode formatting controls (U+202E RTL Override, U+2066 isolates, U+200B ZWSP, …) that would otherwise let a malicious path forge the prompt label — the Trojan-Source vector.
- `DENO_PERMISSION_BROKER_PATH` connects permission decisions to an external process over an IPC pipe; a connection failure calls `std::process::exit(87)` with a TODO noting the exit code was randomly chosen and undocumented.
- The Deno Deploy token is stored in the OS keyring under service 'Deno Deploy Token' / user 'Deno Deploy' and is read during tunnel setup; on an `Unauthorized` response the code deletes the credential and re-runs interactive auth.
- Startup performs multiple `unsafe { std::env::set_var(...) }` calls — `DENO_CONNECTED`, `DENO_COVERAGE_DIR`, `DENO_DEPLOY_CLI_SANDBOX`, `PATH`, and every key/value the tunnel server returns in its metadata — each justified only by a comment asserting single-threadedness; adding a thread earlier in startup would make these unsound.
- The tunnel injects arbitrary server-supplied environment variables into the process: `for (k, v) in metadata.env { std::env::set_var(k, v) }` with no allowlist, so the tunnel endpoint is effectively trusted to configure the local process.
- The `node` PATH shim writes a symlink (Unix) or `.exe`-named shim (Windows) into DENO_DIR and prepends that directory to `PATH`, meaning any process spawned by user code — not just Deno — resolves `node` to the Deno binary; correctness depends on the scan finding no real Node first.
- The extension registration order in `common_extensions` is a cross-file invariant with no automated check: a comment says it must be kept in sync with `runtime/worker.rs`, `runtime/web_worker.rs`, `runtime/snapshot_info.rs`, and `runtime/snapshot.rs`, and a mismatch surfaces as snapshot corruption rather than a compile error.
- `ext/node`'s lazy/eager split is a hand-maintained list with a documented trap: an explicit `esm` entry that no eager module imports 'would land in the module map unevaluated and fail snapshot validation with NonEvaluatedModules'.
- `runtime/permissions/lib.rs` is a single 11,756-line module owning every descriptor type, the allow/deny/prompt state machine, and `PermissionsContainer` — the highest-blast-radius file in the repo for security regressions.
Extension points
New native capability is added as an `ext/<name>` crate using `deno_core::extension!` (ops + esm + lazy_loaded_esm), registered into the ordered list in `runtime/worker.rs::common_extensions`, and exposed to JS from the extension's own `.js`/`.ts` files. New CLI surface is added by extending the `const` tables in `libs/cli_parser/src/defs.rs`, adding a `DenoSubcommand` variant in `flags.rs`, a conversion in `convert.rs`, an arm in `run_subcommand`, and a handler module under `cli/tools/`. New shared services are added as `Deferred<T>` fields on `CliFactoryServices` with an accessor method. The `deno task` shell has its own `ShellCommand` registry, the module loader has a JS-level load-hook registry, and the developer tooling itself is extensible via `tools/x.ts`'s command record and `.claude/skills/`.
- An extension is declared with the `deno_core::extension!` macro taking `deps`, `parameters`, `ops = [...]`, `esm = [...]`, `lazy_loaded_esm = [...]`, and `synthetic_esm = [...]` — `ext/node/lib.rs:201` is the most elaborate example, parameterised over `InNpmPackageChecker`, `NpmPackageFolderResolver`, and `ExtNodeSys`.
- Ops are written with the `#[op2]` proc macro defined at `libs/ops/lib.rs:25`, with the codegen split across `libs/ops/op2/`, `conversion/`, `cppgc.rs`, and `webidl/`, and a dedicated `libs/ops/compile_test_runner` crate that asserts macro expansion still compiles.
- Registering a new extension means adding one line to the `vec![]` in `common_extensions` at the correct position; `.disable()` is available to register ops without activating them, as done for `ops::web_worker::deno_web_worker` in the main worker.
- Adding a CLI subcommand is a five-file change: a `DenoSubcommand` variant + flags struct in `libs/cli_parser/src/flags.rs`, a `CommandDef` in `defs.rs`, a converter in `convert.rs`, an arm in `cli/lib.rs::run_subcommand`, and a handler under `cli/tools/`.
- New shared services plug into `CliFactoryServices` as a `Deferred<T>` field plus a `pub fn` accessor using `get_or_try_init` / `get_or_try_init_async`; the async variant exists specifically because some initialisation futures were large enough to overflow the Windows stack.
- `deno task` exposes a shell-command registry: implementing `deno_task_shell::ShellCommand` and registering it (as `NpmCommand`, `NpxCommand`, `NodeCommand`, `NodeGypCommand`, `DenoCommand` do) intercepts that binary name inside task scripts.
- The module loader exposes a JS-facing hook system: `ops::module_hooks::{op_module_hooks_register, op_module_hooks_poll_load, op_module_hooks_respond_load, op_module_default_resolve}` back a `LoaderHookRegistry` that `CliModuleLoader::load` consults before the graph, supporting Node-style `nextLoad(newUrl)` delegation.
- Alternative module sources plug in at `CliModuleLoaderFactory` construction: an `EszipModuleLoader` (from `eszip_module_loader_provider`) short-circuits `load()` entirely, which is how `deno run --eszip` works.
- CLI-only ops live in `cli/ops/` (`bench.rs`, `deploy.rs`, `jupyter.rs`, `lint.rs`, `testing.rs`) and are injected per-subcommand via the `custom_extensions: Vec<Extension>` parameter of `create_custom_worker` — the deploy path passes `ops::deploy::deno_deploy::init()`.
- The developer CLI is itself a registry: `buildCommands(root)` in `tools/x.ts` returns a `Record<string, Command>` where each entry supplies `description`, a long `help` string explicitly written for LLM consumption, and an async `fn`; `cargoTestCommand` is a factory for the five test commands.
- Agent workflows plug in via `.claude/skills/`: six skills (`fmt`, `lint-all`, `lint-js`, `node-compat`, `issue-triage`, `review-pr`) each declare frontmatter with `allowed-tools` — e.g. `review-pr` is scoped to `Bash(gh *) Bash(git *) Read Glob Grep Agent`.
- New CI jobs are added by editing the TypeScript generators (`.github/workflows/ci.ts` etc.) and regenerating; `job()`, `step()`, `defineMatrix()` and condition helpers like `hasPrLabel("ci-full")` compose the workflow, and lint fails if the `.generated.yml` is stale.
Contributor handoff
Read `CLAUDE.md`, `tools/x.ts`, `cli/lib.rs`, and `cli/factory.rs` first — those four give you the command surface, the dispatch table, and the service graph. Use `./x` rather than raw cargo: `./x check` for fast feedback, `./x build` for a binary, `./x test-spec <filter>` for integration tests (a filter is mandatory and the runner will panic without one under Claude Code), and `./x verify` (fmt + lint-js) before committing. The repo's conventions are enforced mechanically: dprint formatting, a copyright-header check, generated-workflow freshness, and a clippy `disallowed_methods` list that is why you see `#[allow(clippy::disallowed_methods, reason = "…")]` sprinkled through startup code. Keep diffs minimal and avoid drive-by changes — the CLAUDE.md git workflow says so explicitly, and PRs are squash-merged so never force-push.
- Read first, in order: `CLAUDE.md` (build/test/PR conventions), `tools/x.ts` (the command registry with long help strings), `cli/lib.rs` (`main()` at :863 and `run_subcommand` at :124), `cli/factory.rs` (`CliFactoryServices` at :302), and `runtime/worker.rs` (`common_extensions` at :1199).
- Never run the full spec suite: `tests/specs/mod.rs::main` panics when `CLAUDE_CODE_ENTRYPOINT` is set without a filter — always use `./x test-spec <filter>` or `./x test-spec --list`.
- Pre-commit is `./x fmt` then `./x lint-js` (bundled as `./x verify`) for JS/TS-only changes; if Rust changed, also run `./x lint` (includes clippy) and `./x check`.
- Unit tests must go through cargo, not the built binary: `./x test-unit <filter>` runs `cargo test -p unit_tests --test unit -- <filter>`; `CLAUDE.md` explicitly warns against `./target/debug/deno test` for `tests/unit/`.
- Formatting is dprint-driven with a repo config; do not hand-format. `tools/format.js` runs `npm:dprint@0.47.2 fmt --config .dprint.json`, and `--check` mode is available for verification.
- Every source file needs the copyright header `// Copyright 2018-2026 the Deno authors. MIT license.` — `checkCopyright()` runs as part of `tools/lint.js` whenever both JS and Rust linting are enabled.
- Clippy enforces a disallowed-methods/types list per crate via `clippy.toml` files (present in `cli/`, `runtime/`, `ext/*`, `libs/*`); bypassing one requires an `#[allow(clippy::disallowed_methods, reason = "...")]` with a written reason, as seen around `std::env::current_dir` and `std::env::var_os` in startup.
- Safe edit boundary for new native APIs: add ops inside the relevant `ext/<name>` crate, keep the `common_extensions` position stable, and remember the snapshot files (`runtime/snapshot.rs`, `runtime/snapshot_info.rs`, `runtime/web_worker.rs`) must stay in sync.
- Anything in `.github/workflows/*.generated.yml` is build output — edit the `.ts` sibling and regenerate, or `ensureWorkflowYmlsUpToDate()` in `tools/lint.js` will fail CI.
- Trap: adding a thread earlier in `main()` breaks the safety argument for the startup `env::set_var` calls; the node-shim setup, tunnel init, and `DENO_CONNECTED` assignment all carry comments stating they must run before any thread spawns.
- Trap: large futures overflow the stack on Windows debug builds. `spawn_subcommand` uses `boxed_local()`, several subcommands add explicit `.boxed_local()`, and `Deferred::get_or_try_init_async` documents boxing for the same reason — preserve these when refactoring.
- Git workflow per `CLAUDE.md`: branch off `main`, never force-push to a PR (commits are squashed on merge so incremental commits help reviewers), and keep changes scoped — no drive-by fixes in an unrelated PR.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 3573 | 55% |
| JSON | 2016 | docs/config |
| JavaScript | 1712 | 26% |
| Rust | 1052 | 16% |
| TOML | 152 | 2% |
| Markdown | 129 | docs/config |
| CSS | 28 | docs/config |
| YAML | 27 | docs/config |
Start reading here
Probable source entrypoint: Cargo.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
libs/ (801 files) ext/ (752 files) cli/ (517 files)
Source layout — cli/
| Path | Files |
|---|---|
cli/args/ | 3 |
cli/benches/ | 1 |
cli/cache/ | 9 |
cli/examples/ | 1 |
cli/js/ | 9 |
cli/lib/ | 25 |
cli/lsp/ | 38 |
cli/ops/ | 6 |
cli/rt/ | 12 |
cli/rt_desktop/ | 3 |
cli/schemas/ | 10 |
cli/snapshot/ | 5 |
cli/standalone/ | 4 |
cli/tools/ | 98 |
Repository structure
| Top-level path | Files |
|---|---|
tests/ | 1720 |
libs/ | 801 |
ext/ | 752 |
cli/ | 517 |
runtime/ | 74 |
tools/ | 69 |
.github/ | 32 |
doc/ | 7 |
Docs found: CLAUDE.mdLICENSE.mdREADME.md
Package
Cargo.toml — , 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeCLAUDE.md— Claude instructions.github/workflows/cargo_publish.generated.yml— GitHub Actions workflow.github/workflows/ci.generated.yml— GitHub Actions workflow.github/workflows/create_prerelease_tag.generated.yml— GitHub Actions workflow.github/workflows/ecosystem_compat_test.generated.yml— GitHub Actions workflow.github/workflows/node_compat_test.generated.yml— GitHub Actions workflow.github/workflows/npm_publish.generated.yml— GitHub Actions workflow.github/workflows/post_publish.generated.yml— GitHub Actions workflow.github/workflows/pr.generated.yml— GitHub Actions workflow
Structural health signals
Average score 5.5/10 across 96 analyzed source files; 85 hotspots surfaced.
cli/tools/desktop.rs— 2344 lines, score 4 (Large file, Deep nesting)cli/lsp/tsc.rs— 2813 lines, score 4 (Large file, Deep nesting)cli/lsp/language_server.rs— 2521 lines, score 4 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
461f32ec61742026-09-21 — fix(check): pin @types/node and resolve it through the configured registry (#36883)abd22074e47c2026-09-17 — chore: forward v2.9.7 release commit to main (#36859)685eb3b4b4ff2026-09-16 — fix(cli): preserve double dash before the entrypoint (#36835)057a3da097622026-09-16 — fix(ext/http): extract traceparent from Deno.serve regardless of header name case (#36840)800a5e996c002026-09-16 — fix(tests): update check_node_builtin_modules output for new @types/node (#36848)