Technical Documentation: denoland/deno
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (README.md)
Deno
Deno
(/หdiหnoส/, pronounceddee-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):
curl -fsSL https://deno.land/install.sh | shPowerShell (Windows):
irm https://deno.land/install.ps1 | iexHomebrew (Mac):
brew install denoChocolatey (Windows):
choco install denoWinGet (Windows):
winget install --id=DenoLand.DenoScoop
(Windows):
scoop install main/denoBuild 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:
Deno.serve((_req: Request) => {
return new Response("Hello, world!");
});Run your server with the following command:
deno run --allow-net server.tsThis 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:
thedenoCLI crate, thedeno_runtimecrate, theext/*extensions, and
thedeno_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โ howdeno 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โ howdeno add/deno installadd dependencies: the two flag parsers, the config
writer, the shared install path, and the--dev/--save-optional/--no-saveflags (including whyoptionalDependenciesneed 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
const x = 1;- Item 1
- Item 2
- Item 3
- First
- Second
A blockquote.
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 fullclapflag and subcommand definition. Adding a
flag or a subcommand starts here.cli/tools/<tool>/โ one module per subcommand (for examplecli/tools/fmt.rsfor 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 ofdeno_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 thenode:*builtins, both Rust
ops and JavaScript polyfills), plusext/node_cryptoandext/node_sqlite.
The typical shape of an extension is:
- Rust
#[op2]functions that do the privileged work and take a permission
check where needed. - A
00_*.js/01_*.jsset of JavaScript modules that build the public API
and call the ops throughDeno.core.ops. - 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 standalonedeno_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_coreitself: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_coreand 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 andtesting.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:
deno run -A .github/workflows/ci.tsOther 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) andbuild libsโ compile Deno and thedeno_corelibraries.test(per platform, sharded) โ the spec, unit, node-compat and WPT
suites, depending onbuild.benchโ benchmarks.deno-core-testanddeno-core-miriโ thedeno_core/libs/*
crate tests (the latter under Miri).lintโ formatting check, jsdoc check, andtools/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 anddeno_corejobs are all skipped.skip_deno_core_testโ set when nodeno_core-related files changed, so thedeno_corecrate tests can be skipped (tools/check_deno_core_changes.js).docs_onlyโ set when the PR only touches thedoc/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:
- In
pre-build,tools/check_docs_only_changes.jsdiffs the PR against its
base SHA. If every changed file is underdoc/(and at least one file
changed), it writesdocs_only=trueto the job output. An empty or failed
diff defaults tofalse, so the full pipeline runs when in doubt. - The build, build-libs, test, bench,
deno-core-testanddeno-core-miri
jobs each add&& docs_only != 'true'to their run condition, so they are
skipped on a docs-only PR. Thetestjobs depend onbuild, so skippingbuildcascades to them automatically. - The
lintjob is intentionally not gated ondocs_only. It still runs,
which means Markdown is still formatted and linted (deno fmt --checkcoversdoc/). This is the "lint and nothing else" behavior. ci-statusstill runs and passes: its skipped dependencies count as neither
failure nor cancellation, andlintsucceeds, 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, seearchitecture.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
cli/main.rsโ entry point and command routing.cli/args/flags.rsโ every CLI flag and subcommand (clap).runtime/worker.rsโ how a worker/runtime is initialized.runtime/permissions/โ the permission system that gates ops.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: thecli/tools/test/directory. Other notable tools includecompile.rs,bundle/,coverage/,lint/,pm/(package management),installer/,jupyter/,publish/,repl/, andserve.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(mostnode:*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__.jsoncfiles.tests/unit/โ JavaScript/TypeScript unit tests for runtime APIs
(*_test.ts).tests/unit_node/โ unit tests for thenode:*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 fmtplus extras).tools/lint.jsโ lint Rust and JS/TS; pass--jsfor 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:
exec "$DIR/laufey_webview" --runtime "$DIR/libdenort.so" "$@" # desktop.rs:1407So 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 linkedlaufeycrate's ABI version drifts from what the shipped backend speaks,cargo buildfails loudly instead of producing a dylib that silently won't
launch. - Build-pin (
desktop.rs:29,LAUFEY_VERSIONinjected bycli/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.LaufeyBackendResolverresolves in
order:LAUFEY_DEV_DIRcheckout โ 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 withlibsui::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:
- Port publish before runtime build (
lib.rs:1222): allocate a random
loopback port, thenset_var("DENO_SERVE_ADDRESS", "tcp:127.0.0.1:").setenvis not thread-safe on glibc (Rust 1.81+ marks itunsafe), so it
must precede the mio IO thread and inspector thread. The user'sDeno.serve
/export default { fetch }later binds this pre-set address with no
coordination needed. - chdir into the extracted VFS (
lib.rs:1262): process-wide, so it must
happen before any task resolves a relative path. Frameworks (Next.next/,
Vitedist/) 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, withauto_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, thenWindow::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 athttp://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) usetry_sendand 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_JSawaitingop_desktop_recv_event()(desktop.rs:702), dispatching each event to the
matching DOM-styleEventTarget. The op promise isunrefOpPromise'd so the
pump never keeps the event loop alive by itself.
A bind call round-trip (the interesting one):
- JS registers a handler:
window.bind(name, fn)storesfnin a per-windowMapand callslaufey add_binding_async(name)(desktop.rs:229,lib.rs:361). - Webview calls
window.bindings.<name>(...). The native async binding
serializes args (laufey::Valueโ JSON), allocates acall_idfrom a globalAtomicU32and registers aoneshotsender under it (register_bind_call,ops/desktop.rs:297), then sendsDesktopEvent::BindCall { window_id, name, args, call_id }. - The JS pump's
"bindCall"arm looks up the handler,awaits it, and callsop_desktop_resolve_bind_call(call_id, result)/op_desktop_reject_bind_call(call_id, err)(desktop.rs:736). - The op pops the
oneshotfor thatcall_idand fires it
(ops/desktop.rs:1051); back in the binding future,resp_rx.awaitresolves
and callsjs_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 aReloadKind callback (lib.rs:1642):
- Replaced โ a script edit that V8 accepts via
Debugger.setScriptSource
afterdeno_asttranspile. Hot-patched in place, no reload. - SoftReload โ ReloadKind::Soft โ a static asset changed, or a module was
removed. Runslocation.reload()on every window inopen_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-levelletchanged). The
module graph can't be patched and a reload wouldn't help (the stale runtime
keeps serving), sorestart_desktop_app()callsexit(75)(lib.rs:1537).
Thedeno desktop --hmrsupervisor 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.jsand 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 toraw-window-handletypes 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.rsruns a CDP multiplexer in the
parent fronting both the Deno inspector and the renderer's debug port behind
one/unifiedwebsocket.
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 legacyclap-based parser. Theaddsubcommand is
defined inadd_subcommand();installreuses the shared argument builders
(add_dev_arg(),add_optional_arg(),add_no_save_arg()). Both funnel intoadd_parse_inner(), which builds theAddFlagsstruct.libs/cli_parser/โ the newer hand-written parser. Command shape is declared
insrc/defs.rs(ADD_SUBCOMMAND,INSTALL_SUBCOMMAND) and converted to
flags insrc/convert.rs(add_parse, and the install branch that producesInstallFlagsLocal::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 andinstall).
There is a lint (ensureNoNonPermissionCapitalLetterShortFlags intools/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) andpackage.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 aDependencyKind enum (Normal / Dev / Optional):
deno.json:kindis ignored โ everything goes underimports.package.json:kindselects the section (dependencies,devDependencies,optionalDependencies).add()also removes the package from the other two
sections so it is never declared twice, andnew_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() callsnpm_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 intonode_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 nooptional_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 anadditional_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 todevDependencies, installs
normally (dev deps are in the config-derived roots).--save-optional/-O:DependencyKind::Optional. Writes tooptionalDependencies; because the installer ignores that section, the
package is also pushed toadditional_rootsso it is installed on add.--no-save: resolve and install the package intonode_modulesand the
lockfile, but do not rewrite or commit any config file. Implemented by
skipping theConfigUpdater::add/commitcalls and pushing the package toadditional_roots.- These three are mutually exclusive (enforced with
conflicts_within 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 theadd
step and asserts the file contents in a followingevalstep. - Parser unit tests:
add_or_install_subcommandin bothcli/args/flags.rs
(run viacargo test -p deno --lib add_or_install) andlibs/cli_parser/src/tests_full.rs
(cargo test -p deno_cli_parser add_or_install).
Related work / next steps
- Make the installer honor
optionalDependenciesfrompackage.json(upstreamdeno_package_jsonresolve_local_package_json_deps+ the npm installer),
which would let--save-optionalinstall through the normal config-derived
path and remove theadditional_rootsworkaround for it. --no-savein a directory with no config still creates an empty config file
(a side effect ofload_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:
- Make a directory under
tests/specs/with a descriptive name. - Add
__test__.jsoncwith the step(s). - Add any input files the test needs.
- Put expected output inline or in a
.outfile.
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 thenode:*builtins.tests/node_compat/โ Node.js's own test files, executed against Deno to
measure compatibility. The set that runs is controlled bytests/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_nodetests.
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. Seeci.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