Skip to main content

script/modules/
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::RefCell;
12use std::ffi::c_void;
13use std::ptr;
14use std::rc::Rc;
15
16use js::context::{JSContext, RawJSContext};
17use js::jsapi::{
18    CallArgs, GetErrorType, GetFunctionNativeReserved, Heap, JS_GetFunctionObject, JSExnType,
19    JSObject, JSScript, ModuleType, SetFunctionNativeReserved,
20};
21use js::jsval::{JSVal, ObjectValue, PrivateValue, UndefinedValue};
22use js::realm::CurrentRealm;
23use js::rust::Handle;
24use js::rust::wrappers2::{
25    AddPromiseReactions, FinishLoadingDynamicImportedModule, FinishLoadingImportedModule,
26    FinishLoadingImportedModuleFailed, GetModuleNamespace, GetModuleRequestType, IsPromiseObject,
27    JS_GetScriptPrivate, LoadRequestedModules1, ModuleEvaluate, ModuleLink,
28    NewFunctionWithReserved,
29};
30use net_traits::blob_url_store::UrlWithBlobClaim;
31use net_traits::request::{Destination, Referrer, RequestClient};
32use script_bindings::cell::DomRefCell;
33use script_bindings::settings_stack::run_a_callback;
34
35use crate::DomTypeHolder;
36use crate::dom::bindings::error::Error;
37use crate::dom::bindings::root::DomRoot;
38use crate::dom::bindings::trace::RootedTraceableBox;
39use crate::dom::globalscope::GlobalScope;
40use crate::dom::promise::Promise;
41use crate::dom::promise::promisenativehandler::{Callback, PromiseNativeHandler};
42use crate::modules::script_module::{
43    ModuleHandler, ModuleObject, ModuleTree, RethrowError, ScriptFetchOptions,
44    fetch_a_single_module_script, gen_type_error, module_script_from_reference_private,
45};
46use crate::realms::enter_auto_realm;
47use crate::runtime::script_runtime::IntroductionType;
48use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
49
50#[derive(JSTraceable, MallocSizeOf)]
51struct OnRejectedHandler {
52    #[conditional_malloc_size_of]
53    promise: Rc<Promise>,
54}
55
56impl Callback for OnRejectedHandler {
57    fn callback(&self, cx: &mut CurrentRealm, v: Handle<JSVal>) {
58        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « reason »).
59        self.promise.reject(cx, v);
60    }
61}
62
63#[derive(JSTraceable, MallocSizeOf)]
64pub(crate) struct LoadState {
65    #[ignore_malloc_size_of = "mozjs"]
66    pub(crate) error_to_rethrow: RefCell<Option<RethrowError>>,
67    #[no_trace]
68    pub(crate) destination: Destination,
69    #[no_trace]
70    pub(crate) fetch_client: RequestClient,
71    #[conditional_malloc_size_of]
72    pub(crate) module_script: DomRefCell<Option<Rc<ModuleTree>>>,
73    #[ignore_malloc_size_of = "Measuring trait objects is hard"]
74    #[no_trace]
75    #[allow(clippy::type_complexity)]
76    pub(crate) on_complete:
77        DomRefCell<Option<Box<dyn FnOnce(&mut JSContext, Option<Rc<ModuleTree>>)>>>,
78}
79
80const LOAD_REACTION_HOST_DEFINED_SLOT: usize = 0;
81
82fn take_state_from_reserved_slot(cx: &mut JSContext, args: &CallArgs) -> Box<LoadState> {
83    rooted!(&in(cx) let host_defined = unsafe { *GetFunctionNativeReserved(args.callee(), LOAD_REACTION_HOST_DEFINED_SLOT) });
84    unsafe {
85        SetFunctionNativeReserved(
86            args.callee(),
87            LOAD_REACTION_HOST_DEFINED_SLOT,
88            &UndefinedValue(),
89        )
90    };
91    assert!(!host_defined.get().is_undefined());
92    unsafe { Box::from_raw((*host_defined).to_private() as *mut LoadState) }
93}
94
95unsafe extern "C" fn on_load_requested_modules_resolved(
96    cx: *mut RawJSContext,
97    argc: u32,
98    vp: *mut JSVal,
99) -> bool {
100    // SAFETY: it is safe to construct a JSContext from engine hook.
101    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
102    let mut realm = CurrentRealm::assert(&mut cx);
103    let cx = &mut realm;
104
105    let args = unsafe { CallArgs::from_vp(vp, argc) };
106
107    let state = take_state_from_reserved_slot(cx, &args);
108
109    let on_complete = state.on_complete.safe_borrow_mut(cx).take().unwrap();
110    let module_script = state.module_script.safe_borrow_mut(cx).take().unwrap();
111
112    let record = module_script
113        .get_record()
114        .map(|module| module.handle())
115        .unwrap();
116
117    // https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script
118    // Step 6. Upon fulfillment of loadingPromise, run the following steps:
119
120    // Step 6.1. Perform record.Link().
121    let link = unsafe { ModuleLink(cx, record) };
122
123    // If this throws an exception, catch it, and set moduleScript's error to rethrow to that exception.
124    if !link {
125        let exception = RethrowError::from_pending_exception(cx);
126        module_script.set_rethrow_error(exception);
127    }
128
129    // Step 6.2. Run onComplete given moduleScript.
130    on_complete(cx, Some(module_script));
131
132    true
133}
134
135unsafe extern "C" fn on_load_requested_modules_rejected(
136    cx: *mut RawJSContext,
137    argc: u32,
138    vp: *mut JSVal,
139) -> bool {
140    // SAFETY: it is safe to construct a JSContext from engine hook.
141    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
142    let mut realm = CurrentRealm::assert(&mut cx);
143    let cx = &mut realm;
144
145    let args = unsafe { CallArgs::from_vp(vp, argc) };
146
147    let state = take_state_from_reserved_slot(cx, &args);
148
149    let error = unsafe { Handle::from_raw(args.get(0)) };
150
151    let on_complete = state.on_complete.safe_borrow_mut(cx).take().unwrap();
152    let module_script = state.module_script.safe_borrow_mut(cx).take().unwrap();
153
154    // https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script
155    // Step 7. Upon rejection of loadingPromise, run the following steps:
156
157    // Note: When the error is thrown on the SpiderMonkey side, which happens when encountering
158    // an unsupported attribute inside `InnerModuleLoading`, state.[[ErrorToRethrow]] doesn't
159    // contain the expected error, we need to use the value passed down.
160    if unsafe { GetErrorType(error.as_ref(cx)) } == JSExnType::JSEXN_SYNTAXERR {
161        module_script.set_rethrow_error(RethrowError::new(Heap::boxed(*error.as_ref(cx))));
162        on_complete(cx, Some(module_script));
163        return true;
164    }
165
166    // Step 7.1. If state.[[ErrorToRethrow]] is not null, set moduleScript's error to rethrow to
167    // state.[[ErrorToRethrow]] and run onComplete given moduleScript.
168    let error_to_rethrow = state.error_to_rethrow.borrow().as_ref().cloned();
169    if let Some(error) = error_to_rethrow {
170        module_script.set_rethrow_error(error);
171        on_complete(cx, Some(module_script));
172    } else {
173        // Step 7.2. Otherwise, run onComplete given null.
174        on_complete(cx, None);
175    }
176
177    true
178}
179
180struct ImportRequest {
181    referrer: RootedTraceableBox<Heap<*mut JSScript>>,
182    module_request: RootedTraceableBox<Heap<*mut JSObject>>,
183    payload: RootedTraceableBox<Heap<JSVal>>,
184    host_defined: RootedTraceableBox<Heap<JSVal>>,
185}
186
187fn load_state_from_handle_value<'a>(reference_private: Handle<'a, JSVal>) -> Option<&'a LoadState> {
188    if reference_private.get().is_undefined() {
189        return None;
190    }
191    unsafe { (reference_private.get().to_private() as *const LoadState).as_ref() }
192}
193
194pub(crate) fn load_requested_modules(
195    cx: &mut CurrentRealm,
196    module: Handle<*mut JSObject>,
197    load_state: Box<LoadState>,
198) {
199    rooted!(&in(cx) let host_defined = PrivateValue(Box::into_raw(load_state) as *const _ as *const c_void));
200
201    unsafe {
202        let on_resolved = NewFunctionWithReserved(
203            cx,
204            Some(on_load_requested_modules_resolved),
205            0,
206            0,
207            ptr::null(),
208        );
209        let on_rejected = NewFunctionWithReserved(
210            cx,
211            Some(on_load_requested_modules_rejected),
212            1,
213            0,
214            ptr::null(),
215        );
216
217        rooted!(&in(cx) let resolved_function_object = JS_GetFunctionObject(on_resolved));
218        SetFunctionNativeReserved(
219            resolved_function_object.get(),
220            LOAD_REACTION_HOST_DEFINED_SLOT,
221            host_defined.handle().as_ref(cx),
222        );
223
224        rooted!(&in(cx) let rejected_function_object = JS_GetFunctionObject(on_rejected));
225        SetFunctionNativeReserved(
226            rejected_function_object.get(),
227            LOAD_REACTION_HOST_DEFINED_SLOT,
228            host_defined.handle().as_ref(cx),
229        );
230
231        rooted!(&in(cx) let mut promise_obj = ptr::null_mut::<JSObject>());
232        assert!(LoadRequestedModules1(
233            cx,
234            module,
235            host_defined.handle(),
236            promise_obj.handle_mut(),
237        ));
238
239        AddPromiseReactions(
240            cx,
241            promise_obj.handle(),
242            resolved_function_object.handle(),
243            rejected_function_object.handle(),
244        );
245    }
246}
247
248/// <https://tc39.es/ecma262/#sec-FinishLoadingImportedModule>
249fn finish_loading_imported_module(
250    cx: &mut CurrentRealm,
251    referrer: Handle<*mut JSScript>,
252    module_request: Handle<*mut JSObject>,
253    payload: Handle<JSVal>,
254    result: Result<Rc<ModuleTree>, RethrowError>,
255) {
256    match result {
257        Ok(module_tree) => {
258            let module_handle = module_tree
259                .get_record()
260                .map(|module| module.handle())
261                .unwrap();
262
263            if payload.is_object() {
264                rooted!(&in(cx) let object = payload.to_object());
265                let is_promise = unsafe { IsPromiseObject(object.handle()) };
266
267                if is_promise {
268                    unsafe {
269                        FinishLoadingDynamicImportedModule(
270                            cx,
271                            referrer,
272                            module_request,
273                            payload,
274                            module_handle,
275                        )
276                    };
277                    let promise = Promise::new_with_js_promise(cx, object.handle());
278                    let record = ModuleObject::new(module_handle);
279                    return continue_dynamic_import(cx, promise, record);
280                }
281            }
282
283            assert!(unsafe {
284                FinishLoadingImportedModule(
285                    cx,
286                    referrer,
287                    module_request,
288                    payload,
289                    module_handle,
290                    true,
291                )
292            });
293        },
294        Err(error) => {
295            unsafe { FinishLoadingImportedModuleFailed(cx, payload, error.handle()) };
296        },
297    }
298}
299
300/// <https://tc39.es/ecma262/#sec-ContinueDynamicImport>
301fn continue_dynamic_import(realm: &mut CurrentRealm, promise: Rc<Promise>, module: ModuleObject) {
302    // Step 1. If moduleCompletion is an abrupt completion, then
303    // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « moduleCompletion.[[Value]] »).
304    // b. Return unused.
305    // Note: Done inside `finish_loading_imported_module`
306
307    let global = GlobalScope::from_current_realm(realm);
308
309    // Step 2. Let module be moduleCompletion.[[Value]].
310
311    rooted!(&in(*realm) let host_defined = UndefinedValue());
312    rooted!(&in(*realm) let mut promise_obj = ptr::null_mut::<JSObject>());
313
314    // Step 3. Let loadPromise be module.LoadRequestedModules().
315    unsafe {
316        LoadRequestedModules1(
317            realm,
318            module.handle(),
319            host_defined.handle(),
320            promise_obj.handle_mut(),
321        )
322    };
323
324    let load_promise = Promise::new_with_js_promise(realm, promise_obj.handle());
325
326    // Step 4. Let rejectedClosure be a new Abstract Closure with parameters (reason)
327    // that captures promiseCapability and performs the following steps when called:
328    // Step 5. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
329    // Note: implemented by OnRejectedHandler.
330
331    let global_scope = global.clone();
332    let inner_promise = promise.clone();
333    let fulfilled_promise = promise.clone();
334
335    // Step 6. Let linkAndEvaluateClosure be a new Abstract Closure with no parameters that captures
336    // module, promiseCapability, and onRejected and performs the following steps when called:
337    // Step 7. Let linkAndEvaluate be CreateBuiltinFunction(linkAndEvaluateClosure, 0, "", « »).
338    let link_and_evaluate = ModuleHandler::new_boxed(Box::new(
339        task!(link_and_evaluate: |cx, global_scope: DomRoot<GlobalScope>, inner_promise: Rc<Promise>, module: ModuleObject| {
340            let mut realm = enter_auto_realm(cx, &*global_scope);
341            let cx = &mut realm.current_realm();
342
343            // a. Let link be Completion(module.Link()).
344            let link = unsafe { ModuleLink(cx, module.handle()) };
345
346            // b. If link is an abrupt completion, then
347            if !link {
348                // i. Perform ! Call(promiseCapability.[[Reject]], undefined, « link.[[Value]] »).
349                let exception = RethrowError::from_pending_exception(cx);
350                inner_promise.reject(cx, exception.handle());
351
352                // ii. Return NormalCompletion(undefined).
353                return;
354            }
355
356            rooted!(&in(cx) let mut rval = UndefinedValue());
357
358            // c. Let evaluatePromise be module.Evaluate().
359            assert!(unsafe { ModuleEvaluate(cx, module.handle(), rval.handle_mut()) });
360
361            if !rval.is_object() {
362                let error = RethrowError::from_pending_exception(cx);
363                return inner_promise.reject(cx, error.handle());
364            }
365
366            rooted!(&in(cx) let evaluate_promise = rval.to_object());
367            let evaluate_promise = Promise::new_with_js_promise(cx, evaluate_promise.handle());
368
369            // d. Let fulfilledClosure be a new Abstract Closure with no parameters that captures
370            // module and promiseCapability and performs the following steps when called:
371            // e. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »).
372            let on_fulfilled = ModuleHandler::new_boxed(Box::new(
373                task!(on_fulfilled: |cx, fulfilled_promise: Rc<Promise>, module: ModuleObject| {
374
375                    // i. Let namespace be GetModuleNamespace(module).
376                    rooted!(&in(cx) let rval = unsafe { GetModuleNamespace(cx, module.handle()) });
377                    rooted!(&in(cx) let namespace = ObjectValue(rval.get()));
378
379                    // ii. Perform ! Call(promiseCapability.[[Resolve]], undefined, « namespace »).
380                    fulfilled_promise.resolve(cx, namespace.handle());
381
382                    // iii. Return NormalCompletion(undefined).
383            })));
384
385            // f. Perform PerformPromiseThen(evaluatePromise, onFulfilled, onRejected).
386            let handler = PromiseNativeHandler::new(
387                cx,
388                &global_scope,
389                Some(on_fulfilled),
390                Some(Box::new(OnRejectedHandler { promise: inner_promise }))
391            );
392            evaluate_promise.append_native_handler(cx, &handler);
393
394            // g. Return unused.
395        }),
396    ));
397
398    run_a_callback::<DomTypeHolder, _>(&*global, || {
399        // Step 8. Perform PerformPromiseThen(loadPromise, linkAndEvaluate, onRejected).
400        let handler = PromiseNativeHandler::new(
401            realm,
402            &global,
403            Some(link_and_evaluate),
404            Some(Box::new(OnRejectedHandler { promise })),
405        );
406        load_promise.append_native_handler(realm, &handler);
407    });
408    // Step 9. Return unused.
409}
410
411/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
412pub(crate) fn host_load_imported_module(
413    cx: &mut CurrentRealm,
414    referrer: Handle<*mut JSScript>,
415    module_request: Handle<*mut JSObject>,
416    specifier: String,
417    host_defined: Handle<JSVal>,
418    payload: Handle<JSVal>,
419) {
420    // Step 1. Let settingsObject be the current settings object.
421    let mut realm = CurrentRealm::assert(cx);
422    let mut global_scope = GlobalScope::from_current_realm(&mut realm);
423
424    let load_state = load_state_from_handle_value(host_defined);
425
426    // TODO Step 2. If settingsObject's global object implements WorkletGlobalScope or ServiceWorkerGlobalScope and loadState is undefined, then:
427
428    // Step 3. Let referencingScript be null.
429    rooted!(&in(cx) let mut script_private = UndefinedValue());
430
431    // Step 6.1. Set referencingScript to referrer.[[HostDefined]].
432    unsafe { JS_GetScriptPrivate(*referrer.as_ref(cx), script_private.handle_mut()) };
433    let referencing_script =
434        unsafe { module_script_from_reference_private(script_private.handle()) };
435
436    // Step 6. If referrer is a Script Record or a Cyclic Module Record, then:
437    let (original_fetch_options, fetch_referrer) = match referencing_script {
438        Some(module) => (
439            // Step 6.4. Set originalFetchOptions to referencingScript's fetch options.
440            module.options.clone(),
441            // Step 6.3. Set fetchReferrer to referencingScript's base URL.
442            Referrer::ReferrerUrl(module.base_url.clone()),
443        ),
444        None => (
445            // Step 4. Let originalFetchOptions be the default script fetch options.
446            ScriptFetchOptions::default_classic_script(),
447            // Step 5. Let fetchReferrer be "client".
448            global_scope.get_referrer(),
449        ),
450    };
451
452    // TODO: investigate providing a `ModuleOwner` to classic scripts.
453    let script_owner = referencing_script.and_then(|script| script.owner.clone());
454
455    // Step 6.2. Set settingsObject to referencingScript's settings object.
456    if let Some(ref owner) = script_owner {
457        global_scope = owner.root();
458    }
459
460    let global = &global_scope.clone();
461
462    // Step 7. If referrer is a Cyclic Module Record and moduleRequest is equal to the first element of referrer.[[RequestedModules]], then:
463    // Note: Spidermonkey removed the API for iterating through a module's requested modules,
464    // preventing upfront validation.
465    // Additionally, we skip step 7.1.1 (handled internally by Spidermonkey in `InnerModuleLoading`),
466    // as well as steps 7.1.2 and 7.1.3 (executed later in steps 8 and 9).
467
468    // Step 7.1.4. Let moduleType be the result of running the module type from module request steps given requested.
469    let module_type = unsafe { GetModuleRequestType(cx, module_request) };
470
471    // Step 7.1.5. If the result of running the module type allowed steps given moduleType and settingsObject is false:
472    if let ModuleType::Unknown = module_type {
473        // Step 7.1.5.1. Let error be a new TypeError exception.
474        let error = gen_type_error(
475            cx,
476            global,
477            Error::Type(c"Found invalid module type attribute".to_owned()),
478        );
479
480        // Step 7.1.5.2. If loadState is not undefined and loadState.[[ErrorToRethrow]] is null, set
481        // loadState.[[ErrorToRethrow]] to error.
482        if let Some(load_state) = load_state {
483            load_state
484                .error_to_rethrow
485                .borrow_mut()
486                .get_or_insert(error.clone());
487        }
488
489        // Step 7.1.5.3. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, ThrowCompletion(error)).
490        finish_loading_imported_module(cx, referrer, module_request, payload, Err(error));
491
492        // Step 7.1.5.4. Return.
493        return;
494    }
495
496    // Step 8 Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]],
497    // catching any exceptions. If they throw an exception, let resolutionError be the thrown exception.
498    let url = ModuleTree::resolve_module_specifier(global, referencing_script, specifier);
499
500    // Step 9 If the previous step threw an exception, then:
501    if let Err(error) = url {
502        let resolution_error = gen_type_error(cx, &global_scope, error);
503
504        // Step 9.1. If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
505        // set loadState.[[ErrorToRethrow]] to resolutionError.
506        load_state.as_ref().inspect(|load_state| {
507            load_state
508                .error_to_rethrow
509                .borrow_mut()
510                .get_or_insert(resolution_error.clone());
511        });
512
513        // Step 9.2. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, ThrowCompletion(resolutionError)).
514        finish_loading_imported_module(
515            cx,
516            referrer,
517            module_request,
518            payload,
519            Err(resolution_error),
520        );
521
522        // Step 9.3. Return.
523        return;
524    };
525
526    let url = ensure_blob_referenced_by_url_is_kept_alive(global, url.unwrap());
527
528    // Step 10. Let fetchOptions be the result of getting the descendant script fetch options given
529    // originalFetchOptions, url, and settingsObject.
530    let fetch_options = original_fetch_options.descendant_fetch_options(&url.url(), &global_scope);
531
532    // Step 13. If loadState is not undefined, then:
533    // Note: loadState is undefined only in dynamic imports
534    let (destination, fetch_client) = match load_state {
535        // Step 13.1. Set destination to loadState.[[Destination]].
536        // Step 13.2. Set fetchClient to loadState.[[FetchClient]].
537        Some(load_state) => (load_state.destination, load_state.fetch_client.clone()),
538        None => (
539            // Step 11. Let destination be "script".
540            Destination::Script,
541            // Step 12. Let fetchClient be settingsObject.
542            global_scope.request_client(Some(cx.no_gc())),
543        ),
544    };
545
546    let request = ImportRequest {
547        referrer: RootedTraceableBox::from_box(Heap::boxed(*referrer.as_ref(cx))),
548        module_request: RootedTraceableBox::from_box(Heap::boxed(*module_request.as_ref(cx))),
549        payload: RootedTraceableBox::from_box(Heap::boxed(*payload.as_ref(cx))),
550        host_defined: RootedTraceableBox::from_box(Heap::boxed(*host_defined.as_ref(cx))),
551    };
552
553    let on_single_fetch_complete =
554        move |cx: &mut JSContext, module_tree: Option<Rc<ModuleTree>>| {
555            let mut realm = CurrentRealm::assert(cx);
556            let cx = &mut realm;
557
558            // Step 1. Let completion be null.
559            let completion = match module_tree {
560                // Step 2. If moduleScript is null, then set completion to ThrowCompletion(a new TypeError).
561                None => Err(gen_type_error(
562                    cx,
563                    &global_scope,
564                    Error::Type(c"Module fetching failed".to_owned()),
565                )),
566                Some(module_tree) => {
567                    // Step 3. Otherwise, if moduleScript's parse error is not null, then:
568                    // Step 3.1 Let parseError be moduleScript's parse error.
569                    if let Some(parse_error) = module_tree.get_parse_error() {
570                        // Step 3.3 If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
571                        // set loadState.[[ErrorToRethrow]] to parseError.
572                        let load_state =
573                            load_state_from_handle_value(request.host_defined.handle());
574                        load_state.inspect(|load_state| {
575                            load_state
576                                .error_to_rethrow
577                                .borrow_mut()
578                                .get_or_insert(parse_error.clone());
579                        });
580
581                        // Step 3.2 Set completion to ThrowCompletion(parseError).
582                        Err(parse_error.clone())
583                    } else {
584                        // Step 4. Otherwise, set completion to NormalCompletion(moduleScript's record).
585                        Ok(module_tree)
586                    }
587                },
588            };
589
590            // Step 5. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion).
591            finish_loading_imported_module(
592                cx,
593                request.referrer.handle(),
594                request.module_request.handle(),
595                request.payload.handle(),
596                completion,
597            );
598        };
599
600    // Step 14 Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject,
601    // fetchReferrer, moduleRequest, and onSingleFetchComplete as defined below.
602    // If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
603    // Note: we don't have access to the requested `ModuleObject`, so we pass only its type.
604    fetch_a_single_imported_module_script(
605        cx,
606        url,
607        fetch_client,
608        global,
609        destination,
610        fetch_options,
611        fetch_referrer,
612        module_type,
613        on_single_fetch_complete,
614    );
615}
616
617/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-imported-module-script>
618#[expect(clippy::too_many_arguments)]
619fn fetch_a_single_imported_module_script(
620    cx: &mut JSContext,
621    url: UrlWithBlobClaim,
622    fetch_client: RequestClient,
623    global: &GlobalScope,
624    destination: Destination,
625    options: ScriptFetchOptions,
626    referrer: Referrer,
627    module_type: ModuleType,
628    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
629) {
630    // TODO Step 1. Assert: moduleRequest.[[Attributes]] does not contain any Record entry such that entry.[[Key]] is not "type",
631    // because we only asked for "type" attributes in HostGetSupportedImportAttributes.
632
633    // TODO Step 2. Let moduleType be the result of running the module type from module request steps given moduleRequest.
634
635    // Step 3. If the result of running the module type allowed steps given moduleType and settingsObject is false,
636    // then run onComplete given null, and return.
637    match module_type {
638        ModuleType::Unknown | ModuleType::Bytes | ModuleType::Text | ModuleType::CSS => {
639            return on_complete(cx, None);
640        },
641        ModuleType::JavaScript | ModuleType::JSON => (),
642    }
643
644    // Step 4. Fetch a single module script given url, fetchClient, destination, options, settingsObject, referrer,
645    // moduleRequest, false, and onComplete. If performFetch was given, pass it along as well.
646    fetch_a_single_module_script(
647        cx,
648        url,
649        fetch_client,
650        global,
651        destination,
652        options,
653        referrer,
654        Some(module_type),
655        false,
656        Some(IntroductionType::IMPORTED_MODULE),
657        on_complete,
658    );
659}