Skip to content

Commit

Permalink
A simple module loader from a function (#3932)
Browse files Browse the repository at this point in the history
* 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
hansl authored Sep 9, 2024
1 parent cdb4751 commit c21f10e
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 3 deletions.
13 changes: 10 additions & 3 deletions core/interop/src/loaders.rs
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;
80 changes: 80 additions & 0 deletions core/interop/src/loaders/cached.rs
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,
);
}
}
}
46 changes: 46 additions & 0 deletions core/interop/src/loaders/fallback.rs
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,
);
}
}
59 changes: 59 additions & 0 deletions core/interop/src/loaders/filesystem.rs
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);
}
}
130 changes: 130 additions & 0 deletions core/interop/src/loaders/functions.rs
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);
}
}
}
}

0 comments on commit c21f10e

Please sign in to comment.