deno Documentation

A modern runtime for JavaScript and TypeScript.

108,725 stars Rust Rust 1 file ยท ~10,144 tokens #deno#javascript#rust#typescript
RAW Doc

Technical Documentation: denoland/deno

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (README.md)

Deno


Twitter badge Bluesky badge
Discord badge YouTube badge

Deno
(/หˆdiหnoสŠ/, pronounced
dee-no) is a JavaScript, TypeScript, and WebAssembly runtime with secure
defaults and a great developer experience. It's built on V8,
Rust, and Tokio.

Learn more about the Deno runtime
in the documentation.

Installation

Install the Deno runtime on your system using one of the commands below. Note
that there are a number of ways to install Deno - a comprehensive list of
installation options can be found
here.

Shell (Mac, Linux):

sh
curl -fsSL https://deno.land/install.sh | sh

PowerShell (Windows):

powershell
irm https://deno.land/install.ps1 | iex

Homebrew (Mac):

sh
brew install deno

Chocolatey (Windows):

powershell
choco install deno

WinGet (Windows):

powershell
winget install --id=DenoLand.Deno

Scoop
(Windows):

powershell
scoop install main/deno

Build and install from source

Complete instructions for building Deno from source can be found
here.

Your first Deno program

Deno can be used for many different applications, but is most commonly used to
build web servers. Create a file called server.ts and include the following
TypeScript code:

ts
Deno.serve((_req: Request) => {
  return new Response("Hello, world!");
});

Run your server with the following command:

sh
deno run --allow-net server.ts

This should start a local web server on
http://localhost:8000.

Learn more about writing and running Deno programs
in the docs.

Additional resources

  • Deno Docs: official guides and reference docs for
    the Deno runtime, Deno Deploy, and beyond.
  • Deno Standard Library: officially supported common
    utilities for Deno programs.
  • JSR: The open-source package registry for modern
    JavaScript and TypeScript
  • Developer Blog: Product updates, tutorials, and
    more from the Deno team.

Contributing

We appreciate your help! To contribute, please read our
contributing instructions.


Doc README (`doc/README.md`)

Agent & contributor documentation

This directory holds long-form, mostly-stable context about how the Deno
codebase is structured. It is meant to be read by both humans and AI coding
agents before they start working in the repository.

CLAUDE.md at the repository root is the short, operational guide: how to
build, which command runs which test, formatting and lint rules, the git
workflow. The files in doc/ are the longer companion to it: the architectural
"why" and the map of where things live, the kind of background that is too large
to keep in CLAUDE.md but that saves a lot of exploration time up front.

Contents

  • architecture.md โ€” the layered design of the runtime:
    the deno CLI crate, the deno_runtime crate, the ext/* extensions, and
    the deno_core / libs/* foundation. Read this first.
  • codebase-map.md โ€” a directory-by-directory map plus the
    handful of files worth understanding before anything else.
  • testing.md โ€” the test taxonomy (spec, unit, unit_node, node
    compat, WPT) and which command runs each one.
  • ci.md โ€” how the CI workflow is generated and how it decides which
    jobs to run, including the docs-only fast path that brought this directory
    into existence.
  • desktop-architecture.md โ€” how deno desktop
    wires a native window to a Deno runtime: the load model, the ABI handshake,
    the two transports, and the lifecycle edges.
  • package-management.md โ€” how deno add /
    deno install add dependencies: the two flag parsers, the config
    writer, the shared install path, and the --dev / --save-optional /
    --no-save flags (including why optionalDependencies need special
    handling).

Editing these docs

These files are plain Markdown and are formatted by deno fmt like the rest of
the repository. Run tools/format.js before committing. Keep prose hard-wrapped
at 80 columns to match the existing style.

A pull request that touches only doc/ runs the lint job and nothing
else. The build, test, bench and deno_core jobs are skipped, because editing
Markdown cannot break the binary. See ci.md for how that is wired
up. If your PR changes anything outside doc/, the full pipeline runs as usual.


Tests Specs Doc Markdown README (`tests/specs/doc/markdown/README.md`)

Hello World

This is a paragraph with bold and italic text.

Code Example

ts
const x = 1;
  • Item 1
  • Item 2
  • Item 3
  1. First
  2. Second

A blockquote.


Example

Inline code here.


Doc Architecture (`doc/architecture.md`)

Architecture overview

Deno is built as a stack of layers. Each layer depends only on the ones below
it, which keeps the system testable and lets the lower layers be reused outside
of the deno binary. From the top down:

text
+-----------------------------------------------------------+
|  cli/            the `deno` binary: subcommands, tooling   |
+-----------------------------------------------------------+
|  runtime/        deno_runtime: assembles the JS runtime    |
+-----------------------------------------------------------+
|  ext/*           extensions: native capabilities for JS    |
+-----------------------------------------------------------+
|  libs/*          deno_core + supporting crates (V8 bridge) |
+-----------------------------------------------------------+
|  V8 + Tokio      JavaScript engine and async runtime       |
+-----------------------------------------------------------+

The CLI layer (`cli/`)

The deno crate in cli/ is everything a user touches directly. It owns flag
parsing, the subcommands (run, test, fmt, lint, compile, bundle,
install, publish, and so on), package-management tooling, the LSP, and the
module loader that ties module resolution to the runtime.

Key entry points:

  • cli/main.rs โ€” process entry point and command routing.
  • cli/args/flags.rs โ€” the full clap flag and subcommand definition. Adding a
    flag or a subcommand starts here.
  • cli/tools/<tool>/ โ€” one module per subcommand (for example
    cli/tools/fmt.rs for a simple command, cli/tools/test/ for a complex one).
  • cli/module_loader.rs โ€” resolves and loads modules, bridging the resolver and
    the graph to the runtime.

The CLI is intentionally heavy: it pulls in TypeScript checking, npm and JSR
resolution, the lockfile, and the bundler. Lower layers must not depend back up
into it.

The runtime layer (`runtime/`)

The deno_runtime crate assembles a working JavaScript runtime out of
deno_core plus a curated set of extensions. It is the piece embedders use when
they want "Deno the runtime" without "Deno the CLI".

Key files:

  • runtime/worker.rs โ€” constructs the main worker: an isolate, the op set, and
    the bootstrap sequence.
  • runtime/web_worker.rs โ€” the Web Worker variant.
  • runtime/permissions/ โ€” the permission model that gates every sensitive op
    (read, write, net, env, run, ffi, sys). Permissions are checked in Rust at the
    op boundary, never in JavaScript.

The extension layer (`ext/*`)

Each directory under ext/ is a self-contained extension: a Rust crate that
defines ops (native functions callable from JS) plus the JavaScript that
exposes a higher-level API on top of them. This is where the platform actually
lives. Examples:

  • Web platform: ext/web, ext/fetch, ext/url, ext/crypto, ext/console,
    ext/webidl, ext/websocket, ext/webgpu, ext/canvas.
  • System access: ext/fs, ext/net, ext/io, ext/os, ext/process,
    ext/signals, ext/tls.
  • Deno-specific: ext/kv, ext/cron, ext/cache, ext/ffi, ext/napi,
    ext/bundle.
  • Node compatibility: ext/node (the bulk of the node:* builtins, both Rust
    ops and JavaScript polyfills), plus ext/node_crypto and ext/node_sqlite.

The typical shape of an extension is:

  1. Rust #[op2] functions that do the privileged work and take a permission
    check where needed.
  2. A 00_*.js / 01_*.js set of JavaScript modules that build the public API
    and call the ops through Deno.core.ops.
  3. Registration of the extension in runtime/worker.rs (and the CLI's snapshot)
    so it is part of the assembled runtime.

When adding native functionality, add the op in the relevant ext/<name>/
crate; do not reach into the runtime or CLI for it.

The core layer (`libs/*`)

libs/ holds deno_core and the crates merged in from the former standalone
deno_core repository. This is the bridge between Rust and V8: it owns the op
infrastructure, the module loader trait, the snapshotting machinery, the
JsRuntime event loop, and the serde_v8 serialization layer.

Notable members:

  • libs/core โ€” deno_core itself: JsRuntime, op registration, the module
    map, the inspector integration.
  • libs/ops โ€” the #[op2] proc-macro that generates the Rust/V8 glue.
  • libs/serde_v8 โ€” zero-copy-ish serialization between Rust types and V8
    values.
  • libs/resolver, libs/node_resolver, libs/npm, libs/npm_installer,
    libs/package_json, libs/lockfile, libs/config, libs/npmrc โ€” the
    resolution and package-management building blocks the CLI composes.

These crates are deliberately free of CLI concerns so they can be unit-tested in
isolation and reused by other tools.

Cross-cutting concepts

  • Ops are the only way JavaScript reaches native code. An op is a Rust
    function exposed to JS; sync ops return immediately, async ops return a future
    that resolves on the event loop.
  • Extensions bundle ops with their JavaScript and are the unit of
    composition for a runtime.
  • Workers are isolated JavaScript execution contexts (the main worker and
    Web Workers); each has its own V8 isolate.
  • Resources are managed handles (open files, sockets, readers) tracked by
    deno_core and passed across the Rust/JS boundary by integer id.
  • Permissions are enforced in Rust at the op boundary. A capability that is
    not granted causes the op to error before doing any work.

Where to make a change

You want toโ€ฆ Start inโ€ฆ
Add or change a CLI flag/subcommand cli/args/flags.rs, cli/tools/
Add a native capability to JS ext/<name>/ (op + JS)
Change how the runtime is assembled runtime/worker.rs
Touch the Rust/V8 bridge or op macro libs/core, libs/ops
Change module/npm/JSR resolution libs/resolver, libs/npm, CLI

See codebase-map.md for a finer-grained directory map and
testing.md for how to test each layer.


Doc Ci (`doc/ci.md`)

Continuous integration

This page explains how the main CI workflow is structured, how it decides which
jobs to run, and the docs-only fast path.

The workflow is generated, not hand-written

The CI workflow is generated. The source of truth is
.github/workflows/ci.ts, a Deno script that builds the workflow object and
writes it to .github/workflows/ci.generated.yml. Never edit the .yml
directly; it carries a GENERATED BY ./ci.ts -- DO NOT DIRECTLY EDIT header.

To regenerate after changing ci.ts:

bash
deno run -A .github/workflows/ci.ts

Other workflows in .github/workflows/ follow the same *.ts โ†’
*.generated.yml pattern.

Job overview

On a pull request the workflow runs, roughly:

  • pre-build โ€” a fast gating job. It decides whether downstream jobs should
    run at all and exposes the decision through job outputs.
  • build (per platform) and build libs โ€” compile Deno and the
    deno_core libraries.
  • test (per platform, sharded) โ€” the spec, unit, node-compat and WPT
    suites, depending on build.
  • bench โ€” benchmarks.
  • deno-core-test and deno-core-miri โ€” the deno_core / libs/*
    crate tests (the latter under Miri).
  • lint โ€” formatting check, jsdoc check, and tools/lint.js (Rust + JS).
  • ci-status โ€” an aggregator that the branch protection rule keys on. It
    passes as long as none of its dependencies failed or were cancelled; skipped
    dependencies are fine.

How `pre-build` gates the rest

pre-build runs a few cheap checks and publishes their results as outputs that
downstream jobs read in their if: conditions:

  • skip_build โ€” set when a draft PR has not opted into CI. When true, the
    build, test, lint, bench and deno_core jobs are all skipped.
  • skip_deno_core_test โ€” set when no deno_core-related files changed, so the
    deno_core crate tests can be skipped (tools/check_deno_core_changes.js).
  • docs_only โ€” set when the PR only touches the doc/ directory
    (tools/check_docs_only_changes.js).

Each gated job's if: combines the relevant outputs, for example:

text
needs.pre_build.outputs.skip_build != 'true' &&
  needs.pre_build.outputs.docs_only != 'true'

The docs-only fast path

A pull request that changes only files under doc/ does not need to compile
or test anything. For those PRs CI runs the lint job and skips everything
else.

How it works:

  1. In pre-build, tools/check_docs_only_changes.js diffs the PR against its
    base SHA. If every changed file is under doc/ (and at least one file
    changed), it writes docs_only=true to the job output. An empty or failed
    diff defaults to false, so the full pipeline runs when in doubt.
  2. The build, build-libs, test, bench, deno-core-test and deno-core-miri
    jobs each add && docs_only != 'true' to their run condition, so they are
    skipped on a docs-only PR. The test jobs depend on build, so skipping
    build cascades to them automatically.
  3. The lint job is intentionally not gated on docs_only. It still runs,
    which means Markdown is still formatted and linted (deno fmt --check covers
    doc/). This is the "lint and nothing else" behavior.
  4. ci-status still runs and passes: its skipped dependencies count as neither
    failure nor cancellation, and lint succeeds, so the required status check
    goes green and the PR can merge.

The moment a PR also touches anything outside doc/, docs_only is false and
the full pipeline runs as normal.

Changing CI behavior

When you change which jobs run or add a new gate, edit ci.ts, regenerate the
YAML, and commit both. The generated file is checked in so reviewers can see the
effective workflow in the diff.


Doc Codebase Map (`doc/codebase-map.md`)

Codebase map

A directory-by-directory tour of the repository, plus the files worth reading
first. For the conceptual layering behind this layout, see
architecture.md.

Top-level directories

Directory What lives here
cli/ The deno binary: subcommands, tooling, LSP, module loader
runtime/ deno_runtime: assembles the JS runtime and workers
ext/ Extensions โ€” native capabilities exposed to JavaScript
libs/ deno_core and the supporting resolution/packaging crates
tests/ All test suites (see testing.md)
tools/ Dev scripts: format.js, lint.js, CI helpers, releases
third_party/ Vendored dependencies and test fixtures
coverage/ Coverage output

Files to understand first

  1. cli/main.rs โ€” entry point and command routing.
  2. cli/args/flags.rs โ€” every CLI flag and subcommand (clap).
  3. runtime/worker.rs โ€” how a worker/runtime is initialized.
  4. runtime/permissions/ โ€” the permission system that gates ops.
  5. cli/module_loader.rs โ€” module loading and resolution.

Inside `cli/`

  • cli/args/ โ€” flag parsing (flags.rs) and resolved configuration.
  • cli/tools/ โ€” one module per subcommand. Simple example: cli/tools/fmt.rs.
    Complex example: the cli/tools/test/ directory. Other notable tools include
    compile.rs, bundle/, coverage/, lint/, pm/ (package management),
    installer/, jupyter/, publish/, repl/, and serve.rs.
  • cli/lsp/ โ€” the language server.
  • cli/module_loader.rs, cli/graph_util.rs โ€” module graph construction and
    loading.

Inside `ext/`

Each subdirectory is one extension (a Rust crate plus its JavaScript). Grouped
roughly by purpose:

  • Web platform: web, fetch, url, crypto, console, webidl,
    websocket, webgpu, canvas, image, broadcast_channel, webstorage.
  • System access: fs, net, io, os, process, signals, tls.
  • Deno APIs: kv, cron, cache, ffi, napi, bundle, telemetry.
  • Node compatibility: node (most node:* builtins), node_crypto,
    node_sqlite.

The conventional layout inside an extension is Rust ops in lib.rs (and
submodules) and JavaScript API files named with a numeric prefix that controls
load order (00_*.js, 01_*.js, โ€ฆ).

Inside `libs/`

The deno_core foundation and the crates the CLI composes for resolution and
packaging:

  • Core/V8 bridge: core, core_testing, ops, serde_v8, dcore.
  • Resolution & packaging: resolver, node_resolver, npm, npm_cache,
    npm_installer, npmrc, package_json, lockfile, config, cli_parser,
    cache_dir.
  • Other building blocks: crypto, dotenv, eszip, http_h1,
    inspector_server, maybe_sync, napi_sys, node_shim.

Inside `tests/`

  • tests/specs/ โ€” the primary integration tests (CLI command + asserted
    output), driven by __test__.jsonc files.
  • tests/unit/ โ€” JavaScript/TypeScript unit tests for runtime APIs
    (*_test.ts).
  • tests/unit_node/ โ€” unit tests for the node:* compatibility layer.
  • tests/node_compat/ โ€” Node.js's own test suite, run against Deno.
  • tests/wpt/ โ€” Web Platform Tests.
  • tests/testdata/ โ€” fixtures shared across suites.

See testing.md for the command that runs each suite.

Inside `tools/`

Developer and CI scripts, all run with Deno:

  • tools/format.js โ€” format the whole tree (deno fmt plus extras).
  • tools/lint.js โ€” lint Rust and JS/TS; pass --js for JS/TS only.
  • tools/check_deno_core_changes.js, tools/check_docs_only_changes.js โ€” CI
    helpers that decide which jobs to run based on the changed files.
  • tools/release/ โ€” release automation.

The ./x helper at the repository root wraps the common build/test commands;
run ./x --help to see what it offers.


Doc Desktop Architecture (`doc/desktop-architecture.md`)

Deno Desktop โ€” internals

How deno desktop actually wires a native window to a Deno runtime. This skips
the obvious (what a WebView is, what Deno.serve does) and documents the
non-obvious mechanics: the load model, the ABI handshake, the two transports,
and the lifecycle edges.

The inversion: Deno is the library, not the host

The usual embedding has the app dlopen a UI lib. Desktop is inverted. The
executable is the native backend (laufey for CEF, laufey_webview for the
system WebView); the Deno runtime is a cdylib named libdenort
(crate-type = ["cdylib"], cli/rt_desktop/). The backend is launched with
--runtime , dlopens it, and calls into it through a C ABI.

Every launcher the packager writes is just that one invocation:

text
exec "$DIR/laufey_webview" --runtime "$DIR/libdenort.so" "$@"   # desktop.rs:1407

So the binary that owns main(), the event loop, and the window is not Deno โ€”
Deno is a guest the backend boots and drives.

The ABI handshake

The dylib exports the Laufey C ABI โ€” laufey_runtime_init / _start /
_shutdown โ€” generated by the laufey::main!(|| { โ€ฆ }) macro (lib.rs:1095).
The closure body is the runtime entrypoint the backend calls after init.

Version safety is two-layered:

  • Compile time (lib.rs:47):
    const _: () = assert!(laufey::LAUFEY_API_VERSION == 26, โ€ฆ). If the linked
    laufey crate's ABI version drifts from what the shipped backend speaks,
    cargo build fails loudly instead of producing a dylib that silently won't
    launch.
  • Build-pin (desktop.rs:29, LAUFEY_VERSION injected by cli/build.rs):
    the backend binary version is pinned, and downloads are integrity-checked
    against in-repo SHA-256 digests (cli/laufey_sums.lock, LAUFEY_PINNED_SUMS)
    โ€” no TOFU on the GitHub releases page. LaufeyBackendResolver resolves in
    order: LAUFEY_DEV_DIR checkout โ†’ cached download โ†’ fresh download.

The dylib finds itself on disk via dladdr on one of its own functions
(get_dylib_path, lib.rs:986) โ€” needed for the auto-update sentinel and for
locating the embedded payload.

The embedded payload

User code + assets are not files next to the dylib; they're a section inside
the dylib
named d3n0l4nd, read back with
libsui::find_section_in_current_image (find_section_in_dylib,
lib.rs:1512). extract_standalone_with_finder parses it into the standalone
metadata + VFS, exactly like deno compile, but sourced from the loaded image
rather than argv0.

Boot ordering (and why it's strict)

Inside laufey::main! everything up to the tokio runtime is deliberately
single-threaded, because two process-global operations happen there that are
unsafe once threads exist:

  1. Port publish before runtime build (lib.rs:1222): allocate a random
    loopback port, then set_var("DENO_SERVE_ADDRESS", "tcp:127.0.0.1:").
    setenv is not thread-safe on glibc (Rust 1.81+ marks it unsafe), so it
    must precede the mio IO thread and inspector thread. The user's Deno.serve
    / export default { fetch } later binds this pre-set address with no
    coordination needed.
  2. chdir into the extracted VFS (lib.rs:1262): process-wide, so it must
    happen before any task resolves a relative path. Frameworks (Next .next/,
    Vite dist/) resolve build output relative to cwd.

Then run_desktop runs two futures concurrently under tokio::select!
(lib.rs:1848): denort::run::run_with_options(โ€ฆ) (the Deno runtime, with
auto_serve: true, serve_port, op_state_init) and laufey::run() (the
native event loop). A third spawned task, navigate_fut, bridges them.

navigate_fut (lib.rs:1773) waits for the server before pointing the window
at it. It does a full GET / HTTP/1.1 and checks for a 2xx/3xx status line
โ€” not a bare TCP connect โ€” because dev servers like Vite accept the socket
before they can actually serve. 60 attempts ร— 250ms, then
Window::navigate(url) on the initial window id. Under
--inspect-brk/--inspect-wait it first blocks until a DevTools client
attaches to the mux.

Two transports

App content and JSโ†”native calls travel on completely separate paths.

Content โ€” loopback HTTP. The WebView is a real browser pointed at
http://127.0.0.1:/. Nothing special.

Bindings โ€” mpsc events + per-call oneshot. Deno.desktop/window APIs and
webviewโ†’Deno function calls do not use HTTP:

  • Native โ†’ runtime: a single bounded mpsc channel of DesktopEvent
    (ops/desktop.rs:119), capacity 1024. High-frequency events (mouse move,
    wheel) use try_send and are dropped on backpressure rather than blocking
    or OOMing the runtime (DesktopEventSender::try_send, ops/desktop.rs:244).
  • One JS consumer: an async loop in DESKTOP_JS awaiting
    op_desktop_recv_event() (desktop.rs:702), dispatching each event to the
    matching DOM-style EventTarget. The op promise is unrefOpPromise'd so the
    pump never keeps the event loop alive by itself.

A bind call round-trip (the interesting one):

  1. JS registers a handler: window.bind(name, fn) stores fn in a per-window
    Map and calls laufey add_binding_async(name) (desktop.rs:229,
    lib.rs:361).
  2. Webview calls window.bindings.<name>(...). The native async binding
    serializes args (laufey::Value โ†’ JSON), allocates a call_id from a global
    AtomicU32 and registers a oneshot sender under it (register_bind_call,
    ops/desktop.rs:297), then sends
    DesktopEvent::BindCall { window_id, name, args, call_id }.
  3. The JS pump's "bindCall" arm looks up the handler, awaits it, and calls
    op_desktop_resolve_bind_call(call_id, result) /
    op_desktop_reject_bind_call(call_id, err) (desktop.rs:736).
  4. The op pops the oneshot for that call_id and fires it
    (ops/desktop.rs:1051); back in the binding future, resp_rx.await resolves
    and calls js_call.resolve(...) / .reject(...) (lib.rs:391).

The call_id keying is what lets many concurrent binding calls from the same
window multiplex over the one event channel and route their replies back
correctly.

The JS namespace the webview sees is bindings โ€” set_js_namespace("bindings")
at lib.rs:1212, so calls are window.bindings.foo().

HMR: three outcomes, decided by V8

cli/rt/hmr.rs watches the source dir (notify, debounced) and classifies each
change into ChangeOutcome (hmr.rs:331), which the desktop side maps to a
ReloadKind callback (lib.rs:1642):

  • Replaced โ€” a script edit that V8 accepts via Debugger.setScriptSource
    after deno_ast transpile. Hot-patched in place, no reload.
  • SoftReload โ†’ ReloadKind::Soft โ€” a static asset changed, or a module was
    removed. Runs location.reload() on every window in open_windows
    (lib.rs:1648), which re-fetches from the still-running server.
  • Restart โ†’ ReloadKind::Restart โ€” V8 rejected the edit as a top-level
    ES-module change (imports / exported bindings / top-level let changed). The
    module graph can't be patched and a reload wouldn't help (the stale runtime
    keeps serving), so restart_desktop_app() calls exit(75) (lib.rs:1537).
    The deno desktop --hmr supervisor owns the child, catches code 75, and
    relaunches โ€” exiting via sentinel rather than re-exec keeps the process group,
    Ctrl-C handling, and temp-entrypoint cleanup with the supervisor
    (HMR_RESTART_EXIT_CODE, lib.rs:1529).

For framework dev servers (is_framework_dev), Deno-level HMR is disabled
entirely โ€” the framework's own websocket HMR runs and cwd is left at the source
dir so its watcher sees real files.

Lifecycle edges worth knowing

  • Fork re-entry guard (lib.rs:1145): framework dev servers fork worker
    processes that re-exec this same dylib. A worker is detected by the
    combination of argv shaped like <exe> run โ€ฆ script.js and a parent
    worker env var (NODE_CHANNEL_FD / NEXT_PRIVATE_WORKER) โ€” the env var alone
    was a false-positive trap when a user shell already had it set. Detected
    workers run headless (no window).
  • Window-handle interop (get_raw_window_handle, lib.rs:467): laufey's
    native handle is converted to raw-window-handle types per platform โ€” AppKit
    / Win32 / X11 / Wayland (LAUFEY_WINDOW_HANDLE_WAYLAND, lib.rs:518).
    Wayland is detected at runtime, giving native Wayland rather than XWayland.
  • --inspect: cli/tools/desktop_devtools.rs runs a CDP multiplexer in the
    parent fronting both the Deno inspector and the renderer's debug port behind
    one /unified websocket.

Doc Package Management (`doc/package-management.md`)

Package management: `deno add` / `deno install`

This is the map for how Deno adds dependencies to a project: how the CLI flags
are parsed, how the configuration file is rewritten, and how packages are
actually installed into node_modules and the lockfile. Read this before
touching deno add, deno install , or the flags that control where a
dependency is written (--dev, --save-optional, --no-save, --save-exact,
--package-json).

The two flag parsers

Flag parsing currently lives in two places and both must be kept in sync
(the second is replacing the first, see the CLI-parser-split work):

  • cli/args/flags.rs โ€” the legacy clap-based parser. The add subcommand is
    defined in add_subcommand(); install reuses the shared argument builders
    (add_dev_arg(), add_optional_arg(), add_no_save_arg()). Both funnel into
    add_parse_inner(), which builds the AddFlags struct.
  • libs/cli_parser/ โ€” the newer hand-written parser. Command shape is declared
    in src/defs.rs (ADD_SUBCOMMAND, INSTALL_SUBCOMMAND) and converted to
    flags in src/convert.rs (add_parse, and the install branch that produces
    InstallFlagsLocal::Add).

AddFlags itself is defined in libs/cli_parser/src/flags.rs and re-exported
through crate::args. When you add a field, you must update: the struct, both
parsers, and every AddFlags { .. } literal (there are literals in the parser
test suites cli/args/flags.rs and libs/cli_parser/src/tests_full.rs; the
shared test case is add_or_install_subcommand, which loops over both add and
install).

There is a lint (ensureNoNonPermissionCapitalLetterShortFlags in
tools/lint.js) that forbids capital-letter short flags unless they are on an
explicit allowlist with a documented precedent. -D (dev) and -O
(save-optional) are on it, both justified by the matching npm install short
flags.

Where a dependency gets written: the config writer

cli/tools/pm/mod.rs is the core. The add() entry point resolves each
requested package to a concrete version
(find_package_and_select_version_for_req) and then decides which config file
to touch.

Two config files can be in play: deno.json (writes to imports) and
package.json (writes to a dependency section). load_configs() discovers
them, and โ€” importantly โ€” will create one if none exists, because Deno needs a
config to manage node_modules. prefer_npm_config / --package-json /
preferPackageJson decide which one an npm package lands in when both exist.

The actual rewrite is ConfigUpdater::add(selected, kind). kind is a
DependencyKind enum (Normal / Dev / Optional):

  • deno.json: kind is ignored โ€” everything goes under imports.
  • package.json: kind selects the section (dependencies, devDependencies,
    optionalDependencies). add() also removes the package from the other two
    sections so it is never declared twice, and new_dependency_section_index()
    inserts a newly created section in a stable order (dependencies โ†’
    devDependencies โ†’ optionalDependencies).

ConfigUpdater::remove() mirrors this and cleans all three sections.

How packages actually get installed

After the config is (optionally) rewritten and committed, add() calls
npm_install_after_modification(), which builds a fresh CliFactory (to pick
up the edited config from disk) and calls cache_deps::cache_top_level_deps().

cache_top_level_deps() (in cli/tools/pm/cache_deps.rs) is the shared install
routine used by add, remove, install, outdated, audit, x, etc. Its
model is: derive the set of graph roots from the project's import map
(deno.json imports) and its package.json dependencies, build the module
graph with npm resolution, then cache_packages() materializes everything into
node_modules.

Gotcha: `optionalDependencies` are never installed from `package.json`

The installer only sees dependencies and devDependencies. This is a
limitation of the external deno_package_json crate: PackageJsonDeps /
resolve_local_package_json_deps() expose only those two maps โ€” there is no
optional_dependencies in the resolved deps used by the installer. So a package
written to optionalDependencies will not be materialized by the normal
install path, even on a plain deno install.

To keep --save-optional at parity with --save-dev (which does install on
add), add() installs optional packages directly instead of relying on the
config-derived roots. CacheTopLevelDepsOptions has an
additional_roots: Vec field: any specifier put there is added to the
graph roots and installed regardless of whether it appears in the config.
add() populates it for both --save-optional and --no-save (see below). A
proper fix โ€” teaching the installer to honor optionalDependencies โ€” is a
separate, cross-crate change and is not done yet.

The flags, end to end

  • --dev / -D: DependencyKind::Dev. Writes to devDependencies, installs
    normally (dev deps are in the config-derived roots).
  • --save-optional / -O: DependencyKind::Optional. Writes to
    optionalDependencies; because the installer ignores that section, the
    package is also pushed to additional_roots so it is installed on add.
  • --no-save: resolve and install the package into node_modules and the
    lockfile, but do not rewrite or commit any config file. Implemented by
    skipping the ConfigUpdater::add/commit calls and pushing the package to
    additional_roots.
  • These three are mutually exclusive (enforced with conflicts_with in both
    parsers).

additional_roots also flows through cache_top_level_deps(): the graph-build
section now runs when there is either an import map or additional roots, so
--no-save works even in a package.json-only project with no import map.

Tests

  • Spec tests live under tests/specs/add/. Relevant ones: dev/,
    save_optional/, no_save/, package_json_flag/, exiting_dev_deps/. Run a
    subset with ./x test-spec add::. A spec test that only needs the config
    result (not the download noise) uses "output": "[WILDCARD]" for the add
    step and asserts the file contents in a following eval step.
  • Parser unit tests: add_or_install_subcommand in both cli/args/flags.rs
    (run via cargo test -p deno --lib add_or_install) and
    libs/cli_parser/src/tests_full.rs
    (cargo test -p deno_cli_parser add_or_install).
  • Make the installer honor optionalDependencies from package.json (upstream
    deno_package_json resolve_local_package_json_deps + the npm installer),
    which would let --save-optional install through the normal config-derived
    path and remove the additional_roots workaround for it.
  • --no-save in a directory with no config still creates an empty config file
    (a side effect of load_configs), which slightly contradicts "no save"; the
    common case (existing project) is unaffected.

Doc Testing (`doc/testing.md`)

Testing

Deno has several test suites, each aimed at a different layer. This page is a
map of what each one is for and how to run it. For the operational command
reference, CLAUDE.md at the repository root is the source of truth; this page
adds the "when do I reach for which suite" context.

The suites at a glance

Suite Location Testsโ€ฆ
Spec tests tests/specs/ CLI commands end-to-end (the main one)
Unit tests tests/unit/ Runtime/web APIs from JS/TS
unit_node tests/unit_node/ The node:* compatibility layer
Node compat tests/node_compat/ Node's own test suite, run on Deno
WPT tests/wpt/ Web Platform Tests (web standards)
Rust tests throughout Crate-level Rust logic

Spec tests โ€” the main integration tests

Spec tests live in tests/specs/. Each test directory contains a
__test__.jsonc file describing one or more steps; a step is a deno
invocation whose output is captured and matched against an expectation. The test
name is the directory name. The expectation language supports wildcards
([WILDCARD], [WILDLINE]), unordered blocks, and inline comments; the schema
is tests/specs/schema.json.

Reach for a spec test whenever a change is observable from the command line: a
new flag, a subcommand behavior, an error message, a resolution outcome.

To create one:

  1. Make a directory under tests/specs/ with a descriptive name.
  2. Add __test__.jsonc with the step(s).
  3. Add any input files the test needs.
  4. Put expected output inline or in a .out file.

Unit tests (`tests/unit/`)

JavaScript/TypeScript tests for runtime and web APIs, named *_test.ts. Use
these for behavior that is best asserted from inside the runtime (a Web API's
semantics, a Deno.* method). They run under the cargo test harness, not under
a bare deno test, because they rely on harness setup.

Node compatibility tests

Two distinct things, often confused:

  • tests/unit_node/ โ€” Deno-authored unit tests for the node:* builtins.
  • tests/node_compat/ โ€” Node.js's own test files, executed against Deno to
    measure compatibility. The set that runs is controlled by
    tests/node_compat/config.jsonc. OS-specific skips use per-OS flags (for
    example "windows": false), not a blanket "ignore".

Web Platform Tests (`tests/wpt/`)

The upstream Web Platform Tests, used to check standards conformance for web
APIs. Run on Linux in CI.

Rust tests

Standard cargo test, plus cargo nextest for the deno_core / libs/*
crates in CI. Lower-layer crates under libs/ are designed to be testable in
isolation; some need a sibling crate co-selected to compile (see CLAUDE.md and
the project memory for the specific gotchas).

Running the suites

CLAUDE.md lists the exact commands. In short, the ./x helper wraps the
common ones:

  • ./x test-spec <name> โ€” spec tests.
  • ./x test-unit <name> โ€” unit tests.
  • ./x test-compat <name> โ€” node compat tests.
  • ./x test-napi โ€” NAPI tests.
  • cargo test unit_node::<module> โ€” unit_node tests.

Run ./x --help to see everything. Before pushing, run tools/format.js and
the appropriate tools/lint.js invocation (--js when only JS/TS changed).

What CI runs

On a pull request, CI builds Deno across the supported platforms and runs the
spec, unit, node-compat and WPT suites, plus the deno_core crate tests and
lint. The exception is a PR that only edits doc/: it runs the lint job alone
and skips the rest, since Markdown changes cannot affect the binary. See
ci.md for how that decision is made.


--- METRICS ---