Skip to main content

boa_engine/module/
mod.rs

1//! Boa's implementation of the ECMAScript's module system.
2//!
3//! This module contains the [`Module`] type, which represents an [**Abstract Module Record**][module],
4//! a [`ModuleLoader`] trait for custom module loader implementations, and [`SimpleModuleLoader`],
5//! the default `ModuleLoader` for [`Context`] which can be used for most simple usecases.
6//!
7//! Every module roughly follows the same lifecycle:
8//! - Parse using [`Module::parse`].
9//! - Load all its dependencies using [`Module::load`].
10//! - Link its dependencies together using [`Module::link`].
11//! - Evaluate the module and its dependencies using [`Module::evaluate`].
12//!
13//! The [`ModuleLoader`] trait allows customizing the "load" step on the lifecycle
14//! of a module, which allows doing things like fetching modules from urls, having multiple
15//! "modpaths" from where to import modules, or using Rust futures to avoid blocking the main thread
16//! on loads.
17//!
18//! More information:
19//!  - [ECMAScript reference][spec]
20//!
21//! [spec]: https://tc39.es/ecma262/#sec-modules
22//! [module]: https://tc39.es/ecma262/#sec-abstract-module-records
23
24use 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/// Import attribute.
64///
65/// [spec]: https://tc39.es/ecma262/#table-importattribute-fields
66#[derive(Debug, Clone, PartialEq, Eq, Hash, Trace, Finalize)]
67pub struct ImportAttribute {
68    key: JsString,
69    value: JsString,
70}
71
72impl ImportAttribute {
73    /// Creates a new import attribute.
74    #[must_use]
75    pub fn new(key: JsString, value: JsString) -> Self {
76        Self { key, value }
77    }
78
79    /// Gets the attribute key.
80    #[must_use]
81    pub fn key(&self) -> &JsString {
82        &self.key
83    }
84
85    /// Gets the attribute value.
86    #[must_use]
87    pub fn value(&self) -> &JsString {
88        &self.value
89    }
90}
91
92/// A module request with optional import attributes.
93///
94/// Represents a module specifier and its associated import attributes.
95/// According to the [ECMAScript specification][spec], the module cache key
96/// should be (referrer, specifier, attributes).
97///
98/// [spec]: https://tc39.es/ecma262/#sec-modulerequest-record
99#[derive(Debug, Clone, PartialEq, Eq, Hash, Trace, Finalize)]
100pub struct ModuleRequest {
101    specifier: JsString,
102    attributes: Box<[ImportAttribute]>,
103}
104
105impl ModuleRequest {
106    /// Creates a new module request from a specifier and attributes.
107    #[must_use]
108    pub fn new(specifier: JsString, mut attributes: Box<[ImportAttribute]>) -> Self {
109        // Sort attributes by key to ensure canonical cache keys.
110        attributes.sort_unstable_by(|k1, k2| k1.key.cmp(&k2.key));
111        Self {
112            specifier,
113            attributes,
114        }
115    }
116
117    /// Creates a new module request from only a specifier with no attributes.
118    #[must_use]
119    pub fn from_specifier(specifier: JsString) -> Self {
120        Self {
121            specifier,
122            attributes: Box::default(),
123        }
124    }
125
126    /// Creates a new module request from an AST specifier and attributes.
127    #[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    /// Gets the module specifier.
147    #[must_use]
148    pub fn specifier(&self) -> &JsString {
149        &self.specifier
150    }
151
152    /// Gets the import attributes as key-value pairs.
153    #[must_use]
154    pub fn attributes(&self) -> &[ImportAttribute] {
155        &self.attributes
156    }
157
158    /// Gets the value of a specific attribute by key.
159    #[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/// ECMAScript's [**Abstract module record**][spec].
169///
170/// [spec]: https://tc39.es/ecma262/#sec-abstract-module-records
171#[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/// The kind of a [`Module`].
196#[derive(Debug, Trace, Finalize)]
197pub(crate) enum ModuleKind {
198    /// A [**Source Text Module Record**](https://tc39.es/ecma262/#sec-source-text-module-records)
199    SourceText(Box<SourceTextModule>),
200    /// A [**Synthetic Module Record**](https://tc39.es/proposal-json-modules/#sec-synthetic-module-records)
201    Synthetic(Box<SyntheticModule>),
202}
203
204impl ModuleKind {
205    /// Returns the inner `SourceTextModule`.
206    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/// Return value of the [`Module::resolve_export`] operation.
215///
216/// Indicates how to access a specific export in a module.
217#[derive(Debug, Clone)]
218pub(crate) struct ResolvedBinding {
219    module: Module,
220    binding_name: BindingName,
221}
222
223/// The local name of the resolved binding within its containing module.
224///
225/// Note that a resolved binding can resolve to a single binding inside a module (`export var a = 1"`)
226/// or to a whole module namespace (`export * as ns from "mod.js"`).
227#[derive(Debug, Clone)]
228pub(crate) enum BindingName {
229    /// A local binding.
230    Name(JsString),
231    /// The whole namespace of the containing module.
232    Namespace,
233}
234
235impl ResolvedBinding {
236    /// Gets the module from which the export resolved.
237    pub(crate) const fn module(&self) -> &Module {
238        &self.module
239    }
240
241    /// Consumes `self` and returns the module from which the export resolved.
242    pub(crate) fn into_module(self) -> Module {
243        self.module
244    }
245
246    /// Gets a reference to the binding associated with the resolved export.
247    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    /// Abstract operation [`ParseModule ( sourceText, realm, hostDefined )`][spec].
268    ///
269    /// Parses the provided `src` as an ECMAScript module, returning an error if parsing fails.
270    ///
271    /// [spec]: https://tc39.es/ecma262/#sec-parsemodule
272    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    /// Abstract operation [`CreateSyntheticModule ( exportNames, evaluationSteps, realm )`][spec].
300    ///
301    /// Creates a new Synthetic Module from its list of exported names, its evaluation steps and
302    /// optionally a root realm.
303    ///
304    /// [spec]: https://tc39.es/proposal-json-modules/#sec-createsyntheticmodule
305    #[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    /// Create a [`Module`] from a `JsValue`, exporting that value as the default export.
329    /// This will clone the module everytime it is initialized.
330    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    /// Create a module that exports a single JSON value as the default export, from its
347    /// JSON string.
348    ///
349    /// # Specification
350    /// This is a custom extension to the ECMAScript specification. The current proposal
351    /// for JSON modules is being considered in <https://github.com/tc39/proposal-json-modules>
352    /// and might differ from this implementation.
353    ///
354    /// This method is provided as a convenience for hosts to create JSON modules.
355    ///
356    /// # Errors
357    /// This will return an error if the JSON string is invalid or cannot be converted.
358    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    /// Gets the realm of this `Module`.
364    #[inline]
365    #[must_use]
366    pub fn realm(&self) -> &Realm {
367        &self.inner.realm
368    }
369
370    /// Returns the [`ECMAScript specification`][spec] defined [`\[\[HostDefined\]\]`][`HostDefined`] field of the [`Module`].
371    ///
372    /// [spec]: https://tc39.es/ecma262/#sec-abstract-module-records
373    #[inline]
374    #[must_use]
375    pub fn host_defined(&self) -> &HostDefined {
376        &self.inner.host_defined
377    }
378
379    /// Gets the kind of this `Module`.
380    pub(crate) fn kind(&self) -> &ModuleKind {
381        &self.inner.kind
382    }
383
384    /// Gets the declarative environment of this `Module`.
385    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    /// Abstract method [`LoadRequestedModules ( [ hostDefined ] )`][spec].
393    ///
394    /// Prepares the module for linking by loading all its module dependencies. Returns a `JsPromise`
395    /// that will resolve when the loading process either completes or fails.
396    ///
397    /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
398    #[allow(clippy::missing_panics_doc)]
399    #[inline]
400    pub fn load(&self, context: &mut Context) -> JsPromise {
401        match self.kind() {
402            ModuleKind::SourceText(_) => {
403                // Concrete method [`LoadRequestedModules ( [ hostDefined ] )`][spec].
404                //
405                // [spec]: https://tc39.es/ecma262/#sec-LoadRequestedModules
406                // 1. If hostDefined is not present, let hostDefined be empty.
407
408                // 2. Let pc be ! NewPromiseCapability(%Promise%).
409                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                // 4. Perform InnerModuleLoading(state, module).
418                self.inner_load(
419                    // 3. Let state be the GraphLoadingState Record {
420                    //     [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « »,
421                    //     [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined
422                    // }.
423                    &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                // 5. Return pc.[[Promise]].
433                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    /// Abstract operation [`InnerModuleLoading`][spec].
441    ///
442    /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLoading
443    fn inner_load(&self, state: &Rc<GraphLoadingState>, context: &mut Context) {
444        // 1. Assert: state.[[IsLoading]] is true.
445        assert!(state.loading.get());
446
447        if let ModuleKind::SourceText(src) = self.kind() {
448            // continues on `inner_load
449            src.inner_load(self, state, context);
450            if !state.loading.get() {
451                return;
452            }
453        }
454
455        // 3. Assert: state.[[PendingModulesCount]] ≥ 1.
456        assert!(state.pending_modules.get() >= 1);
457
458        // 4. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] - 1.
459        state.pending_modules.set(state.pending_modules.get() - 1);
460        // 5. If state.[[PendingModulesCount]] = 0, then
461
462        if state.pending_modules.get() == 0 {
463            // a. Set state.[[IsLoading]] to false.
464            state.loading.set(false);
465            // b. For each Cyclic Module Record loaded of state.[[Visited]], do
466            //    i. If loaded.[[Status]] is new, set loaded.[[Status]] to unlinked.
467            // By default, all modules start on `unlinked`.
468
469            // c. Perform ! Call(state.[[PromiseCapability]].[[Resolve]], undefined, « undefined »).
470            state
471                .capability
472                .resolve()
473                .call(&JsValue::undefined(), &[], context)
474                .expect("marking a module as loaded should not fail");
475        }
476        // 6. Return unused.
477    }
478
479    /// Abstract method [`GetExportedNames([exportStarSet])`][spec].
480    ///
481    /// Returns a list of all the names exported from this module.
482    ///
483    /// # Note
484    ///
485    /// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
486    ///
487    /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
488    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    /// Abstract method [`ResolveExport(exportName [, resolveSet])`][spec].
500    ///
501    /// Returns the corresponding local binding of a binding exported by this module.
502    /// The spec requires that this operation must be idempotent; calling this multiple times
503    /// with the same `export_name` and `resolve_set` should always return the same result.
504    ///
505    /// # Note
506    ///
507    /// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
508    ///
509    /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
510    #[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    /// Abstract method [`Link() `][spec].
526    ///
527    /// Prepares this module for evaluation by resolving all its module dependencies and initializing
528    /// its environment.
529    ///
530    /// # Note
531    ///
532    /// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
533    ///
534    /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
535    #[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    /// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec].
548    ///
549    /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking
550    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            // If module is not a Cyclic Module Record, then
559            ModuleKind::Synthetic(synth) => {
560                // a. Perform ? module.Link().
561                synth.link(self, context);
562                // b. Return index.
563                Ok(index)
564            }
565        }
566    }
567
568    /// Abstract method [`Evaluate()`][spec].
569    ///
570    /// Evaluates this module, returning a promise for the result of the evaluation of this module
571    /// and its dependencies.
572    /// If the promise is rejected, hosts are expected to handle the promise rejection and rethrow
573    /// the evaluation error.
574    ///
575    /// # Note
576    ///
577    /// This must only be called if the [`Module::link`] method finished successfully.
578    ///
579    /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
580    #[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    /// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec].
589    ///
590    /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking
591    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            // 1. If module is not a Cyclic Module Record, then
600            ModuleKind::Synthetic(synth) => {
601                // a. Let promise be ! module.Evaluate().
602                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                    // d. Return index.
609                    PromiseState::Fulfilled(_) => Ok(index),
610                    // c. If promise.[[PromiseState]] is rejected, then
611                    //    i. Return ThrowCompletion(promise.[[PromiseResult]]).
612                    PromiseState::Rejected(err) => Err(JsError::from_opaque(err)),
613                }
614            }
615        }
616    }
617
618    /// Loads, links and evaluates this module, returning a promise that will resolve after the module
619    /// finishes its lifecycle.
620    ///
621    /// # Examples
622    /// ```
623    /// # use std::{path::Path, rc::Rc};
624    /// # use boa_engine::{Context, Source, Module, JsValue};
625    /// # use boa_engine::builtins::promise::PromiseState;
626    /// # use boa_engine::module::{ModuleLoader, SimpleModuleLoader};
627    /// let loader = Rc::new(SimpleModuleLoader::new(Path::new(".")).unwrap());
628    /// let mut context = &mut Context::builder()
629    ///     .module_loader(loader.clone())
630    ///     .build()
631    ///     .unwrap();
632    ///
633    /// let source = Source::from_bytes("1 + 3");
634    ///
635    /// let module = Module::parse(source, None, context).unwrap();
636    ///
637    /// loader.insert(Path::new("main.mjs").to_path_buf(), module.clone());
638    ///
639    /// let promise = module.load_link_evaluate(context);
640    ///
641    /// context.run_jobs().unwrap();
642    ///
643    /// assert_eq!(
644    ///     promise.state(),
645    ///     PromiseState::Fulfilled(JsValue::undefined())
646    /// );
647    /// ```
648    #[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    /// Abstract operation [`GetModuleNamespace ( module )`][spec].
682    ///
683    /// Gets the [**Module Namespace Object**][ns] that represents this module's exports.
684    ///
685    /// [spec]: https://tc39.es/ecma262/#sec-getmodulenamespace
686    /// [ns]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
687    pub fn namespace(&self, context: &mut Context) -> JsObject {
688        // 1. Assert: If module is a Cyclic Module Record, then module.[[Status]] is not new or unlinked.
689        // 2. Let namespace be module.[[Namespace]].
690        // 3. If namespace is empty, then
691        // 4. Return namespace.
692        self.inner
693            .namespace
694            .borrow_mut()
695            .get_or_insert_with(|| {
696                // a. Let exportedNames be module.GetExportedNames().
697                let exported_names =
698                    self.get_exported_names(&mut Vec::default(), context.interner());
699
700                // b. Let unambiguousNames be a new empty List.
701                let unambiguous_names = exported_names
702                    .into_iter()
703                    // c. For each element name of exportedNames, do
704                    .filter_map(|name| {
705                        // i. Let resolution be module.ResolveExport(name).
706                        // ii. If resolution is a ResolvedBinding Record, append name to unambiguousNames.
707                        self.resolve_export(&name, &mut HashSet::default(), context.interner())
708                            .ok()
709                            .map(|_| name)
710                    })
711                    .collect();
712
713                //     d. Set namespace to ModuleNamespaceCreate(module, unambiguousNames).
714                ModuleNamespace::create(self.clone(), unambiguous_names, context)
715            })
716            .clone()
717    }
718
719    /// Get an exported value from the module.
720    #[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    /// Get an exported function, typed, from the module.
730    #[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    /// Returns the path of the module, if it was created from a file or assigned.
749    #[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
771/// A trait to convert a type into a JS module.
772pub trait IntoJsModule {
773    /// Converts the type into a JS module.
774    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    // Checking if the final promise didn't return an error.
900    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    // Checking if the final promise didn't return an error.
954    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}