deno_core/modules/
loaders.rs

1// Copyright 2018-2025 the Deno authors. MIT license.
2
3use crate::ModuleSourceCode;
4use crate::error::CoreError;
5use crate::error::CoreErrorKind;
6use crate::module_specifier::ModuleSpecifier;
7use crate::modules::IntoModuleCodeString;
8use crate::modules::ModuleCodeString;
9use crate::modules::ModuleName;
10use crate::modules::ModuleSource;
11use crate::modules::ModuleSourceFuture;
12use crate::modules::ModuleType;
13use crate::modules::RequestedModuleType;
14use crate::modules::ResolutionKind;
15use crate::resolve_import;
16use deno_error::JsErrorBox;
17
18use futures::future::FutureExt;
19use std::borrow::Cow;
20use std::cell::RefCell;
21use std::collections::HashMap;
22use std::future::Future;
23use std::pin::Pin;
24use std::rc::Rc;
25
26use super::SourceCodeCacheInfo;
27
28pub type ModuleLoaderError = JsErrorBox;
29
30/// Result of calling `ModuleLoader::load`.
31pub enum ModuleLoadResponse {
32  /// Source file is available synchronously - eg. embedder might have
33  /// collected all the necessary sources in `ModuleLoader::prepare_module_load`.
34  /// Slightly cheaper than `Async` as it avoids boxing.
35  Sync(Result<ModuleSource, ModuleLoaderError>),
36
37  /// Source file needs to be loaded. Requires boxing due to recrusive
38  /// nature of module loading.
39  Async(Pin<Box<ModuleSourceFuture>>),
40}
41
42pub struct ModuleLoadOptions {
43  pub is_dynamic_import: bool,
44  /// If this is a synchronous ES module load.
45  pub is_synchronous: bool,
46  pub requested_module_type: RequestedModuleType,
47}
48
49#[derive(Debug, Clone)]
50pub struct ModuleLoadReferrer {
51  pub specifier: ModuleSpecifier,
52  /// 1-based.
53  pub line_number: i64,
54  /// 1-based.
55  pub column_number: i64,
56}
57
58pub trait ModuleLoader {
59  /// Returns an absolute URL.
60  /// When implementing an spec-complaint VM, this should be exactly the
61  /// algorithm described here:
62  /// <https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier>
63  ///
64  /// [`ResolutionKind::MainModule`] can be used to resolve from current working directory or
65  /// apply import map for child imports.
66  ///
67  /// [`ResolutionKind::DynamicImport`] can be used to check permissions or deny
68  /// dynamic imports altogether.
69  fn resolve(
70    &self,
71    specifier: &str,
72    referrer: &str,
73    kind: ResolutionKind,
74  ) -> Result<ModuleSpecifier, ModuleLoaderError>;
75
76  /// Override to customize the behavior of `import.meta.resolve` resolution.
77  fn import_meta_resolve(
78    &self,
79    specifier: &str,
80    referrer: &str,
81  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
82    self.resolve(specifier, referrer, ResolutionKind::DynamicImport)
83  }
84
85  /// Given ModuleSpecifier, load its source code.
86  ///
87  /// `is_dyn_import` can be used to check permissions or deny
88  /// dynamic imports altogether.
89  fn load(
90    &self,
91    module_specifier: &ModuleSpecifier,
92    maybe_referrer: Option<&ModuleLoadReferrer>,
93    options: ModuleLoadOptions,
94  ) -> ModuleLoadResponse;
95
96  /// This hook can be used by implementors to do some preparation
97  /// work before starting loading of modules.
98  ///
99  /// For example implementor might download multiple modules in
100  /// parallel and transpile them to final JS sources before
101  /// yielding control back to the runtime.
102  ///
103  /// It's not required to implement this method.
104  fn prepare_load(
105    &self,
106    _module_specifier: &ModuleSpecifier,
107    _maybe_referrer: Option<String>,
108    _options: ModuleLoadOptions,
109  ) -> Pin<Box<dyn Future<Output = Result<(), ModuleLoaderError>>>> {
110    async { Ok(()) }.boxed_local()
111  }
112
113  /// This hook can be used by implementors to do some cleanup
114  /// work after loading of modules. The hook is called for
115  /// all loads, whether they succeeded or not.
116  ///
117  /// For example implementor might drop transpilation and
118  /// static analysis caches before
119  /// yielding control back to the runtime.
120  ///
121  /// It's not required to implement this method.
122  fn finish_load(&self) {}
123
124  /// Called when new v8 code cache is available for this module. Implementors
125  /// can store the provided code cache for future executions of the same module.
126  ///
127  /// It's not required to implement this method.
128  fn code_cache_ready(
129    &self,
130    _module_specifier: ModuleSpecifier,
131    _hash: u64,
132    _code_cache: &[u8],
133  ) -> Pin<Box<dyn Future<Output = ()>>> {
134    async {}.boxed_local()
135  }
136
137  /// Called when V8 code cache should be ignored for this module. This can happen
138  /// if eg. module causes a V8 warning, like when using deprecated import assertions.
139  /// Implementors should make sure that the code cache for this module is purged and not saved anymore.
140  ///
141  /// It's not required to implement this method.
142  fn purge_and_prevent_code_cache(&self, _module_specifier: &str) {}
143
144  /// Returns a source map for given `file_name`.
145  ///
146  /// This function will soon be deprecated or renamed.
147  fn get_source_map(&self, _file_name: &str) -> Option<Cow<'_, [u8]>> {
148    None
149  }
150
151  /// Loads an external source map file referenced by a module.
152  fn load_external_source_map(
153    &self,
154    _source_map_url: &str,
155  ) -> Option<Cow<'_, [u8]>> {
156    None
157  }
158
159  fn get_source_mapped_source_line(
160    &self,
161    _file_name: &str,
162    _line_number: usize,
163  ) -> Option<String> {
164    None
165  }
166
167  /// Implementors can attach arbitrary data to scripts and modules
168  /// by implementing this method. V8 currently requires that the
169  /// returned data be a `v8::PrimitiveArray`.
170  fn get_host_defined_options<'s, 'i>(
171    &self,
172    _scope: &mut v8::PinScope<'s, 'i>,
173    _name: &str,
174  ) -> Option<v8::Local<'s, v8::Data>> {
175    None
176  }
177}
178
179/// Placeholder structure used when creating
180/// a runtime that doesn't support module loading.
181pub struct NoopModuleLoader;
182
183impl ModuleLoader for NoopModuleLoader {
184  fn resolve(
185    &self,
186    specifier: &str,
187    referrer: &str,
188    _kind: ResolutionKind,
189  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
190    resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
191  }
192
193  fn load(
194    &self,
195    _module_specifier: &ModuleSpecifier,
196    _maybe_referrer: Option<&ModuleLoadReferrer>,
197    _options: ModuleLoadOptions,
198  ) -> ModuleLoadResponse {
199    ModuleLoadResponse::Sync(Err(JsErrorBox::generic(
200      "Module loading is not supported.",
201    )))
202  }
203}
204
205pub trait ExtCodeCache {
206  fn get_code_cache_info(
207    &self,
208    specifier: &ModuleSpecifier,
209    code: &ModuleSourceCode,
210    esm: bool,
211  ) -> SourceCodeCacheInfo;
212
213  fn code_cache_ready(
214    &self,
215    specifier: ModuleSpecifier,
216    hash: u64,
217    code_cache: &[u8],
218    esm: bool,
219  );
220}
221
222pub(crate) struct ExtModuleLoader {
223  sources: RefCell<HashMap<ModuleName, ModuleCodeString>>,
224  ext_code_cache: Option<Rc<dyn ExtCodeCache>>,
225}
226
227impl ExtModuleLoader {
228  pub fn new(
229    loaded_sources: Vec<(ModuleName, ModuleCodeString)>,
230    ext_code_cache: Option<Rc<dyn ExtCodeCache>>,
231  ) -> Self {
232    // Guesstimate a length
233    let mut sources = HashMap::with_capacity(loaded_sources.len());
234    for source in loaded_sources {
235      sources.insert(source.0, source.1);
236    }
237    ExtModuleLoader {
238      sources: RefCell::new(sources),
239      ext_code_cache,
240    }
241  }
242
243  pub fn finalize(&self) -> Result<(), CoreError> {
244    let sources = self.sources.take();
245    let unused_modules: Vec<_> = sources.iter().collect();
246
247    if !unused_modules.is_empty() {
248      return Err(
249        CoreErrorKind::UnusedModules(
250          unused_modules
251            .into_iter()
252            .map(|(name, _)| name.to_string())
253            .collect::<Vec<_>>(),
254        )
255        .into_box(),
256      );
257    }
258
259    Ok(())
260  }
261}
262
263impl ModuleLoader for ExtModuleLoader {
264  fn resolve(
265    &self,
266    specifier: &str,
267    referrer: &str,
268    _kind: ResolutionKind,
269  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
270    // If specifier is relative to an extension module, we need to do some special handling
271    if specifier.starts_with("../")
272      || specifier.starts_with("./")
273      || referrer.starts_with("ext:")
274    {
275      // add `/` to the referrer to make it a valid base URL, so we can join the specifier to it
276      return crate::resolve_url(
277        &crate::resolve_url(referrer.replace("ext:", "ext:/").as_str())
278          .map_err(JsErrorBox::from_err)?
279          .join(specifier)
280          .map_err(crate::ModuleResolutionError::InvalidBaseUrl)
281          .map_err(JsErrorBox::from_err)?
282          .as_str()
283          // remove the `/` we added
284          .replace("ext:/", "ext:"),
285      )
286      .map_err(JsErrorBox::from_err);
287    }
288    resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
289  }
290
291  fn load(
292    &self,
293    specifier: &ModuleSpecifier,
294    _maybe_referrer: Option<&ModuleLoadReferrer>,
295    _options: ModuleLoadOptions,
296  ) -> ModuleLoadResponse {
297    let mut sources = self.sources.borrow_mut();
298    let source = match sources.remove(specifier.as_str()) {
299      Some(source) => source,
300      None => {
301        return ModuleLoadResponse::Sync(Err(JsErrorBox::generic(format!(
302          "Specifier \"{0}\" was not passed as an extension module and was not included in the snapshot.",
303          specifier
304        ))));
305      }
306    };
307    let code = ModuleSourceCode::String(source);
308    let code_cache = self
309      .ext_code_cache
310      .as_ref()
311      .map(|cache| cache.get_code_cache_info(specifier, &code, true));
312    ModuleLoadResponse::Sync(Ok(ModuleSource::new(
313      ModuleType::JavaScript,
314      code,
315      specifier,
316      code_cache,
317    )))
318  }
319
320  fn prepare_load(
321    &self,
322    _specifier: &ModuleSpecifier,
323    _maybe_referrer: Option<String>,
324    _options: ModuleLoadOptions,
325  ) -> Pin<Box<dyn Future<Output = Result<(), ModuleLoaderError>>>> {
326    async { Ok(()) }.boxed_local()
327  }
328
329  fn code_cache_ready(
330    &self,
331    module_specifier: ModuleSpecifier,
332    hash: u64,
333    code_cache: &[u8],
334  ) -> Pin<Box<dyn Future<Output = ()>>> {
335    if let Some(ext_code_cache) = &self.ext_code_cache {
336      ext_code_cache.code_cache_ready(module_specifier, hash, code_cache, true);
337    }
338    std::future::ready(()).boxed_local()
339  }
340}
341
342/// A loader that is used in `op_lazy_load_esm` to load and execute
343/// ES modules that were embedded in the binary using `lazy_loaded_esm`
344/// option in `extension!` macro.
345pub(crate) struct LazyEsmModuleLoader {
346  sources: Rc<RefCell<HashMap<ModuleName, ModuleCodeString>>>,
347}
348
349impl LazyEsmModuleLoader {
350  pub fn new(
351    sources: Rc<RefCell<HashMap<ModuleName, ModuleCodeString>>>,
352  ) -> Self {
353    LazyEsmModuleLoader { sources }
354  }
355}
356
357impl ModuleLoader for LazyEsmModuleLoader {
358  fn resolve(
359    &self,
360    specifier: &str,
361    referrer: &str,
362    _kind: ResolutionKind,
363  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
364    resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
365  }
366
367  fn load(
368    &self,
369    specifier: &ModuleSpecifier,
370    _maybe_referrer: Option<&ModuleLoadReferrer>,
371    _options: ModuleLoadOptions,
372  ) -> ModuleLoadResponse {
373    let mut sources = self.sources.borrow_mut();
374    let source = match sources.remove(specifier.as_str()) {
375      Some(source) => source,
376      None => {
377        return ModuleLoadResponse::Sync(Err(JsErrorBox::generic(format!(
378          "Specifier \"{0}\" cannot be lazy-loaded as it was not included in the binary.",
379          specifier
380        ))));
381      }
382    };
383    ModuleLoadResponse::Sync(Ok(ModuleSource::new(
384      ModuleType::JavaScript,
385      ModuleSourceCode::String(source),
386      specifier,
387      None,
388    )))
389  }
390
391  fn prepare_load(
392    &self,
393    _specifier: &ModuleSpecifier,
394    _maybe_referrer: Option<String>,
395    _options: ModuleLoadOptions,
396  ) -> Pin<Box<dyn Future<Output = Result<(), ModuleLoaderError>>>> {
397    async { Ok(()) }.boxed_local()
398  }
399}
400
401#[derive(Debug, thiserror::Error, deno_error::JsError)]
402#[class(inherit)]
403#[error("Failed to load {specifier}")]
404pub struct LoadFailedError {
405  specifier: ModuleSpecifier,
406  #[source]
407  #[inherit]
408  source: std::io::Error,
409}
410
411/// Basic file system module loader.
412///
413/// Note that this loader will **block** event loop
414/// when loading file as it uses synchronous FS API
415/// from standard library.
416pub struct FsModuleLoader;
417
418impl ModuleLoader for FsModuleLoader {
419  fn resolve(
420    &self,
421    specifier: &str,
422    referrer: &str,
423    _kind: ResolutionKind,
424  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
425    resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
426  }
427
428  fn load(
429    &self,
430    module_specifier: &ModuleSpecifier,
431    _maybe_referrer: Option<&ModuleLoadReferrer>,
432    options: ModuleLoadOptions,
433  ) -> ModuleLoadResponse {
434    let module_specifier = module_specifier.clone();
435    let fut = async move {
436      let path = module_specifier.to_file_path().map_err(|_| {
437        JsErrorBox::generic(format!(
438          "Provided module specifier \"{module_specifier}\" is not a file URL."
439        ))
440      })?;
441      let module_type = if let Some(extension) = path.extension() {
442        let ext = extension.to_string_lossy().to_lowercase();
443        // We only return JSON modules if extension was actually `.json`.
444        // In other cases we defer to actual requested module type, so runtime
445        // can decide what to do with it.
446        if ext == "json" {
447          ModuleType::Json
448        } else if ext == "wasm" {
449          ModuleType::Wasm
450        } else {
451          match &options.requested_module_type {
452            RequestedModuleType::Other(ty) => ModuleType::Other(ty.clone()),
453            RequestedModuleType::Text => ModuleType::Text,
454            RequestedModuleType::Bytes => ModuleType::Bytes,
455            _ => ModuleType::JavaScript,
456          }
457        }
458      } else {
459        ModuleType::JavaScript
460      };
461
462      // If we loaded a JSON file, but the "requested_module_type" (that is computed from
463      // import attributes) is not JSON we need to fail.
464      if module_type == ModuleType::Json
465        && options.requested_module_type != RequestedModuleType::Json
466      {
467        return Err(JsErrorBox::generic("Attempted to load JSON module without specifying \"type\": \"json\" attribute in the import statement."));
468      }
469
470      let code = std::fs::read(path).map_err(|source| {
471        JsErrorBox::from_err(LoadFailedError {
472          specifier: module_specifier.clone(),
473          source,
474        })
475      })?;
476      let module = ModuleSource::new(
477        module_type,
478        ModuleSourceCode::Bytes(code.into_boxed_slice().into()),
479        &module_specifier,
480        None,
481      );
482      Ok(module)
483    }
484    .boxed_local();
485
486    ModuleLoadResponse::Async(fut)
487  }
488}
489
490/// A module loader that you can pre-load a number of modules into and resolve from. Useful for testing and
491/// embedding situations where the filesystem and snapshot systems are not usable or a good fit.
492#[derive(Default)]
493pub struct StaticModuleLoader {
494  map: HashMap<ModuleSpecifier, ModuleCodeString>,
495}
496
497impl StaticModuleLoader {
498  /// Create a new [`StaticModuleLoader`] from an `Iterator` of specifiers and code.
499  pub fn new(
500    from: impl IntoIterator<Item = (ModuleSpecifier, impl IntoModuleCodeString)>,
501  ) -> Self {
502    Self {
503      map: HashMap::from_iter(
504        from.into_iter().map(|(url, code)| {
505          (url, code.into_module_code().into_cheap_copy().0)
506        }),
507      ),
508    }
509  }
510
511  /// Create a new [`StaticModuleLoader`] from a single code item.
512  pub fn with(
513    specifier: ModuleSpecifier,
514    code: impl IntoModuleCodeString,
515  ) -> Self {
516    Self::new([(specifier, code)])
517  }
518}
519
520impl ModuleLoader for StaticModuleLoader {
521  fn resolve(
522    &self,
523    specifier: &str,
524    referrer: &str,
525    _kind: ResolutionKind,
526  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
527    resolve_import(specifier, referrer).map_err(JsErrorBox::from_err)
528  }
529
530  fn load(
531    &self,
532    module_specifier: &ModuleSpecifier,
533    _maybe_referrer: Option<&ModuleLoadReferrer>,
534    _options: ModuleLoadOptions,
535  ) -> ModuleLoadResponse {
536    let res = if let Some(code) = self.map.get(module_specifier) {
537      Ok(ModuleSource::new(
538        ModuleType::JavaScript,
539        ModuleSourceCode::String(code.try_clone().unwrap()),
540        module_specifier,
541        None,
542      ))
543    } else {
544      Err(JsErrorBox::generic("Module not found"))
545    };
546    ModuleLoadResponse::Sync(res)
547  }
548}
549
550/// Annotates a `ModuleLoader` with a log of all `load()` calls.
551/// as well as a count of all `resolve()`, `prepare()`, and `load()` calls.
552#[cfg(test)]
553pub struct TestingModuleLoader<L: ModuleLoader> {
554  loader: L,
555  log: RefCell<Vec<ModuleSpecifier>>,
556  load_count: std::cell::Cell<usize>,
557  prepare_count: std::cell::Cell<usize>,
558  finish_count: std::cell::Cell<usize>,
559  resolve_count: std::cell::Cell<usize>,
560}
561
562#[cfg(test)]
563impl<L: ModuleLoader> TestingModuleLoader<L> {
564  pub fn new(loader: L) -> Self {
565    Self {
566      loader,
567      log: RefCell::new(vec![]),
568      load_count: Default::default(),
569      prepare_count: Default::default(),
570      finish_count: Default::default(),
571      resolve_count: Default::default(),
572    }
573  }
574
575  /// Retrieve the current module load event counts.
576  pub fn counts(&self) -> ModuleLoadEventCounts {
577    ModuleLoadEventCounts {
578      load: self.load_count.get(),
579      prepare: self.prepare_count.get(),
580      finish: self.finish_count.get(),
581      resolve: self.resolve_count.get(),
582    }
583  }
584}
585
586#[cfg(test)]
587impl<L: ModuleLoader> ModuleLoader for TestingModuleLoader<L> {
588  fn resolve(
589    &self,
590    specifier: &str,
591    referrer: &str,
592    kind: ResolutionKind,
593  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
594    self.resolve_count.set(self.resolve_count.get() + 1);
595    self.loader.resolve(specifier, referrer, kind)
596  }
597
598  fn prepare_load(
599    &self,
600    module_specifier: &ModuleSpecifier,
601    maybe_referrer: Option<String>,
602    options: ModuleLoadOptions,
603  ) -> Pin<Box<dyn Future<Output = Result<(), ModuleLoaderError>>>> {
604    self.prepare_count.set(self.prepare_count.get() + 1);
605    self
606      .loader
607      .prepare_load(module_specifier, maybe_referrer, options)
608  }
609
610  fn finish_load(&self) {
611    self.finish_count.set(self.finish_count.get() + 1);
612    self.loader.finish_load();
613  }
614
615  fn load(
616    &self,
617    module_specifier: &ModuleSpecifier,
618    maybe_referrer: Option<&ModuleLoadReferrer>,
619    options: ModuleLoadOptions,
620  ) -> ModuleLoadResponse {
621    self.load_count.set(self.load_count.get() + 1);
622    self.log.borrow_mut().push(module_specifier.clone());
623    self.loader.load(module_specifier, maybe_referrer, options)
624  }
625
626  fn load_external_source_map(
627    &self,
628    source_map_url: &str,
629  ) -> Option<Cow<'_, [u8]>> {
630    self.loader.load_external_source_map(source_map_url)
631  }
632}
633
634#[cfg(test)]
635#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
636pub struct ModuleLoadEventCounts {
637  pub resolve: usize,
638  pub prepare: usize,
639  pub finish: usize,
640  pub load: usize,
641}
642
643#[cfg(test)]
644impl ModuleLoadEventCounts {
645  pub fn new(
646    resolve: usize,
647    prepare: usize,
648    finish: usize,
649    load: usize,
650  ) -> Self {
651    Self {
652      resolve,
653      prepare,
654      finish,
655      load,
656    }
657  }
658}