Open-Source Wikis

/

Deno

/

Systems

/

Module loading

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| JsRT

Phase 1 — graph build

Before evaluation begins, the CLI typically builds the full module graph for the entry point:

  1. cli/factory.rs exposes a ModuleGraphBuilder (in cli/graph_util.rs).
  2. The builder uses deno_graph to walk imports recursively, resolving each one through CliResolver.
  3. For each module, FileFetcher (cli/file_fetcher.rs) fetches the bytes (cached under DENO_DIR/deps/ for remote modules, with integrity checks against deno.lock).
  4. deno_ast parses each module to discover its dependencies. Parsed ASTs are cached in ParsedSourceCache.
  5. 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:

  1. JsRuntime calls CliModuleLoader::load with the resolved specifier.
  2. The loader pulls the source from the graph (or fetches lazily for dynamic imports).
  3. If the source is TypeScript / TSX / JSX, deno_ast transpiles it to JavaScript and registers the source map with deno_core.
  4. The transpiled JS is wrapped in a ModuleSource and 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" in deno.json, Deno uses a node_modules/ directory created by npm install and 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 (via cli/worker.rs), WebWorker, the LSP (via its own LspResolver mirror), deno bundle and deno 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-write mode), DENO_DIR/{deps,npm,gen}/ cache.

Entry points for modification

  • New specifier scheme: extend CliResolver in libs/resolver, add a fetch path in cli/file_fetcher.rs, wire scheme detection in cli/module_loader.rs::resolve.
  • Change cache layout: libs/cache_dir, cli/cache/, plus cli/file_fetcher.rs for the on-disk format.
  • New transpilation behavior: deno_ast (workspace dep), and the call site in cli/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.

Module loading – Deno wiki | Factory