denoland/deno
Module loading
Active contributors: David Sherret, Bartek Iwańczuk, Nayeem Rahman
Purpose
Whenever Deno encounters an import statement (or dynamic import, or Worker(specifier), or a top-level deno run <specifier>), the path from raw specifier string to executable JavaScript runs through the module loader. This system spans cli/module_loader.rs (1,668 lines), cli/graph_util.rs (1,195 lines), cli/file_fetcher.rs (1,234 lines), libs/resolver, libs/node_resolver, deno_graph (workspace dep), libs/npm, cli/jsr.rs, and cli/cache/.
This page explains the pieces and how they fit together. For the npm/JSR specifics see npm support and JSR support.
Specifier schemes
| Scheme | Example | Handler |
|---|---|---|
file: |
import "./mod.ts" |
cli/file_fetcher.rs::fetch_local, plus transpilation |
http: / https: |
import "https://esm.sh/foo" |
cli/file_fetcher.rs::fetch_remote, cached under DENO_DIR/deps/ |
npm: |
import "npm:react@18" |
libs/npm, libs/npm_installer, resolved into either the global cache or node_modules/ |
jsr: |
import "jsr:@std/path" |
cli/jsr.rs + libs/resolver (queries https://jsr.io) |
node: |
import "node:fs" |
ext/node polyfills (in-snapshot) |
data: |
import "data:text/javascript,export const x=1" |
inlined; cli/file_fetcher.rs |
Key abstractions
| Type | File | Role |
|---|---|---|
ModuleLoader (trait) |
libs/core (deno_core) |
The trait JsRuntime calls to resolve and load modules |
CliModuleLoader |
cli/module_loader.rs |
The CLI's implementation; entry into the whole pipeline |
ModuleLoaderFactory |
libs/cli/lib (deno_lib) crate |
Constructs a CliModuleLoader per worker with shared caches |
FileFetcher |
cli/file_fetcher.rs |
HTTPS / file: / data: fetching with caching and integrity checks |
ModuleGraph |
deno_graph (workspace dep) |
The cached set of modules + edges for an entry point |
ModuleGraphBuilder |
cli/graph_util.rs |
Builds and caches the graph |
CliResolver |
libs/resolver |
The shared resolution algorithm (npm, jsr, http, file) |
NodeResolver |
libs/node_resolver |
Node.js-style require/ESM resolution including package.json exports |
ParsedSourceCache |
libs/resolver/cache |
Cache of parsed ASTs across the graph |
LockfileLock |
libs/lockfile |
Records resolved versions and integrity hashes |
How it works
graph TD
Import["import 'X'"] --> JsRT["JsRuntime asks ModuleLoader.resolve"]
JsRT --> Loader["CliModuleLoader<br/>cli/module_loader.rs"]
Loader --> Cli["CliResolver<br/>libs/resolver"]
Cli --> SchemeCheck{"scheme?"}
SchemeCheck -->|file:| FileFetch["fetch_local"]
SchemeCheck -->|http(s):| HttpFetch["fetch_remote → DENO_DIR cache"]
SchemeCheck -->|npm:| Npm["NpmResolver<br/>libs/npm"]
SchemeCheck -->|jsr:| Jsr["cli/jsr.rs"]
SchemeCheck -->|node:| NodePoly["ext/node polyfills"]
Npm --> NodeMods["node_modules/ or global cache"]
Jsr --> JsrMeta["jsr.io meta lookup"]
Loader --> Graph["ModuleGraphBuilder<br/>cli/graph_util.rs"]
Graph --> DenoGraph["deno_graph crate"]
Loader --> Transpile["deno_ast transpile"]
Transpile --> SourceMap["source map registered with deno_core"]
Loader --> Cache["ParsedSourceCache + DENO_DIR/gen/"]
Loader -->|ModuleSource| JsRTPhase 1 — graph build
Before evaluation begins, the CLI typically builds the full module graph for the entry point:
cli/factory.rsexposes aModuleGraphBuilder(incli/graph_util.rs).- The builder uses
deno_graphto walk imports recursively, resolving each one throughCliResolver. - For each module,
FileFetcher(cli/file_fetcher.rs) fetches the bytes (cached underDENO_DIR/deps/for remote modules, with integrity checks againstdeno.lock). deno_astparses each module to discover its dependencies. Parsed ASTs are cached inParsedSourceCache.- The completed graph is checked for errors: missing imports, version conflicts, npm/jsr lockfile mismatches.
Phase 2 — runtime load
Once the graph is built, the JsRuntime evaluates the entry point. As V8 encounters import statements:
JsRuntimecallsCliModuleLoader::loadwith the resolved specifier.- The loader pulls the source from the graph (or fetches lazily for dynamic imports).
- If the source is TypeScript / TSX / JSX,
deno_asttranspiles it to JavaScript and registers the source map withdeno_core. - The transpiled JS is wrapped in a
ModuleSourceand returned to V8.
npm specifically
npm: specifiers route through libs/npm (registry client + dependency resolution graph) and libs/npm_installer (installation into either a global cache directory or a local node_modules/). Two modes:
- Auto-managed (default): Deno downloads packages into
DENO_DIR/npm/and resolves imports against that. - BYONM ("bring your own node_modules"): with
"nodeModulesDir": "manual"indeno.json, Deno uses anode_modules/directory created bynpm installand just reads from it.
libs/node_resolver handles the actual require/exports/conditional-export resolution against whichever directory layout is in play.
JSR
jsr: specifiers go through cli/jsr.rs. JSR-published packages are TypeScript-first and have a registry at https://jsr.io; Deno fetches a "jsr meta" file describing exports, then resolves to concrete file URLs.
Lockfile and integrity
deno.lock (parsed/written by libs/lockfile) records:
- Resolved npm package versions and their integrity hashes
- Resolved JSR package versions and integrity hashes
- Resolved HTTPS URLs and their content hashes
When a graph build sees a specifier already in the lockfile, it pins to the recorded version. When a fetched file's hash doesn't match the lockfile, the CLI errors out (--lock-write or --frozen-lockfile=false adjust this).
Source maps
Transpiled TS → JS retains a source map. deno_core is told about it during module load, so any thrown JS error gets remapped through runtime/fmt_errors.rs back to TS line:column before printing.
Integration points
- Constructed by:
cli/factory.rs::CliFactory::module_loader_factory(). - Used by:
MainWorker(viacli/worker.rs),WebWorker, the LSP (via its ownLspResolvermirror),deno bundleanddeno compile(which crawl the same graph and serialize it to eszip). - Reads:
deno.json,deno.lock,package.json(for npm/BYONM),DENO_DIR/{deps,npm,gen}/. - Writes:
deno.lock(in--lock-writemode),DENO_DIR/{deps,npm,gen}/cache.
Entry points for modification
- New specifier scheme: extend
CliResolverinlibs/resolver, add a fetch path incli/file_fetcher.rs, wire scheme detection incli/module_loader.rs::resolve. - Change cache layout:
libs/cache_dir,cli/cache/, pluscli/file_fetcher.rsfor the on-disk format. - New transpilation behavior:
deno_ast(workspace dep), and the call site incli/module_loader.rs::load. - Lockfile format change:
libs/lockfile. Any format change requires a backwards-compat path because users have existing lockfiles.
Key source files
| File | Purpose |
|---|---|
cli/module_loader.rs |
CliModuleLoader — implements deno_core::ModuleLoader |
cli/file_fetcher.rs |
HTTPS, file:, data: fetching with caching |
cli/graph_util.rs |
Module graph construction and caching |
cli/jsr.rs |
JSR registry client |
cli/npm.rs |
CLI's npm wiring (registry, installer, BYONM detection) |
cli/cache/ |
On-disk caches: parsed source, emit, type-check |
libs/resolver/ |
The shared resolution algorithm |
libs/node_resolver/ |
Node-style require / ESM resolution including conditional exports |
libs/npm/ |
npm registry client + dependency graph |
libs/npm_installer/ |
npm package installation |
libs/lockfile/ |
deno.lock parser/writer |
libs/eszip/ |
Module-graph archive used by deno compile |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.