use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
mod import_map;
pub(crate) use import_map::ImportMap;
use boa_engine::module::{ModuleLoader, ModuleRequest, Referrer, SyntheticModuleInitializer};
use boa_engine::{Context, JsNativeError, JsResult, JsString, JsValue, Module, Source, js_string};
use url::Url;
use crate::fetch::ScriptFetcher;
pub(crate) type SharedFetcher = Rc<RefCell<Rc<dyn ScriptFetcher>>>;
pub(crate) struct BlitzModuleLoader {
fetcher: SharedFetcher,
base_url: Option<Url>,
cache: RefCell<HashMap<String, Module>>,
import_map: RefCell<ImportMap>,
}
const MAX_MODULES_PER_DOCUMENT: usize = 4096;
impl BlitzModuleLoader {
pub(crate) fn new(fetcher: SharedFetcher, base_url: Option<Url>) -> Self {
Self {
fetcher,
base_url,
cache: RefCell::new(HashMap::new()),
import_map: RefCell::new(ImportMap::default()),
}
}
pub(crate) fn set_import_map(&self, map: ImportMap) {
let mut installed = self.import_map.borrow_mut();
if installed.is_empty() {
*installed = map;
}
}
pub(crate) fn register(&self, url: &Url, module: Module) {
self.cache
.borrow_mut()
.insert(url.as_str().to_owned(), module);
}
fn module_url(module_path: Option<&Path>) -> Option<Url> {
Url::parse(module_path?.to_str()?).ok()
}
fn resolve(&self, referrer: &Referrer, specifier: &JsString) -> JsResult<Url> {
let specifier = specifier.to_std_string_escaped();
let referrer_url = Self::module_url(referrer.path());
if let Some(url) = self
.import_map
.borrow()
.resolve(&specifier, referrer_url.as_ref().or(self.base_url.as_ref()))
{
return Ok(url);
}
if let Ok(url) = Url::parse(&specifier) {
return Ok(url);
}
let base = referrer_url.or_else(|| self.base_url.clone());
let Some(base) = base else {
return Err(JsNativeError::typ()
.with_message(format!(
"cannot resolve module specifier {specifier:?}: the importing script has no URL"
))
.into());
};
if !specifier.starts_with('/') && !specifier.starts_with('.') {
return Err(JsNativeError::typ()
.with_message(format!(
"cannot resolve bare module specifier {specifier:?}: no import map entry matches it"
))
.into());
}
base.join(&specifier).map_err(|error| {
JsNativeError::typ()
.with_message(format!(
"cannot resolve module specifier {specifier:?} against {base}: {error}"
))
.into()
})
}
fn load(
&self,
referrer: &Referrer,
request: &ModuleRequest,
context: &mut Context,
) -> JsResult<Module> {
let url = self.resolve(referrer, request.specifier())?;
if let Some(module) = self.cache.borrow().get(url.as_str()) {
return Ok(module.clone());
}
if self.cache.borrow().len() >= MAX_MODULES_PER_DOCUMENT {
return Err(JsNativeError::typ()
.with_message(format!(
"refusing to load {url}: this document has already loaded \
{MAX_MODULES_PER_DOCUMENT} modules"
))
.into());
}
let fetcher = Rc::clone(&self.fetcher.borrow());
let source = fetcher.fetch(&url).map_err(|error| {
JsNativeError::typ().with_message(format!("failed to fetch module {url}: {error}"))
})?;
let path = url.as_str().to_owned();
let module = if requests_json(request) {
parse_json_module(&source, &url, &path, context)?
} else {
Module::parse(
Source::from_bytes(source.as_bytes()).with_path(Path::new(&path)),
None,
context,
)?
};
self.cache.borrow_mut().insert(path, module.clone());
Ok(module)
}
}
fn requests_json(request: &ModuleRequest) -> bool {
request.attributes().iter().any(|attribute| {
attribute.key().to_std_string_escaped() == "type"
&& attribute.value().to_std_string_escaped() == "json"
})
}
fn parse_json_module(
source: &str,
url: &Url,
path: &str,
context: &mut Context,
) -> JsResult<Module> {
let json: serde_json::Value = serde_json::from_str(source).map_err(|error| {
JsNativeError::typ().with_message(format!("{url} is not valid JSON: {error}"))
})?;
let value = JsValue::from_json(&json, context)?;
Ok(Module::synthetic(
&[js_string!("default")],
SyntheticModuleInitializer::from_copy_closure_with_captures(
|module, value, _context| module.set_export(&js_string!("default"), value.clone()),
value,
),
Some(PathBuf::from(path)),
None,
context,
))
}
impl ModuleLoader for BlitzModuleLoader {
fn load_imported_module(
self: Rc<Self>,
referrer: Referrer,
request: ModuleRequest,
context: &RefCell<&mut Context>,
) -> impl Future<Output = JsResult<Module>> {
let result = self.load(&referrer, &request, &mut context.borrow_mut());
async { result }
}
fn init_import_meta(
self: Rc<Self>,
import_meta: &boa_engine::JsObject,
module: &Module,
context: &mut Context,
) {
let url = module
.path()
.and_then(Path::to_str)
.map(str::to_owned)
.or_else(|| self.base_url.as_ref().map(|base| base.to_string()));
if let Some(url) = url {
let _ = import_meta.set(
js_string!("url"),
JsString::from(url.as_str()),
false,
context,
);
}
}
}