use std::any::Any;
use std::cell::RefCell;
use std::path::{Component, Path, PathBuf};
use std::rc::Rc;
use dynify::dynify;
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;
use crate::module::{ImportAttribute, ModuleRequest};
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 = cow_utils::CowUtils::cow_replace(&*specifier, '/', "\\");
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(js_error!(TypeError: "relative path without referrer"));
}
} else {
base_path.join(&*specifier)
};
if long_path.is_relative() && base.is_some() {
return Err(js_error!(TypeError: "resolved path is relative"));
}
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(js_error!(TypeError: "path is outside the module root"));
}
acc.pop();
} else {
acc.push(c);
}
Ok(acc)
})?;
if path.starts_with(&base_path) {
Ok(path)
} else {
Err(js_error!(TypeError: "path is outside the module root"))
}
}
#[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),
}
}
}
#[dynify]
pub trait ModuleLoader: Any {
#[allow(async_fn_in_trait, reason = "all our APIs are single-threaded")]
async fn load_imported_module(
self: Rc<Self>,
referrer: Referrer,
request: ModuleRequest,
context: &RefCell<&mut Context>,
) -> JsResult<Module>;
#[allow(unused_variables, reason = "this should be overridden by implementors")]
fn init_import_meta(
self: Rc<Self>,
import_meta: &JsObject,
module: &Module,
context: &mut Context,
) {
}
}
#[derive(Debug, Clone, Copy)]
pub struct IdleModuleLoader;
impl ModuleLoader for IdleModuleLoader {
fn load_imported_module(
self: Rc<Self>,
_referrer: Referrer,
_request: ModuleRequest,
_context: &RefCell<&mut Context>,
) -> impl Future<Output = JsResult<Module>> {
std::future::ready(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,
request: ModuleRequest,
context: &RefCell<&mut Context>,
) -> impl Future<Output = JsResult<Module>> {
let result = (|| {
let path = resolve_module_specifier(
None,
request.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, Clone, PartialEq, Eq, Hash)]
struct ModuleCacheKey {
path: PathBuf,
attributes: Box<[ImportAttribute]>,
}
impl ModuleCacheKey {
fn new(path: PathBuf, attributes: &[ImportAttribute]) -> Self {
let mut attributes = attributes.to_vec();
attributes.sort_unstable_by(|left, right| left.key().cmp(right.key()));
Self {
path,
attributes: attributes.into_boxed_slice(),
}
}
}
#[derive(Debug)]
pub struct SimpleModuleLoader {
root: PathBuf,
module_map: GcRefCell<FxHashMap<ModuleCacheKey, 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_rust(e))
})?;
Ok(Self {
root: absolute,
module_map: GcRefCell::default(),
})
}
#[inline]
pub fn insert(&self, path: PathBuf, module: Module) {
self.insert_with_attributes(path, &[], module);
}
#[inline]
pub fn insert_with_attributes(
&self,
path: PathBuf,
attributes: &[ImportAttribute],
module: Module,
) {
self.module_map
.borrow_mut()
.insert(ModuleCacheKey::new(path, attributes), module);
}
#[inline]
pub fn get(&self, path: &Path) -> Option<Module> {
self.get_with_attributes(path, &[])
}
#[inline]
pub fn get_with_attributes(
&self,
path: &Path,
attributes: &[ImportAttribute],
) -> Option<Module> {
self.module_map
.borrow()
.get(&ModuleCacheKey::new(path.to_path_buf(), attributes))
.cloned()
}
}
impl ModuleLoader for SimpleModuleLoader {
fn load_imported_module(
self: Rc<Self>,
referrer: Referrer,
request: ModuleRequest,
context: &RefCell<&mut Context>,
) -> impl Future<Output = JsResult<Module>> {
let result = (|| {
let short_path = request.specifier().to_std_string_escaped();
let path = resolve_module_specifier(
Some(&self.root),
request.specifier(),
referrer.path(),
&mut context.borrow_mut(),
)?;
if let Some(module) = self.get_with_attributes(&path, request.attributes()) {
return Ok(module);
}
let mut module_type = None;
let type_key = js_string!("type");
for attr in request.attributes() {
if attr.key() == &type_key {
module_type = Some(attr.value());
}
}
if path
.extension()
.is_some_and(|ext| ext.to_string_lossy() == "json")
{
let is_json_type = module_type.is_some_and(|t| t == &js_string!("json"));
if !is_json_type {
return Err(JsNativeError::typ()
.with_message(format!(
"module `{short_path}` needs an import attribute of type \"json\""
))
.into());
}
}
let module = if let Some(ty) = module_type {
match ty.to_std_string_escaped().as_str() {
"json" => {
let json_content = std::fs::read_to_string(&path).map_err(|err| {
JsNativeError::typ()
.with_message(format!("could not open file `{short_path}`"))
.with_cause(JsError::from_rust(err))
})?;
let json_string = js_string!(json_content.as_str());
Module::parse_json(json_string, &mut context.borrow_mut()).map_err(
|err| {
JsNativeError::syntax()
.with_message(format!(
"could not parse JSON module `{short_path}`"
))
.with_cause(err)
},
)?
}
other => {
return Err(JsNativeError::typ()
.with_message(format!(
"unsupported module type `{other}` for module `{short_path}`"
))
.into());
}
}
} else {
let source = Source::from_filepath(&path).map_err(|err| {
JsNativeError::typ()
.with_message(format!("could not open file `{short_path}`"))
.with_cause(JsError::from_rust(err))
})?;
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_with_attributes(path, request.attributes(), 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));
}
}