mirror of https://github.com/boa-dev/boa.git
Browse Source
* A simple module loader from a function This will be the foundation for having a combinatoric module loader system. * Add more utility module loader types * clippies * Remove convenience functions and allow AsRef<Path> for constructing fs * clippies * Move FnModuleLoader to return a result, and add a new simpler loader * Address commentpull/3994/head
Hans Larsen
3 months ago
committed by
GitHub
5 changed files with 325 additions and 3 deletions
@ -1,7 +1,14 @@ |
|||||||
//! A collection of JS [`boa_engine::module::ModuleLoader`]s utilities to help in
|
//! A collection of JS [`boa_engine::module::ModuleLoader`]s utilities to help in
|
||||||
//! creating custom module loaders.
|
//! creating custom module loaders.
|
||||||
|
pub mod cached; |
||||||
pub use hashmap::HashMapModuleLoader; |
|
||||||
|
|
||||||
pub mod embedded; |
pub mod embedded; |
||||||
|
pub mod fallback; |
||||||
|
pub mod filesystem; |
||||||
|
pub mod functions; |
||||||
pub mod hashmap; |
pub mod hashmap; |
||||||
|
|
||||||
|
pub use cached::CachedModuleLoader; |
||||||
|
pub use fallback::FallbackModuleLoader; |
||||||
|
pub use filesystem::FsModuleLoader; |
||||||
|
pub use functions::FnModuleLoader; |
||||||
|
pub use hashmap::HashMapModuleLoader; |
||||||
|
@ -0,0 +1,80 @@ |
|||||||
|
//! A module loader that caches modules once they're resolved.
|
||||||
|
use boa_engine::module::{resolve_module_specifier, ModuleLoader, Referrer}; |
||||||
|
use boa_engine::{Context, JsError, JsNativeError, JsResult, JsString, Module}; |
||||||
|
use std::cell::RefCell; |
||||||
|
use std::collections::HashMap; |
||||||
|
use std::path::PathBuf; |
||||||
|
use std::rc::Rc; |
||||||
|
|
||||||
|
/// A module loader that caches modules once they're resolved.
|
||||||
|
#[allow(clippy::module_name_repetitions)] |
||||||
|
#[derive(Clone, Debug)] |
||||||
|
pub struct CachedModuleLoader<B> |
||||||
|
where |
||||||
|
B: ModuleLoader + Clone + 'static, |
||||||
|
{ |
||||||
|
inner: B, |
||||||
|
// TODO: Use a specifier instead of a PathBuf.
|
||||||
|
cache: Rc<RefCell<HashMap<PathBuf, Module>>>, |
||||||
|
} |
||||||
|
|
||||||
|
impl<B> CachedModuleLoader<B> |
||||||
|
where |
||||||
|
B: ModuleLoader + Clone + 'static, |
||||||
|
{ |
||||||
|
/// Create a new [`CachedModuleLoader`] from an inner module loader and
|
||||||
|
/// an empty cache.
|
||||||
|
pub fn new(inner: B) -> Self { |
||||||
|
Self { |
||||||
|
inner, |
||||||
|
cache: Rc::new(RefCell::new(HashMap::new())), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<B> ModuleLoader for CachedModuleLoader<B> |
||||||
|
where |
||||||
|
B: ModuleLoader + Clone + 'static, |
||||||
|
{ |
||||||
|
fn load_imported_module( |
||||||
|
&self, |
||||||
|
referrer: Referrer, |
||||||
|
specifier: JsString, |
||||||
|
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>, |
||||||
|
context: &mut Context, |
||||||
|
) { |
||||||
|
let path = match resolve_module_specifier(None, &specifier, referrer.path(), context) { |
||||||
|
Ok(path) => path, |
||||||
|
Err(err) => { |
||||||
|
finish_load( |
||||||
|
Err(JsError::from_native( |
||||||
|
JsNativeError::typ() |
||||||
|
.with_message("could not resolve module specifier") |
||||||
|
.with_cause(err), |
||||||
|
)), |
||||||
|
context, |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if let Some(module) = self.cache.borrow().get(&path).cloned() { |
||||||
|
finish_load(Ok(module), context); |
||||||
|
} else { |
||||||
|
self.inner.load_imported_module( |
||||||
|
referrer, |
||||||
|
specifier, |
||||||
|
{ |
||||||
|
let cache = self.cache.clone(); |
||||||
|
Box::new(move |result: JsResult<Module>, context| { |
||||||
|
if let Ok(module) = &result { |
||||||
|
cache.borrow_mut().insert(path, module.clone()); |
||||||
|
} |
||||||
|
finish_load(result, context); |
||||||
|
}) |
||||||
|
}, |
||||||
|
context, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
//! A module loader that tries to load modules from multiple loaders.
|
||||||
|
use boa_engine::module::{ModuleLoader, Referrer}; |
||||||
|
use boa_engine::{Context, JsResult, JsString, Module}; |
||||||
|
|
||||||
|
/// A [`ModuleLoader`] that tries to load a module from one loader, and if that fails,
|
||||||
|
/// falls back to another loader.
|
||||||
|
#[allow(clippy::module_name_repetitions)] |
||||||
|
#[derive(Clone, Debug)] |
||||||
|
pub struct FallbackModuleLoader<L, R>(L, R); |
||||||
|
|
||||||
|
impl<L, R> FallbackModuleLoader<L, R> { |
||||||
|
/// Create a new [`FallbackModuleLoader`] from two loaders.
|
||||||
|
pub fn new(loader: L, fallback: R) -> Self { |
||||||
|
Self(loader, fallback) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<L, R> ModuleLoader for FallbackModuleLoader<L, R> |
||||||
|
where |
||||||
|
L: ModuleLoader, |
||||||
|
R: ModuleLoader + Clone + 'static, |
||||||
|
{ |
||||||
|
fn load_imported_module( |
||||||
|
&self, |
||||||
|
referrer: Referrer, |
||||||
|
specifier: JsString, |
||||||
|
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>, |
||||||
|
context: &mut Context, |
||||||
|
) { |
||||||
|
self.0.load_imported_module( |
||||||
|
referrer.clone(), |
||||||
|
specifier.clone(), |
||||||
|
{ |
||||||
|
let fallback = self.1.clone(); |
||||||
|
Box::new(move |result, context| { |
||||||
|
if result.is_ok() { |
||||||
|
finish_load(result, context); |
||||||
|
} else { |
||||||
|
fallback.load_imported_module(referrer, specifier, finish_load, context); |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
context, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
//! Filesystem module loader. Loads modules from the filesystem.
|
||||||
|
|
||||||
|
use boa_engine::module::{resolve_module_specifier, ModuleLoader, Referrer}; |
||||||
|
use boa_engine::{js_string, Context, JsError, JsNativeError, JsResult, JsString, Module, Source}; |
||||||
|
use std::path::{Path, PathBuf}; |
||||||
|
|
||||||
|
/// A module loader that loads modules from the filesystem.
|
||||||
|
#[derive(Clone, Debug)] |
||||||
|
pub struct FsModuleLoader { |
||||||
|
root: PathBuf, |
||||||
|
} |
||||||
|
|
||||||
|
impl FsModuleLoader { |
||||||
|
/// Create a new [`FsModuleLoader`] from a root path.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// An error happens if the root path cannot be canonicalized (e.g. does
|
||||||
|
/// not exists).
|
||||||
|
pub fn new(root: impl AsRef<Path>) -> JsResult<Self> { |
||||||
|
let root = root.as_ref(); |
||||||
|
let root = 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 }) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl ModuleLoader for FsModuleLoader { |
||||||
|
fn load_imported_module( |
||||||
|
&self, |
||||||
|
referrer: Referrer, |
||||||
|
specifier: JsString, |
||||||
|
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>, |
||||||
|
context: &mut Context, |
||||||
|
) { |
||||||
|
let result = (|| -> JsResult<Module> { |
||||||
|
let short_path = specifier.to_std_string_escaped(); |
||||||
|
let path = |
||||||
|
resolve_module_specifier(Some(&self.root), &specifier, referrer.path(), context)?; |
||||||
|
|
||||||
|
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, context).map_err(|err| { |
||||||
|
JsNativeError::syntax() |
||||||
|
.with_message(format!("could not parse module `{short_path}`")) |
||||||
|
.with_cause(err) |
||||||
|
})?; |
||||||
|
Ok(module) |
||||||
|
})(); |
||||||
|
|
||||||
|
finish_load(result, context); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,130 @@ |
|||||||
|
//! This module contains types that help create custom module loaders from functions.
|
||||||
|
use boa_engine::module::{resolve_module_specifier, ModuleLoader, Referrer}; |
||||||
|
use boa_engine::{Context, JsError, JsNativeError, JsResult, JsString, Module, Source}; |
||||||
|
use std::io::Cursor; |
||||||
|
|
||||||
|
/// Create a [`ModuleLoader`] from a function that takes a referrer and a path,
|
||||||
|
/// and returns a [Module] if it exists, or an error.
|
||||||
|
///
|
||||||
|
/// This function cannot be `async` and must be blocking. An `async` version of
|
||||||
|
/// this code will likely exist as a separate function in the future.
|
||||||
|
///
|
||||||
|
/// `F` cannot be a mutable closure as it could recursively call itself.
|
||||||
|
#[derive(Copy, Clone)] |
||||||
|
pub struct FnModuleLoader<F> |
||||||
|
where |
||||||
|
F: Fn(&Referrer, &JsString) -> JsResult<Module>, |
||||||
|
{ |
||||||
|
factory: F, |
||||||
|
name: &'static str, |
||||||
|
} |
||||||
|
|
||||||
|
impl<F> FnModuleLoader<F> |
||||||
|
where |
||||||
|
F: Fn(&Referrer, &JsString) -> JsResult<Module>, |
||||||
|
{ |
||||||
|
/// Create a new [`FnModuleLoader`] from a function that takes a path and returns
|
||||||
|
/// a [Module] if it exists.
|
||||||
|
pub const fn new(factory: F) -> Self { |
||||||
|
Self::named(factory, "Unnamed") |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a new [`FnModuleLoader`] from a function that takes a path and returns
|
||||||
|
/// a [Module] if it exists, with a name.
|
||||||
|
pub const fn named(factory: F, name: &'static str) -> Self { |
||||||
|
Self { factory, name } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<F> std::fmt::Debug for FnModuleLoader<F> |
||||||
|
where |
||||||
|
F: Fn(&Referrer, &JsString) -> JsResult<Module>, |
||||||
|
{ |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.debug_tuple("FnModuleLoader").field(&self.name).finish() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<F> ModuleLoader for FnModuleLoader<F> |
||||||
|
where |
||||||
|
F: Fn(&Referrer, &JsString) -> JsResult<Module>, |
||||||
|
{ |
||||||
|
fn load_imported_module( |
||||||
|
&self, |
||||||
|
referrer: Referrer, |
||||||
|
specifier: JsString, |
||||||
|
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>, |
||||||
|
context: &mut Context, |
||||||
|
) { |
||||||
|
finish_load((self.factory)(&referrer, &specifier), context); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a module loader from a function that takes a resolved path
|
||||||
|
/// and optionally returns the source code. The path is resolved before
|
||||||
|
/// passing it. If the source cannot be found or would generate an
|
||||||
|
/// error, the function should return `None`.
|
||||||
|
///
|
||||||
|
/// This function cannot be `async` and must be blocking. An `async` version of
|
||||||
|
/// this code will likely exist as a separate function in the future.
|
||||||
|
///
|
||||||
|
/// `F` cannot be a mutable closure as it could recursively call itself.
|
||||||
|
pub struct SourceFnModuleLoader<F>(F, &'static str) |
||||||
|
where |
||||||
|
F: Fn(&str) -> Option<String>; |
||||||
|
|
||||||
|
impl<F> std::fmt::Debug for SourceFnModuleLoader<F> |
||||||
|
where |
||||||
|
F: Fn(&str) -> Option<String>, |
||||||
|
{ |
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
||||||
|
f.debug_tuple("SourceFnModuleLoader") |
||||||
|
.field(&self.1) |
||||||
|
.finish() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<F> SourceFnModuleLoader<F> |
||||||
|
where |
||||||
|
F: Fn(&str) -> Option<String>, |
||||||
|
{ |
||||||
|
/// Create a new [`SourceFnModuleLoader`] from a function.
|
||||||
|
pub const fn new(f: F) -> Self { |
||||||
|
Self(f, "Unnamed") |
||||||
|
} |
||||||
|
|
||||||
|
/// Create a new [`SourceFnModuleLoader`] from a function, with a name.
|
||||||
|
/// The name is used in error messages and debug strings.
|
||||||
|
pub const fn named(f: F, name: &'static str) -> Self { |
||||||
|
Self(f, name) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl<F> ModuleLoader for SourceFnModuleLoader<F> |
||||||
|
where |
||||||
|
F: Fn(&str) -> Option<String>, |
||||||
|
{ |
||||||
|
fn load_imported_module( |
||||||
|
&self, |
||||||
|
referrer: Referrer, |
||||||
|
specifier: JsString, |
||||||
|
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>, |
||||||
|
context: &mut Context, |
||||||
|
) { |
||||||
|
match resolve_module_specifier(None, &specifier, referrer.path(), context) { |
||||||
|
Err(e) => finish_load(Err(e), context), |
||||||
|
Ok(p) => { |
||||||
|
let m = match self.0(&p.to_string_lossy()) { |
||||||
|
Some(source) => Ok(Source::from_reader( |
||||||
|
Cursor::new(source.into_bytes()), |
||||||
|
Some(&p), |
||||||
|
)), |
||||||
|
None => Err(JsError::from_native( |
||||||
|
JsNativeError::error().with_message("Module not found"), |
||||||
|
)), |
||||||
|
}; |
||||||
|
finish_load(m.and_then(|s| Module::parse(s, None, context)), context); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue