1use std::cell::{Cell, RefCell};
25use std::collections::HashSet;
26use std::hash::Hash;
27use std::path::{Path, PathBuf};
28use std::rc::Rc;
29
30use rustc_hash::FxHashSet;
31
32use boa_ast::declaration::ImportAttribute as AstImportAttribute;
33use boa_engine::js_string;
34use boa_engine::property::PropertyKey;
35use boa_engine::value::TryFromJs;
36use boa_gc::{Finalize, Gc, GcRefCell, Trace};
37use boa_interner::Interner;
38use boa_parser::source::ReadChar;
39use boa_parser::{Parser, Source};
40
41pub use loader::*;
42pub use namespace::ModuleNamespace;
43use source::SourceTextModule;
44pub use synthetic::{SyntheticModule, SyntheticModuleInitializer};
45
46use crate::bytecompiler::ToJsString;
47use crate::object::TypedJsFunction;
48use crate::spanned_source_text::SourceText;
49use crate::{
50 Context, HostDefined, JsError, JsNativeError, JsResult, JsString, JsValue, NativeFunction,
51 builtins,
52 builtins::promise::{PromiseCapability, PromiseState},
53 environments::DeclarativeEnvironment,
54 object::{JsObject, JsPromise},
55 realm::Realm,
56};
57
58mod loader;
59mod namespace;
60mod source;
61mod synthetic;
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Trace, Finalize)]
67pub struct ImportAttribute {
68 key: JsString,
69 value: JsString,
70}
71
72impl ImportAttribute {
73 #[must_use]
75 pub fn new(key: JsString, value: JsString) -> Self {
76 Self { key, value }
77 }
78
79 #[must_use]
81 pub fn key(&self) -> &JsString {
82 &self.key
83 }
84
85 #[must_use]
87 pub fn value(&self) -> &JsString {
88 &self.value
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash, Trace, Finalize)]
100pub struct ModuleRequest {
101 specifier: JsString,
102 attributes: Box<[ImportAttribute]>,
103}
104
105impl ModuleRequest {
106 #[must_use]
108 pub fn new(specifier: JsString, mut attributes: Box<[ImportAttribute]>) -> Self {
109 attributes.sort_unstable_by(|k1, k2| k1.key.cmp(&k2.key));
111 Self {
112 specifier,
113 attributes,
114 }
115 }
116
117 #[must_use]
119 pub fn from_specifier(specifier: JsString) -> Self {
120 Self {
121 specifier,
122 attributes: Box::default(),
123 }
124 }
125
126 #[must_use]
128 pub(crate) fn from_ast(
129 specifier: JsString,
130 attributes: &[AstImportAttribute],
131 interner: &Interner,
132 ) -> Self {
133 let attributes = attributes
134 .iter()
135 .map(|attr| {
136 ImportAttribute::new(
137 attr.key().to_js_string(interner),
138 attr.value().to_js_string(interner),
139 )
140 })
141 .collect::<Vec<_>>()
142 .into_boxed_slice();
143 Self::new(specifier, attributes)
144 }
145
146 #[must_use]
148 pub fn specifier(&self) -> &JsString {
149 &self.specifier
150 }
151
152 #[must_use]
154 pub fn attributes(&self) -> &[ImportAttribute] {
155 &self.attributes
156 }
157
158 #[must_use]
160 pub fn get_attribute(&self, key: &str) -> Option<&JsString> {
161 self.attributes
162 .iter()
163 .find(|attr| attr.key == key)
164 .map(|attr| &attr.value)
165 }
166}
167
168#[derive(Clone, Trace, Finalize)]
172pub struct Module {
173 inner: Gc<ModuleRepr>,
174}
175
176impl std::fmt::Debug for Module {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.debug_struct("Module")
179 .field("realm", &self.inner.realm.addr())
180 .field("namespace", &self.inner.namespace)
181 .field("kind", &self.inner.kind)
182 .finish()
183 }
184}
185
186#[derive(Trace, Finalize)]
187struct ModuleRepr {
188 realm: Realm,
189 namespace: GcRefCell<Option<JsObject>>,
190 kind: ModuleKind,
191 host_defined: HostDefined,
192 path: Option<PathBuf>,
193}
194
195#[derive(Debug, Trace, Finalize)]
197pub(crate) enum ModuleKind {
198 SourceText(Box<SourceTextModule>),
200 Synthetic(Box<SyntheticModule>),
202}
203
204impl ModuleKind {
205 pub(crate) fn as_source_text(&self) -> Option<&SourceTextModule> {
207 match self {
208 ModuleKind::SourceText(src) => Some(src),
209 ModuleKind::Synthetic(_) => None,
210 }
211 }
212}
213
214#[derive(Debug, Clone)]
218pub(crate) struct ResolvedBinding {
219 module: Module,
220 binding_name: BindingName,
221}
222
223#[derive(Debug, Clone)]
228pub(crate) enum BindingName {
229 Name(JsString),
231 Namespace,
233}
234
235impl ResolvedBinding {
236 pub(crate) const fn module(&self) -> &Module {
238 &self.module
239 }
240
241 pub(crate) fn into_module(self) -> Module {
243 self.module
244 }
245
246 pub(crate) const fn binding_name(&self) -> &BindingName {
248 &self.binding_name
249 }
250}
251
252#[derive(Debug, Clone)]
253struct GraphLoadingState {
254 capability: PromiseCapability,
255 loading: Cell<bool>,
256 pending_modules: Cell<usize>,
257 visited: RefCell<HashSet<Module>>,
258}
259
260#[derive(Debug, Clone, Copy)]
261pub(crate) enum ResolveExportError {
262 NotFound,
263 Ambiguous,
264}
265
266impl Module {
267 pub fn parse<R: ReadChar>(
273 src: Source<'_, R>,
274 realm: Option<Realm>,
275 context: &mut Context,
276 ) -> JsResult<Self> {
277 let path = src.path().map(Path::to_path_buf);
278 let realm = realm.unwrap_or_else(|| context.realm().clone());
279
280 let mut parser = Parser::new(src);
281 parser.set_identifier(context.next_parser_identifier());
282 let (module, source) =
283 parser.parse_module_with_source(realm.scope(), context.interner_mut())?;
284
285 let source_text = SourceText::new(source);
286 let src = SourceTextModule::new(module, context.interner(), source_text, path.clone());
287
288 Ok(Self {
289 inner: Gc::new(ModuleRepr {
290 realm,
291 namespace: GcRefCell::default(),
292 kind: ModuleKind::SourceText(Box::new(src)),
293 host_defined: HostDefined::default(),
294 path,
295 }),
296 })
297 }
298
299 #[inline]
306 pub fn synthetic(
307 export_names: &[JsString],
308 evaluation_steps: SyntheticModuleInitializer,
309 path: Option<PathBuf>,
310 realm: Option<Realm>,
311 context: &mut Context,
312 ) -> Self {
313 let names = export_names.iter().cloned().collect();
314 let realm = realm.unwrap_or_else(|| context.realm().clone());
315 let synth = SyntheticModule::new(names, evaluation_steps);
316
317 Self {
318 inner: Gc::new(ModuleRepr {
319 realm,
320 namespace: GcRefCell::default(),
321 kind: ModuleKind::Synthetic(Box::new(synth)),
322 host_defined: HostDefined::default(),
323 path,
324 }),
325 }
326 }
327
328 pub fn from_value_as_default(value: JsValue, context: &mut Context) -> Self {
331 Module::synthetic(
332 &[js_string!("default")],
333 SyntheticModuleInitializer::from_copy_closure_with_captures(
334 move |m, value, _ctx| {
335 m.set_export(&js_string!("default"), value.clone())?;
336 Ok(())
337 },
338 value,
339 ),
340 None,
341 None,
342 context,
343 )
344 }
345
346 pub fn parse_json(json: JsString, context: &mut Context) -> JsResult<Self> {
359 let value = builtins::Json::parse(&JsValue::undefined(), &[json.into()], context)?;
360 Ok(Self::from_value_as_default(value, context))
361 }
362
363 #[inline]
365 #[must_use]
366 pub fn realm(&self) -> &Realm {
367 &self.inner.realm
368 }
369
370 #[inline]
374 #[must_use]
375 pub fn host_defined(&self) -> &HostDefined {
376 &self.inner.host_defined
377 }
378
379 pub(crate) fn kind(&self) -> &ModuleKind {
381 &self.inner.kind
382 }
383
384 pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
386 match self.kind() {
387 ModuleKind::SourceText(src) => src.environment(),
388 ModuleKind::Synthetic(syn) => syn.environment(),
389 }
390 }
391
392 #[allow(clippy::missing_panics_doc)]
399 #[inline]
400 pub fn load(&self, context: &mut Context) -> JsPromise {
401 match self.kind() {
402 ModuleKind::SourceText(_) => {
403 let pc = PromiseCapability::new(
410 &context.intrinsics().constructors().promise().constructor(),
411 context,
412 )
413 .expect(
414 "capability creation must always succeed when using the `%Promise%` intrinsic",
415 );
416
417 self.inner_load(
419 &Rc::new(GraphLoadingState {
424 capability: pc.clone(),
425 loading: Cell::new(true),
426 pending_modules: Cell::new(1),
427 visited: RefCell::default(),
428 }),
429 context,
430 );
431
432 JsPromise::from_object(pc.promise().clone())
434 .expect("promise created from the %Promise% intrinsic is always native")
435 }
436 ModuleKind::Synthetic(_) => SyntheticModule::load(context),
437 }
438 }
439
440 fn inner_load(&self, state: &Rc<GraphLoadingState>, context: &mut Context) {
444 assert!(state.loading.get());
446
447 if let ModuleKind::SourceText(src) = self.kind() {
448 src.inner_load(self, state, context);
450 if !state.loading.get() {
451 return;
452 }
453 }
454
455 assert!(state.pending_modules.get() >= 1);
457
458 state.pending_modules.set(state.pending_modules.get() - 1);
460 if state.pending_modules.get() == 0 {
463 state.loading.set(false);
465 state
471 .capability
472 .resolve()
473 .call(&JsValue::undefined(), &[], context)
474 .expect("marking a module as loaded should not fail");
475 }
476 }
478
479 fn get_exported_names(
489 &self,
490 export_star_set: &mut Vec<Module>,
491 interner: &Interner,
492 ) -> FxHashSet<JsString> {
493 match self.kind() {
494 ModuleKind::SourceText(src) => src.get_exported_names(self, export_star_set, interner),
495 ModuleKind::Synthetic(synth) => synth.get_exported_names(),
496 }
497 }
498
499 #[allow(clippy::mutable_key_type)]
511 pub(crate) fn resolve_export(
512 &self,
513 export_name: &JsString,
514 resolve_set: &mut FxHashSet<(Self, JsString)>,
515 interner: &Interner,
516 ) -> Result<ResolvedBinding, ResolveExportError> {
517 match self.kind() {
518 ModuleKind::SourceText(src) => {
519 src.resolve_export(self, export_name, resolve_set, interner)
520 }
521 ModuleKind::Synthetic(synth) => synth.resolve_export(self, export_name),
522 }
523 }
524
525 #[allow(clippy::missing_panics_doc)]
536 #[inline]
537 pub fn link(&self, context: &mut Context) -> JsResult<()> {
538 match self.kind() {
539 ModuleKind::SourceText(src) => src.link(self, context),
540 ModuleKind::Synthetic(synth) => {
541 synth.link(self, context);
542 Ok(())
543 }
544 }
545 }
546
547 fn inner_link(
551 &self,
552 stack: &mut Vec<Module>,
553 index: usize,
554 context: &mut Context,
555 ) -> JsResult<usize> {
556 match self.kind() {
557 ModuleKind::SourceText(src) => src.inner_link(self, stack, index, context),
558 ModuleKind::Synthetic(synth) => {
560 synth.link(self, context);
562 Ok(index)
564 }
565 }
566 }
567
568 #[inline]
581 pub fn evaluate(&self, context: &mut Context) -> JsResult<JsPromise> {
582 match self.kind() {
583 ModuleKind::SourceText(src) => src.evaluate(self, context),
584 ModuleKind::Synthetic(synth) => synth.evaluate(self, context),
585 }
586 }
587
588 fn inner_evaluate(
592 &self,
593 stack: &mut Vec<Module>,
594 index: usize,
595 context: &mut Context,
596 ) -> JsResult<usize> {
597 match self.kind() {
598 ModuleKind::SourceText(src) => src.inner_evaluate(self, stack, index, None, context),
599 ModuleKind::Synthetic(synth) => {
601 let promise: JsPromise = synth.evaluate(self, context)?;
603 let state = promise.state();
604 match state {
605 PromiseState::Pending => {
606 unreachable!("b. Assert: promise.[[PromiseState]] is not pending.")
607 }
608 PromiseState::Fulfilled(_) => Ok(index),
610 PromiseState::Rejected(err) => Err(JsError::from_opaque(err)),
613 }
614 }
615 }
616 }
617
618 #[allow(dropping_copy_types)]
649 #[inline]
650 pub fn load_link_evaluate(&self, context: &mut Context) -> JsPromise {
651 self.load(context)
652 .then(
653 Some(
654 NativeFunction::from_copy_closure_with_captures(
655 |_, _, module, context| {
656 module.link(context)?;
657 Ok(JsValue::undefined())
658 },
659 self.clone(),
660 )
661 .to_js_function(context.realm()),
662 ),
663 None,
664 context,
665 )
666 .expect("`then` cannot fail for a native `JsPromise`")
667 .then(
668 Some(
669 NativeFunction::from_copy_closure_with_captures(
670 |_, _, module, context| Ok(module.evaluate(context)?.into()),
671 self.clone(),
672 )
673 .to_js_function(context.realm()),
674 ),
675 None,
676 context,
677 )
678 .expect("`then` cannot fail for a native `JsPromise`")
679 }
680
681 pub fn namespace(&self, context: &mut Context) -> JsObject {
688 self.inner
693 .namespace
694 .borrow_mut()
695 .get_or_insert_with(|| {
696 let exported_names =
698 self.get_exported_names(&mut Vec::default(), context.interner());
699
700 let unambiguous_names = exported_names
702 .into_iter()
703 .filter_map(|name| {
705 self.resolve_export(&name, &mut HashSet::default(), context.interner())
708 .ok()
709 .map(|_| name)
710 })
711 .collect();
712
713 ModuleNamespace::create(self.clone(), unambiguous_names, context)
715 })
716 .clone()
717 }
718
719 #[inline]
721 pub fn get_value<K>(&self, name: K, context: &mut Context) -> JsResult<JsValue>
722 where
723 K: Into<PropertyKey>,
724 {
725 let namespace = self.namespace(context);
726 namespace.get(name, context)
727 }
728
729 #[inline]
731 #[allow(clippy::needless_pass_by_value)]
732 pub fn get_typed_fn<A, R>(
733 &self,
734 name: JsString,
735 context: &mut Context,
736 ) -> JsResult<TypedJsFunction<A, R>>
737 where
738 A: crate::object::TryIntoJsArguments,
739 R: TryFromJs,
740 {
741 let func = self.get_value(name.clone(), context)?;
742 let func = func.as_function().ok_or_else(|| {
743 JsNativeError::typ().with_message(format!("{name:?} is not a function"))
744 })?;
745 Ok(func.typed())
746 }
747
748 #[must_use]
750 pub fn path(&self) -> Option<&Path> {
751 self.inner.path.as_deref()
752 }
753}
754
755impl PartialEq for Module {
756 #[inline]
757 fn eq(&self, other: &Self) -> bool {
758 Gc::ptr_eq(&self.inner, &other.inner)
759 }
760}
761
762impl Eq for Module {}
763
764impl Hash for Module {
765 #[inline]
766 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
767 std::ptr::hash(self.inner.as_ref(), state);
768 }
769}
770
771pub trait IntoJsModule {
773 fn into_js_module(self, context: &mut Context) -> Module;
775}
776
777impl<T: IntoIterator<Item = (JsString, NativeFunction)> + Clone> IntoJsModule for T {
778 fn into_js_module(self, context: &mut Context) -> Module {
779 let (names, fns): (Vec<_>, Vec<_>) = self.into_iter().unzip();
780 let exports = names.clone();
781
782 Module::synthetic(
783 exports.as_slice(),
784 unsafe {
785 SyntheticModuleInitializer::from_closure(move |module, context| {
786 for (name, f) in names.iter().zip(fns.iter()) {
787 module
788 .set_export(name, f.clone().to_js_function(context.realm()).into())?;
789 }
790 Ok(())
791 })
792 },
793 None,
794 None,
795 context,
796 )
797 }
798}
799
800#[test]
801#[allow(clippy::missing_panics_doc)]
802fn into_js_module() {
803 use boa_engine::interop::{ContextData, JsRest};
804 use boa_engine::{
805 Context, IntoJsFunctionCopied, JsValue, Module, Source, UnsafeIntoJsFunction, js_string,
806 };
807 use boa_gc::{Gc, GcRefCell};
808 use std::cell::RefCell;
809 use std::rc::Rc;
810
811 type ResultType = Gc<GcRefCell<JsValue>>;
812
813 let loader = Rc::new(MapModuleLoader::default());
814 let mut context = Context::builder()
815 .module_loader(loader.clone())
816 .build()
817 .unwrap();
818
819 let foo_count = Rc::new(RefCell::new(0));
820 let bar_count = Rc::new(RefCell::new(0));
821 let dad_count = Rc::new(RefCell::new(0));
822
823 context.insert_data(Gc::new(GcRefCell::new(JsValue::undefined())));
824
825 let module = unsafe {
826 vec![
827 (
828 js_string!("foo"),
829 {
830 let counter = foo_count.clone();
831 move || {
832 *counter.borrow_mut() += 1;
833
834 *counter.borrow()
835 }
836 }
837 .into_js_function_unsafe(&mut context),
838 ),
839 (
840 js_string!("bar"),
841 UnsafeIntoJsFunction::into_js_function_unsafe(
842 {
843 let counter = bar_count.clone();
844 move |i: i32| {
845 *counter.borrow_mut() += i;
846 }
847 },
848 &mut context,
849 ),
850 ),
851 (
852 js_string!("dad"),
853 UnsafeIntoJsFunction::into_js_function_unsafe(
854 {
855 let counter = dad_count.clone();
856 move |args: JsRest<'_>, context: &mut Context| {
857 *counter.borrow_mut() += args
858 .into_iter()
859 .map(|i| i.try_js_into::<i32>(context).unwrap())
860 .sum::<i32>();
861 }
862 },
863 &mut context,
864 ),
865 ),
866 (
867 js_string!("send"),
868 (move |value: JsValue, ContextData(result): ContextData<ResultType>| {
869 *result.borrow_mut() = value;
870 })
871 .into_js_function_copied(&mut context),
872 ),
873 ]
874 }
875 .into_js_module(&mut context);
876
877 loader.insert("test", module);
878
879 let source = Source::from_bytes(
880 r"
881 import * as test from 'test';
882 let result = test.foo();
883 test.foo();
884 for (let i = 1; i <= 5; i++) {
885 test.bar(i);
886 }
887 for (let i = 1; i < 5; i++) {
888 test.dad(1, 2, 3);
889 }
890
891 test.send(result);
892 ",
893 );
894 let root_module = Module::parse(source, None, &mut context).unwrap();
895
896 let promise_result = root_module.load_link_evaluate(&mut context);
897 context.run_jobs().unwrap();
898
899 assert!(
901 promise_result.state().as_fulfilled().is_some(),
902 "module didn't execute successfully! Promise: {:?}",
903 promise_result.state()
904 );
905
906 let result = context.get_data::<ResultType>().unwrap().borrow().clone();
907
908 assert_eq!(*foo_count.borrow(), 2);
909 assert_eq!(*bar_count.borrow(), 15);
910 assert_eq!(*dad_count.borrow(), 24);
911 assert_eq!(result.try_js_into(&mut context), Ok(1u32));
912}
913
914#[test]
915fn can_throw_exception() {
916 use boa_engine::{
917 Context, IntoJsFunctionCopied, JsError, JsResult, JsValue, Module, Source, js_string,
918 };
919 use std::rc::Rc;
920
921 let loader = Rc::new(MapModuleLoader::default());
922 let mut context = Context::builder()
923 .module_loader(loader.clone())
924 .build()
925 .unwrap();
926
927 let module = vec![(
928 js_string!("doTheThrow"),
929 IntoJsFunctionCopied::into_js_function_copied(
930 |message: JsValue| -> JsResult<()> { Err(JsError::from_opaque(message)) },
931 &mut context,
932 ),
933 )]
934 .into_js_module(&mut context);
935
936 loader.insert("test", module);
937
938 let source = Source::from_bytes(
939 r"
940 import * as test from 'test';
941 try {
942 test.doTheThrow('javascript');
943 } catch(e) {
944 throw 'from ' + e;
945 }
946 ",
947 );
948 let root_module = Module::parse(source, None, &mut context).unwrap();
949
950 let promise_result = root_module.load_link_evaluate(&mut context);
951 context.run_jobs().unwrap();
952
953 assert_eq!(
955 promise_result.state().as_rejected(),
956 Some(&js_string!("from javascript").into())
957 );
958}
959
960#[test]
961fn test_module_request_attribute_sorting() {
962 let request1 = ModuleRequest::new(
963 js_string!("specifier"),
964 Box::new([
965 ImportAttribute::new(js_string!("key2"), js_string!("val2")),
966 ImportAttribute::new(js_string!("key1"), js_string!("val1")),
967 ]),
968 );
969
970 let request2 = ModuleRequest::new(
971 js_string!("specifier"),
972 Box::new([
973 ImportAttribute::new(js_string!("key1"), js_string!("val1")),
974 ImportAttribute::new(js_string!("key2"), js_string!("val2")),
975 ]),
976 );
977
978 assert_eq!(request1, request2);
979 assert_eq!(request1.attributes()[0].key(), &js_string!("key1"));
980 assert_eq!(request1.attributes()[1].key(), &js_string!("key2"));
981}