// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::error::generic_error; use crate::error::AnyError; use crate::extensions::ExtensionFileSource; use crate::module_specifier::ModuleSpecifier; use crate::modules::ModuleCode; use crate::modules::ModuleSource; use crate::modules::ModuleSourceFuture; use crate::modules::ModuleType; use crate::modules::ResolutionKind; use crate::resolve_import; use crate::Extension; use anyhow::anyhow; use anyhow::Error; use futures::future::FutureExt; use std::cell::RefCell; use std::collections::HashMap; use std::collections::HashSet; use std::future::Future; use std::pin::Pin; use std::rc::Rc; pub trait ModuleLoader { /// Returns an absolute URL. /// When implementing an spec-complaint VM, this should be exactly the /// algorithm described here: /// /// /// `is_main` can be used to resolve from current working directory or /// apply import map for child imports. /// /// `is_dyn_import` can be used to check permissions or deny /// dynamic imports altogether. fn resolve( &self, specifier: &str, referrer: &str, kind: ResolutionKind, ) -> Result; /// Given ModuleSpecifier, load its source code. /// /// `is_dyn_import` can be used to check permissions or deny /// dynamic imports altogether. fn load( &self, module_specifier: &ModuleSpecifier, maybe_referrer: Option<&ModuleSpecifier>, is_dyn_import: bool, ) -> Pin>; /// This hook can be used by implementors to do some preparation /// work before starting loading of modules. /// /// For example implementor might download multiple modules in /// parallel and transpile them to final JS sources before /// yielding control back to the runtime. /// /// It's not required to implement this method. fn prepare_load( &self, _module_specifier: &ModuleSpecifier, _maybe_referrer: Option, _is_dyn_import: bool, ) -> Pin>>> { async { Ok(()) }.boxed_local() } } /// Placeholder structure used when creating /// a runtime that doesn't support module loading. pub struct NoopModuleLoader; impl ModuleLoader for NoopModuleLoader { fn resolve( &self, specifier: &str, referrer: &str, _kind: ResolutionKind, ) -> Result { Err(generic_error( format!("Module loading is not supported; attempted to resolve: \"{specifier}\" from \"{referrer}\"") )) } fn load( &self, module_specifier: &ModuleSpecifier, maybe_referrer: Option<&ModuleSpecifier>, _is_dyn_import: bool, ) -> Pin> { let err = generic_error( format!( "Module loading is not supported; attempted to load: \"{module_specifier}\" from \"{maybe_referrer:?}\"", ) ); async move { Err(err) }.boxed_local() } } /// Function that can be passed to the `ExtModuleLoader` that allows to /// transpile sources before passing to V8. pub type ExtModuleLoaderCb = Box Result>; pub(crate) struct ExtModuleLoader { maybe_load_callback: Option>, sources: RefCell>, used_specifiers: RefCell>, } impl ExtModuleLoader { pub fn new( extensions: &[Extension], maybe_load_callback: Option>, ) -> Self { let mut sources = HashMap::new(); sources.extend( extensions .iter() .flat_map(|e| e.get_esm_sources()) .flatten() .map(|s| (s.specifier.to_string(), s.clone())), ); ExtModuleLoader { maybe_load_callback, sources: RefCell::new(sources), used_specifiers: Default::default(), } } } impl ModuleLoader for ExtModuleLoader { fn resolve( &self, specifier: &str, referrer: &str, _kind: ResolutionKind, ) -> Result { Ok(resolve_import(specifier, referrer)?) } fn load( &self, specifier: &ModuleSpecifier, _maybe_referrer: Option<&ModuleSpecifier>, _is_dyn_import: bool, ) -> Pin> { let sources = self.sources.borrow(); let source = match sources.get(specifier.as_str()) { Some(source) => source, None => return futures::future::err(anyhow!("Specifier \"{}\" was not passed as an extension module and was not included in the snapshot.", specifier)).boxed_local(), }; self .used_specifiers .borrow_mut() .insert(specifier.to_string()); let result = if let Some(load_callback) = &self.maybe_load_callback { load_callback(source) } else { source.load() }; match result { Ok(code) => { let res = ModuleSource::new(ModuleType::JavaScript, code, specifier); return futures::future::ok(res).boxed_local(); } Err(err) => return futures::future::err(err).boxed_local(), } } fn prepare_load( &self, _specifier: &ModuleSpecifier, _maybe_referrer: Option, _is_dyn_import: bool, ) -> Pin>>> { async { Ok(()) }.boxed_local() } } impl Drop for ExtModuleLoader { fn drop(&mut self) { let sources = self.sources.get_mut(); let used_specifiers = self.used_specifiers.get_mut(); let unused_modules: Vec<_> = sources .iter() .filter(|(k, _)| !used_specifiers.contains(k.as_str())) .collect(); if !unused_modules.is_empty() { let mut msg = "Following modules were passed to ExtModuleLoader but never used:\n" .to_string(); for m in unused_modules { msg.push_str(" - "); msg.push_str(m.0); msg.push('\n'); } panic!("{}", msg); } } } /// Basic file system module loader. /// /// Note that this loader will **block** event loop /// when loading file as it uses synchronous FS API /// from standard library. pub struct FsModuleLoader; impl ModuleLoader for FsModuleLoader { fn resolve( &self, specifier: &str, referrer: &str, _kind: ResolutionKind, ) -> Result { Ok(resolve_import(specifier, referrer)?) } fn load( &self, module_specifier: &ModuleSpecifier, _maybe_referrer: Option<&ModuleSpecifier>, _is_dynamic: bool, ) -> Pin> { fn load( module_specifier: &ModuleSpecifier, ) -> Result { let path = module_specifier.to_file_path().map_err(|_| { generic_error(format!( "Provided module specifier \"{module_specifier}\" is not a file URL." )) })?; let module_type = if let Some(extension) = path.extension() { let ext = extension.to_string_lossy().to_lowercase(); if ext == "json" { ModuleType::Json } else { ModuleType::JavaScript } } else { ModuleType::JavaScript }; let code = std::fs::read_to_string(path)?.into(); let module = ModuleSource::new(module_type, code, module_specifier); Ok(module) } futures::future::ready(load(module_specifier)).boxed_local() } }