use std::any::Any;
use std::cell::RefCell;
use std::path::{Component, Path, PathBuf};
use std::rc::Rc;
use dynify::{Fn, from_fn};
use rustc_hash::FxHashMap;
use boa_gc::GcRefCell;
use boa_parser::Source;
use crate::script::Script;
use crate::{
Context, JsError, JsNativeError, JsResult, JsString, js_error, js_string, object::JsObject,
realm::Realm, vm::ActiveRunnable,
};
use super::Module;
pub mod embedded;
pub fn resolve_module_specifier(
base: Option<&Path>,
specifier: &JsString,
referrer: Option<&Path>,
_context: &mut Context,
) -> JsResult<PathBuf> {
let base_path = base.map_or_else(|| PathBuf::from(""), PathBuf::from);
let referrer_dir = referrer.and_then(|p| p.parent());
let specifier = specifier.to_std_string_escaped();
#[cfg(target_family = "windows")]
let specifier = specifier.replace('/', "\\");
let short_path = Path::new(&specifier);
let is_relative = short_path.starts_with(".") || short_path.starts_with("..");
let long_path = if is_relative {
if let Some(r_path) = referrer_dir {
base_path.join(r_path).join(short_path)
} else {
return Err(JsError::from_opaque(
js_string!("relative path without referrer").into(),
));
}
} else {
base_path.join(&specifier)
};
if long_path.is_relative() && base.is_some() {
return Err(JsError::from_opaque(
js_string!("resolved path is relative").into(),
));
}
let path = long_path
.components()
.filter(|c| c != &Component::CurDir || c == &Component::Normal("".as_ref()))
.try_fold(PathBuf::new(), |mut acc, c| {
if c == Component::ParentDir {
if acc.as_os_str().is_empty() {
return Err(JsError::from_opaque(
js_string!("path is outside the module root").into(),
));
}
acc.pop();
} else {
acc.push(c);
}
Ok(acc)
})?;
if path.starts_with(&base_path) {
Ok(path)
} else {
Err(JsError::from_opaque(
js_string!("path is outside the module root").into(),
))
}
}
#[derive(Debug, Clone)]
pub enum Referrer {
Module(Module),
Realm(Realm),
Script(Script),
}
impl Referrer {
#[must_use]
pub fn path(&self) -> Option<&Path> {
match self {
Self::Module(module) => module.path(),
Self::Realm(_) => None,
Self::Script(script) => script.path(),
}
}
}
impl From<ActiveRunnable> for Referrer {
fn from(value: ActiveRunnable) -> Self {
match value {
ActiveRunnable::Script(script) => Self::Script(script),
ActiveRunnable::Module(module) => Self::Module(module),
}
}
}
pub trait ModuleLoader: Any {
#[expect(async_fn_in_trait, reason = "all our APIs are single-threaded")]
async fn load_imported_module(
self: Rc<Self>,
referrer: Referrer,
specifier: JsString,
context: &RefCell<&mut Context>,
) -> JsResult<Module>;
fn init_import_meta(
self: Rc<Self>,
_import_meta: &JsObject,
_module: &Module,
_context: &mut Context,
) {
}
}
pub(crate) trait DynModuleLoader: Any {
fn load_imported_module<'a, 'b, 'fut>(
self: Rc<Self>,
referrer: Referrer,
specifier: JsString,
context: &'a RefCell<&'b mut Context>,
) -> Fn!(Rc<Self>, Referrer, JsString, &'a RefCell<&'b mut Context> => dyn 'fut + Future<Output = JsResult<Module>>)
where
'a: 'fut,
'b: 'fut;
fn init_import_meta(
self: Rc<Self>,
import_meta: &JsObject,
module: &Module,
context: &mut Context,
);
}
impl<T: ModuleLoader> DynModuleLoader for T {
fn load_imported_module<'a, 'b, 'fut>(
self: Rc<Self>,
referrer: Referrer,
specifier: JsString,
context: &'a RefCell<&'b mut Context>,
) -> Fn!(Rc<Self>, Referrer, JsString, &'a RefCell<&'b mut Context> => dyn 'fut + Future<Output = JsResult<Module>>)
where
'a: 'fut,
'b: 'fut,
{
from_fn!(T::load_imported_module, self, referrer, specifier, context)
}
fn init_import_meta(
self: Rc<Self>,
import_meta: &JsObject,
module: &Module,
context: &mut Context,
) {
T::init_import_meta(self, import_meta, module, context);
}
}
#[derive(Debug, Clone, Copy)]
pub struct IdleModuleLoader;
impl ModuleLoader for IdleModuleLoader {
async fn load_imported_module(
self: Rc<Self>,
_referrer: Referrer,
_specifier: JsString,
_context: &RefCell<&mut Context>,
) -> JsResult<Module> {
Err(JsNativeError::typ()
.with_message("module resolution is disabled for this context")
.into())
}
}
#[derive(Default, Debug, Clone)]
pub struct MapModuleLoader {
inner: RefCell<FxHashMap<PathBuf, Module>>,
}
impl MapModuleLoader {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn insert(&self, specifier: impl AsRef<str>, module: Module) -> Option<Module> {
self.inner
.borrow_mut()
.insert(PathBuf::from(specifier.as_ref()), module)
}
pub fn clear(&self) {
self.inner.borrow_mut().clear();
}
}
impl FromIterator<(String, Module)> for MapModuleLoader {
fn from_iter<T: IntoIterator<Item = (String, Module)>>(iter: T) -> Self {
Self {
inner: RefCell::new(
iter.into_iter()
.map(|(k, v)| (PathBuf::from(k), v))
.collect(),
),
}
}
}
impl ModuleLoader for MapModuleLoader {
fn load_imported_module(
self: Rc<Self>,
referrer: Referrer,
specifier: JsString,
context: &RefCell<&mut Context>,
) -> impl Future<Output = JsResult<Module>> {
let result = (|| {
let path = resolve_module_specifier(
None,
&specifier,
referrer.path(),
&mut context.borrow_mut(),
)?;
if let Some(module) = self.inner.borrow().get(&path) {
Ok(module.clone())
} else {
Err(js_error!(TypeError: "Module could not be found."))
}
})();
async { result }
}
}
#[derive(Debug)]
pub struct SimpleModuleLoader {
root: PathBuf,
module_map: GcRefCell<FxHashMap<PathBuf, Module>>,
}
impl SimpleModuleLoader {
pub fn new<P: AsRef<Path>>(root: P) -> JsResult<Self> {
if cfg!(target_family = "wasm") {
return Err(JsNativeError::typ()
.with_message("cannot resolve a relative path in WASM targets")
.into());
}
let root = root.as_ref();
let absolute = root.canonicalize().map_err(|e| {
JsNativeError::typ()
.with_message(format!("could not set module root `{}`", root.display()))
.with_cause(JsError::from_opaque(js_string!(e.to_string()).into()))
})?;
Ok(Self {
root: absolute,
module_map: GcRefCell::default(),
})
}
#[inline]
pub fn insert(&self, path: PathBuf, module: Module) {
self.module_map.borrow_mut().insert(path, module);
}
#[inline]
pub fn get(&self, path: &Path) -> Option<Module> {
self.module_map.borrow().get(path).cloned()
}
}
impl ModuleLoader for SimpleModuleLoader {
fn load_imported_module(
self: Rc<Self>,
referrer: Referrer,
specifier: JsString,
context: &RefCell<&mut Context>,
) -> impl Future<Output = JsResult<Module>> {
let result = (|| {
let short_path = specifier.to_std_string_escaped();
let path = resolve_module_specifier(
Some(&self.root),
&specifier,
referrer.path(),
&mut context.borrow_mut(),
)?;
if let Some(module) = self.get(&path) {
return Ok(module);
}
let source = Source::from_filepath(&path).map_err(|err| {
JsNativeError::typ()
.with_message(format!("could not open file `{short_path}`"))
.with_cause(JsError::from_opaque(js_string!(err.to_string()).into()))
})?;
let module = Module::parse(source, None, &mut context.borrow_mut()).map_err(|err| {
JsNativeError::syntax()
.with_message(format!("could not parse module `{short_path}`"))
.with_cause(err)
})?;
self.insert(path, module.clone());
Ok(module)
})();
async { result }
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use test_case::test_case;
use super::*;
#[rustfmt::skip]
#[cfg(target_family = "unix")]
#[test_case(Some("/hello/ref.js"), "a.js", Ok("/base/a.js"))]
#[test_case(Some("/base/ref.js"), "./b.js", Ok("/base/b.js"))]
#[test_case(Some("/base/other/ref.js"), "./c.js", Ok("/base/other/c.js"))]
#[test_case(Some("/base/other/ref.js"), "../d.js", Ok("/base/d.js"))]
#[test_case(Some("/base/ref.js"), "e.js", Ok("/base/e.js"))]
#[test_case(Some("/base/ref.js"), "./f.js", Ok("/base/f.js"))]
#[test_case(Some("./ref.js"), "./g.js", Ok("/base/g.js"))]
#[test_case(Some("./other/ref.js"), "./other/h.js", Ok("/base/other/other/h.js"))]
#[test_case(Some("./other/ref.js"), "./other/../h1.js", Ok("/base/other/h1.js"))]
#[test_case(Some("./other/ref.js"), "./../h2.js", Ok("/base/h2.js"))]
#[test_case(None, "./i.js", Err(()))]
#[test_case(None, "j.js", Ok("/base/j.js"))]
#[test_case(None, "other/k.js", Ok("/base/other/k.js"))]
#[test_case(None, "other/../../l.js", Err(()))]
#[test_case(Some("/base/ref.js"), "other/../../m.js", Err(()))]
#[test_case(None, "../n.js", Err(()))]
fn resolve_test(ref_path: Option<&str>, spec: &str, expected: Result<&str, ()>) {
let base = PathBuf::from("/base");
let mut context = Context::default();
let spec = js_string!(spec);
let ref_path = ref_path.map(PathBuf::from);
let actual = resolve_module_specifier(
Some(&base),
&spec,
ref_path.as_deref(),
&mut context,
);
assert_eq!(actual.map_err(|_| ()), expected.map(PathBuf::from));
}
#[rustfmt::skip]
#[cfg(target_family = "unix")]
#[test_case(Some("hello/ref.js"), "a.js", Ok("a.js"))]
#[test_case(Some("base/ref.js"), "./b.js", Ok("base/b.js"))]
#[test_case(Some("base/other/ref.js"), "./c.js", Ok("base/other/c.js"))]
#[test_case(Some("base/other/ref.js"), "../d.js", Ok("base/d.js"))]
#[test_case(Some("base/ref.js"), "e.js", Ok("e.js"))]
#[test_case(Some("base/ref.js"), "./f.js", Ok("base/f.js"))]
#[test_case(Some("./ref.js"), "./g.js", Ok("g.js"))]
#[test_case(Some("./other/ref.js"), "./other/h.js", Ok("other/other/h.js"))]
#[test_case(Some("./other/ref.js"), "./other/../h1.js", Ok("other/h1.js"))]
#[test_case(Some("./other/ref.js"), "./../h2.js", Ok("h2.js"))]
#[test_case(None, "./i.js", Err(()))]
#[test_case(None, "j.js", Ok("j.js"))]
#[test_case(None, "other/k.js", Ok("other/k.js"))]
#[test_case(None, "other/../../l.js", Err(()))]
#[test_case(Some("/base/ref.js"), "other/../../m.js", Err(()))]
#[test_case(None, "../n.js", Err(()))]
fn resolve_test_no_base(ref_path: Option<&str>, spec: &str, expected: Result<&str, ()>) {
let mut context = Context::default();
let spec = js_string!(spec);
let ref_path = ref_path.map(PathBuf::from);
let actual = resolve_module_specifier(
None,
&spec,
ref_path.as_deref(),
&mut context,
);
assert_eq!(actual.map_err(|_| ()), expected.map(PathBuf::from));
}
#[rustfmt::skip]
#[cfg(target_family = "windows")]
#[test_case(Some("a:\\hello\\ref.js"), "a.js", Ok("a:\\base\\a.js"))]
#[test_case(Some("a:\\base\\ref.js"), "./b.js", Ok("a:\\base\\b.js"))]
#[test_case(Some("a:\\base\\other\\ref.js"), "./c.js", Ok("a:\\base\\other\\c.js"))]
#[test_case(Some("a:\\base\\other\\ref.js"), "../d.js", Ok("a:\\base\\d.js"))]
#[test_case(Some("a:\\base\\ref.js"), "e.js", Ok("a:\\base\\e.js"))]
#[test_case(Some("a:\\base\\ref.js"), "./f.js", Ok("a:\\base\\f.js"))]
#[test_case(Some(".\\ref.js"), "./g.js", Ok("a:\\base\\g.js"))]
#[test_case(Some(".\\other\\ref.js"), "./other/h.js", Ok("a:\\base\\other\\other\\h.js"))]
#[test_case(Some(".\\other\\ref.js"), "./other/../h1.js", Ok("a:\\base\\other\\h1.js"))]
#[test_case(Some(".\\other\\ref.js"), "./../h2.js", Ok("a:\\base\\h2.js"))]
#[test_case(None, "./i.js", Err(()))]
#[test_case(None, "j.js", Ok("a:\\base\\j.js"))]
#[test_case(None, "other/k.js", Ok("a:\\base\\other\\k.js"))]
#[test_case(None, "other/../../l.js", Err(()))]
#[test_case(Some("\\base\\ref.js"), "other/../../m.js", Err(()))]
#[test_case(None, "../n.js", Err(()))]
fn resolve_test(ref_path: Option<&str>, spec: &str, expected: Result<&str, ()>) {
let base = PathBuf::from("a:\\base");
let mut context = Context::default();
let spec = js_string!(spec);
let ref_path = ref_path.map(PathBuf::from);
let actual = resolve_module_specifier(
Some(&base),
&spec,
ref_path.as_deref(),
&mut context,
);
assert_eq!(actual.map_err(|_| ()), expected.map(PathBuf::from));
}
}