Browse Source

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
pull/3994/head
Hans Larsen 3 months ago committed by GitHub
parent
commit
c21f10efae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      core/interop/src/loaders.rs
  2. 80
      core/interop/src/loaders/cached.rs
  3. 46
      core/interop/src/loaders/fallback.rs
  4. 59
      core/interop/src/loaders/filesystem.rs
  5. 130
      core/interop/src/loaders/functions.rs

13
core/interop/src/loaders.rs

@ -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;

80
core/interop/src/loaders/cached.rs

@ -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,
);
}
}
}

46
core/interop/src/loaders/fallback.rs

@ -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,
);
}
}

59
core/interop/src/loaders/filesystem.rs

@ -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);
}
}

130
core/interop/src/loaders/functions.rs

@ -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…
Cancel
Save