script/
module_loading.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! An implementation of ecma262's [LoadRequestedModules](https://tc39.es/ecma262/#sec-LoadRequestedModules)
6//! Partly inspired by mozjs implementation: <https://searchfox.org/firefox-main/source/js/src/vm/Modules.cpp#1450>
7//! Since we can't access ModuleObject internals (eg. ModuleRequest records), we deviate from the spec in some aspects.
8
9#![expect(unsafe_code)]
10
11use std::cell::{Cell, RefCell};
12use std::collections::HashSet;
13use std::rc::Rc;
14
15use js::context::JSContext;
16use js::conversions::jsstr_to_string;
17use js::jsapi::{HandleValue as RawHandleValue, IsCyclicModule, JSObject, ModuleType};
18use js::jsval::{ObjectValue, UndefinedValue};
19use js::realm::{AutoRealm, CurrentRealm};
20use js::rust::wrappers2::{
21    GetModuleNamespace, GetRequestedModuleSpecifier, GetRequestedModuleType,
22    GetRequestedModulesCount, JS_GetModulePrivate, ModuleEvaluate, ModuleLink,
23};
24use js::rust::{HandleValue, IntoHandle};
25use net_traits::request::{Destination, Referrer};
26use script_bindings::settings_stack::run_a_callback;
27use servo_url::ServoUrl;
28
29use crate::DomTypeHolder;
30use crate::dom::bindings::error::Error;
31use crate::dom::bindings::refcounted::Trusted;
32use crate::dom::bindings::reflector::DomObject;
33use crate::dom::bindings::root::DomRoot;
34use crate::dom::globalscope::GlobalScope;
35use crate::dom::promise::Promise;
36use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
37use crate::realms::{InRealm, enter_auto_realm};
38use crate::script_module::{
39    ModuleFetchClient, ModuleHandler, ModuleObject, ModuleOwner, ModuleTree, RethrowError,
40    ScriptFetchOptions, fetch_a_single_module_script, gen_type_error,
41    module_script_from_reference_private,
42};
43use crate::script_runtime::{CanGc, IntroductionType};
44
45#[derive(JSTraceable, MallocSizeOf)]
46struct OnRejectedHandler {
47    #[conditional_malloc_size_of]
48    promise: Rc<Promise>,
49}
50
51impl Callback for OnRejectedHandler {
52    fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
53        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « reason »).
54        self.promise.reject(cx.into(), v, CanGc::from_cx(cx));
55    }
56}
57
58pub(crate) enum Payload {
59    GraphRecord(Rc<GraphLoadingState>),
60    PromiseRecord(Rc<Promise>),
61}
62
63#[derive(JSTraceable)]
64pub(crate) struct LoadState {
65    pub(crate) error_to_rethrow: RefCell<Option<RethrowError>>,
66    #[no_trace]
67    pub(crate) destination: Destination,
68    #[no_trace]
69    pub(crate) fetch_client: ModuleFetchClient,
70}
71
72/// <https://tc39.es/ecma262/#graphloadingstate-record>
73pub(crate) struct GraphLoadingState {
74    /// [[PromiseCapability]]
75    promise: Rc<Promise>,
76    /// [[IsLoading]]
77    is_loading: Cell<bool>,
78    /// [[PendingModulesCount]]
79    pending_modules_count: Cell<u32>,
80    /// [[Visited]]
81    visited: RefCell<HashSet<ServoUrl>>,
82    /// [[HostDefined]]
83    load_state: Option<Rc<LoadState>>,
84}
85
86/// <https://tc39.es/ecma262/#sec-LoadRequestedModules>
87pub(crate) fn load_requested_modules(
88    cx: &mut CurrentRealm,
89    module: Rc<ModuleTree>,
90    load_state: Option<Rc<LoadState>>,
91) -> Rc<Promise> {
92    // Step 1. If hostDefined is not present, let hostDefined be empty.
93    //
94    // Not required, since we implement it as an `Option`
95
96    // Step 2. Let pc be ! NewPromiseCapability(%Promise%).
97    let mut realm = CurrentRealm::assert(cx);
98    let promise = Promise::new_in_realm(&mut realm);
99
100    // Step 3. Let state be the GraphLoadingState Record
101    // { [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « », [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined }.
102    let state = GraphLoadingState {
103        promise: promise.clone(),
104        is_loading: Cell::new(true),
105        pending_modules_count: Cell::new(1),
106        visited: RefCell::new(HashSet::new()),
107        load_state,
108    };
109
110    // Step 4. Perform InnerModuleLoading(state, module).
111    inner_module_loading(cx, &Rc::new(state), module);
112
113    // Step 5. Return pc.[[Promise]].
114    promise
115}
116
117/// <https://tc39.es/ecma262/#sec-InnerModuleLoading>
118fn inner_module_loading(
119    cx: &mut CurrentRealm,
120    state: &Rc<GraphLoadingState>,
121    module: Rc<ModuleTree>,
122) {
123    // Step 1. Assert: state.[[IsLoading]] is true.
124    assert!(state.is_loading.get());
125
126    let module_handle = module.get_record().map(|module| module.handle()).unwrap();
127
128    let module_url = module.get_url();
129    let visited_contains_module = state.visited.borrow().contains(&module_url);
130
131    // Step 2. If module is a Cyclic Module Record, module.[[Status]] is new, and state.[[Visited]] does not contain module, then
132    // Note: mozjs doesn't expose a way to check the ModuleStatus of a ModuleObject.
133    if unsafe { IsCyclicModule(module_handle.get()) } && !visited_contains_module {
134        // a. Append module to state.[[Visited]].
135        state.visited.borrow_mut().insert(module_url);
136
137        // b. Let requestedModulesCount be the number of elements in module.[[RequestedModules]].
138        let requested_modules_count = unsafe { GetRequestedModulesCount(cx, module_handle) };
139
140        // c. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] + requestedModulesCount.
141        let pending_modules_count = state.pending_modules_count.get();
142        state
143            .pending_modules_count
144            .set(pending_modules_count + requested_modules_count);
145
146        // d. For each ModuleRequest Record request of module.[[RequestedModules]], do
147        for index in 0..requested_modules_count {
148            // i. If AllImportAttributesSupported(request.[[Attributes]]) is false, then
149            // Note: Gecko will call hasFirstUnsupportedAttributeKey on each module request,
150            // GetRequestedModuleSpecifier will do it for us.
151            // In addition it will also check if specifier has an unknown module type.
152            let jsstr = unsafe { GetRequestedModuleSpecifier(cx, module_handle, index) };
153
154            if jsstr.is_null() {
155                // 1. Let error be ThrowCompletion(a newly created SyntaxError object).
156                let error = RethrowError::from_pending_exception(cx.into());
157
158                // See Step 7. of `host_load_imported_module`.
159                state.load_state.as_ref().inspect(|load_state| {
160                    load_state
161                        .error_to_rethrow
162                        .borrow_mut()
163                        .get_or_insert(error.clone());
164                });
165
166                // 2. Perform ContinueModuleLoading(state, error).
167                continue_module_loading(cx, state, Err(error));
168            } else {
169                let specifier =
170                    unsafe { jsstr_to_string(cx.raw_cx(), std::ptr::NonNull::new(jsstr).unwrap()) };
171                let module_type = unsafe { GetRequestedModuleType(cx, module_handle, index) };
172
173                let realm = CurrentRealm::assert(cx);
174                let global = GlobalScope::from_current_realm(&realm);
175
176                // ii. Else if module.[[LoadedModules]] contains a LoadedModuleRequest Record record
177                // such that ModuleRequestsEqual(record, request) is true, then
178                let loaded_module =
179                    module.find_descendant_inside_module_map(&global, &specifier, module_type);
180
181                match loaded_module {
182                    // 1. Perform InnerModuleLoading(state, record.[[Module]]).
183                    Some(module) => inner_module_loading(cx, state, module),
184                    // iii. Else,
185                    None => {
186                        rooted!(&in(cx) let mut referrer = UndefinedValue());
187                        unsafe { JS_GetModulePrivate(module_handle.get(), referrer.handle_mut()) };
188
189                        // 1. Perform HostLoadImportedModule(module, request, state.[[HostDefined]], state).
190                        host_load_imported_module(
191                            cx,
192                            Some(module.clone()),
193                            referrer.handle().into_handle(),
194                            specifier,
195                            module_type,
196                            state.load_state.clone(),
197                            Payload::GraphRecord(state.clone()),
198                        );
199                    },
200                }
201            }
202
203            // iv. If state.[[IsLoading]] is false, return unused.
204            if !state.is_loading.get() {
205                return;
206            }
207        }
208    }
209
210    // Step 3. Assert: state.[[PendingModulesCount]] ≥ 1.
211    assert!(state.pending_modules_count.get() >= 1);
212
213    // Step 4. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] - 1.
214    let pending_modules_count = state.pending_modules_count.get();
215    state.pending_modules_count.set(pending_modules_count - 1);
216
217    // Step 5. If state.[[PendingModulesCount]] = 0, then
218    if state.pending_modules_count.get() == 0 {
219        // a. Set state.[[IsLoading]] to false.
220        state.is_loading.set(false);
221
222        // b. For each Cyclic Module Record loaded of state.[[Visited]], do
223        // i. If loaded.[[Status]] is new, set loaded.[[Status]] to unlinked.
224        // Note: mozjs defaults to the unlinked status.
225
226        // c. Perform ! Call(state.[[PromiseCapability]].[[Resolve]], undefined, « undefined »).
227        state.promise.resolve_native(&(), CanGc::from_cx(cx));
228    }
229
230    // Step 6. Return unused.
231}
232
233/// <https://tc39.es/ecma262/#sec-ContinueModuleLoading>
234fn continue_module_loading(
235    cx: &mut CurrentRealm,
236    state: &Rc<GraphLoadingState>,
237    module_completion: Result<Rc<ModuleTree>, RethrowError>,
238) {
239    // Step 1. If state.[[IsLoading]] is false, return unused.
240    if !state.is_loading.get() {
241        return;
242    }
243
244    match module_completion {
245        // Step 2. If moduleCompletion is a normal completion, then
246        // a. Perform InnerModuleLoading(state, moduleCompletion.[[Value]]).
247        Ok(module) => inner_module_loading(cx, state, module),
248
249        // Step 3. Else,
250        Err(exception) => {
251            // a. Set state.[[IsLoading]] to false.
252            state.is_loading.set(false);
253
254            // b. Perform ! Call(state.[[PromiseCapability]].[[Reject]], undefined, « moduleCompletion.[[Value]] »).
255            state
256                .promise
257                .reject(cx.into(), exception.handle(), CanGc::from_cx(cx));
258        },
259    }
260
261    // Step 4. Return unused.
262}
263
264/// <https://tc39.es/ecma262/#sec-FinishLoadingImportedModule>
265fn finish_loading_imported_module(
266    cx: &mut CurrentRealm,
267    referrer_module: Option<Rc<ModuleTree>>,
268    module_request_specifier: String,
269    payload: Payload,
270    result: Result<Rc<ModuleTree>, RethrowError>,
271) {
272    match payload {
273        // Step 2. If payload is a GraphLoadingState Record, then
274        Payload::GraphRecord(state) => {
275            let module_tree =
276                referrer_module.expect("Module must not be None in non dynamic imports");
277
278            // Step 1. If result is a normal completion, then
279            if let Ok(ref module) = result {
280                module_tree.insert_module_dependency(module, module_request_specifier);
281            }
282
283            // a. Perform ContinueModuleLoading(payload, result).
284            continue_module_loading(cx, &state, result);
285        },
286
287        // Step 3. Else,
288        // a. Perform ContinueDynamicImport(payload, result).
289        Payload::PromiseRecord(promise) => continue_dynamic_import(cx, promise, result),
290    }
291
292    // 4. Return unused.
293}
294
295/// <https://tc39.es/ecma262/#sec-ContinueDynamicImport>
296fn continue_dynamic_import(
297    cx: &mut CurrentRealm,
298    promise: Rc<Promise>,
299    module_completion: Result<Rc<ModuleTree>, RethrowError>,
300) {
301    // Step 1. If moduleCompletion is an abrupt completion, then
302    if let Err(exception) = module_completion {
303        // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « moduleCompletion.[[Value]] »).
304        promise.reject(cx.into(), exception.handle(), CanGc::from_cx(cx));
305
306        // b. Return unused.
307        return;
308    }
309
310    let realm = CurrentRealm::assert(cx);
311    let global = GlobalScope::from_current_realm(&realm);
312
313    // Step 2. Let module be moduleCompletion.[[Value]].
314    let module = module_completion.unwrap();
315    let record = ModuleObject::new(module.get_record().map(|module| module.handle()).unwrap());
316
317    // Step 3. Let loadPromise be module.LoadRequestedModules().
318    let load_promise = load_requested_modules(cx, module, None);
319
320    // Step 4. Let rejectedClosure be a new Abstract Closure with parameters (reason)
321    // that captures promiseCapability and performs the following steps when called:
322    // Step 5. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »).
323    // Note: implemented by OnRejectedHandler.
324
325    let global_scope = global.clone();
326    let inner_promise = promise.clone();
327    let fulfilled_promise = promise.clone();
328
329    // Step 6. Let linkAndEvaluateClosure be a new Abstract Closure with no parameters that captures
330    // module, promiseCapability, and onRejected and performs the following steps when called:
331    // Step 7. Let linkAndEvaluate be CreateBuiltinFunction(linkAndEvaluateClosure, 0, "", « »).
332    let link_and_evaluate = ModuleHandler::new_boxed(Box::new(
333        task!(link_and_evaluate: |cx, global_scope: DomRoot<GlobalScope>, inner_promise: Rc<Promise>, record: ModuleObject| {
334            let mut realm = AutoRealm::new(
335                cx,
336                std::ptr::NonNull::new(global_scope.reflector().get_jsobject().get()).unwrap(),
337            );
338            let in_realm_proof = (&mut realm.current_realm()).into();
339            let cx = &mut *realm;
340            // a. Let link be Completion(module.Link()).
341            let link = unsafe { ModuleLink(cx, record.handle()) };
342
343            // b. If link is an abrupt completion, then
344            if !link {
345                // i. Perform ! Call(promiseCapability.[[Reject]], undefined, « link.[[Value]] »).
346                let exception = RethrowError::from_pending_exception(cx.into());
347                inner_promise.reject(cx.into(), exception.handle(), CanGc::from_cx(cx));
348
349                // ii. Return NormalCompletion(undefined).
350                return;
351            }
352
353            rooted!(&in(cx) let mut rval = UndefinedValue());
354            rooted!(&in(cx) let mut evaluate_promise = std::ptr::null_mut::<JSObject>());
355
356            // c. Let evaluatePromise be module.Evaluate().
357            assert!(unsafe { ModuleEvaluate(cx, record.handle(), rval.handle_mut()) });
358
359            if !rval.is_object() {
360                let error = RethrowError::from_pending_exception(cx.into());
361                return inner_promise.reject(cx.into(), error.handle(), CanGc::from_cx(cx));
362            }
363            evaluate_promise.set(rval.to_object());
364            let evaluate_promise = Promise::new_with_js_promise(evaluate_promise.handle(), cx.into());
365
366            // d. Let fulfilledClosure be a new Abstract Closure with no parameters that captures
367            // module and promiseCapability and performs the following steps when called:
368            // e. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »).
369            let on_fulfilled = ModuleHandler::new_boxed(Box::new(
370                task!(on_fulfilled: |cx, fulfilled_promise: Rc<Promise>, record: ModuleObject| {
371                    rooted!(&in(cx) let mut rval: *mut JSObject = std::ptr::null_mut());
372                    rooted!(&in(cx) let mut namespace = UndefinedValue());
373
374                    // i. Let namespace be GetModuleNamespace(module).
375                    rval.set(unsafe { GetModuleNamespace(cx, record.handle()) });
376                    namespace.handle_mut().set(ObjectValue(rval.get()));
377
378                    // ii. Perform ! Call(promiseCapability.[[Resolve]], undefined, « namespace »).
379                    fulfilled_promise.resolve(cx.into(), namespace.handle(), CanGc::from_cx(cx));
380
381                    // iii. Return NormalCompletion(undefined).
382            })));
383
384            // f. Perform PerformPromiseThen(evaluatePromise, onFulfilled, onRejected).
385            let handler = PromiseNativeHandler::new(
386                &global_scope,
387                Some(on_fulfilled),
388                Some(Box::new(OnRejectedHandler {
389                    promise: inner_promise,
390                })),
391                CanGc::note(),
392            );
393            let in_realm = InRealm::Already(&in_realm_proof);
394            evaluate_promise.append_native_handler(&handler, in_realm, CanGc::from_cx(cx));
395
396            // g. Return unused.
397        }),
398    ));
399
400    let mut realm = enter_auto_realm(cx, &*global);
401    let mut realm = realm.current_realm();
402    run_a_callback::<DomTypeHolder, _>(&*global, || {
403        // Step 8. Perform PerformPromiseThen(loadPromise, linkAndEvaluate, onRejected).
404        let handler = PromiseNativeHandler::new(
405            &global,
406            Some(link_and_evaluate),
407            Some(Box::new(OnRejectedHandler {
408                promise: promise.clone(),
409            })),
410            CanGc::from_cx(&mut realm),
411        );
412        let in_realm_proof = (&mut realm).into();
413        let in_realm = InRealm::Already(&in_realm_proof);
414        load_promise.append_native_handler(&handler, in_realm, CanGc::from_cx(&mut realm));
415    });
416    // Step 9. Return unused.
417}
418
419/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
420pub(crate) fn host_load_imported_module(
421    cx: &mut CurrentRealm,
422    referrer_module: Option<Rc<ModuleTree>>,
423    referrer: RawHandleValue,
424    specifier: String,
425    module_type: ModuleType,
426    load_state: Option<Rc<LoadState>>,
427    payload: Payload,
428) {
429    // Step 1. Let settingsObject be the current settings object.
430    let realm = CurrentRealm::assert(cx);
431    let mut global_scope = GlobalScope::from_current_realm(&realm);
432
433    // TODO Step 2. If settingsObject's global object implements WorkletGlobalScope or ServiceWorkerGlobalScope and loadState is undefined, then:
434
435    // Step 3. Let referencingScript be null.
436    // Step 6.1. Set referencingScript to referrer.[[HostDefined]].
437    let referencing_script = unsafe { module_script_from_reference_private(&referrer) };
438
439    // Step 6. If referrer is a Script Record or a Cyclic Module Record, then:
440    let (original_fetch_options, fetch_referrer) = match referencing_script {
441        Some(module) => (
442            // Step 6.4. Set originalFetchOptions to referencingScript's fetch options.
443            module.options.clone(),
444            // Step 6.3. Set fetchReferrer to referencingScript's base URL.
445            Referrer::ReferrerUrl(module.base_url.clone()),
446        ),
447        None => (
448            // Step 4. Let originalFetchOptions be the default script fetch options.
449            ScriptFetchOptions::default_classic_script(),
450            // Step 5. Let fetchReferrer be "client".
451            global_scope.get_referrer(),
452        ),
453    };
454
455    // TODO: investigate providing a `ModuleOwner` to classic scripts.
456    let script_owner = referencing_script.and_then(|script| script.owner.clone());
457
458    // Step 6.2. Set settingsObject to referencingScript's settings object.
459    if let Some(ref owner) = script_owner {
460        global_scope = owner.global();
461    }
462
463    // Note: loadState is undefined when performing a dynamic import, fall back to `ModuleOwner::DynamicModule`.
464    let owner = script_owner
465        .filter(|_| load_state.is_some())
466        .unwrap_or(ModuleOwner::DynamicModule(Trusted::new(&global_scope)));
467
468    // Step 7 If referrer is a Cyclic Module Record and moduleRequest is equal to the first element of referrer.[[RequestedModules]], then:
469    // Note: These substeps are implemented by `GetRequestedModuleSpecifier`,
470    // setting loadState.[[ErrorToRethrow]] is done by `inner_module_loading`.
471
472    // Step 8 Let url be the result of resolving a module specifier given referencingScript and moduleRequest.[[Specifier]],
473    // catching any exceptions. If they throw an exception, let resolutionError be the thrown exception.
474    let url = ModuleTree::resolve_module_specifier(
475        &global_scope,
476        referencing_script,
477        specifier.clone().into(),
478    );
479
480    // Step 9 If the previous step threw an exception, then:
481    if let Err(error) = url {
482        let resolution_error = gen_type_error(&global_scope, error, CanGc::from_cx(cx));
483
484        // Step 9.1. If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
485        // set loadState.[[ErrorToRethrow]] to resolutionError.
486        load_state.as_ref().inspect(|load_state| {
487            load_state
488                .error_to_rethrow
489                .borrow_mut()
490                .get_or_insert(resolution_error.clone());
491        });
492
493        // Step 9.2. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, ThrowCompletion(resolutionError)).
494        finish_loading_imported_module(
495            cx,
496            referrer_module,
497            specifier,
498            payload,
499            Err(resolution_error),
500        );
501
502        // Step 9.3. Return.
503        return;
504    };
505
506    let url = url.unwrap();
507
508    // Step 10. Let fetchOptions be the result of getting the descendant script fetch options given
509    // originalFetchOptions, url, and settingsObject.
510    let fetch_options = original_fetch_options.descendant_fetch_options(&url, &global_scope);
511
512    // Step 13. If loadState is not undefined, then:
513    // Note: loadState is undefined only in dynamic imports
514    let (destination, fetch_client) = match load_state.as_ref() {
515        // Step 13.1. Set destination to loadState.[[Destination]].
516        // Step 13.2. Set fetchClient to loadState.[[FetchClient]].
517        Some(load_state) => (load_state.destination, load_state.fetch_client.clone()),
518        None => (
519            // Step 11. Let destination be "script".
520            Destination::Script,
521            // Step 12. Let fetchClient be settingsObject.
522            ModuleFetchClient::from_global_scope(&global_scope),
523        ),
524    };
525
526    let on_single_fetch_complete =
527        move |cx: &mut JSContext, module_tree: Option<Rc<ModuleTree>>| {
528            let mut realm = CurrentRealm::assert(cx);
529            let cx = &mut realm;
530
531            // Step 1. Let completion be null.
532            let completion = match module_tree {
533                // Step 2. If moduleScript is null, then set completion to ThrowCompletion(a new TypeError).
534                None => Err(gen_type_error(
535                    &global_scope,
536                    Error::Type(c"Module fetching failed".to_owned()),
537                    CanGc::from_cx(cx),
538                )),
539                Some(module_tree) => {
540                    // Step 3. Otherwise, if moduleScript's parse error is not null, then:
541                    // Step 3.1 Let parseError be moduleScript's parse error.
542                    if let Some(parse_error) = module_tree.get_parse_error() {
543                        // Step 3.3 If loadState is not undefined and loadState.[[ErrorToRethrow]] is null,
544                        // set loadState.[[ErrorToRethrow]] to parseError.
545                        load_state.as_ref().inspect(|load_state| {
546                            load_state
547                                .error_to_rethrow
548                                .borrow_mut()
549                                .get_or_insert(parse_error.clone());
550                        });
551
552                        // Step 3.2 Set completion to ThrowCompletion(parseError).
553                        Err(parse_error.clone())
554                    } else {
555                        // Step 4. Otherwise, set completion to NormalCompletion(moduleScript's record).
556                        Ok(module_tree)
557                    }
558                },
559            };
560
561            // Step 5. Perform FinishLoadingImportedModule(referrer, moduleRequest, payload, completion).
562            finish_loading_imported_module(cx, referrer_module, specifier, payload, completion);
563        };
564
565    // Step 14 Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject,
566    // fetchReferrer, moduleRequest, and onSingleFetchComplete as defined below.
567    // If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
568    // Note: we don't have access to the requested `ModuleObject`, so we pass only its type.
569    fetch_a_single_imported_module_script(
570        cx,
571        url,
572        fetch_client,
573        owner,
574        destination,
575        fetch_options,
576        fetch_referrer,
577        module_type,
578        on_single_fetch_complete,
579    );
580}
581
582/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-imported-module-script>
583#[expect(clippy::too_many_arguments)]
584fn fetch_a_single_imported_module_script(
585    cx: &mut JSContext,
586    url: ServoUrl,
587    fetch_client: ModuleFetchClient,
588    owner: ModuleOwner,
589    destination: Destination,
590    options: ScriptFetchOptions,
591    referrer: Referrer,
592    module_type: ModuleType,
593    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
594) {
595    // TODO Step 1. Assert: moduleRequest.[[Attributes]] does not contain any Record entry such that entry.[[Key]] is not "type",
596    // because we only asked for "type" attributes in HostGetSupportedImportAttributes.
597
598    // TODO Step 2. Let moduleType be the result of running the module type from module request steps given moduleRequest.
599
600    // Step 3. If the result of running the module type allowed steps given moduleType and settingsObject is false,
601    // then run onComplete given null, and return.
602    match module_type {
603        ModuleType::Unknown => return on_complete(cx, None),
604        ModuleType::JavaScript | ModuleType::JSON => (),
605    }
606
607    // Step 4. Fetch a single module script given url, fetchClient, destination, options, settingsObject, referrer,
608    // moduleRequest, false, and onComplete. If performFetch was given, pass it along as well.
609    fetch_a_single_module_script(
610        cx,
611        url,
612        fetch_client,
613        owner,
614        destination,
615        options,
616        referrer,
617        Some(module_type),
618        false,
619        Some(IntroductionType::IMPORTED_MODULE),
620        on_complete,
621    );
622}