Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an embed_module!() macro to boa_interop #3784

Merged
merged 22 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions core/interop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ rust-version.workspace = true
[dependencies]
boa_engine.workspace = true
boa_gc.workspace = true
boa_macros.workspace = true
rustc-hash = { workspace = true, features = ["std"] }

[lints]
Expand Down
1 change: 1 addition & 0 deletions core/interop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use boa_engine::module::SyntheticModuleInitializer;
use boa_engine::value::TryFromJs;
use boa_engine::{Context, JsResult, JsString, JsValue, Module, NativeFunction};
pub use boa_macros;

pub mod loaders;

Expand Down
1 change: 1 addition & 0 deletions core/interop/src/loaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

pub use hashmap::HashMapModuleLoader;

pub mod embedded;
pub mod hashmap;
163 changes: 163 additions & 0 deletions core/interop/src/loaders/embedded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! Embedded module loader. Creates a `ModuleLoader` instance that contains
//! files embedded in the binary at build time.

use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;

use boa_engine::module::{ModuleLoader, Referrer};
use boa_engine::{Context, JsError, JsNativeError, JsResult, JsString, Module, Source};

/// Normalize a specifier to remove `.` and `..` components.
fn normalize_specifier(specifier: &JsString) -> JsResult<JsString> {
let specifier = specifier.to_std_string_escaped();
let components = specifier.split('/').collect::<Vec<_>>();
let specifier = components
.into_iter()
.try_fold(String::new(), |mut acc, component| {
if component == "." {
return Ok(acc);
}

if component == ".." {
if acc.is_empty() {
return Err(JsError::from_native(
JsNativeError::typ().with_message("invalid specifier".to_string()),
));
}

acc.pop();
return Ok(acc);
}

if !acc.is_empty() {
acc.push('/');
}
acc.push_str(component);
Ok(acc)
})?;

Ok(specifier.into())
hansl marked this conversation as resolved.
Show resolved Hide resolved
}

/// Create a module loader that embeds files from the filesystem at build
/// time. This is useful for bundling assets with the binary.
///
/// By default, will error if the total file size exceeds 1MB. This can be
/// changed by specifying the `max_size` parameter.
///
/// The embedded module will only contain files that have the `.js`, `.mjs`,
/// or `.cjs` extension.
#[macro_export]
macro_rules! embed_module {
($path: literal, max_size = $max_size: literal) => {
$crate::loaders::embedded::EmbeddedModuleLoader::from_iter(
$crate::boa_macros::embed_module_inner!($path, $max_size),
)
};
($path: literal) => {
embed_module!($path, max_size = 1_048_576)
};
}

#[derive(Debug, Clone)]
enum EmbeddedModuleEntry {
Source(JsString, &'static [u8]),
Module(Module),
Error(JsError),
jedel1043 marked this conversation as resolved.
Show resolved Hide resolved
}

impl EmbeddedModuleEntry {
fn from_source(path: JsString, source: &'static [u8]) -> Self {
Self::Source(path, source)
}

fn cache(&mut self, context: &mut Context) -> JsResult<&Module> {
if let Self::Source(path, source) = self {
let mut bytes: &[u8] = source;
let path = path.to_std_string_escaped();
let source = Source::from_reader(&mut bytes, Some(Path::new(&path)));
match Module::parse(source, None, context) {
Ok(module) => {
*self = Self::Module(module);
}
Err(err) => {
*self = Self::Error(err);
}
}
};

match self {
Self::Module(module) => Ok(module),
Self::Error(err) => Err(err.clone()),
EmbeddedModuleEntry::Source(_, _) => unreachable!(),
}
}

fn as_module(&self) -> Option<&Module> {
match self {
Self::Module(module) => Some(module),
_ => None,
}
}
}

/// The resulting type of creating an embedded module loader.
#[derive(Debug, Clone)]
#[allow(clippy::module_name_repetitions)]
pub struct EmbeddedModuleLoader {
map: HashMap<JsString, RefCell<EmbeddedModuleEntry>>,
}

impl FromIterator<(JsString, &'static [u8])> for EmbeddedModuleLoader {
fn from_iter<T: IntoIterator<Item = (JsString, &'static [u8])>>(iter: T) -> Self {
Self {
map: iter
.into_iter()
.map(|(path, source)| {
(
path.clone(),
RefCell::new(EmbeddedModuleEntry::from_source(path, source)),
)
})
.collect(),
}
}
}

impl ModuleLoader for EmbeddedModuleLoader {
fn load_imported_module(
&self,
_referrer: Referrer,
specifier: JsString,
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context)>,
context: &mut Context,
) {
let specifier = match normalize_specifier(&specifier) {
Ok(specifier) => specifier,
Err(err) => {
finish_load(Err(err), context);
return;
}
};

if let Some(module) = self.map.get(&specifier) {
let mut embedded = module.borrow_mut();
let module = embedded.cache(context);

finish_load(module.cloned(), context);
} else {
let err = JsNativeError::typ().with_message(format!(
"could not find module `{}`",
specifier.to_std_string_escaped()
));
finish_load(Err(err.into()), context);
}
}

fn get_module(&self, specifier: JsString) -> Option<Module> {
self.map
.get(&specifier)
.and_then(|module| module.borrow().as_module().cloned())
}
}
67 changes: 67 additions & 0 deletions core/interop/tests/embedded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#![allow(unused_crate_dependencies)]

use std::rc::Rc;

use boa_engine::builtins::promise::PromiseState;
use boa_engine::module::ModuleLoader;
use boa_engine::{js_string, Context, JsString, JsValue, Module, Source};
use boa_interop::embed_module;

#[test]
fn simple() {
#[cfg(target_family = "unix")]
let module_loader = Rc::new(embed_module!("tests/embedded/"));
#[cfg(target_family = "windows")]
let module_loader = Rc::new(embed_module!("tests\\embedded\\"));

let mut context = Context::builder()
.module_loader(module_loader.clone())
.build()
.unwrap();

// Resolving modules that exist but haven't been cached yet should return None.
assert_eq!(module_loader.get_module(JsString::from("file1.js")), None);
assert_eq!(
module_loader.get_module(JsString::from("non-existent.js")),
None
);

let module = Module::parse(
Source::from_bytes(b"export { bar } from 'file1.js';"),
None,
&mut context,
)
.expect("failed to parse module");
let promise = module.load_link_evaluate(&mut context);
context.run_jobs();

match promise.state() {
PromiseState::Fulfilled(value) => {
assert!(
value.is_undefined(),
"Expected undefined, got {}",
value.display()
);

let bar = module
.namespace(&mut context)
.get(js_string!("bar"), &mut context)
.unwrap()
.as_callable()
.cloned()
.unwrap();
let value = bar.call(&JsValue::undefined(), &[], &mut context).unwrap();
assert_eq!(
value.as_number(),
Some(6.),
"Expected 6, got {}",
value.display()
);
}
PromiseState::Rejected(err) => panic!(
"promise was not fulfilled: {:?}",
err.to_string(&mut context)
),
PromiseState::Pending => panic!("Promise was not settled"),
}
}
6 changes: 6 additions & 0 deletions core/interop/tests/embedded/dir1/file3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Enable this when https://github.com/boa-dev/boa/pull/3781 is fixed and merged.
// export { foo } from "./file4.js";

export function foo() {
return 3;
}
3 changes: 3 additions & 0 deletions core/interop/tests/embedded/dir1/file4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function foo() {
return 3;
}
6 changes: 6 additions & 0 deletions core/interop/tests/embedded/file1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { foo } from "./file2.js";
import { foo as foo2 } from "./dir1/file3.js";

export function bar() {
return foo() + foo2() + 1;
}
3 changes: 3 additions & 0 deletions core/interop/tests/embedded/file2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function foo() {
return 2;
}
Loading
Loading