# Technical Documentation: denoland/deno

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/denoland/deno)

### Overview (README.md)

# Deno

[](https://crates.io/crates/deno)
[![Twitter badge][]][Twitter link] [![Bluesky badge][]][Bluesky link]
[![Discord badge][]][Discord link] [![YouTube badge][]][YouTube link]

<img align="right" src="https://deno.land/logo.svg" height="150px" alt="the deno mascot dinosaur standing in the rain">

[Deno](https://deno.com)
([/ˈdiːnoʊ/](https://ipa-reader.com/?text=%CB%88di%CB%90no%CA%8A), pronounced
`dee-no`) is a JavaScript, TypeScript, and WebAssembly runtime with secure
defaults and a great developer experience. It's built on [V8](https://v8.dev/),
[Rust](https://www.rust-lang.org/), and [Tokio](https://tokio.rs/).

Learn more about the Deno runtime
[in the documentation](https://docs.deno.com/runtime/manual).

## 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](https://docs.deno.com/runtime/manual/getting_started/installation).

Shell (Mac, Linux):

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

PowerShell (Windows):

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

[Homebrew](https://formulae.brew.sh/formula/deno) (Mac):

```sh
brew install deno
```

[Chocolatey](https://community.chocolatey.org/packages/deno) (Windows):

```powershell
choco install deno
```

[WinGet](https://winstall.app/apps/DenoLand.Deno) (Windows):

```powershell
winget install --id=DenoLand.Deno
```

[Scoop](https://scoop.sh/#/apps?q=deno&id=678d8fb557b611df996989c675b1099630a5bbee)
(Windows):

```powershell
scoop install main/deno
```

### Build and install from source

Complete instructions for building Deno from source can be found
[here](https://github.com/denoland/deno/blob/main/.github/CONTRIBUTING.md#building-from-source).

## 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](http://localhost:8000).

Learn more about writing and running Deno programs
[in the docs](https://docs.deno.com/runtime/manual).

## Additional resources

- **[Deno Docs](https://docs.deno.com)**: official guides and reference docs for
  the Deno runtime, [Deno Deploy](https://deno.com/deploy), and beyond.
- **[Deno Standard Library](https://jsr.io/@std)**: officially supported common
  utilities for Deno programs.
- **[JSR](https://jsr.io/)**: The open-source package registry for modern
  JavaScript and TypeScript
- **[Developer Blog](https://deno.com/blog)**: Product updates, tutorials, and
  more from the Deno team.

## Contributing

We appreciate your help! To contribute, please read our
[contributing instructions](.github/CONTRIBUTING.md).

[Build status - Cirrus]: https://github.com/denoland/deno/workflows/ci/badge.svg?branch=main&event=push
[Build status]: https://github.com/denoland/deno/actions
[Twitter badge]: https://img.shields.io/twitter/follow/deno_land.svg?style=social&label=Follow
[Twitter link]: https://twitter.com/intent/follow?screen_name=deno_land
[Bluesky badge]: https://img.shields.io/badge/Follow-whitesmoke?logo=bluesky
[Bluesky link]: https://bsky.app/profile/deno.land
[YouTube badge]: https://img.shields.io/youtube/channel/subscribers/UCqC2G2M-rg4fzg1esKFLFIw?style=social
[YouTube link]: https://www.youtube.com/@deno_land
[Discord badge]: https://img.shields.io/discord/684898665143206084?logo=discord&style=social
[Discord link]: https://discord.gg/deno

---

### 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`](./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`](./codebase-map.md) — a directory-by-directory map plus the
  handful of files worth understanding before anything else.
- [`testing.md`](./testing.md) — the test taxonomy (spec, unit, unit_node, node
  compat, WPT) and which command runs each one.
- [`ci.md`](./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`](./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`](./package-management.md) — how `deno add` /
  `deno install <pkg>` 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`](./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](https://example.com)

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:

```
+-----------------------------------------------------------+
|  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`](./codebase-map.md) for a finer-grained directory map and
[`testing.md`](./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:

```
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`](./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`](./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`](./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 <path-to-dylib>`, `dlopen`s it, and calls into it through a C ABI.

Every launcher the packager writes is just that one invocation:

```
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:<port>")`.
   `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.

## Navigation: poll with a real GET, not a connect

`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:<port>/`. 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, `await`s 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 <pkg>`, 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<Url>` 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`).

## Related work / next steps

- 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`](./ci.md) for how that decision is made.

---

--- METRICS ---
- Files Extracted: 9
- Estimated Token Budget: ~10067 tokens
- Canonical Reference: https://codewiki.google/github.com/denoland/deno
