Skip to main content

script/
module_loading.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! An implementation of ecma262's [LoadRequestedModules](https://tc39.es/ecma262/#sec-LoadRequestedModules)
6//! Partly inspired by mozjs implementation: <https://searchfox.org/firefox-main/source/js/src/vm/Modules.cpp#1450>
7//! Since we can't access ModuleObject internals (eg. ModuleRequest records), we deviate from the spec in some aspects.
8
9#![expect(unsafe_code)]
10
11use std::cell::{Cell, RefCell};
12use std::collections::HashSet;
13use std::rc::Rc;
14
15use js::context::JSContext;
16use js::conversions::jsstr_to_string;
17use js::jsapi::{HandleValue as RawHandleValue, IsCyclicModule, ModuleType};
18use js::jsval::{ObjectValue, UndefinedValue};
19use js::realm::{AutoRealm, CurrentRealm};
20use js::rust::wrappers2::{
21    GetModuleNamespace, GetRequestedModuleSpecifier, GetRequestedModuleType,
22    GetRequestedModulesCount, JS_GetModulePrivate, ModuleEvaluate, ModuleLink,
23};
24use js::rust::{HandleValue, IntoHandle};
25use net_traits::request::{Destination, Referrer};
26use script_bindings::reflector::DomObject;
27use script_bindings::settings_stack::run_a_callback;
28use servo_url::ServoUrl;
29
30use crate::DomTypeHolder;
31use crate::dom::bindings::error::Error;
32use crate::dom::bindings::root::DomRoot;
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::promise::Promise;
35use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
36use crate::realms::enter_auto_realm;
37use crate::script_module::{
38    ModuleFetchClient, ModuleHandler, ModuleObject, ModuleTree, RethrowError, ScriptFetchOptions,
39    fetch_a_single_module_script, gen_type_error, module_script_from_reference_private,
40};
41use crate::script_runtime::{CanGc, IntroductionType};
42
43#[derive(JSTraceable, MallocSizeOf)]
44struct OnRejectedHandler {
45    #[conditional_malloc_size_of]
46    promise: Rc<Promise>,
47}
48
49impl Callback for OnRejectedHandler {
50    fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
51        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « reason »).
52        self.promise.reject(cx.into(), v, CanGc::from_cx(cx));
53    }
54}
55
56pub(crate) enum Payload {
57    GraphRecord(Rc<GraphLoadingState>),
58    PromiseRecord(Rc<Promise>),
59}
60
61#[derive(JSTraceable)]
62pub(crate) struct LoadState {
63    pub(crate) error_to_rethrow: RefCell<Option<RethrowError>>,
64    #[no_trace]
65    pub(crate) destination: Destination,
66    #[no_trace]
67    pub(crate) fetch_client: ModuleFetchClient,
68}
69
70/// <https://tc39.es/ecma262/#graphloadingstate-record>
71pub(crate) struct GraphLoadingState {
72    /// [[PromiseCapability]]
73    promise: Rc<Promise>,
74    /// [[IsLoading]]
75    is_loading: Cell<bool>,
76    /// [[PendingModulesCount]]
77    pending_modules_count: Cell<u32>,
78    /// [[Visited]]
79    visited: RefCell<HashSet<ServoUrl>>,
80    /// [[HostDefined]]
81    load_state: Option<Rc<LoadState>>,
82}
83
84/// <https://tc39.es/ecma262/#sec-LoadRequestedModules>
85pub(crate) fn load_requested_modules(
86    cx: &mut CurrentRealm,
87    module: Rc<ModuleTree>,
88    load_state: Option<Rc<LoadState>>,
89) -> Rc<Promise> {
90    // Step 1. If hostDefined is not present, let hostDefined be empty.
91    //
92    // Not required, since we implement it as an `Option`
93
94    // Step 2. Let pc be ! NewPromiseCapability(%Promise%).
95    let mut realm = CurrentRealm::assert(cx);
96    let promise = Promise::new_in_realm(&mut realm);
97
98    // Step 3. Let state be the GraphLoadingState Record
99    // { [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « », [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined }.
100    let state = GraphLoadingState {
101        promise: promise.clone(),
102        is_loading: Cell::new(true),
103        pending_modules_count: Cell::new(1),
104        visited: RefCell::new(HashSet::new()),
105        load_state,
106    };
107
108    // Step 4. Perform InnerModuleLoading(state, module).
109    inner_module_loading(cx, &Rc::new(state), module);
110
111    // Step 5. Return pc.[[Promise]].
112    promise
113}
114
115/// <https://tc39.es/ecma262/#sec-InnerModuleLoading>
116fn inner_module_loading(
117    cx: &mut CurrentRealm,
118    state: &Rc<GraphLoadingState>,
119    module: Rc<ModuleTree>,
120) {
121    // Step 1. Assert: state.[[IsLoading]] is true.
122    assert!(state.is_loading.get());
123
124    let module_handle = module.get_record().map(|module| module.handle()).unwrap();
125
126    let module_url = module.get_url();
127    let visited_contains_module = state.visited.borrow().contains(&module_url);
128
129    // Step 2. If module is a Cyclic Module Record, module.[[Status]] is new, and state.[[Visited]] does not contain module, then
130    // Note: mozjs doesn't expose a way to check the ModuleStatus of a ModuleObject.
131    if unsafe { IsCyclicModule(module_handle.get()) } && !visited_contains_module {
132        // a. Append module to state.[[Visited]].
133        state.visited.borrow_mut().insert(module_url);
134
135        // b. Let requestedModulesCount be the number of elements in module.[[RequestedModules]].
136        let requested_modules_count = unsafe { GetRequestedModulesCount(cx, module_handle) };
137
138        // c. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] + requestedModulesCount.
139        let pending_modules_count = state.pending_modules_count.get();
140        state
141            .pending_modules_count
142            .set(pending_modules_count + requested_modules_count);
143
144        // d. For each ModuleRequest Record request of module.[[RequestedModules]], do
145        for index in 0..requested_modules_count {
146            // i. If AllImportAttributesSupported(request.[[Attributes]]) is false, then
147            // Note: Gecko will call hasFirstUnsupportedAttributeKey on each module request,
148            // GetRequestedModuleSpecifier will do it for us.
149            // In addition it will also check if specifier has an unknown module type.
150            let jsstr = unsafe { GetRequestedModuleSpecifier(cx, module_handle, index) };
151
152            if jsstr.is_null() {
153                // 1. Let error be ThrowCompletion(a newly created SyntaxError object).
154                let error = RethrowError::from_pending_exception(cx);
155
156                // See Step 7. of `host_load_imported_module`.
157                state.load_state.as_ref().inspect(|load_state| {
158                    load_state
159                        .error_to_rethrow
160                        .borrow_mut()
161                        .get_or_insert(error.clone());
162                });
163
164                // 2. Perform ContinueModuleLoading(state, error).
165                continue_module_loading(cx, state, Err(error));
166            } else {
167                let specifier =
168                    unsafe { jsstr_to_string(cx.raw_cx(), std::ptr::NonNull::new(jsstr).unwrap()) };
169                let module_type = unsafe { GetRequestedModuleType(cx, module_handle, index) };
170
171                let realm = CurrentRealm::assert(cx);
172                let global = GlobalScope::from_current_realm(&realm);
173
174                // ii. Else if module.[[LoadedModules]] contains a LoadedModuleRequest Record record
175                // such that ModuleRequestsEqual(record, request) is true, then
176                let loaded_module =
177                    module.find_descendant_inside_module_map(&global, &specifier, module_type);
178
179                match loaded_module {
180                    // 1. Perform InnerModuleLoading(state, record.[[Module]]).
181                    Some(module) => inner_module_loading(cx, state, module),
182                    // iii. Else,
183                    None => {
184                        rooted!(&in(cx) let mut referrer = UndefinedValue());
185                        unsafe { JS_GetModulePrivate(module_handle.get(), referrer.handle_mut()) };
186
187                        // 1. Perform HostLoadImportedModule(module, request, state.[[HostDefined]], state).
188                        host_load_imported_module(
189                            cx,
190                            Some(module.clone()),
191                            referrer.handle().into_handle(),
192                            specifier,
193                            module_type,
194                            state.load_state.clone(),
195                            Payload::GraphRecord(state.clone()),
196                        );
197                    },
198                }
199            }
200
201            // iv. If state.[[IsLoading]] is false, return unused.
202            if !state.is_loading.get() {
203                return;
204            }
205        }
206    }
207
208    // Step 3. Assert: state.[[PendingModulesCount]] ≥ 1.
209    assert!(state.pending_modules_count.get() >= 1);
210
211    // Step 4. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] - 1.
212    let pending_modules_count = state.pending_modules_count.get();
213    state.pending_modules_count.set(pending_modules_count - 1);
214
215    // Step 5. If state.[[PendingModulesCount]] = 0, then
216    if state.pending_modules_count.get() == 0 {
217        // a. Set state.[[IsLoading]] to false.
218        state.is_loading.set(false);
219
220        // b. For each Cyclic Module Record loaded of state.[[Visited]], do
221        // i. If loaded.[[Status]] is new, set loaded.[[Status]] to unlinked.
222        // Note: mozjs defaults to the unlinked status.
223
224        // c. Perform ! Call(state.[[PromiseCapability]].[[Resolve]], undefined, « undefined »).
225        state.promise.resolve_native(&(), CanGc::from_cx(cx));
226    }
227
228    // Step 6. Return unused.
229}
230
231/// <https://tc39.es/ecma262/#sec-ContinueModuleLoading>
232fn continue_module_loading(
233    cx: &mut CurrentRealm,
234    state: &Rc<GraphLoadingState>,
235    module_completion: Result<Rc<ModuleTree>, RethrowError>,
236) {
237    // Step 1. If state.[[IsLoading]] is false, return unused.
238    if !state.is_loading.get() {
239        return;
240    }
241
242    match module_completion {
243        // Step 2. If moduleCompletion is a normal completion, then
244        // a. Perform InnerModuleLoading(state, moduleCompletion.[[Value]]).
245        Ok(module) => inner_module_loading(cx, state, module),
246
247        // Step 3. Else,
248        Err(exception) => {
249            // a. Set state.[[IsLoading]] to false.
250            state.is_loading.set(false);
251
252            // b. Perform ! Call(state.[[PromiseCapability]].[[Reject]], undefined, « moduleCompletion.[[Value]] »).
253            state
254                .promise
255                .reject(cx.into(), exception.handle(), CanGc::from_cx(cx));
256        },
257    }
258
259    // Step 4. Return unused.
260}
261
262/// <https://tc39.es/ecma262/#sec-FinishLoadingImportedModule>
263fn finish_loading_imported_module(
264    cx: &mut CurrentRealm,
265    referrer_module: Option<Rc<ModuleTree>>,
266    module_request_specifier: String,
267    payload: Payload,
268    result: Result<Rc<ModuleTree>, RethrowError>,
269) {
270    match payload {
271        // Step 2. If payload is a GraphLoadingState Record, then
272        Payload::GraphRecord(state) => {
273            let module_tree =
274                referrer_module.expect("Module must not be None in non dynamic imports");
275
276            // Step 1. If result is a normal completion, then
277            if let Ok(ref module) = result {
278                module_tree.insert_module_dependency(module, module_request_specifier);
279            }
280
281            // a. Perform ContinueModuleLoading(payload, result).
282            continue_module_loading(cx, &state, result);
283        },
284
285        // Step 3. Else,
286        // a. Perform ContinueDynamicImport(payload, result).
287        Payload::PromiseRecord(promise) => continue_dynamic_import(cx, promise, result),
288    }
289
290    // 4. Return unused.
291}
292
293/// <https://tc39.es/ecma262/#sec-ContinueDynamicImport>
294fn continue_dynamic_import(
295    cx: &mut CurrentRealm,
296    promise: Rc<Promise>,
297    module_completion: Result<Rc<ModuleTree>, RethrowError>,
298) {
299    // Step 1. If moduleCompletion is an abrupt completion, then
300    if let Err(exception) = module_completion {
301        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « moduleCompletion.[[Value]] »).
302        promise.reject(cx.into(), exception.handle(), CanGc::from_cx(cx));
303
304        // b. Return unused.
305        return;
306    }
307
308    let realm = CurrentRealm::assert(cx);
309    let global = GlobalScope::from_current_realm(&realm);
310
311    // Step 2. Let module be moduleCompletion.[[Value]].
312    let module = module_completion.unwrap();
313    let record = ModuleObject::new(module.get_record().map(|module| module.handle()).unwrap());
314
315    // Step 3. Let loadPromise be module.LoadRequestedModules().
316    let load_promise = load_requested_modules(cx, module, None);
317
318    // Step 4. Let rejectedClosure be a new Abstract Closure with parameters (reason)
319    // that captures promiseCapability and performs the following steps when called:
320    // Step 5. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
321    // Note: implemented by OnRejectedHandler.
322
323    let global_scope = global.clone();
324    let inner_promise = promise.clone();
325    let fulfilled_promise = promise.clone();
326
327    // Step 6. Let linkAndEvaluateClosure be a new Abstract Closure with no parameters that captures
328    // module, promiseCapability, and onRejected and performs the following steps when called:
329    // Step 7. Let linkAndEvaluate be CreateBuiltinFunction(linkAndEvaluateClosure, 0, "", « »).
330    let link_and_evaluate = ModuleHandler::new_boxed(Box::new(
331        task!(link_and_evaluate: |cx, global_scope: DomRoot<GlobalScope>, inner_promise: Rc<Promise>, record: ModuleObject| {
332            let mut realm = AutoRealm::new(
333                cx,
334                std::ptr::NonNull::new(global_scope.reflector().get_jsobject().get()).unwrap(),
335            );
336            let cx = &mut realm.current_realm();
337            // a. Let link be Completion(module.Link()).
338            let link = unsafe { ModuleLink(cx, record.handle()) };
339
340            // b. If link is an abrupt completion, then
341            if !link {
342                // i. Perform ! Call(promiseCapability.[[Reject]], undefined, « link.[[Value]] »).
343                let exception = RethrowError::from_pending_exception(cx);
344                inner_promise.reject(cx.into(), exception.handle(), CanGc::from_cx(cx));
345
346                // ii. Return NormalCompletion(undefined).
347                return;
348            }
349
350            rooted!(&in(cx) let mut rval = UndefinedValue());
351
352            // c. Let evaluatePromise be module.Evaluate().
353            assert!(unsafe { ModuleEvaluate(cx, record.handle(), rval.handle_mut()) });
354
355            if !rval.is_object() {
356                let error = RethrowError::from_pending_exception(cx);
357                return inner_promise.reject(cx.into(), error.handle(), CanGc::from_cx(cx));
358            }
359
360            rooted!(&in(cx) let evaluate_promise = rval.to_object());
361            let evaluate_promise = Promise::new_with_js_promise(evaluate_promise.handle(), cx.into());
362
363            // d. Let fulfilledClosure be a new Abstract Closure with no parameters that captures
364            // module and promiseCapability and performs the following steps when called:
365            // e. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »).
366            let on_fulfilled = ModuleHandler::new_boxed(Box::new(
367                task!(on_fulfilled: |cx, fulfilled_promise: Rc<Promise>, record: ModuleObject| {
368
369                    // i. Let namespace be GetModuleNamespace(module).
370                    rooted!(&in(cx) let rval = unsafe { GetModuleNamespace(cx, record.handle()) });
371                    rooted!(&in(cx) let namespace = ObjectValue(rval.get()));
372
373                    // ii. Perform ! Call(promiseCapability.[[Resolve]], undefined, « namespace »).
374                    fulfilled_promise.resolve(cx.into(), namespace.handle(), CanGc::from_cx(cx));
375
376                    // iii. Return NormalCompletion(undefined).
377            })));
378
379            // f. Perform PerformPromiseThen(evaluatePromise, onFulfilled, onRejected).
380            let handler = PromiseNativeHandler::new(
381                &global_scope,
382                Some(on_fulfilled),
383                Some(Box::new(OnRejectedHandler { promise: inner_promise })),
384                CanGc::from_cx(cx),
385            );
386            evaluate_promise.append_native_handler(cx, &handler);
387
388            // g. Return unused.
389        }),
390    ));
391
392    let mut realm = enter_auto_realm(cx, &*global);
393    let cx = &mut realm.current_realm();
394    run_a_callback::<DomTypeHolder, _>(&*global, || {
395        // Step 8. Perform PerformPromiseThen(loadPromise, linkAndEvaluate, onRejected).
396        let handler = PromiseNativeHandler::new(
397            &global,
398            Some(link_and_evaluate),
399            Some(Box::new(OnRejectedHandler { promise })),
400            CanGc::from_cx(cx),
401        );
402        load_promise.append_native_handler(cx, &handler);
403    });
404    // Step 9. Return unused.
405}
406
407/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
408pub(crate) fn host_load_imported_module(
409    cx: &mut CurrentRealm,
410    referrer_module: Option<Rc<ModuleTree>>,
411    referrer: RawHandleValue,
412    specifier: String,
413    module_type: ModuleType,
414    load_state: Option<Rc<LoadState>>,
415    payload: Payload,
416) {
417    // Step 1. Let settingsObject be the current settings object.
418    let realm = CurrentRealm::assert(cx);
419    let mut global_scope = GlobalScope::from_current_realm(&realm);
420
421    // TODO Step 2. If settingsObject's global object implements WorkletGlobalScope or ServiceWorkerGlobalScope and loadState is undefined, then:
422
423    // Step 3. Let referencingScript be null.
424    // Step 6.1. Set referencingScript to referrer.[[HostDefined]].
425    let referencing_script = unsafe { module_script_from_reference_private(&referrer) };
426
427    // Step 6. If referrer is a Script Record or a Cyclic Module Record, then:
428    let (original_fetch_options, fetch_referrer) = match referencing_script {
429        Some(module) => (
430            // Step 6.4. Set originalFetchOptions to referencingScript's fetch options.
431            module.options.clone(),
432            // Step 6.3. Set fetchReferrer to referencingScript's base URL.
433            Referrer::ReferrerUrl(module.base_url.clone()),
434        ),
435        None => (
436            // Step 4. Let originalFetchOptions be the default script fetch options.
437            ScriptFetchOptions::default_classic_script(),
438            // Step 5. Let fetchReferrer be "client".
439            global_scope.get_referrer(),
440        ),
441    };
442
443    // TODO: investigate providing a `ModuleOwner` to classic scripts.
444    let script_owner = referencing_script.and_then(|script| script.owner.clone());
445
446    // Step 6.2. Set settingsObject to referencingScript's settings object.
447    if let Some(ref owner) = script_owner {
448        global_scope = owner.root();
449    }
450
451    let global = &global_scope.clone();
452
453    // Step 7 If referrer is a Cyclic Module Record and moduleRequest is equal to the first element of referrer.[[RequestedModules]], then:
454    // Note: These substeps are implemented by `GetRequestedModuleSpecifier`,
455    // setting loadState.[[ErrorToRethrow]] is done by `inner_module_loading`.
456
457    // Step 8 Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]],
458    // catching any exceptions. If they throw an exception, let resolutionError be the thrown exception.
459    let url =
460        ModuleTree::resolve_module_specifier(global, referencing_script, specifier.clone().into());
461
462    // Step 9 If the previous step threw an exception, then:
463    if let Err(error) = url {
464        let resolution_error = gen_type_error(cx, &global_scope, error);
465
466        // Step 9.1. If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
467        // set loadState.[[ErrorToRethrow]] to resolutionError.
468        load_state.as_ref().inspect(|load_state| {
469            load_state
470                .error_to_rethrow
471                .borrow_mut()
472                .get_or_insert(resolution_error.clone());
473        });
474
475        // Step 9.2. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, ThrowCompletion(resolutionError)).
476        finish_loading_imported_module(
477            cx,
478            referrer_module,
479            specifier,
480            payload,
481            Err(resolution_error),
482        );
483
484        // Step 9.3. Return.
485        return;
486    };
487
488    let url = url.unwrap();
489
490    // Step 10. Let fetchOptions be the result of getting the descendant script fetch options given
491    // originalFetchOptions, url, and settingsObject.
492    let fetch_options = original_fetch_options.descendant_fetch_options(&url, &global_scope);
493
494    // Step 13. If loadState is not undefined, then:
495    // Note: loadState is undefined only in dynamic imports
496    let (destination, fetch_client) = match load_state.as_ref() {
497        // Step 13.1. Set destination to loadState.[[Destination]].
498        // Step 13.2. Set fetchClient to loadState.[[FetchClient]].
499        Some(load_state) => (load_state.destination, load_state.fetch_client.clone()),
500        None => (
501            // Step 11. Let destination be "script".
502            Destination::Script,
503            // Step 12. Let fetchClient be settingsObject.
504            ModuleFetchClient::from_global_scope(&global_scope),
505        ),
506    };
507
508    let on_single_fetch_complete =
509        move |cx: &mut JSContext, module_tree: Option<Rc<ModuleTree>>| {
510            let mut realm = CurrentRealm::assert(cx);
511            let cx = &mut realm;
512
513            // Step 1. Let completion be null.
514            let completion = match module_tree {
515                // Step 2. If moduleScript is null, then set completion to ThrowCompletion(a new TypeError).
516                None => Err(gen_type_error(
517                    cx,
518                    &global_scope,
519                    Error::Type(c"Module fetching failed".to_owned()),
520                )),
521                Some(module_tree) => {
522                    // Step 3. Otherwise, if moduleScript's parse error is not null, then:
523                    // Step 3.1 Let parseError be moduleScript's parse error.
524                    if let Some(parse_error) = module_tree.get_parse_error() {
525                        // Step 3.3 If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
526                        // set loadState.[[ErrorToRethrow]] to parseError.
527                        load_state.as_ref().inspect(|load_state| {
528                            load_state
529                                .error_to_rethrow
530                                .borrow_mut()
531                                .get_or_insert(parse_error.clone());
532                        });
533
534                        // Step 3.2 Set completion to ThrowCompletion(parseError).
535                        Err(parse_error.clone())
536                    } else {
537                        // Step 4. Otherwise, set completion to NormalCompletion(moduleScript's record).
538                        Ok(module_tree)
539                    }
540                },
541            };
542
543            // Step 5. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion).
544            finish_loading_imported_module(cx, referrer_module, specifier, payload, completion);
545        };
546
547    // Step 14 Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject,
548    // fetchReferrer, moduleRequest, and onSingleFetchComplete as defined below.
549    // If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
550    // Note: we don't have access to the requested `ModuleObject`, so we pass only its type.
551    fetch_a_single_imported_module_script(
552        cx,
553        url,
554        fetch_client,
555        global,
556        destination,
557        fetch_options,
558        fetch_referrer,
559        module_type,
560        on_single_fetch_complete,
561    );
562}
563
564/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-imported-module-script>
565#[expect(clippy::too_many_arguments)]
566fn fetch_a_single_imported_module_script(
567    cx: &mut JSContext,
568    url: ServoUrl,
569    fetch_client: ModuleFetchClient,
570    global: &GlobalScope,
571    destination: Destination,
572    options: ScriptFetchOptions,
573    referrer: Referrer,
574    module_type: ModuleType,
575    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
576) {
577    // TODO Step 1. Assert: moduleRequest.[[Attributes]] does not contain any Record entry such that entry.[[Key]] is not "type",
578    // because we only asked for "type" attributes in HostGetSupportedImportAttributes.
579
580    // TODO Step 2. Let moduleType be the result of running the module type from module request steps given moduleRequest.
581
582    // Step 3. If the result of running the module type allowed steps given moduleType and settingsObject is false,
583    // then run onComplete given null, and return.
584    match module_type {
585        ModuleType::Unknown => return on_complete(cx, None),
586        ModuleType::JavaScript | ModuleType::JSON => (),
587    }
588
589    // Step 4. Fetch a single module script given url, fetchClient, destination, options, settingsObject, referrer,
590    // moduleRequest, false, and onComplete. If performFetch was given, pass it along as well.
591    fetch_a_single_module_script(
592        cx,
593        url,
594        fetch_client,
595        global,
596        destination,
597        options,
598        referrer,
599        Some(module_type),
600        false,
601        Some(IntroductionType::IMPORTED_MODULE),
602        on_complete,
603    );
604}