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