mirror of https://github.com/boa-dev/boa.git
Browse Source
* Add an embed_module!() macro to boa_interop The macro creates a ModuleLoader that includes all JS files from a directory. * Add description of function * Run prettier * Remove reference to a unaccessible crate * Remove one more reference to a unaccessible crate * Disable test that plays with paths on Windows * Block the whole test module instead of just the fn * This is a bit insane * Replace path separators into JavaScript specifier separators * cargo fmt * cargo fmt part deux * fix some issues with relative path and pathing on windows * fix module resolver when there are no base path * use the platform's path separator * cargo fmt * prettier * Remove caching of the error * Pedantic clippy gonna pedantpull/3820/head
Hans Larsen
8 months ago
committed by
GitHub
13 changed files with 393 additions and 5 deletions
@ -0,0 +1,139 @@
|
||||
//! 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, JsNativeError, JsResult, JsString, Module, Source}; |
||||
|
||||
/// 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), |
||||
} |
||||
|
||||
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) => { |
||||
return Err(err); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
match self { |
||||
Self::Module(module) => Ok(module), |
||||
EmbeddedModuleEntry::Source(_, _) => unreachable!(), |
||||
} |
||||
} |
||||
|
||||
fn as_module(&self) -> Option<&Module> { |
||||
match self { |
||||
Self::Module(module) => Some(module), |
||||
Self::Source(_, _) => 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<(&'static str, &'static [u8])> for EmbeddedModuleLoader { |
||||
fn from_iter<T: IntoIterator<Item = (&'static str, &'static [u8])>>(iter: T) -> Self { |
||||
Self { |
||||
map: iter |
||||
.into_iter() |
||||
.map(|(path, source)| { |
||||
let p = JsString::from(path); |
||||
( |
||||
p.clone(), |
||||
RefCell::new(EmbeddedModuleEntry::from_source(p, 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 Ok(specifier_path) = boa_engine::module::resolve_module_specifier( |
||||
None, |
||||
&specifier, |
||||
referrer.path(), |
||||
context, |
||||
) else { |
||||
let err = JsNativeError::typ().with_message(format!( |
||||
"could not resolve module specifier `{}`", |
||||
specifier.to_std_string_escaped() |
||||
)); |
||||
finish_load(Err(err.into()), context); |
||||
return; |
||||
}; |
||||
|
||||
if let Some(module) = self |
||||
.map |
||||
.get(&JsString::from(specifier_path.to_string_lossy().as_ref())) |
||||
{ |
||||
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()) |
||||
} |
||||
} |
@ -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"), |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
// Enable this when https://github.com/boa-dev/boa/pull/3781 is fixed and merged.
|
||||
export { foo } from "./file4.js"; |
@ -0,0 +1,3 @@
|
||||
export function foo() { |
||||
return 3; |
||||
} |
@ -0,0 +1,6 @@
|
||||
import { foo } from "./file2.js"; |
||||
import { foo as foo2 } from "./dir1/file3.js"; |
||||
|
||||
export function bar() { |
||||
return foo() + foo2() + 1; |
||||
} |
@ -0,0 +1,3 @@
|
||||
export function foo() { |
||||
return 2; |
||||
} |
@ -0,0 +1,118 @@
|
||||
//! Embedded module loader. Creates a `ModuleLoader` instance that contains
|
||||
//! files embedded in the binary at build time.
|
||||
|
||||
use proc_macro::TokenStream; |
||||
use std::path::PathBuf; |
||||
|
||||
use quote::quote; |
||||
use syn::{parse::Parse, LitInt, LitStr, Token}; |
||||
|
||||
struct EmbedModuleMacroInput { |
||||
path: LitStr, |
||||
max_size: u64, |
||||
} |
||||
|
||||
impl Parse for EmbedModuleMacroInput { |
||||
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> { |
||||
let path = input.parse()?; |
||||
let _comma: Token![,] = input.parse()?; |
||||
let max_size = input.parse::<LitInt>()?.base10_parse()?; |
||||
|
||||
Ok(Self { path, max_size }) |
||||
} |
||||
} |
||||
|
||||
/// List all the files readable from the given directory, recursively.
|
||||
fn find_all_files(dir: &mut std::fs::ReadDir, root: &PathBuf) -> Vec<PathBuf> { |
||||
let mut files = Vec::new(); |
||||
for entry in dir { |
||||
let Ok(entry) = entry else { |
||||
continue; |
||||
}; |
||||
|
||||
let path = entry.path(); |
||||
if path.is_dir() { |
||||
let Ok(mut sub_dir) = std::fs::read_dir(&path) else { |
||||
continue; |
||||
}; |
||||
files.append(&mut find_all_files(&mut sub_dir, root)); |
||||
} else if let Ok(path) = path.strip_prefix(root) { |
||||
files.push(path.to_path_buf()); |
||||
} |
||||
} |
||||
files |
||||
} |
||||
|
||||
/// Implementation of the `embed_module_inner!` macro.
|
||||
/// This should not be used directly. Use the `embed_module!` macro from the `boa_interop`
|
||||
/// crate instead.
|
||||
pub(crate) fn embed_module_impl(input: TokenStream) -> TokenStream { |
||||
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default()); |
||||
|
||||
let input = syn::parse_macro_input!(input as EmbedModuleMacroInput); |
||||
|
||||
let root = manifest_dir.join(input.path.value()); |
||||
let max_size = input.max_size; |
||||
|
||||
let mut dir = match std::fs::read_dir(root.clone()) { |
||||
Ok(dir) => dir, |
||||
Err(e) => { |
||||
return syn::Error::new_spanned( |
||||
input.path.clone(), |
||||
format!("Error reading directory: {e}"), |
||||
) |
||||
.to_compile_error() |
||||
.into(); |
||||
} |
||||
}; |
||||
|
||||
let mut total = 0; |
||||
let files = find_all_files(&mut dir, &root); |
||||
|
||||
let inner = match files.into_iter().try_fold(quote! {}, |acc, relative_path| { |
||||
let path = root.join(&relative_path); |
||||
let absolute_path = manifest_dir.join(&path).to_string_lossy().to_string(); |
||||
let Some(relative_path) = relative_path.to_str() else { |
||||
return Err(syn::Error::new_spanned( |
||||
input.path.clone(), |
||||
"Path has non-Unicode characters", |
||||
)); |
||||
}; |
||||
let relative_path = format!("{}{}", std::path::MAIN_SEPARATOR, relative_path); |
||||
|
||||
// Check the size.
|
||||
let size = std::fs::metadata(&path) |
||||
.map_err(|e| { |
||||
syn::Error::new_spanned(input.path.clone(), format!("Error reading file size: {e}")) |
||||
})? |
||||
.len(); |
||||
|
||||
total += size; |
||||
if total > max_size { |
||||
return Err(syn::Error::new_spanned( |
||||
input.path.clone(), |
||||
"Total embedded file size exceeds the maximum size", |
||||
)); |
||||
} |
||||
|
||||
Ok(quote! { |
||||
#acc |
||||
|
||||
( |
||||
#relative_path, |
||||
include_bytes!(#absolute_path).as_ref(), |
||||
), |
||||
}) |
||||
}) { |
||||
Ok(inner) => inner, |
||||
Err(e) => return e.to_compile_error().into(), |
||||
}; |
||||
|
||||
let stream = quote! { |
||||
[ |
||||
#inner |
||||
] |
||||
}; |
||||
|
||||
stream.into() |
||||
} |
Loading…
Reference in new issue