-
-
Notifications
You must be signed in to change notification settings - Fork 408
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A simple module loader from a function (#3932)
* 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 comment
- Loading branch information
Showing
5 changed files
with
325 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} | ||
} |