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