use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use rustc_hash::FxHashSet;
use boa_engine::js_string;
use boa_engine::property::PropertyKey;
use boa_engine::value::TryFromJs;
use boa_gc::{Finalize, Gc, GcRefCell, Trace};
use boa_interner::Interner;
use boa_parser::source::ReadChar;
use boa_parser::{Parser, Source};
pub use loader::*;
pub use namespace::ModuleNamespace;
use source::SourceTextModule;
pub use synthetic::{SyntheticModule, SyntheticModuleInitializer};
use crate::object::TypedJsFunction;
use crate::spanned_source_text::SourceText;
use crate::{
Context, HostDefined, JsError, JsNativeError, JsResult, JsString, JsValue, NativeFunction,
builtins,
builtins::promise::{PromiseCapability, PromiseState},
environments::DeclarativeEnvironment,
object::{JsObject, JsPromise},
realm::Realm,
};
mod loader;
mod namespace;
mod source;
mod synthetic;
#[derive(Clone, Trace, Finalize)]
pub struct Module {
inner: Gc<ModuleRepr>,
}
impl std::fmt::Debug for Module {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Module")
.field("realm", &self.inner.realm.addr())
.field("namespace", &self.inner.namespace)
.field("kind", &self.inner.kind)
.finish()
}
}
#[derive(Trace, Finalize)]
struct ModuleRepr {
realm: Realm,
namespace: GcRefCell<Option<JsObject>>,
kind: ModuleKind,
host_defined: HostDefined,
path: Option<PathBuf>,
}
#[derive(Debug, Trace, Finalize)]
pub(crate) enum ModuleKind {
SourceText(Box<SourceTextModule>),
Synthetic(Box<SyntheticModule>),
}
impl ModuleKind {
pub(crate) fn as_source_text(&self) -> Option<&SourceTextModule> {
match self {
ModuleKind::SourceText(src) => Some(src),
ModuleKind::Synthetic(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedBinding {
module: Module,
binding_name: BindingName,
}
#[derive(Debug, Clone)]
pub(crate) enum BindingName {
Name(JsString),
Namespace,
}
impl ResolvedBinding {
pub(crate) const fn module(&self) -> &Module {
&self.module
}
pub(crate) fn binding_name(&self) -> BindingName {
self.binding_name.clone()
}
}
#[derive(Debug, Clone)]
struct GraphLoadingState {
capability: PromiseCapability,
loading: Cell<bool>,
pending_modules: Cell<usize>,
visited: RefCell<HashSet<Module>>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum ResolveExportError {
NotFound,
Ambiguous,
}
impl Module {
pub fn parse<R: ReadChar>(
src: Source<'_, R>,
realm: Option<Realm>,
context: &mut Context,
) -> JsResult<Self> {
let path = src.path().map(Path::to_path_buf);
let realm = realm.unwrap_or_else(|| context.realm().clone());
let mut parser = Parser::new(src);
parser.set_identifier(context.next_parser_identifier());
let (module, source) =
parser.parse_module_with_source(realm.scope(), context.interner_mut())?;
let source_text = SourceText::new(source);
let src = SourceTextModule::new(module, context.interner(), source_text, path.clone());
Ok(Self {
inner: Gc::new(ModuleRepr {
realm,
namespace: GcRefCell::default(),
kind: ModuleKind::SourceText(Box::new(src)),
host_defined: HostDefined::default(),
path,
}),
})
}
#[inline]
pub fn synthetic(
export_names: &[JsString],
evaluation_steps: SyntheticModuleInitializer,
path: Option<PathBuf>,
realm: Option<Realm>,
context: &mut Context,
) -> Self {
let names = export_names.iter().cloned().collect();
let realm = realm.unwrap_or_else(|| context.realm().clone());
let synth = SyntheticModule::new(names, evaluation_steps);
Self {
inner: Gc::new(ModuleRepr {
realm,
namespace: GcRefCell::default(),
kind: ModuleKind::Synthetic(Box::new(synth)),
host_defined: HostDefined::default(),
path,
}),
}
}
pub fn from_value_as_default(value: JsValue, context: &mut Context) -> Self {
Module::synthetic(
&[js_string!("default")],
SyntheticModuleInitializer::from_copy_closure_with_captures(
move |m, value, _ctx| {
m.set_export(&js_string!("default"), value.clone())?;
Ok(())
},
value,
),
None,
None,
context,
)
}
pub fn parse_json(json: JsString, context: &mut Context) -> JsResult<Self> {
let value = builtins::Json::parse(&JsValue::undefined(), &[json.into()], context)?;
Ok(Self::from_value_as_default(value, context))
}
#[inline]
#[must_use]
pub fn realm(&self) -> &Realm {
&self.inner.realm
}
#[inline]
#[must_use]
pub fn host_defined(&self) -> &HostDefined {
&self.inner.host_defined
}
pub(crate) fn kind(&self) -> &ModuleKind {
&self.inner.kind
}
pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
match self.kind() {
ModuleKind::SourceText(src) => src.environment(),
ModuleKind::Synthetic(syn) => syn.environment(),
}
}
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn load(&self, context: &mut Context) -> JsPromise {
match self.kind() {
ModuleKind::SourceText(_) => {
let pc = PromiseCapability::new(
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect(
"capability creation must always succeed when using the `%Promise%` intrinsic",
);
self.inner_load(
&Rc::new(GraphLoadingState {
capability: pc.clone(),
loading: Cell::new(true),
pending_modules: Cell::new(1),
visited: RefCell::default(),
}),
context,
);
JsPromise::from_object(pc.promise().clone())
.expect("promise created from the %Promise% intrinsic is always native")
}
ModuleKind::Synthetic(_) => SyntheticModule::load(context),
}
}
fn inner_load(&self, state: &Rc<GraphLoadingState>, context: &mut Context) {
assert!(state.loading.get());
if let ModuleKind::SourceText(src) = self.kind() {
src.inner_load(self, state, context);
if !state.loading.get() {
return;
}
}
assert!(state.pending_modules.get() >= 1);
state.pending_modules.set(state.pending_modules.get() - 1);
if state.pending_modules.get() == 0 {
state.loading.set(false);
state
.capability
.resolve()
.call(&JsValue::undefined(), &[], context)
.expect("marking a module as loaded should not fail");
}
}
fn get_exported_names(
&self,
export_star_set: &mut Vec<Module>,
interner: &Interner,
) -> FxHashSet<JsString> {
match self.kind() {
ModuleKind::SourceText(src) => src.get_exported_names(self, export_star_set, interner),
ModuleKind::Synthetic(synth) => synth.get_exported_names(),
}
}
#[allow(clippy::mutable_key_type)]
pub(crate) fn resolve_export(
&self,
export_name: JsString,
resolve_set: &mut FxHashSet<(Self, JsString)>,
interner: &Interner,
) -> Result<ResolvedBinding, ResolveExportError> {
match self.kind() {
ModuleKind::SourceText(src) => {
src.resolve_export(self, &export_name, resolve_set, interner)
}
ModuleKind::Synthetic(synth) => synth.resolve_export(self, export_name),
}
}
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn link(&self, context: &mut Context) -> JsResult<()> {
match self.kind() {
ModuleKind::SourceText(src) => src.link(self, context),
ModuleKind::Synthetic(synth) => {
synth.link(self, context);
Ok(())
}
}
}
fn inner_link(
&self,
stack: &mut Vec<Module>,
index: usize,
context: &mut Context,
) -> JsResult<usize> {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_link(self, stack, index, context),
ModuleKind::Synthetic(synth) => {
synth.link(self, context);
Ok(index)
}
}
}
#[inline]
pub fn evaluate(&self, context: &mut Context) -> JsPromise {
match self.kind() {
ModuleKind::SourceText(src) => src.evaluate(self, context),
ModuleKind::Synthetic(synth) => synth.evaluate(self, context),
}
}
fn inner_evaluate(
&self,
stack: &mut Vec<Module>,
index: usize,
context: &mut Context,
) -> JsResult<usize> {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_evaluate(self, stack, index, None, context),
ModuleKind::Synthetic(synth) => {
let promise: JsPromise = synth.evaluate(self, context);
let state = promise.state();
match state {
PromiseState::Pending => {
unreachable!("b. Assert: promise.[[PromiseState]] is not pending.")
}
PromiseState::Fulfilled(_) => Ok(index),
PromiseState::Rejected(err) => Err(JsError::from_opaque(err)),
}
}
}
}
#[allow(dropping_copy_types)]
#[inline]
pub fn load_link_evaluate(&self, context: &mut Context) -> JsPromise {
self.load(context)
.then(
Some(
NativeFunction::from_copy_closure_with_captures(
|_, _, module, context| {
module.link(context)?;
Ok(JsValue::undefined())
},
self.clone(),
)
.to_js_function(context.realm()),
),
None,
context,
)
.then(
Some(
NativeFunction::from_copy_closure_with_captures(
|_, _, module, context| Ok(module.evaluate(context).into()),
self.clone(),
)
.to_js_function(context.realm()),
),
None,
context,
)
}
pub fn namespace(&self, context: &mut Context) -> JsObject {
self.inner
.namespace
.borrow_mut()
.get_or_insert_with(|| {
let exported_names =
self.get_exported_names(&mut Vec::default(), context.interner());
let unambiguous_names = exported_names
.into_iter()
.filter_map(|name| {
self.resolve_export(
name.clone(),
&mut HashSet::default(),
context.interner(),
)
.ok()
.map(|_| name)
})
.collect();
ModuleNamespace::create(self.clone(), unambiguous_names, context)
})
.clone()
}
#[inline]
pub fn get_value<K>(&self, name: K, context: &mut Context) -> JsResult<JsValue>
where
K: Into<PropertyKey>,
{
let namespace = self.namespace(context);
namespace.get(name, context)
}
#[inline]
#[allow(clippy::needless_pass_by_value)]
pub fn get_typed_fn<A, R>(
&self,
name: JsString,
context: &mut Context,
) -> JsResult<TypedJsFunction<A, R>>
where
A: crate::object::TryIntoJsArguments,
R: TryFromJs,
{
let func = self.get_value(name.clone(), context)?;
let func = func.as_function().ok_or_else(|| {
JsNativeError::typ().with_message(format!("{name:?} is not a function"))
})?;
Ok(func.typed())
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
self.inner.path.as_deref()
}
}
impl PartialEq for Module {
#[inline]
fn eq(&self, other: &Self) -> bool {
Gc::ptr_eq(&self.inner, &other.inner)
}
}
impl Eq for Module {}
impl Hash for Module {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(self.inner.as_ref(), state);
}
}
pub trait IntoJsModule {
fn into_js_module(self, context: &mut Context) -> Module;
}
impl<T: IntoIterator<Item = (JsString, NativeFunction)> + Clone> IntoJsModule for T {
fn into_js_module(self, context: &mut Context) -> Module {
let (names, fns): (Vec<_>, Vec<_>) = self.into_iter().unzip();
let exports = names.clone();
Module::synthetic(
exports.as_slice(),
unsafe {
SyntheticModuleInitializer::from_closure(move |module, context| {
for (name, f) in names.iter().zip(fns.iter()) {
module
.set_export(name, f.clone().to_js_function(context.realm()).into())?;
}
Ok(())
})
},
None,
None,
context,
)
}
}
#[test]
#[allow(clippy::missing_panics_doc)]
fn into_js_module() {
use boa_engine::interop::{ContextData, JsRest};
use boa_engine::{
Context, IntoJsFunctionCopied, JsValue, Module, Source, UnsafeIntoJsFunction, js_string,
};
use boa_gc::{Gc, GcRefCell};
use std::cell::RefCell;
use std::rc::Rc;
type ResultType = Gc<GcRefCell<JsValue>>;
let loader = Rc::new(MapModuleLoader::default());
let mut context = Context::builder()
.module_loader(loader.clone())
.build()
.unwrap();
let foo_count = Rc::new(RefCell::new(0));
let bar_count = Rc::new(RefCell::new(0));
let dad_count = Rc::new(RefCell::new(0));
context.insert_data(Gc::new(GcRefCell::new(JsValue::undefined())));
let module = unsafe {
vec![
(
js_string!("foo"),
{
let counter = foo_count.clone();
move || {
*counter.borrow_mut() += 1;
*counter.borrow()
}
}
.into_js_function_unsafe(&mut context),
),
(
js_string!("bar"),
UnsafeIntoJsFunction::into_js_function_unsafe(
{
let counter = bar_count.clone();
move |i: i32| {
*counter.borrow_mut() += i;
}
},
&mut context,
),
),
(
js_string!("dad"),
UnsafeIntoJsFunction::into_js_function_unsafe(
{
let counter = dad_count.clone();
move |args: JsRest<'_>, context: &mut Context| {
*counter.borrow_mut() += args
.into_iter()
.map(|i| i.try_js_into::<i32>(context).unwrap())
.sum::<i32>();
}
},
&mut context,
),
),
(
js_string!("send"),
(move |value: JsValue, ContextData(result): ContextData<ResultType>| {
*result.borrow_mut() = value;
})
.into_js_function_copied(&mut context),
),
]
}
.into_js_module(&mut context);
loader.insert("test", module);
let source = Source::from_bytes(
r"
import * as test from 'test';
let result = test.foo();
test.foo();
for (let i = 1; i <= 5; i++) {
test.bar(i);
}
for (let i = 1; i < 5; i++) {
test.dad(1, 2, 3);
}
test.send(result);
",
);
let root_module = Module::parse(source, None, &mut context).unwrap();
let promise_result = root_module.load_link_evaluate(&mut context);
context.run_jobs().unwrap();
assert!(
promise_result.state().as_fulfilled().is_some(),
"module didn't execute successfully! Promise: {:?}",
promise_result.state()
);
let result = context.get_data::<ResultType>().unwrap().borrow().clone();
assert_eq!(*foo_count.borrow(), 2);
assert_eq!(*bar_count.borrow(), 15);
assert_eq!(*dad_count.borrow(), 24);
assert_eq!(result.try_js_into(&mut context), Ok(1u32));
}
#[test]
fn can_throw_exception() {
use boa_engine::{
Context, IntoJsFunctionCopied, JsError, JsResult, JsValue, Module, Source, js_string,
};
use std::rc::Rc;
let loader = Rc::new(MapModuleLoader::default());
let mut context = Context::builder()
.module_loader(loader.clone())
.build()
.unwrap();
let module = vec![(
js_string!("doTheThrow"),
IntoJsFunctionCopied::into_js_function_copied(
|message: JsValue| -> JsResult<()> { Err(JsError::from_opaque(message)) },
&mut context,
),
)]
.into_js_module(&mut context);
loader.insert("test", module);
let source = Source::from_bytes(
r"
import * as test from 'test';
try {
test.doTheThrow('javascript');
} catch(e) {
throw 'from ' + e;
}
",
);
let root_module = Module::parse(source, None, &mut context).unwrap();
let promise_result = root_module.load_link_evaluate(&mut context);
context.run_jobs().unwrap();
assert_eq!(
promise_result.state().as_rejected(),
Some(&js_string!("from javascript").into())
);
}