Skip to main content

script/modules/
script_module.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//! The script module mod contains common traits and structs
6//! related to `type=module` for script thread or worker threads.
7
8use std::borrow::Cow;
9use std::cell::{OnceCell, RefCell};
10use std::collections::hash_map::Entry;
11use std::ffi::CStr;
12use std::fmt::Debug;
13use std::ptr::NonNull;
14use std::rc::Rc;
15use std::{mem, ptr};
16
17use encoding_rs::UTF_8;
18use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
19use hyper_serde::Serde;
20use indexmap::IndexMap;
21use indexmap::map::Entry as IndexMapEntry;
22use js::context::JSContext;
23use js::conversions::{ToJSValConvertible, jsstr_to_string};
24use js::gc::{HandleObject, MutableHandleValue};
25use js::jsapi::{
26    CallArgs, ExceptionStackBehavior, GetFunctionNativeReserved, GetModuleResolveHook,
27    Handle as RawHandle, HandleValue as RawHandleValue, Heap, JS_GetFunctionObject,
28    JSContext as RawJSContext, JSObject, JSPROP_ENUMERATE, JSRuntime, ModuleErrorBehaviour,
29    ModuleType, SetFunctionNativeReserved, SetModuleDynamicImportHook, SetModuleMetadataHook,
30    SetModulePrivate, SetModuleResolveHook, SetScriptPrivateReferenceHooks, Value,
31};
32use js::jsval::{JSVal, PrivateValue, UndefinedValue};
33use js::realm::{AutoRealm, CurrentRealm};
34use js::rust::wrappers2::{
35    CompileJsonModule1, CompileModule1, DefineFunctionWithReserved, GetModuleRequestSpecifier,
36    GetModuleRequestType, JS_ClearPendingException, JS_DefineProperty4, JS_GetPendingException,
37    JS_NewStringCopyN, JS_SetPendingException, ModuleEvaluate, ModuleLink,
38    ThrowOnModuleEvaluationFailure,
39};
40use js::rust::{Handle, HandleValue, ToString, transform_str_to_source_text};
41use mime::Mime;
42use net_traits::blob_url_store::UrlWithBlobClaim;
43use net_traits::http_status::HttpStatus;
44use net_traits::mime_classifier::MimeClassifier;
45use net_traits::policy_container::PolicyContainer;
46use net_traits::request::{
47    CredentialsMode, Destination, ParserMetadata, Referrer, RequestBuilder, RequestClient,
48    RequestId, RequestMode,
49};
50use net_traits::{FetchMetadata, Metadata, NetworkError, ReferrerPolicy, ResourceFetchTiming};
51use script_bindings::cell::DomRefCell;
52use script_bindings::error::Fallible;
53use script_bindings::reflector::DomObject;
54use script_bindings::settings_stack::run_a_callback;
55use script_bindings::trace::CustomTraceable;
56use servo_config::pref;
57use servo_url::ServoUrl;
58
59use crate::DomTypeHolder;
60use crate::dom::bindings::error::{Error, ErrorToJsval, report_pending_exception};
61use crate::dom::bindings::inheritance::Castable;
62use crate::dom::bindings::refcounted::Trusted;
63use crate::dom::bindings::root::DomRoot;
64use crate::dom::bindings::str::DOMString;
65use crate::dom::bindings::trace::RootedTraceableBox;
66use crate::dom::csp::{GlobalCspReporting, Violation};
67use crate::dom::globalscope::GlobalScope;
68use crate::dom::globalscope::script_execution::{ErrorReporting, fill_compile_options};
69use crate::dom::html::htmlscriptelement::{SCRIPT_JS_MIMES, substitute_with_local_script};
70use crate::dom::performance::performanceresourcetiming::InitiatorType;
71use crate::dom::promise::Promise;
72use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
73use crate::dom::types::{
74    DedicatedWorkerGlobalScope, SharedWorkerGlobalScope, WorkerGlobalScope, WorkletGlobalScope,
75};
76use crate::dom::window::Window;
77use crate::modules::import_map::{ModuleSpecifierMap, resolve_url_like_module_specifier};
78use crate::modules::module_loading::{
79    LoadState, Payload, host_load_imported_module, load_requested_modules,
80};
81use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
82use crate::realms::enter_auto_realm;
83use crate::script_runtime::IntroductionType;
84use crate::tasks::task::NonSendTaskBox;
85
86pub(crate) fn gen_type_error(
87    cx: &mut JSContext,
88    global: &GlobalScope,
89    error: Error,
90) -> RethrowError {
91    rooted!(&in(cx) let mut thrown = UndefinedValue());
92    error.to_jsval(cx, global, thrown.handle_mut());
93
94    RethrowError(RootedTraceableBox::from_box(Heap::boxed(thrown.get())))
95}
96
97#[derive(JSTraceable)]
98pub(crate) struct ModuleObject(RootedTraceableBox<Heap<*mut JSObject>>);
99
100impl ModuleObject {
101    pub(crate) fn new(obj: HandleObject) -> ModuleObject {
102        ModuleObject(RootedTraceableBox::from_box(Heap::boxed(obj.get())))
103    }
104
105    pub(crate) fn handle(&'_ self) -> HandleObject<'_> {
106        self.0.handle()
107    }
108}
109
110#[derive(JSTraceable)]
111pub(crate) struct RethrowError(RootedTraceableBox<Heap<JSVal>>);
112
113impl RethrowError {
114    pub(crate) fn new(val: Box<Heap<JSVal>>) -> Self {
115        Self(RootedTraceableBox::from_box(val))
116    }
117
118    #[expect(unsafe_code)]
119    pub(crate) fn from_pending_exception(cx: &mut JSContext) -> Self {
120        rooted!(&in(cx) let mut exception = UndefinedValue());
121        assert!(unsafe { JS_GetPendingException(cx, exception.handle_mut()) });
122        unsafe { JS_ClearPendingException(cx) };
123
124        Self::new(Heap::boxed(exception.get()))
125    }
126
127    pub(crate) fn handle(&self) -> Handle<'_, JSVal> {
128        self.0.handle()
129    }
130}
131
132impl Debug for RethrowError {
133    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
134        "RethrowError(...)".fmt(fmt)
135    }
136}
137
138impl Clone for RethrowError {
139    fn clone(&self) -> Self {
140        Self(RootedTraceableBox::from_box(Heap::boxed(self.0.get())))
141    }
142}
143
144pub(crate) struct ModuleScript {
145    pub(crate) base_url: ServoUrl,
146    pub(crate) options: ScriptFetchOptions,
147    pub(crate) owner: Option<Trusted<GlobalScope>>,
148}
149
150impl ModuleScript {
151    pub(crate) fn new(
152        base_url: ServoUrl,
153        options: ScriptFetchOptions,
154        owner: Option<Trusted<GlobalScope>>,
155    ) -> Self {
156        ModuleScript {
157            base_url,
158            options,
159            owner,
160        }
161    }
162}
163
164pub(crate) type ModuleRequest = (ServoUrl, ModuleType);
165
166#[derive(JSTraceable)]
167pub(crate) enum ModuleStatus {
168    #[expect(clippy::type_complexity)]
169    Fetching(#[no_trace] Vec<Box<dyn FnOnce(&mut JSContext, Option<Rc<ModuleTree>>)>>),
170    Loaded(Rc<ModuleTree>),
171}
172
173#[derive(JSTraceable, MallocSizeOf)]
174pub(crate) struct ModuleTree {
175    #[no_trace]
176    url: ServoUrl,
177    #[ignore_malloc_size_of = "mozjs"]
178    record: OnceCell<ModuleObject>,
179    #[ignore_malloc_size_of = "mozjs"]
180    parse_error: OnceCell<RethrowError>,
181    #[ignore_malloc_size_of = "mozjs"]
182    rethrow_error: DomRefCell<Option<RethrowError>>,
183    #[no_trace]
184    loaded_modules: DomRefCell<IndexMap<String, ServoUrl>>,
185}
186
187impl ModuleTree {
188    pub(crate) fn get_url(&self) -> ServoUrl {
189        self.url.clone()
190    }
191
192    pub(crate) fn get_record(&self) -> Option<&ModuleObject> {
193        self.record.get()
194    }
195
196    pub(crate) fn get_parse_error(&self) -> Option<&RethrowError> {
197        self.parse_error.get()
198    }
199
200    pub(crate) fn get_rethrow_error(&self) -> &DomRefCell<Option<RethrowError>> {
201        &self.rethrow_error
202    }
203
204    pub(crate) fn set_rethrow_error(&self, rethrow_error: RethrowError) {
205        *self.rethrow_error.borrow_mut() = Some(rethrow_error);
206    }
207
208    pub(crate) fn find_descendant_inside_module_map(
209        &self,
210        global: &GlobalScope,
211        specifier: &String,
212        module_type: ModuleType,
213    ) -> Option<Rc<ModuleTree>> {
214        self.loaded_modules
215            .borrow()
216            .get(specifier)
217            .and_then(|url| global.module_tree_for_request_if_loaded(&(url.clone(), module_type)))
218    }
219
220    pub(crate) fn insert_module_dependency(
221        &self,
222        module: &Rc<ModuleTree>,
223        module_request_specifier: String,
224    ) {
225        // Store the url which is used to retrieve the module from module map when needed.
226        let url = module.url.clone();
227        match self
228            .loaded_modules
229            .borrow_mut()
230            .entry(module_request_specifier)
231        {
232            // a. If referrer.[[LoadedModules]] contains a LoadedModuleRequest Record record such that
233            // ModuleRequestsEqual(record, moduleRequest) is true, then
234            IndexMapEntry::Occupied(entry) => {
235                // i. Assert: record.[[Module]] and result.[[Value]] are the same Module Record.
236                assert_eq!(*entry.get(), url);
237            },
238            // b. Else,
239            IndexMapEntry::Vacant(entry) => {
240                // i. Append the LoadedModuleRequest Record { [[Specifier]]: moduleRequest.[[Specifier]],
241                // [[Attributes]]: moduleRequest.[[Attributes]], [[Module]]: result.[[Value]] } to referrer.[[LoadedModules]].
242                entry.insert(url);
243            },
244        }
245    }
246}
247
248pub(crate) struct ModuleSource<'a> {
249    pub source: Cow<'a, str>,
250    pub unminified_dir: Option<String>,
251    pub external: bool,
252    pub url: ServoUrl,
253}
254
255impl<'a> crate::unminify::ScriptSource for ModuleSource<'a> {
256    fn unminified_dir(&self) -> Option<String> {
257        self.unminified_dir.clone()
258    }
259
260    fn extract_bytes(&self) -> &[u8] {
261        self.source.as_bytes()
262    }
263
264    fn rewrite_source(&mut self, source: String) {
265        self.source = source.into();
266    }
267
268    fn url(&self) -> ServoUrl {
269        self.url.clone()
270    }
271
272    fn is_external(&self) -> bool {
273        self.external
274    }
275}
276
277impl ModuleTree {
278    #[expect(unsafe_code)]
279    #[expect(clippy::too_many_arguments)]
280    /// <https://html.spec.whatwg.org/multipage/#creating-a-javascript-module-script>
281    fn create_a_javascript_module_script(
282        cx: &mut JSContext,
283        source: Cow<'_, str>,
284        global: &GlobalScope,
285        url: &ServoUrl,
286        options: ScriptFetchOptions,
287        external: bool,
288        line_number: u32,
289        introduction_type: Option<&'static CStr>,
290    ) -> Self {
291        let mut realm = AutoRealm::new(
292            cx,
293            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
294        );
295        let cx = &mut *realm;
296
297        let owner = Trusted::new(global);
298
299        // Step 2. Let script be a new module script that this algorithm will subsequently initialize.
300        // Step 6. Set script's parse error and error to rethrow to null.
301        let module = ModuleTree {
302            url: url.clone(),
303            record: OnceCell::new(),
304            parse_error: OnceCell::new(),
305            rethrow_error: DomRefCell::new(None),
306            loaded_modules: DomRefCell::new(IndexMap::new()),
307        };
308
309        let compile_options = fill_compile_options(
310            cx,
311            url.as_str(),
312            introduction_type,
313            ErrorReporting::Unmuted,
314            true, // noScriptRval
315            line_number,
316        );
317
318        let mut source = if global.unminified_js_dir().is_some() {
319            let mut module_source = ModuleSource {
320                source,
321                unminified_dir: global.unminified_js_dir(),
322                external,
323                url: url.clone(),
324            };
325            crate::unminify::unminify_js(&mut module_source);
326            transform_str_to_source_text(&module_source.source)
327        } else {
328            transform_str_to_source_text(&source)
329        };
330
331        unsafe {
332            // Step 7. Let result be ParseModule(source, settings's realm, script).
333            rooted!(&in(cx) let mut module_script: *mut JSObject = std::ptr::null_mut());
334            module_script.set(CompileModule1(cx, compile_options.ptr, &mut source));
335
336            // Step 8. If result is a list of errors, then:
337            if module_script.is_null() {
338                warn!("fail to compile module script of {}", url);
339
340                // Step 8.1. Set script's parse error to result[0].
341                let _ = module
342                    .parse_error
343                    .set(RethrowError::from_pending_exception(cx));
344
345                // Step 8.2. Return script.
346                return module;
347            }
348
349            // Step 3. Set script's settings object to settings.
350            // Step 4. Set script's base URL to baseURL.
351            // Step 5. Set script's fetch options to options.
352            let module_script_data = Rc::new(ModuleScript::new(url.clone(), options, Some(owner)));
353
354            SetModulePrivate(
355                module_script.get(),
356                &PrivateValue(Rc::into_raw(module_script_data) as *const _),
357            );
358
359            // Step 9. Set script's record to result.
360            let _ = module.record.set(ModuleObject::new(module_script.handle()));
361        }
362
363        // Step 10. Return script.
364        module
365    }
366
367    #[expect(unsafe_code)]
368    /// <https://html.spec.whatwg.org/multipage/#creating-a-json-module-script>
369    fn create_a_json_module_script(
370        cx: &mut JSContext,
371        source: &str,
372        global: &GlobalScope,
373        url: &ServoUrl,
374        introduction_type: Option<&'static CStr>,
375    ) -> Self {
376        let mut realm = AutoRealm::new(
377            cx,
378            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
379        );
380        let cx = &mut *realm;
381
382        // Step 1. Let script be a new module script that this algorithm will subsequently initialize.
383        // Step 4. Set script's parse error and error to rethrow to null.
384        let module = ModuleTree {
385            url: url.clone(),
386            record: OnceCell::new(),
387            parse_error: OnceCell::new(),
388            rethrow_error: DomRefCell::new(None),
389            loaded_modules: DomRefCell::new(IndexMap::new()),
390        };
391
392        // Step 2. Set script's settings object to settings.
393        // Step 3. Set script's base URL and fetch options to null.
394        // Note: We don't need to call `SetModulePrivate` for json scripts
395
396        let compile_options = fill_compile_options(
397            cx,
398            url.as_str(),
399            introduction_type,
400            ErrorReporting::Unmuted,
401            true, // noScriptRval
402            1,    // lineno
403        );
404
405        rooted!(&in(cx) let mut module_script: *mut JSObject = std::ptr::null_mut());
406
407        unsafe {
408            // Step 5. Let result be ParseJSONModule(source).
409            module_script.set(CompileJsonModule1(
410                cx,
411                compile_options.ptr,
412                &mut transform_str_to_source_text(source),
413            ));
414        }
415
416        // If this throws an exception, catch it, and set script's parse error to that exception, and return script.
417        if module_script.is_null() {
418            warn!("fail to compile module script of {}", url);
419
420            let _ = module
421                .parse_error
422                .set(RethrowError::from_pending_exception(cx));
423            return module;
424        }
425
426        // Step 6. Set script's record to result.
427        let _ = module.record.set(ModuleObject::new(module_script.handle()));
428
429        // Step 7. Return script.
430        module
431    }
432
433    /// Execute the provided module, storing the evaluation return value in the provided
434    /// mutable handle.
435    #[expect(unsafe_code)]
436    pub(crate) fn execute_module(
437        &self,
438        cx: &mut JSContext,
439        global: &GlobalScope,
440        module_record: HandleObject,
441        mut eval_result: MutableHandleValue,
442    ) -> Result<(), RethrowError> {
443        let mut realm = AutoRealm::new(
444            cx,
445            NonNull::new(global.reflector().get_jsobject().get()).unwrap(),
446        );
447        let cx = &mut *realm;
448
449        unsafe {
450            let ok = ModuleEvaluate(cx, module_record, eval_result.reborrow());
451            assert!(ok, "module evaluation failed");
452
453            rooted!(&in(cx) let mut evaluation_promise = ptr::null_mut::<JSObject>());
454            if eval_result.is_object() {
455                evaluation_promise.set(eval_result.to_object());
456            }
457
458            let throw_result = ThrowOnModuleEvaluationFailure(
459                cx,
460                evaluation_promise.handle(),
461                ModuleErrorBehaviour::ThrowModuleErrorsSync,
462            );
463            if !throw_result {
464                warn!("fail to evaluate module");
465
466                Err(RethrowError::from_pending_exception(cx))
467            } else {
468                debug!("module evaluated successfully");
469                Ok(())
470            }
471        }
472    }
473
474    #[expect(unsafe_code)]
475    pub(crate) fn report_error(&self, cx: &mut JSContext, global: &GlobalScope) {
476        let module_error = self.rethrow_error.borrow();
477
478        if let Some(exception) = &*module_error {
479            let mut realm = enter_auto_realm(cx, global);
480            let cx = &mut realm.current_realm();
481
482            unsafe {
483                JS_SetPendingException(cx, exception.handle(), ExceptionStackBehavior::Capture);
484            }
485            report_pending_exception(cx);
486        }
487    }
488
489    /// <https://html.spec.whatwg.org/multipage/#resolve-a-module-specifier>
490    pub(crate) fn resolve_module_specifier(
491        global: &GlobalScope,
492        script: Option<&ModuleScript>,
493        specifier: DOMString,
494    ) -> Fallible<ServoUrl> {
495        // Step 1~3 to get settingsObject and baseURL
496        let script_global = script.and_then(|s| s.owner.as_ref().map(|o| o.root()));
497        // Step 1. Let settingsObject and baseURL be null.
498        let (global, base_url): (&GlobalScope, &ServoUrl) = match script {
499            // Step 2. If referringScript is not null, then:
500            // Set settingsObject to referringScript's settings object.
501            // Set baseURL to referringScript's base URL.
502            Some(s) => (script_global.as_ref().map_or(global, |g| g), &s.base_url),
503            // Step 3. Otherwise:
504            // Set settingsObject to the current settings object.
505            // Set baseURL to settingsObject's API base URL.
506            // FIXME(#37553): Is this the correct current settings object?
507            None => (global, &global.api_base_url()),
508        };
509
510        // Step 4. Let importMap be an empty import map.
511        // Step 5. If settingsObject's global object implements Window, then set importMap to settingsObject's
512        // global object's import map.
513        let import_map = if global.is::<Window>() {
514            Some(global.import_map())
515        } else {
516            None
517        };
518        let specifier = &specifier.str();
519
520        // Step 6. Let serializedBaseURL be baseURL, serialized.
521        let serialized_base_url = base_url.as_str();
522        // Step 7. Let asURL be the result of resolving a URL-like module specifier given specifier and baseURL.
523        let as_url = resolve_url_like_module_specifier(specifier, base_url);
524        // Step 8. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null;
525        // otherwise, specifier.
526        let normalized_specifier = match &as_url {
527            Some(url) => url.as_str(),
528            None => specifier,
529        };
530
531        // Step 9. Let result be a URL-or-null, initially null.
532        let mut result = None;
533        if let Some(map) = import_map {
534            // Step 10. For each scopePrefix → scopeImports of importMap's scopes:
535            for (prefix, imports) in &map.scopes {
536                // Step 10.1 If scopePrefix is serializedBaseURL, or if scopePrefix ends with U+002F (/)
537                // and scopePrefix is a code unit prefix of serializedBaseURL, then:
538                let prefix = prefix.as_str();
539                if prefix == serialized_base_url ||
540                    (serialized_base_url.starts_with(prefix) && prefix.ends_with('\u{002f}'))
541                {
542                    // Step 10.1.1 Let scopeImportsMatch be the result of resolving an imports match
543                    // given normalizedSpecifier, asURL, and scopeImports.
544                    let scope_imports_match =
545                        resolve_imports_match(normalized_specifier, as_url.as_ref(), imports)?;
546
547                    // Step 10.1.2 If scopeImportsMatch is not null, then set result to scopeImportsMatch, and break.
548                    if scope_imports_match.is_some() {
549                        result = scope_imports_match;
550                        break;
551                    }
552                }
553            }
554
555            // Step 11. If result is null, set result to the result of resolving an imports match given
556            // normalizedSpecifier, asURL, and importMap's imports.
557            if result.is_none() {
558                result =
559                    resolve_imports_match(normalized_specifier, as_url.as_ref(), &map.imports)?;
560            }
561        }
562
563        // Step 12. If result is null, set it to asURL.
564        if result.is_none() {
565            result = as_url.clone();
566        }
567
568        // Step 13. If result is not null, then:
569        match result {
570            Some(result) => {
571                // Step 13.1 Add module to resolved module set given settingsObject, serializedBaseURL,
572                // normalizedSpecifier, and asURL.
573                global.add_module_to_resolved_module_set(
574                    serialized_base_url,
575                    normalized_specifier,
576                    as_url.clone(),
577                );
578                // Step 13.2 Return result.
579                Ok(result)
580            },
581            // Step 14. Throw a TypeError indicating that specifier was a bare specifier,
582            // but was not remapped to anything by importMap.
583            None => Err(Error::Type(
584                c"Specifier was a bare specifier, but was not remapped to anything by importMap."
585                    .to_owned(),
586            )),
587        }
588    }
589}
590
591#[derive(JSTraceable, MallocSizeOf)]
592pub(crate) struct ModuleHandler {
593    #[ignore_malloc_size_of = "Measuring trait objects is hard"]
594    task: DomRefCell<Option<Box<dyn NonSendTaskBox>>>,
595}
596
597impl ModuleHandler {
598    pub(crate) fn new_boxed(task: Box<dyn NonSendTaskBox>) -> Box<dyn Callback> {
599        Box::new(Self {
600            task: DomRefCell::new(Some(task)),
601        })
602    }
603}
604
605impl Callback for ModuleHandler {
606    fn callback(&self, cx: &mut CurrentRealm, _v: HandleValue) {
607        let task = self.task.borrow_mut().take().unwrap();
608        task.run_box(cx);
609    }
610}
611
612/// The context required for asynchronously loading an external module script source.
613struct ModuleContext {
614    /// The owner of the module that initiated the request.
615    owner: Trusted<GlobalScope>,
616    /// The response body received to date.
617    data: Vec<u8>,
618    /// The response metadata received to date.
619    metadata: Option<Metadata>,
620    /// Url and type of the requested module.
621    module_request: ModuleRequest,
622    /// Options for the current script fetch
623    options: ScriptFetchOptions,
624    /// Indicates whether the request failed, and why
625    status: Result<(), NetworkError>,
626    /// `introductionType` value to set in the `CompileOptionsWrapper`.
627    introduction_type: Option<&'static CStr>,
628    /// <https://html.spec.whatwg.org/multipage/#policy-container>
629    policy_container: Option<PolicyContainer>,
630}
631
632impl FetchResponseListener for ModuleContext {
633    // TODO(cybai): Perhaps add custom steps to perform fetch here?
634    fn process_request_body(&mut self, _: RequestId) {}
635
636    fn process_response(
637        &mut self,
638        _: &mut js::context::JSContext,
639        _: RequestId,
640        metadata: Result<FetchMetadata, NetworkError>,
641    ) {
642        self.metadata = metadata.ok().map(|meta| match meta {
643            FetchMetadata::Unfiltered(m) => m,
644            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
645        });
646
647        let status = self
648            .metadata
649            .as_ref()
650            .map(|m| m.status.clone())
651            .unwrap_or_else(HttpStatus::new_error);
652
653        self.status = {
654            if status.is_error() {
655                Err(NetworkError::ResourceLoadError(
656                    "No http status code received".to_owned(),
657                ))
658            } else if status.is_success() {
659                Ok(())
660            } else {
661                Err(NetworkError::ResourceLoadError(format!(
662                    "HTTP error code {}",
663                    status.code()
664                )))
665            }
666        };
667    }
668
669    fn process_response_chunk(
670        &mut self,
671        _: &mut js::context::JSContext,
672        _: RequestId,
673        mut chunk: Vec<u8>,
674    ) {
675        if self.status.is_ok() {
676            self.data.append(&mut chunk);
677        }
678    }
679
680    /// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
681    /// Step 13
682    fn process_response_eof(
683        mut self,
684        cx: &mut js::context::JSContext,
685        _: RequestId,
686        response: Result<(), NetworkError>,
687        timing: ResourceFetchTiming,
688    ) {
689        let global = self.owner.root();
690        let (_url, module_type) = &self.module_request;
691
692        if !global.is::<WorkletGlobalScope>() {
693            network_listener::submit_timing(cx, &self, &response, &timing);
694        }
695
696        let module_map = global.module_map();
697
698        // Step 1. If any of the following are true: bodyBytes is null or failure; or response's
699        // status is not an ok status, then:
700        if let (Err(error), _) | (_, Err(error)) = (response.as_ref(), self.status.as_ref()) {
701            error!("Fetching module script failed {:?}", error);
702            // Step 1.1. Let callbacks be moduleMap[(url, moduleType)].
703            // Step 1.2. Remove moduleMap[(url, moduleType)].
704            let Some(ModuleStatus::Fetching(callbacks)) =
705                module_map.safe_borrow_mut(cx).remove(&self.module_request)
706            else {
707                return error!("Processing response for a non pending module request");
708            };
709
710            // Step 1.3. For each callback of callbacks: run callback given null.
711            for callback in callbacks {
712                (callback)(cx, None);
713            }
714
715            // Step 1.4. Return.
716            return;
717        }
718
719        let metadata = self.metadata.take().unwrap();
720
721        // The processResponseConsumeBody steps defined inside
722        // [run a worker](https://html.spec.whatwg.org/multipage/#run-a-worker)
723        if let Some(policy_container) = self.policy_container {
724            let workerscope = global.downcast::<WorkerGlobalScope>().expect(
725                "We only need a policy container when initializing a worker's globalscope.",
726            );
727            workerscope.process_response_for_workerscope(&metadata, &policy_container);
728        }
729
730        let final_url = metadata.final_url;
731
732        // Step 2. Let mimeType be the result of extracting a MIME type from response's header list.
733        let mime_type: Option<Mime> = metadata.content_type.map(Serde::into_inner).map(Into::into);
734
735        // Step 3. Let moduleScript be null.
736        let mut module_script = None;
737
738        // Step 4. Let referrerPolicy be the result of parsing the `Referrer-Policy` header given response. [REFERRERPOLICY]
739        let referrer_policy = metadata
740            .headers
741            .and_then(|headers| headers.typed_get::<ReferrerPolicyHeader>())
742            .into();
743
744        // Step 5. If referrerPolicy is not the empty string, set options's referrer policy to referrerPolicy.
745        if referrer_policy != ReferrerPolicy::EmptyString {
746            self.options.referrer_policy = referrer_policy;
747        }
748
749        // TODO Step 6. If mimeType's essence is "application/wasm" and moduleType is "javascript-or-wasm", then set
750        // moduleScript to the result of creating a WebAssembly module script given bodyBytes, settingsObject, response's URL, and options.
751
752        // TODO handle CSS module scripts on the next mozjs ESR bump.
753
754        if let Some(mime) = mime_type {
755            // Step 7.1 Let sourceText be the result of UTF-8 decoding bodyBytes.
756            let (mut source_text, _) = UTF_8.decode_with_bom_removal(&self.data);
757
758            // Step 7.2 If mimeType is a JavaScript MIME type and moduleType is "javascript-or-wasm", then set moduleScript
759            // to the result of creating a JavaScript module script given sourceText, settingsObject, response's URL, and options.
760            if SCRIPT_JS_MIMES.contains(&mime.essence_str()) &&
761                matches!(module_type, ModuleType::JavaScript)
762            {
763                if let Some(window) = global.downcast::<Window>() &&
764                    let Some(script_souce) = window.local_script_source()
765                {
766                    substitute_with_local_script(script_souce, &mut source_text, final_url.clone());
767                }
768
769                let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
770                    cx,
771                    source_text,
772                    &global,
773                    &final_url,
774                    self.options,
775                    true,
776                    1,
777                    self.introduction_type,
778                ));
779                module_script = Some(module_tree);
780            } else if MimeClassifier::is_json(&mime) && matches!(module_type, ModuleType::JSON) {
781                // Step 7.4 If mimeType is a JSON MIME type and moduleType is "json",
782                // then set moduleScript to the result of creating a JSON module script given sourceText and settingsObject.
783                let module_tree = Rc::new(ModuleTree::create_a_json_module_script(
784                    cx,
785                    &source_text,
786                    &global,
787                    &final_url,
788                    self.introduction_type,
789                ));
790                module_script = Some(module_tree);
791            }
792        }
793
794        let callbacks = match module_map
795            .safe_borrow_mut(cx)
796            .entry(self.module_request.clone())
797        {
798            Entry::Occupied(mut entry) => {
799                // Step 9. If moduleScript is null, then remove moduleMap[(url, moduleType)];
800                // otherwise set moduleMap[(url, moduleType)] to moduleScript.
801                let old_value = match module_script.as_ref() {
802                    None => entry.remove(),
803                    Some(module_script) => {
804                        entry.insert(ModuleStatus::Loaded(module_script.clone()))
805                    },
806                };
807
808                match old_value {
809                    ModuleStatus::Loaded(_) => {
810                        return error!("Processing response for a non pending module request");
811                    },
812                    ModuleStatus::Fetching(callbacks) => callbacks,
813                }
814            },
815            Entry::Vacant(_) => {
816                return error!("Processing response for a non pending module request");
817            },
818        };
819
820        // Step 10. For each callback of callbacks: run callback given moduleScript.
821        for callback in callbacks {
822            (callback)(cx, module_script.clone());
823        }
824    }
825
826    fn process_csp_violations(
827        &mut self,
828        cx: &mut js::context::JSContext,
829        _request_id: RequestId,
830        violations: Vec<Violation>,
831    ) {
832        let global = self.owner.root();
833        if let Some(scope) = global.downcast::<DedicatedWorkerGlobalScope>() {
834            scope.report_csp_violations(violations);
835        } else if let Some(scope) = global.downcast::<SharedWorkerGlobalScope>() {
836            scope.report_csp_violations(violations);
837        } else {
838            global.report_csp_violations(cx, violations, None, None);
839        }
840    }
841
842    fn process_content_length(&mut self, _request_id: RequestId, size: usize) {
843        self.data.reserve(size - self.data.len());
844    }
845}
846
847impl ResourceTimingListener for ModuleContext {
848    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
849        let initiator_type = InitiatorType::LocalName("module".to_string());
850        let (url, _) = &self.module_request;
851        (initiator_type, url.clone())
852    }
853
854    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
855        self.owner.root()
856    }
857}
858
859#[expect(unsafe_code)]
860#[expect(non_snake_case)]
861/// A function to register module hooks (e.g. listening on resolving modules,
862/// getting module metadata, getting script private reference and resolving dynamic import)
863pub(crate) unsafe fn EnsureModuleHooksInitialized(rt: *mut JSRuntime) {
864    unsafe {
865        if GetModuleResolveHook(rt).is_some() {
866            return;
867        }
868
869        SetModuleResolveHook(rt, Some(HostResolveImportedModule));
870        SetModuleMetadataHook(rt, Some(HostPopulateImportMeta));
871        SetScriptPrivateReferenceHooks(
872            rt,
873            Some(host_add_ref_top_level_script),
874            Some(host_release_top_level_script),
875        );
876        SetModuleDynamicImportHook(rt, Some(host_import_module_dynamically));
877    }
878}
879
880#[expect(unsafe_code)]
881unsafe extern "C" fn host_add_ref_top_level_script(value: *const Value) {
882    let val = unsafe { Rc::from_raw((*value).to_private() as *const ModuleScript) };
883    mem::forget(val.clone());
884    mem::forget(val);
885}
886
887#[expect(unsafe_code)]
888unsafe extern "C" fn host_release_top_level_script(value: *const Value) {
889    let _val = unsafe { Rc::from_raw((*value).to_private() as *const ModuleScript) };
890}
891
892#[expect(unsafe_code)]
893/// <https://tc39.es/ecma262/#sec-hostimportmoduledynamically>
894/// <https://html.spec.whatwg.org/multipage/#hostimportmoduledynamically(referencingscriptormodule,-specifier,-promisecapability)>
895pub(crate) unsafe extern "C" fn host_import_module_dynamically(
896    cx: *mut RawJSContext,
897    reference_private: RawHandleValue,
898    specifier: RawHandle<*mut JSObject>,
899    promise: RawHandle<*mut JSObject>,
900) -> bool {
901    // SAFETY: it is safe to construct a JSContext from engine hook.
902    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
903    let cx = &mut cx;
904    let promise = Promise::new_with_js_promise(cx, unsafe { Handle::from_raw(promise) });
905
906    let jsstr = unsafe { GetModuleRequestSpecifier(cx, Handle::from_raw(specifier)) };
907    let module_type = unsafe { GetModuleRequestType(cx, Handle::from_raw(specifier)) };
908    let specifier = unsafe { jsstr_to_string(cx, NonNull::new(jsstr).unwrap()) };
909
910    let mut realm = CurrentRealm::assert(cx);
911    let payload = Payload::PromiseRecord(promise);
912    host_load_imported_module(
913        &mut realm,
914        None,
915        reference_private,
916        specifier,
917        module_type,
918        None,
919        payload,
920    );
921
922    true
923}
924
925#[derive(Clone, Debug, JSTraceable, MallocSizeOf)]
926/// <https://html.spec.whatwg.org/multipage/#script-fetch-options>
927pub(crate) struct ScriptFetchOptions {
928    pub(crate) integrity_metadata: String,
929    #[no_trace]
930    pub(crate) credentials_mode: CredentialsMode,
931    pub(crate) cryptographic_nonce: String,
932    #[no_trace]
933    pub(crate) parser_metadata: ParserMetadata,
934    #[no_trace]
935    pub(crate) referrer_policy: ReferrerPolicy,
936    /// <https://html.spec.whatwg.org/multipage/#concept-script-fetch-options-render-blocking>
937    /// The boolean value of render-blocking used for the initial fetch and for fetching any imported modules.
938    /// Unless otherwise stated, its value is false.
939    pub(crate) render_blocking: bool,
940}
941
942impl ScriptFetchOptions {
943    /// <https://html.spec.whatwg.org/multipage/#default-classic-script-fetch-options>
944    pub(crate) fn default_classic_script() -> ScriptFetchOptions {
945        Self {
946            cryptographic_nonce: String::new(),
947            integrity_metadata: String::new(),
948            parser_metadata: ParserMetadata::NotParserInserted,
949            credentials_mode: CredentialsMode::CredentialsSameOrigin,
950            referrer_policy: ReferrerPolicy::EmptyString,
951            render_blocking: false,
952        }
953    }
954
955    /// <https://html.spec.whatwg.org/multipage/#descendant-script-fetch-options>
956    pub(crate) fn descendant_fetch_options(
957        &self,
958        url: &ServoUrl,
959        global: &GlobalScope,
960    ) -> ScriptFetchOptions {
961        // Step 2. Let integrity be the result of resolving a module integrity metadata with url and settingsObject.
962        let integrity = global.import_map().resolve_a_module_integrity_metadata(url);
963
964        // Step 1. Let newOptions be a copy of originalOptions.
965        // TODO Step 4. Set newOptions's fetch priority to "auto".
966        Self {
967            // Step 3. Set newOptions's integrity metadata to integrity.
968            integrity_metadata: integrity,
969            cryptographic_nonce: self.cryptographic_nonce.clone(),
970            credentials_mode: self.credentials_mode,
971            parser_metadata: self.parser_metadata,
972            referrer_policy: self.referrer_policy,
973            render_blocking: self.render_blocking,
974        }
975    }
976}
977
978#[expect(unsafe_code)]
979pub(crate) unsafe fn module_script_from_reference_private(
980    reference_private: &RawHandle<JSVal>,
981) -> Option<&ModuleScript> {
982    if reference_private.get().is_undefined() {
983        return None;
984    }
985    unsafe { (reference_private.get().to_private() as *const ModuleScript).as_ref() }
986}
987
988#[expect(unsafe_code)]
989#[expect(non_snake_case)]
990/// <https://tc39.es/ecma262/#sec-HostLoadImportedModule>
991/// <https://html.spec.whatwg.org/multipage/#hostloadimportedmodule>
992unsafe extern "C" fn HostResolveImportedModule(
993    cx: *mut RawJSContext,
994    reference_private: RawHandleValue,
995    specifier: RawHandle<*mut JSObject>,
996) -> *mut JSObject {
997    // SAFETY: it is safe to construct a JSContext from engine hook.
998    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
999    let mut realm = CurrentRealm::assert(&mut cx);
1000    let global_scope = GlobalScope::from_current_realm(&mut realm);
1001
1002    let cx = &mut realm;
1003
1004    // Step 5.
1005    let module_data = unsafe { module_script_from_reference_private(&reference_private) };
1006    let jsstr = unsafe { GetModuleRequestSpecifier(cx, Handle::from_raw(specifier)) };
1007    let module_type = unsafe { GetModuleRequestType(cx, Handle::from_raw(specifier)) };
1008
1009    let specifier = unsafe { jsstr_to_string(cx, NonNull::new(jsstr).unwrap()) };
1010    let url = ModuleTree::resolve_module_specifier(
1011        &global_scope,
1012        module_data,
1013        DOMString::from(specifier),
1014    );
1015
1016    // Step 6.
1017    assert!(url.is_ok());
1018
1019    let parsed_url = url.unwrap();
1020
1021    // Step 4 & 7.
1022    let module_tree = global_scope.module_tree_for_request_if_loaded(&(parsed_url, module_type));
1023
1024    let module = module_tree.expect("Attempted to link a module not found inside module map");
1025
1026    let fetched_module_object = module.get_record();
1027
1028    // Step 8.
1029    assert!(fetched_module_object.is_some());
1030
1031    // Step 10.
1032    if let Some(record) = fetched_module_object {
1033        return record.handle().get();
1034    }
1035
1036    unreachable!()
1037}
1038
1039// https://searchfox.org/firefox-esr140/rev/3fccb0ec900b931a1a752b02eafab1fb9652d9b9/js/loader/ModuleLoaderBase.h#560
1040const SLOT_MODULEPRIVATE: usize = 0;
1041
1042#[expect(unsafe_code)]
1043#[expect(non_snake_case)]
1044/// <https://tc39.es/ecma262/#sec-hostgetimportmetaproperties>
1045/// <https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties>
1046unsafe extern "C" fn HostPopulateImportMeta(
1047    cx: *mut RawJSContext,
1048    reference_private: RawHandleValue,
1049    meta_object: RawHandle<*mut JSObject>,
1050) -> bool {
1051    // SAFETY: it is safe to construct a JSContext from engine hook.
1052    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
1053    let mut realm = CurrentRealm::assert(&mut cx);
1054    let global_scope = GlobalScope::from_current_realm(&mut realm);
1055
1056    // Step 2.
1057    let base_url = match unsafe { module_script_from_reference_private(&reference_private) } {
1058        Some(module_data) => module_data.base_url.clone(),
1059        None => global_scope.api_base_url(),
1060    };
1061
1062    unsafe {
1063        let url_string = JS_NewStringCopyN(
1064            &mut cx,
1065            base_url.as_str().as_ptr() as *const _,
1066            base_url.as_str().len(),
1067        );
1068        rooted!(&in(cx) let url_string = url_string);
1069
1070        // Step 3.
1071        if !JS_DefineProperty4(
1072            &mut cx,
1073            Handle::from_raw(meta_object),
1074            c"url".as_ptr(),
1075            url_string.handle(),
1076            JSPROP_ENUMERATE.into(),
1077        ) {
1078            return false;
1079        }
1080
1081        // Step 5. Let resolveFunction be ! CreateBuiltinFunction(steps, 1, "resolve", « »).
1082        let resolve_function = DefineFunctionWithReserved(
1083            &mut cx,
1084            meta_object.get(),
1085            c"resolve".as_ptr(),
1086            Some(import_meta_resolve),
1087            1,
1088            JSPROP_ENUMERATE.into(),
1089        );
1090
1091        rooted!(&in(cx) let obj = JS_GetFunctionObject(resolve_function));
1092        assert!(!obj.is_null());
1093        SetFunctionNativeReserved(
1094            obj.get(),
1095            SLOT_MODULEPRIVATE,
1096            &reference_private.get() as *const _,
1097        );
1098    }
1099
1100    true
1101}
1102
1103#[expect(unsafe_code)]
1104unsafe extern "C" fn import_meta_resolve(cx: *mut RawJSContext, argc: u32, vp: *mut JSVal) -> bool {
1105    // SAFETY: it is safe to construct a JSContext from engine hook.
1106    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1107    let mut realm = CurrentRealm::assert(&mut cx);
1108    let global_scope = GlobalScope::from_current_realm(&mut realm);
1109
1110    let cx = &mut realm;
1111
1112    let args = unsafe { CallArgs::from_vp(vp, argc) };
1113
1114    rooted!(&in(cx) let module_private = unsafe { *GetFunctionNativeReserved(args.callee(), SLOT_MODULEPRIVATE) });
1115    let reference_private = module_private.handle().into();
1116    let module_data = unsafe { module_script_from_reference_private(&reference_private) };
1117
1118    // https://html.spec.whatwg.org/multipage/#hostgetimportmetaproperties
1119
1120    // Step 4.1. Set specifier to ? ToString(specifier).
1121    let specifier = unsafe {
1122        let value = HandleValue::from_raw(args.get(0));
1123
1124        match NonNull::new(ToString(cx, value)) {
1125            Some(jsstr) => jsstr_to_string(cx, jsstr).into(),
1126            None => return false,
1127        }
1128    };
1129
1130    // Step 4.2. Let url be the result of resolving a module specifier given moduleScript and specifier.
1131    let url = ModuleTree::resolve_module_specifier(&global_scope, module_data, specifier);
1132
1133    match url {
1134        Ok(url) => {
1135            // Step 4.3. Return the serialization of url.
1136            url.as_str()
1137                .safe_to_jsval(cx, unsafe { MutableHandleValue::from_raw(args.rval()) });
1138            true
1139        },
1140        Err(error) => {
1141            let resolution_error = gen_type_error(cx, &global_scope, error);
1142
1143            unsafe {
1144                JS_SetPendingException(
1145                    cx,
1146                    resolution_error.handle(),
1147                    ExceptionStackBehavior::Capture,
1148                );
1149            }
1150            false
1151        },
1152    }
1153}
1154
1155#[expect(clippy::too_many_arguments)]
1156/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-worker-script-tree>
1157/// <https://html.spec.whatwg.org/multipage/#fetch-a-worklet/module-worker-script-graph>
1158pub(crate) fn fetch_a_module_script_graph(
1159    cx: &mut JSContext,
1160    global: &GlobalScope,
1161    url: UrlWithBlobClaim,
1162    fetch_client: RequestClient,
1163    destination: Destination,
1164    referrer: Referrer,
1165    credentials_mode: CredentialsMode,
1166    introduction_type: Option<&'static CStr>,
1167    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1168) {
1169    let global_scope = DomRoot::from_ref(global);
1170
1171    // Step 1. Let options be a script fetch options whose cryptographic nonce
1172    // is the empty string, integrity metadata is the empty string, parser
1173    // metadata is "not-parser-inserted", credentials mode is credentialsMode,
1174    // referrer policy is the empty string, and fetch priority is "auto".
1175    let options = ScriptFetchOptions {
1176        integrity_metadata: "".into(),
1177        credentials_mode,
1178        cryptographic_nonce: "".into(),
1179        parser_metadata: ParserMetadata::NotParserInserted,
1180        referrer_policy: ReferrerPolicy::EmptyString,
1181        render_blocking: false,
1182    };
1183
1184    // Step 2. Fetch a single module script given url, fetchClient, destination, options,
1185    // settingsObject, "client", true, and onSingleFetchComplete as defined below.
1186    fetch_a_single_module_script(
1187        cx,
1188        url,
1189        fetch_client.clone(),
1190        global,
1191        destination,
1192        options,
1193        referrer,
1194        None,
1195        true,
1196        introduction_type,
1197        move |cx, module_tree| {
1198            let Some(module) = module_tree else {
1199                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
1200                return on_complete(cx, None);
1201            };
1202
1203            // Step 1.2. Fetch the descendants of and link result given fetchClient, destination,
1204            // and onComplete.
1205            fetch_the_descendants_and_link_module_script(
1206                cx,
1207                &global_scope,
1208                module,
1209                fetch_client,
1210                destination,
1211                on_complete,
1212            );
1213        },
1214    );
1215}
1216
1217/// <https://html.spec.whatwg.org/multipage/#fetch-a-module-script-tree>
1218pub(crate) fn fetch_an_external_module_script(
1219    cx: &mut JSContext,
1220    url: UrlWithBlobClaim,
1221    global: &GlobalScope,
1222    options: ScriptFetchOptions,
1223    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1224) {
1225    let referrer = global.get_referrer();
1226    let fetch_client = global.request_client(Some(cx.no_gc()));
1227    let global_scope = DomRoot::from_ref(global);
1228
1229    // Step 1. Fetch a single module script given url, settingsObject, "script", options, settingsObject, "client", true,
1230    // and with the following steps given result:
1231    fetch_a_single_module_script(
1232        cx,
1233        url,
1234        fetch_client.clone(),
1235        global,
1236        Destination::Script,
1237        options,
1238        referrer,
1239        None,
1240        true,
1241        Some(IntroductionType::SRC_SCRIPT),
1242        move |cx, module_tree| {
1243            let Some(module) = module_tree else {
1244                // Step 1.1. If result is null, run onComplete given null, and abort these steps.
1245                return on_complete(cx, None);
1246            };
1247
1248            // Step 1.2. Fetch the descendants of and link result given settingsObject, "script", and onComplete.
1249            fetch_the_descendants_and_link_module_script(
1250                cx,
1251                &global_scope,
1252                module,
1253                fetch_client,
1254                Destination::Script,
1255                on_complete,
1256            );
1257        },
1258    );
1259}
1260
1261/// <https://html.spec.whatwg.org/multipage/#fetch-a-modulepreload-module-script-graph>
1262pub(crate) fn fetch_a_modulepreload_module(
1263    cx: &mut JSContext,
1264    url: UrlWithBlobClaim,
1265    destination: Destination,
1266    global: &GlobalScope,
1267    options: ScriptFetchOptions,
1268    on_complete: impl FnOnce(&mut JSContext, bool) + 'static,
1269) {
1270    let referrer = global.get_referrer();
1271    let fetch_client = global.request_client(Some(cx.no_gc()));
1272    let global_scope = DomRoot::from_ref(global);
1273
1274    // Note: There is a specification inconsistency, `fetch_a_single_module_script` doesn't allow
1275    // fetching top level JSON/CSS module scripts, but should be possible when preloading.
1276    let module_type = if let Destination::Json = destination {
1277        Some(ModuleType::JSON)
1278    } else {
1279        None
1280    };
1281
1282    // Step 1. Fetch a single module script given url, settingsObject, destination, options, settingsObject,
1283    // "client", true, and with the following steps given result:
1284    fetch_a_single_module_script(
1285        cx,
1286        url,
1287        fetch_client.clone(),
1288        global,
1289        destination,
1290        options,
1291        referrer,
1292        module_type,
1293        true,
1294        Some(IntroductionType::SRC_SCRIPT),
1295        move |cx, result| {
1296            // Step 1. Run onComplete given result.
1297            on_complete(cx, result.is_none());
1298
1299            // Step 2. Assert: settingsObject's global object implements Window.
1300            assert!(global_scope.is::<Window>());
1301
1302            // Step 3. If result is not null, optionally fetch the descendants of and link result
1303            // given settingsObject, destination, and an empty algorithm.
1304            if pref!(dom_allow_preloading_module_descendants) &&
1305                let Some(module) = result
1306            {
1307                fetch_the_descendants_and_link_module_script(
1308                    cx,
1309                    &global_scope,
1310                    module,
1311                    fetch_client,
1312                    destination,
1313                    |_, _| {},
1314                );
1315            }
1316        },
1317    );
1318}
1319
1320#[expect(clippy::too_many_arguments)]
1321/// <https://html.spec.whatwg.org/multipage/#fetch-an-inline-module-script-graph>
1322pub(crate) fn fetch_inline_module_script(
1323    cx: &mut JSContext,
1324    global: &GlobalScope,
1325    module_script_text: Cow<'_, str>,
1326    url: ServoUrl,
1327    options: ScriptFetchOptions,
1328    line_number: u32,
1329    introduction_type: Option<&'static CStr>,
1330    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1331) {
1332    // Step 1. Let script be the result of creating a JavaScript module script using sourceText, settingsObject, baseURL, and options.
1333    let module_tree = Rc::new(ModuleTree::create_a_javascript_module_script(
1334        cx,
1335        module_script_text,
1336        global,
1337        &url,
1338        options,
1339        false,
1340        line_number,
1341        introduction_type,
1342    ));
1343    let fetch_client = global.request_client(Some(cx.no_gc()));
1344
1345    // Step 2. Fetch the descendants of and link script, given settingsObject, "script", and onComplete.
1346    fetch_the_descendants_and_link_module_script(
1347        cx,
1348        global,
1349        module_tree,
1350        fetch_client,
1351        Destination::Script,
1352        on_complete,
1353    );
1354}
1355
1356#[expect(unsafe_code)]
1357/// <https://html.spec.whatwg.org/multipage/#fetch-the-descendants-of-and-link-a-module-script>
1358fn fetch_the_descendants_and_link_module_script(
1359    cx: &mut JSContext,
1360    global: &GlobalScope,
1361    module_script: Rc<ModuleTree>,
1362    fetch_client: RequestClient,
1363    destination: Destination,
1364    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + Clone + 'static,
1365) {
1366    // Step 1. Let record be moduleScript's record.
1367    // Step 2. If record is null, then:
1368    if module_script.get_record().is_none() {
1369        let parse_error = module_script.get_parse_error().cloned();
1370
1371        // Step 2.1. Set moduleScript's error to rethrow to moduleScript's parse error.
1372        module_script.set_rethrow_error(parse_error.unwrap());
1373
1374        // Step 2.2. Run onComplete given moduleScript.
1375        on_complete(cx, Some(module_script));
1376
1377        // Step 2.3. Return.
1378        return;
1379    }
1380
1381    // Step 3. Let state be Record
1382    // { [[ErrorToRethrow]]: null, [[Destination]]: destination, [[PerformFetch]]: null, [[FetchClient]]: fetchClient }.
1383    let state = Rc::new(LoadState {
1384        error_to_rethrow: RefCell::new(None),
1385        destination,
1386        fetch_client,
1387    });
1388
1389    // TODO Step 4. If performFetch was given, set state.[[PerformFetch]] to performFetch.
1390
1391    let mut realm = enter_auto_realm(cx, global);
1392    let cx = &mut realm.current_realm();
1393
1394    // Step 5. Let loadingPromise be record.LoadRequestedModules(state).
1395    let loading_promise = load_requested_modules(cx, module_script.clone(), Some(state.clone()));
1396
1397    let global_scope = DomRoot::from_ref(global);
1398    let fulfilled_module = module_script.clone();
1399    let fulfilled_on_complete = on_complete.clone();
1400
1401    // Step 6. Upon fulfillment of loadingPromise, run the following steps:
1402    let loading_promise_fulfillment = ModuleHandler::new_boxed(Box::new(
1403        task!(fulfilled_steps: |cx, global_scope: DomRoot<GlobalScope>| {
1404            let mut realm = AutoRealm::new(
1405                cx,
1406                NonNull::new(global_scope.reflector().get_jsobject().get()).unwrap(),
1407            );
1408            let cx = &mut *realm;
1409
1410            let handle = fulfilled_module.get_record().map(|module| module.handle()).unwrap();
1411
1412            // Step 6.1. Perform record.Link().
1413            let link = unsafe { ModuleLink(cx, handle) };
1414
1415            // If this throws an exception, catch it, and set moduleScript's error to rethrow to that exception.
1416            if !link {
1417                let exception = RethrowError::from_pending_exception(cx);
1418                fulfilled_module.set_rethrow_error(exception);
1419            }
1420
1421            // Step 6.2. Run onComplete given moduleScript.
1422            fulfilled_on_complete(cx, Some(fulfilled_module));
1423        }),
1424    ));
1425
1426    // Step 7. Upon rejection of loadingPromise, run the following steps:
1427    let loading_promise_rejection =
1428        ModuleHandler::new_boxed(Box::new(task!(rejected_steps: |cx, state: Rc<LoadState>| {
1429            // Step 7.1. If state.[[ErrorToRethrow]] is not null, set moduleScript's error to rethrow to state.[[ErrorToRethrow]]
1430            // and run onComplete given moduleScript.
1431            if let Some(error) = state.error_to_rethrow.borrow().as_ref() {
1432                module_script.set_rethrow_error(error.clone());
1433                on_complete(cx, Some(module_script));
1434            } else {
1435                // Step 7.2. Otherwise, run onComplete given null.
1436                on_complete(cx, None);
1437            }
1438        })));
1439
1440    let handler = PromiseNativeHandler::new(
1441        cx,
1442        global,
1443        Some(loading_promise_fulfillment),
1444        Some(loading_promise_rejection),
1445    );
1446
1447    run_a_callback::<DomTypeHolder, _>(global, || {
1448        loading_promise.append_native_handler(cx, &handler);
1449    });
1450}
1451
1452/// <https://html.spec.whatwg.org/multipage/#fetch-a-single-module-script>
1453#[expect(clippy::too_many_arguments)]
1454pub(crate) fn fetch_a_single_module_script(
1455    cx: &mut JSContext,
1456    url: UrlWithBlobClaim,
1457    fetch_client: RequestClient,
1458    global: &GlobalScope,
1459    destination: Destination,
1460    options: ScriptFetchOptions,
1461    referrer: Referrer,
1462    module_type: Option<ModuleType>,
1463    is_top_level: bool,
1464    introduction_type: Option<&'static CStr>,
1465    on_complete: impl FnOnce(&mut JSContext, Option<Rc<ModuleTree>>) + 'static,
1466) {
1467    // Step 1. Let moduleType be "javascript-or-wasm".
1468    // Step 2. If moduleRequest was given, then set moduleType to the result of running the
1469    // module type from module request steps given moduleRequest.
1470    let module_type = module_type.unwrap_or(ModuleType::JavaScript);
1471
1472    // TODO Step 3. Assert: the result of running the module type allowed steps given moduleType and settingsObject is true.
1473    // Otherwise, we would not have reached this point because a failure would have been raised
1474    // when inspecting moduleRequest.[[Attributes]] in HostLoadImportedModule or fetch a single imported module script.
1475
1476    let module_request = (url.url(), module_type);
1477
1478    // Step 4. Let moduleMap be settingsObject's module map.
1479    let module_map = global.module_map();
1480    let mut module_map_borrow = module_map.safe_borrow_mut(cx);
1481
1482    let entry = module_map_borrow.entry(module_request.clone());
1483
1484    match entry {
1485        Entry::Occupied(mut entry) => match entry.get_mut() {
1486            // Step 5. If moduleMap[(url, moduleType)] is a module script, run onComplete given
1487            // moduleMap[(url, moduleType)], and return.
1488            ModuleStatus::Loaded(module_tree) => {
1489                let module = module_tree.clone();
1490                drop(module_map_borrow);
1491                return on_complete(cx, Some(module));
1492            },
1493            // Step 6. If moduleMap[(url, moduleType)] is a list, append onComplete to
1494            // moduleMap[(url, moduleType)], and return.
1495            ModuleStatus::Fetching(callbacks) => return callbacks.push(Box::new(on_complete)),
1496        },
1497        // Step 7. Set moduleMap[(url, moduleType)] to « onComplete ».
1498        Entry::Vacant(entry) => {
1499            entry.insert(ModuleStatus::Fetching(vec![Box::new(on_complete)]));
1500        },
1501    }
1502
1503    // We only need a policy container when fetching the root of a module worker.
1504    let policy_container = (is_top_level && global.is::<WorkerGlobalScope>())
1505        .then(|| fetch_client.policy_container.clone());
1506
1507    // Step 8. Let request be a new request whose URL is url, mode is "cors", referrer is referrer, and client is fetchClient.
1508
1509    // Step 10. If destination is "worker", "sharedworker", or "serviceworker", and isTopLevel is true,
1510    // then set request's mode to "same-origin".
1511    let mode = match destination {
1512        Destination::Worker | Destination::SharedWorker if is_top_level => RequestMode::SameOrigin,
1513        _ => RequestMode::CorsMode,
1514    };
1515
1516    // Step 9. Set request's destination to the result of running the
1517    // fetch destination from module type steps given destination and moduleType.
1518    let destination = match module_type {
1519        ModuleType::JSON => Destination::Json,
1520        ModuleType::JavaScript | ModuleType::Unknown => destination,
1521    };
1522
1523    // TODO Step 11. Set request's initiator type to "script".
1524
1525    // Step 12. Set up the module script request given request and options.
1526    let request = RequestBuilder::new(global.webview_id(), url, referrer)
1527        .destination(destination)
1528        .parser_metadata(options.parser_metadata)
1529        .integrity_metadata(options.integrity_metadata.clone())
1530        .credentials_mode(options.credentials_mode)
1531        .referrer_policy(options.referrer_policy)
1532        .mode(mode)
1533        .cryptographic_nonce_metadata(options.cryptographic_nonce.clone())
1534        .client(fetch_client)
1535        .pipeline_id(Some(global.pipeline_id()));
1536
1537    let context = ModuleContext {
1538        owner: Trusted::new(global),
1539        data: vec![],
1540        metadata: None,
1541        module_request,
1542        options,
1543        status: Ok(()),
1544        introduction_type,
1545        policy_container,
1546    };
1547
1548    let task_source = global.task_manager().networking_task_source().to_sendable();
1549    global.fetch(request, context, task_source);
1550}
1551
1552/// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record>
1553#[derive(Default, Eq, Hash, JSTraceable, MallocSizeOf, PartialEq)]
1554pub(crate) struct ResolvedModule {
1555    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-serialized-base-url>
1556    pub(crate) base_url: String,
1557    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-specifier>
1558    pub(crate) specifier: String,
1559    /// <https://html.spec.whatwg.org/multipage/#specifier-resolution-record-as-url>
1560    #[no_trace]
1561    pub(crate) specifier_url: Option<ServoUrl>,
1562}
1563
1564impl ResolvedModule {
1565    pub(crate) fn new(
1566        base_url: String,
1567        specifier: String,
1568        specifier_url: Option<ServoUrl>,
1569    ) -> Self {
1570        Self {
1571            base_url,
1572            specifier,
1573            specifier_url,
1574        }
1575    }
1576}
1577
1578/// <https://html.spec.whatwg.org/multipage/#resolving-an-imports-match>
1579///
1580/// When the error is thrown, it will terminate the entire resolve a module specifier algorithm
1581/// without any further fallbacks.
1582fn resolve_imports_match(
1583    normalized_specifier: &str,
1584    as_url: Option<&ServoUrl>,
1585    specifier_map: &ModuleSpecifierMap,
1586) -> Fallible<Option<ServoUrl>> {
1587    // Step 1. For each specifierKey → resolutionResult of specifierMap:
1588    for (specifier_key, resolution_result) in specifier_map {
1589        // Step 1.1 If specifierKey is normalizedSpecifier, then:
1590        if specifier_key == normalized_specifier {
1591            if let Some(resolution_result) = resolution_result {
1592                // Step 1.1.2 Assert: resolutionResult is a URL.
1593                // This is checked by Url type already.
1594                // Step 1.1.3 Return resolutionResult.
1595                return Ok(Some(resolution_result.clone()));
1596            } else {
1597                // Step 1.1.1 If resolutionResult is null, then throw a TypeError.
1598                return Err(Error::Type(
1599                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
1600                ));
1601            }
1602        }
1603
1604        // Step 1.2 If all of the following are true:
1605        // - specifierKey ends with U+002F (/)
1606        // - specifierKey is a code unit prefix of normalizedSpecifier
1607        // - either asURL is null, or asURL is special, then:
1608        if specifier_key.ends_with('\u{002f}') &&
1609            normalized_specifier.starts_with(specifier_key) &&
1610            (as_url.is_none() || as_url.is_some_and(|u| u.is_special_scheme()))
1611        {
1612            // Step 1.2.1 If resolutionResult is null, then throw a TypeError.
1613            // Step 1.2.2 Assert: resolutionResult is a URL.
1614            let Some(resolution_result) = resolution_result else {
1615                return Err(Error::Type(
1616                    c"Resolution of specifierKey was blocked by a null entry.".to_owned(),
1617                ));
1618            };
1619
1620            // Step 1.2.3 Let afterPrefix be the portion of normalizedSpecifier after the initial specifierKey prefix.
1621            let after_prefix = normalized_specifier
1622                .strip_prefix(specifier_key)
1623                .expect("specifier_key should be the prefix of normalized_specifier");
1624
1625            // Step 1.2.4 Assert: resolutionResult, serialized, ends with U+002F (/), as enforced during parsing.
1626            debug_assert!(resolution_result.as_str().ends_with('\u{002f}'));
1627
1628            // Step 1.2.5 Let url be the result of URL parsing afterPrefix with resolutionResult.
1629            let url = ServoUrl::parse_with_base(Some(resolution_result), after_prefix);
1630
1631            // Step 1.2.6 If url is failure, then throw a TypeError
1632            // Step 1.2.7 Assert: url is a URL.
1633            let Ok(url) = url else {
1634                return Err(Error::Type(
1635                    c"Resolution of normalizedSpecifier was blocked since
1636                    the afterPrefix portion could not be URL-parsed relative to
1637                    the resolutionResult mapped to by the specifierKey prefix."
1638                        .to_owned(),
1639                ));
1640            };
1641
1642            // Step 1.2.8 If the serialization of resolutionResult is not
1643            // a code unit prefix of the serialization of url, then throw a TypeError
1644            if !url.as_str().starts_with(resolution_result.as_str()) {
1645                return Err(Error::Type(
1646                    c"Resolution of normalizedSpecifier was blocked due to
1647                    it backtracking above its prefix specifierKey."
1648                        .to_owned(),
1649                ));
1650            }
1651
1652            // Step 1.2.9 Return url.
1653            return Ok(Some(url));
1654        }
1655    }
1656
1657    // Step 2. Return null.
1658    Ok(None)
1659}