rw_deno_core/modules/
loaders.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2use crate::error::generic_error;
3use crate::extensions::ExtensionFileSource;
4use crate::module_specifier::ModuleSpecifier;
5use crate::modules::IntoModuleCodeString;
6use crate::modules::ModuleCodeString;
7use crate::modules::ModuleName;
8use crate::modules::ModuleSource;
9use crate::modules::ModuleSourceFuture;
10use crate::modules::ModuleType;
11use crate::modules::RequestedModuleType;
12use crate::modules::ResolutionKind;
13use crate::resolve_import;
14use crate::ModuleSourceCode;
15
16use anyhow::anyhow;
17use anyhow::bail;
18use anyhow::Context;
19use anyhow::Error;
20use futures::future::FutureExt;
21use std::cell::RefCell;
22use std::collections::HashMap;
23use std::future::Future;
24use std::pin::Pin;
25use std::rc::Rc;
26
27/// Result of calling `ModuleLoader::load`.
28pub enum ModuleLoadResponse {
29  /// Source file is available synchronously - eg. embedder might have
30  /// collected all the necessary sources in `ModuleLoader::prepare_module_load`.
31  /// Slightly cheaper than `Async` as it avoids boxing.
32  Sync(Result<ModuleSource, Error>),
33
34  /// Source file needs to be loaded. Requires boxing due to recrusive
35  /// nature of module loading.
36  Async(Pin<Box<ModuleSourceFuture>>),
37}
38
39pub trait ModuleLoader {
40  /// Returns an absolute URL.
41  /// When implementing an spec-complaint VM, this should be exactly the
42  /// algorithm described here:
43  /// <https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier>
44  ///
45  /// [`ResolutionKind::MainModule`] can be used to resolve from current working directory or
46  /// apply import map for child imports.
47  ///
48  /// [`ResolutionKind::DynamicImport`] can be used to check permissions or deny
49  /// dynamic imports altogether.
50  fn resolve(
51    &self,
52    specifier: &str,
53    referrer: &str,
54    kind: ResolutionKind,
55  ) -> Result<ModuleSpecifier, Error>;
56
57  /// Given ModuleSpecifier, load its source code.
58  ///
59  /// `is_dyn_import` can be used to check permissions or deny
60  /// dynamic imports altogether.
61  fn load(
62    &self,
63    module_specifier: &ModuleSpecifier,
64    maybe_referrer: Option<&ModuleSpecifier>,
65    is_dyn_import: bool,
66    requested_module_type: RequestedModuleType,
67  ) -> ModuleLoadResponse;
68
69  /// This hook can be used by implementors to do some preparation
70  /// work before starting loading of modules.
71  ///
72  /// For example implementor might download multiple modules in
73  /// parallel and transpile them to final JS sources before
74  /// yielding control back to the runtime.
75  ///
76  /// It's not required to implement this method.
77  fn prepare_load(
78    &self,
79    _module_specifier: &ModuleSpecifier,
80    _maybe_referrer: Option<String>,
81    _is_dyn_import: bool,
82  ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
83    async { Ok(()) }.boxed_local()
84  }
85
86  /// Called when new v8 code cache is available for this module. Implementors
87  /// can store the provided code cache for future executions of the same module.
88  ///
89  /// It's not required to implement this method.
90  fn code_cache_ready(
91    &self,
92    _module_specifier: &ModuleSpecifier,
93    _code_cache: &[u8],
94  ) -> Pin<Box<dyn Future<Output = ()>>> {
95    async {}.boxed_local()
96  }
97}
98
99/// Placeholder structure used when creating
100/// a runtime that doesn't support module loading.
101pub struct NoopModuleLoader;
102
103impl ModuleLoader for NoopModuleLoader {
104  fn resolve(
105    &self,
106    specifier: &str,
107    referrer: &str,
108    _kind: ResolutionKind,
109  ) -> Result<ModuleSpecifier, Error> {
110    Ok(resolve_import(specifier, referrer)?)
111  }
112
113  fn load(
114    &self,
115    module_specifier: &ModuleSpecifier,
116    maybe_referrer: Option<&ModuleSpecifier>,
117    _is_dyn_import: bool,
118    _requested_module_type: RequestedModuleType,
119  ) -> ModuleLoadResponse {
120    let maybe_referrer = maybe_referrer
121      .map(|s| s.as_str())
122      .unwrap_or("(no referrer)");
123    let err = generic_error(
124      format!(
125        "Module loading is not supported; attempted to load: \"{module_specifier}\" from \"{maybe_referrer}\"",
126      )
127    );
128    ModuleLoadResponse::Sync(Err(err))
129  }
130}
131
132/// Function that can be passed to the `ExtModuleLoader` that allows to
133/// transpile sources before passing to V8.
134pub type ExtModuleLoaderCb =
135  Box<dyn Fn(&ExtensionFileSource) -> Result<ModuleCodeString, Error>>;
136
137pub(crate) struct ExtModuleLoader {
138  sources: RefCell<HashMap<ModuleName, ModuleCodeString>>,
139}
140
141impl ExtModuleLoader {
142  pub fn new(
143    loaded_sources: Vec<(ModuleName, ModuleCodeString)>,
144  ) -> Result<Self, Error> {
145    // Guesstimate a length
146    let mut sources = HashMap::with_capacity(loaded_sources.len());
147    for source in loaded_sources {
148      sources.insert(source.0, source.1);
149    }
150    Ok(ExtModuleLoader {
151      sources: RefCell::new(sources),
152    })
153  }
154
155  pub fn finalize(self) -> Result<(), Error> {
156    let sources = self.sources.take();
157    let unused_modules: Vec<_> = sources.iter().collect();
158
159    if !unused_modules.is_empty() {
160      let mut msg =
161        "Following modules were passed to ExtModuleLoader but never used:\n"
162          .to_string();
163      for m in unused_modules {
164        msg.push_str("  - ");
165        msg.push_str(m.0);
166        msg.push('\n');
167      }
168      bail!(msg);
169    }
170
171    Ok(())
172  }
173}
174
175impl ModuleLoader for ExtModuleLoader {
176  fn resolve(
177    &self,
178    specifier: &str,
179    referrer: &str,
180    _kind: ResolutionKind,
181  ) -> Result<ModuleSpecifier, Error> {
182    // If specifier is relative to an extension module, we need to do some special handling
183    if specifier.starts_with("../")
184      || specifier.starts_with("./")
185      || referrer.starts_with("ext:")
186    {
187      // add `/` to the referrer to make it a valid base URL, so we can join the specifier to it
188      return Ok(crate::resolve_url(
189        &crate::resolve_url(referrer.replace("ext:", "ext:/").as_str())?
190          .join(specifier)
191          .map_err(crate::ModuleResolutionError::InvalidBaseUrl)?
192          .as_str()
193          // remove the `/` we added
194          .replace("ext:/", "ext:"),
195      )?);
196    }
197    Ok(resolve_import(specifier, referrer)?)
198  }
199
200  fn load(
201    &self,
202    specifier: &ModuleSpecifier,
203    _maybe_referrer: Option<&ModuleSpecifier>,
204    _is_dyn_import: bool,
205    _requested_module_type: RequestedModuleType,
206  ) -> ModuleLoadResponse {
207    let mut sources = self.sources.borrow_mut();
208    let source = match sources.remove(specifier.as_str()) {
209      Some(source) => source,
210      None => return ModuleLoadResponse::Sync(Err(anyhow!("Specifier \"{}\" was not passed as an extension module and was not included in the snapshot.", specifier))),
211    };
212    ModuleLoadResponse::Sync(Ok(ModuleSource::new(
213      ModuleType::JavaScript,
214      ModuleSourceCode::String(source),
215      specifier,
216      None,
217    )))
218  }
219
220  fn prepare_load(
221    &self,
222    _specifier: &ModuleSpecifier,
223    _maybe_referrer: Option<String>,
224    _is_dyn_import: bool,
225  ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
226    async { Ok(()) }.boxed_local()
227  }
228}
229
230/// A loader that is used in `op_lazy_load_esm` to load and execute
231/// ES modules that were embedded in the binary using `lazy_loaded_esm`
232/// option in `extension!` macro.
233pub(crate) struct LazyEsmModuleLoader {
234  sources: Rc<RefCell<HashMap<ModuleName, ModuleCodeString>>>,
235}
236
237impl LazyEsmModuleLoader {
238  pub fn new(
239    sources: Rc<RefCell<HashMap<ModuleName, ModuleCodeString>>>,
240  ) -> Self {
241    LazyEsmModuleLoader { sources }
242  }
243}
244
245impl ModuleLoader for LazyEsmModuleLoader {
246  fn resolve(
247    &self,
248    specifier: &str,
249    referrer: &str,
250    _kind: ResolutionKind,
251  ) -> Result<ModuleSpecifier, Error> {
252    Ok(resolve_import(specifier, referrer)?)
253  }
254
255  fn load(
256    &self,
257    specifier: &ModuleSpecifier,
258    _maybe_referrer: Option<&ModuleSpecifier>,
259    _is_dyn_import: bool,
260    _requested_module_type: RequestedModuleType,
261  ) -> ModuleLoadResponse {
262    let mut sources = self.sources.borrow_mut();
263    let source = match sources.remove(specifier.as_str()) {
264      Some(source) => source,
265      None => return ModuleLoadResponse::Sync(Err(anyhow!("Specifier \"{}\" cannot be lazy-loaded as it was not included in the binary.", specifier))),
266    };
267    ModuleLoadResponse::Sync(Ok(ModuleSource::new(
268      ModuleType::JavaScript,
269      ModuleSourceCode::String(source),
270      specifier,
271      None,
272    )))
273  }
274
275  fn prepare_load(
276    &self,
277    _specifier: &ModuleSpecifier,
278    _maybe_referrer: Option<String>,
279    _is_dyn_import: bool,
280  ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
281    async { Ok(()) }.boxed_local()
282  }
283}
284
285/// Basic file system module loader.
286///
287/// Note that this loader will **block** event loop
288/// when loading file as it uses synchronous FS API
289/// from standard library.
290pub struct FsModuleLoader;
291
292impl ModuleLoader for FsModuleLoader {
293  fn resolve(
294    &self,
295    specifier: &str,
296    referrer: &str,
297    _kind: ResolutionKind,
298  ) -> Result<ModuleSpecifier, Error> {
299    Ok(resolve_import(specifier, referrer)?)
300  }
301
302  fn load(
303    &self,
304    module_specifier: &ModuleSpecifier,
305    _maybe_referrer: Option<&ModuleSpecifier>,
306    _is_dynamic: bool,
307    requested_module_type: RequestedModuleType,
308  ) -> ModuleLoadResponse {
309    let module_specifier = module_specifier.clone();
310    let fut = async move {
311      let path = module_specifier.to_file_path().map_err(|_| {
312        generic_error(format!(
313          "Provided module specifier \"{module_specifier}\" is not a file URL."
314        ))
315      })?;
316      let module_type = if let Some(extension) = path.extension() {
317        let ext = extension.to_string_lossy().to_lowercase();
318        // We only return JSON modules if extension was actually `.json`.
319        // In other cases we defer to actual requested module type, so runtime
320        // can decide what to do with it.
321        if ext == "json" {
322          ModuleType::Json
323        } else {
324          match &requested_module_type {
325            RequestedModuleType::Other(ty) => ModuleType::Other(ty.clone()),
326            _ => ModuleType::JavaScript,
327          }
328        }
329      } else {
330        ModuleType::JavaScript
331      };
332
333      // If we loaded a JSON file, but the "requested_module_type" (that is computed from
334      // import attributes) is not JSON we need to fail.
335      if module_type == ModuleType::Json
336        && requested_module_type != RequestedModuleType::Json
337      {
338        return Err(generic_error("Attempted to load JSON module without specifying \"type\": \"json\" attribute in the import statement."));
339      }
340
341      let code = std::fs::read(path).with_context(|| {
342        format!("Failed to load {}", module_specifier.as_str())
343      })?;
344      let module = ModuleSource::new(
345        module_type,
346        ModuleSourceCode::Bytes(code.into_boxed_slice().into()),
347        &module_specifier,
348        None,
349      );
350      Ok(module)
351    }
352    .boxed_local();
353
354    ModuleLoadResponse::Async(fut)
355  }
356}
357
358/// A module loader that you can pre-load a number of modules into and resolve from. Useful for testing and
359/// embedding situations where the filesystem and snapshot systems are not usable or a good fit.
360#[derive(Default)]
361pub struct StaticModuleLoader {
362  map: HashMap<ModuleSpecifier, ModuleCodeString>,
363}
364
365impl StaticModuleLoader {
366  /// Create a new [`StaticModuleLoader`] from an `Iterator` of specifiers and code.
367  pub fn new(
368    from: impl IntoIterator<Item = (ModuleSpecifier, impl IntoModuleCodeString)>,
369  ) -> Self {
370    Self {
371      map: HashMap::from_iter(
372        from.into_iter().map(|(url, code)| {
373          (url, code.into_module_code().into_cheap_copy().0)
374        }),
375      ),
376    }
377  }
378
379  /// Create a new [`StaticModuleLoader`] from a single code item.
380  pub fn with(
381    specifier: ModuleSpecifier,
382    code: impl IntoModuleCodeString,
383  ) -> Self {
384    Self::new([(specifier, code)])
385  }
386}
387
388impl ModuleLoader for StaticModuleLoader {
389  fn resolve(
390    &self,
391    specifier: &str,
392    referrer: &str,
393    _kind: ResolutionKind,
394  ) -> Result<ModuleSpecifier, Error> {
395    Ok(resolve_import(specifier, referrer)?)
396  }
397
398  fn load(
399    &self,
400    module_specifier: &ModuleSpecifier,
401    _maybe_referrer: Option<&ModuleSpecifier>,
402    _is_dyn_import: bool,
403    _requested_module_type: RequestedModuleType,
404  ) -> ModuleLoadResponse {
405    let res = if let Some(code) = self.map.get(module_specifier) {
406      Ok(ModuleSource::new(
407        ModuleType::JavaScript,
408        ModuleSourceCode::String(code.try_clone().unwrap()),
409        module_specifier,
410        None,
411      ))
412    } else {
413      Err(generic_error("Module not found"))
414    };
415    ModuleLoadResponse::Sync(res)
416  }
417}
418
419/// Annotates a `ModuleLoader` with a log of all `load()` calls.
420/// as well as a count of all `resolve()`, `prepare()`, and `load()` calls.
421#[cfg(test)]
422pub struct TestingModuleLoader<L: ModuleLoader> {
423  loader: L,
424  log: RefCell<Vec<ModuleSpecifier>>,
425  load_count: std::cell::Cell<usize>,
426  prepare_count: std::cell::Cell<usize>,
427  resolve_count: std::cell::Cell<usize>,
428}
429
430#[cfg(test)]
431impl<L: ModuleLoader> TestingModuleLoader<L> {
432  pub fn new(loader: L) -> Self {
433    Self {
434      loader,
435      log: RefCell::new(vec![]),
436      load_count: Default::default(),
437      prepare_count: Default::default(),
438      resolve_count: Default::default(),
439    }
440  }
441
442  /// Retrieve the current module load event counts.
443  pub fn counts(&self) -> ModuleLoadEventCounts {
444    ModuleLoadEventCounts {
445      load: self.load_count.get(),
446      prepare: self.prepare_count.get(),
447      resolve: self.resolve_count.get(),
448    }
449  }
450}
451
452#[cfg(test)]
453impl<L: ModuleLoader> ModuleLoader for TestingModuleLoader<L> {
454  fn resolve(
455    &self,
456    specifier: &str,
457    referrer: &str,
458    kind: ResolutionKind,
459  ) -> Result<ModuleSpecifier, Error> {
460    self.resolve_count.set(self.resolve_count.get() + 1);
461    self.loader.resolve(specifier, referrer, kind)
462  }
463
464  fn prepare_load(
465    &self,
466    module_specifier: &ModuleSpecifier,
467    maybe_referrer: Option<String>,
468    is_dyn_import: bool,
469  ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
470    self.prepare_count.set(self.prepare_count.get() + 1);
471    self
472      .loader
473      .prepare_load(module_specifier, maybe_referrer, is_dyn_import)
474  }
475
476  fn load(
477    &self,
478    module_specifier: &ModuleSpecifier,
479    maybe_referrer: Option<&ModuleSpecifier>,
480    is_dyn_import: bool,
481    requested_module_type: RequestedModuleType,
482  ) -> ModuleLoadResponse {
483    self.load_count.set(self.load_count.get() + 1);
484    self.log.borrow_mut().push(module_specifier.clone());
485    self.loader.load(
486      module_specifier,
487      maybe_referrer,
488      is_dyn_import,
489      requested_module_type,
490    )
491  }
492}
493
494#[cfg(test)]
495#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
496pub struct ModuleLoadEventCounts {
497  pub resolve: usize,
498  pub prepare: usize,
499  pub load: usize,
500}
501
502#[cfg(test)]
503impl ModuleLoadEventCounts {
504  pub fn new(resolve: usize, prepare: usize, load: usize) -> Self {
505    Self {
506      resolve,
507      prepare,
508      load,
509    }
510  }
511}