Skip to main content

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