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::ffi::c_void;
12use std::ptr;
13use std::rc::Rc;
14
15use js::context::{JSContext, RawJSContext};
16use js::error::throw_type_error;
17use js::jsapi::{
18    CallArgs, GetFunctionNativeReserved, Heap, JS_GetFunctionObject, JSObject, JSScript,
19    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, FinishLoadingImportedModuleFailedWithPendingException,
27    GetModuleNamespace, GetModuleRequestType, IsPromiseObject, JS_GetScriptPrivate,
28    LoadRequestedModules1, ModuleEvaluate, ModuleLink, NewFunctionWithReserved,
29};
30use net_traits::blob_url_store::UrlWithBlobClaim;
31use net_traits::request::{Destination, Referrer, RequestClient};
32use script_bindings::cell::DomRefCell;
33use script_bindings::inheritance::Castable;
34use script_bindings::settings_stack::run_a_callback;
35
36use crate::DomTypeHolder;
37use crate::dom::bindings::error::throw_dom_exception;
38use crate::dom::bindings::root::DomRoot;
39use crate::dom::bindings::trace::RootedTraceableBox;
40use crate::dom::globalscope::GlobalScope;
41use crate::dom::promise::Promise;
42use crate::dom::promise::promisenativehandler::{Callback, PromiseNativeHandler};
43use crate::dom::window::Window;
44use crate::modules::script_module::{
45    ModuleHandler, ModuleObject, ModuleTree, RethrowError, ScriptFetchOptions,
46    fetch_a_single_module_script, module_script_from_reference_private,
47};
48use crate::realms::enter_auto_realm;
49use crate::runtime::script_runtime::IntroductionType;
50use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
51
52#[derive(JSTraceable, MallocSizeOf)]
53struct OnRejectedHandler {
54    #[conditional_malloc_size_of]
55    promise: Rc<Promise>,
56}
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: Rc<Promise>, 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: Rc<Promise>, 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: Rc<Promise>, 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            // f. Perform PerformPromiseThen(evaluatePromise, onFulfilled, onRejected).
347            let handler = PromiseNativeHandler::new(
348                cx,
349                &global_scope,
350                Some(on_fulfilled),
351                Some(Box::new(OnRejectedHandler { promise: inner_promise }))
352            );
353            evaluate_promise.append_native_handler(cx, &handler);
354
355            // g. Return unused.
356        }),
357    ));
358
359    run_a_callback::<DomTypeHolder, _>(&*global, || {
360        // Step 8. Perform PerformPromiseThen(loadPromise, linkAndEvaluate, onRejected).
361        let handler = PromiseNativeHandler::new(
362            realm,
363            &global,
364            Some(link_and_evaluate),
365            Some(Box::new(OnRejectedHandler { promise })),
366        );
367        load_promise.append_native_handler(realm, &handler);
368    });
369    // Step 9. Return unused.
370}
371
372/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
373pub(crate) fn host_load_imported_module(
374    cx: &mut CurrentRealm,
375    referrer: Handle<*mut JSScript>,
376    module_request: Handle<*mut JSObject>,
377    specifier: String,
378    host_defined: Handle<JSVal>,
379    payload: Handle<JSVal>,
380) {
381    // Step 1. Let settingsObject be the current settings object.
382    let mut realm = CurrentRealm::assert(cx);
383    let mut global_scope = GlobalScope::from_current_realm(&mut realm);
384
385    let load_state = load_state_from_handle_value(host_defined);
386
387    // TODO Step 2. If settingsObject's global object implements WorkletGlobalScope or ServiceWorkerGlobalScope and loadState is undefined, then:
388
389    // Step 3. Let referencingScript be null.
390    rooted!(&in(cx) let mut script_private = UndefinedValue());
391
392    // Step 6.1. Set referencingScript to referrer.[[HostDefined]].
393    unsafe { JS_GetScriptPrivate(*referrer.as_ref(cx), script_private.handle_mut()) };
394    let referencing_script =
395        unsafe { module_script_from_reference_private(script_private.handle()) };
396
397    // Step 6. If referrer is a Script Record or a Cyclic Module Record, then:
398    let (original_fetch_options, fetch_referrer) = match referencing_script {
399        Some(module) => (
400            // Step 6.4. Set originalFetchOptions to referencingScript's fetch options.
401            module.options.clone(),
402            // Step 6.3. Set fetchReferrer to referencingScript's base URL.
403            Referrer::ReferrerUrl(module.base_url.clone()),
404        ),
405        None => (
406            // Step 4. Let originalFetchOptions be the default script fetch options.
407            ScriptFetchOptions::default_classic_script(),
408            // Step 5. Let fetchReferrer be "client".
409            global_scope.get_referrer(),
410        ),
411    };
412
413    // TODO: investigate providing a `ModuleOwner` to classic scripts.
414    let script_owner = referencing_script.and_then(|script| script.owner.clone());
415
416    // Step 6.2. Set settingsObject to referencingScript's settings object.
417    if let Some(ref owner) = script_owner {
418        global_scope = owner.root();
419    }
420
421    // Step 7. If referrer is a Cyclic Module Record and moduleRequest is equal to the first element of referrer.[[RequestedModules]], then:
422    // Note: Spidermonkey removed the API for iterating through a module's requested modules,
423    // preventing upfront validation.
424    // Additionally, we skip step 7.1.1 (handled internally by Spidermonkey in `InnerModuleLoading`),
425    // as well as steps 7.1.2 and 7.1.3 (executed later in steps 8 and 9).
426
427    // Step 7.1.4. Let moduleType be the result of running the module type from module request steps given requested.
428    let module_type = unsafe { GetModuleRequestType(cx, module_request) };
429
430    // Step 7.1.5. If the result of running the module type allowed steps given moduleType and settingsObject is false:
431    if !module_type_allowed(&global_scope, module_type) {
432        // Step 7.1.5.1. Let error be a new TypeError exception.
433        throw_type_error(cx, c"Found invalid module type attribute");
434
435        // Step 7.1.5.2. If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
436        // set loadState.[[ErrorToRethrow]] to error.
437        // Note: ErrorToRethrow is retrieved inside the rejection handler of loadingPromise.
438
439        // Step 7.1.5.3. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, ThrowCompletion(error)).
440        unsafe { FinishLoadingImportedModuleFailedWithPendingException(cx, payload) };
441
442        // Step 7.1.5.4. Return.
443        return;
444    }
445
446    // Step 8. Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]],
447    // catching any exceptions. If they throw an exception, let resolutionError be the thrown exception.
448    let url = ModuleTree::resolve_module_specifier(&global_scope, referencing_script, specifier);
449
450    // Step 9. If the previous step threw an exception, then:
451    if let Err(resolution_error) = url {
452        throw_dom_exception(cx, &global_scope, resolution_error);
453
454        // Step 9.1. If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
455        // set loadState.[[ErrorToRethrow]] to resolutionError.
456        // Note: ErrorToRethrow is retrieved inside the rejection handler of loadingPromise.
457
458        // Step 9.2. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, ThrowCompletion(resolutionError)).
459        unsafe { FinishLoadingImportedModuleFailedWithPendingException(cx, payload) };
460
461        // Step 9.3. Return.
462        return;
463    };
464
465    let url = ensure_blob_referenced_by_url_is_kept_alive(&global_scope, url.unwrap());
466
467    // Step 10. Let fetchOptions be the result of getting the descendant script fetch options given
468    // originalFetchOptions, url, and settingsObject.
469    let fetch_options = original_fetch_options.descendant_fetch_options(&url.url(), &global_scope);
470
471    // Step 13. If loadState is not undefined, then:
472    // Note: loadState is undefined only in dynamic imports
473    let (destination, fetch_client) = match load_state {
474        // Step 13.1. Set destination to loadState.[[Destination]].
475        // Step 13.2. Set fetchClient to loadState.[[FetchClient]].
476        Some(load_state) => (load_state.destination, load_state.fetch_client.clone()),
477        None => (
478            // Step 11. Let destination be "script".
479            Destination::Script,
480            // Step 12. Let fetchClient be settingsObject.
481            global_scope.request_client(Some(cx.no_gc())),
482        ),
483    };
484
485    let is_dynamic_import = load_state.is_none();
486    let request = ImportRequest {
487        referrer: RootedTraceableBox::from_box(Heap::boxed(*referrer.as_ref(cx))),
488        module_request: RootedTraceableBox::from_box(Heap::boxed(*module_request.as_ref(cx))),
489        payload: RootedTraceableBox::from_box(Heap::boxed(*payload.as_ref(cx))),
490    };
491
492    let on_single_fetch_complete =
493        move |cx: &mut JSContext, module_tree: Option<Rc<ModuleTree>>| {
494            let mut realm = CurrentRealm::assert(cx);
495            let cx = &mut realm;
496
497            // Step 1. Let completion be null.
498
499            match module_tree {
500                // Step 2. If moduleScript is null, then set completion to ThrowCompletion(a new TypeError).
501                None => {
502                    if is_dynamic_import {
503                        throw_type_error(cx, c"Module fetching failed");
504                        unsafe {
505                            FinishLoadingImportedModuleFailedWithPendingException(
506                                cx,
507                                request.payload.handle(),
508                            )
509                        };
510                    } else {
511                        // A null moduleScript means that a network error was encountered, and
512                        // state.[[ErrorToRethrow]] isn't set, so pass an undefined value.
513                        // See rejection handler of loadingPromise.
514                        rooted!(&in(cx) let error = UndefinedValue());
515                        unsafe {
516                            FinishLoadingImportedModuleFailed(
517                                cx,
518                                request.payload.handle(),
519                                error.handle(),
520                            )
521                        };
522                    }
523                },
524                Some(module_tree) => {
525                    // Step 3. Otherwise, if moduleScript's parse error is not null, then:
526                    // Step 3.1. Let parseError be moduleScript's parse error.
527                    if let Some(parse_error) = module_tree.get_parse_error() {
528                        // Step 3.3. If loadState is not undefined and loadState.[[ErrorToRethrow]]
529                        // is null, set loadState.[[ErrorToRethrow]] to parseError.
530                        // Note: ErrorToRethrow is retrieved inside the rejection handler of loadingPromise.
531
532                        // Step 3.2. Set completion to ThrowCompletion(parseError).
533                        // Step 5. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion).
534                        unsafe {
535                            FinishLoadingImportedModuleFailed(
536                                cx,
537                                request.payload.handle(),
538                                parse_error.handle(),
539                            )
540                        };
541                    } else {
542                        // Step 4. Otherwise, set completion to NormalCompletion(moduleScript's record).
543                        let completion = module_tree
544                            .get_record()
545                            .map(|module| module.handle())
546                            .unwrap();
547
548                        // Step 5. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion).
549                        finish_loading_imported_module(
550                            cx,
551                            request.referrer.handle(),
552                            request.module_request.handle(),
553                            request.payload.handle(),
554                            completion,
555                        );
556                    }
557                },
558            }
559        };
560
561    // Step 14 Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject,
562    // fetchReferrer, moduleRequest, and onSingleFetchComplete as defined below.
563    // If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
564    // Note: we don't have access to the requested `ModuleObject`, so we pass only its type.
565    fetch_a_single_imported_module_script(
566        cx,
567        url,
568        fetch_client,
569        &global_scope,
570        destination,
571        fetch_options,
572        fetch_referrer,
573        module_type,
574        on_single_fetch_complete,
575    );
576}
577
578/// <https://html.spec.whatwg.org/multipage/#module-type-allowed>
579fn module_type_allowed(global: &GlobalScope, module_type: ModuleType) -> bool {
580    match module_type {
581        // Step 1. If moduleType is not "javascript-or-wasm", "css", "json", or "text", then return
582        // false.
583        ModuleType::Unknown | ModuleType::Bytes => false,
584        // Step 2. If moduleType is "css" and the CSSStyleSheet interface is not exposed in
585        // settings's realm, then return false.
586        ModuleType::CSS => global.is::<Window>(),
587        // Step 3. Return true.
588        ModuleType::JavaScript | ModuleType::JSON | ModuleType::Text => true,
589    }
590}
591
592/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-imported-module-script>
593#[expect(clippy::too_many_arguments)]
594fn fetch_a_single_imported_module_script(
595    cx: &mut JSContext,
596    url: UrlWithBlobClaim,
597    fetch_client: RequestClient,
598    global: &GlobalScope,
599    destination: Destination,
600    options: ScriptFetchOptions,
601    referrer: Referrer,
602    module_type: ModuleType,
603    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
604) {
605    // TODO Step 1. Assert: moduleRequest.[[Attributes]] does not contain any Record entry such that entry.[[Key]] is not "type",
606    // because we only asked for "type" attributes in HostGetSupportedImportAttributes.
607
608    // TODO Step 2. Let moduleType be the result of running the module type from module request steps given moduleRequest.
609
610    // Step 3. If the result of running the module type allowed steps given moduleType and settingsObject is false,
611    // then run onComplete given null, and return.
612    match module_type {
613        ModuleType::Unknown | ModuleType::Bytes => {
614            return on_complete(cx, None);
615        },
616        ModuleType::JavaScript | ModuleType::JSON | ModuleType::CSS | ModuleType::Text => (),
617    }
618
619    // Step 4. Fetch a single module script given url, fetchClient, destination, options, settingsObject, referrer,
620    // moduleRequest, false, and onComplete. If performFetch was given, pass it along as well.
621    fetch_a_single_module_script(
622        cx,
623        url,
624        fetch_client,
625        global,
626        destination,
627        options,
628        referrer,
629        Some(module_type),
630        false,
631        Some(IntroductionType::IMPORTED_MODULE),
632        on_complete,
633    );
634}