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
|
||||
//! creating custom module loaders.
|
||||
|
||||
pub use hashmap::HashMapModuleLoader; |
||||
|
||||
pub mod cached; |
||||
pub mod embedded; |
||||
pub mod fallback; |
||||
pub mod filesystem; |
||||
pub mod functions; |
||||
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